xref: /freebsd-src/libexec/nuageinit/nuageinit (revision e9ac41698b2f322d55ccf9da50a3596edb2c1800)
1#!/usr/libexec/flua
2
3-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
4--
5-- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
6
7local nuage = require("nuage")
8local yaml = require("yaml")
9
10if #arg ~= 2 then
11	nuage.err("Usage ".. arg[0] .." <cloud-init directory> [config-2|nocloud]")
12end
13local path = arg[1]
14local citype = arg[2]
15local ucl = require("ucl")
16
17local default_user = {
18	name = "freebsd",
19	homedir = "/home/freebsd",
20	groups = "wheel",
21	gecos = "FreeBSD User",
22	shell = "/bin/sh",
23	plain_text_passwd = "freebsd"
24}
25
26local root = os.getenv("NUAGE_FAKE_ROOTDIR")
27if not root then
28	root = ""
29end
30
31local function open_config(name)
32	nuage.mkdir_p(root .. "/etc/rc.conf.d")
33	local f,err = io.open(root .. "/etc/rc.conf.d/" .. name, "w")
34	if not f then
35		nuage.err("nuageinit: unable to open "..name.." config: " .. err)
36	end
37	return f
38end
39
40local function get_ifaces()
41	local parser = ucl.parser()
42	-- grab ifaces
43	local ns  = io.popen('netstat -i --libxo json')
44	local netres = ns:read("*a")
45	ns:close()
46	local res,err = parser:parse_string(netres)
47	if not res then
48		nuage.warn("Error parsing netstat -i --libxo json outout: " .. err)
49		return nil
50	end
51	local ifaces = parser:get_object()
52	local myifaces = {}
53	for _,iface in pairs(ifaces["statistics"]["interface"]) do
54		if iface["network"]:match("<Link#%d>") then
55			local s = iface["address"]
56			myifaces[s:lower()] = iface["name"]
57		end
58	end
59	return myifaces
60end
61
62local function config2_network(p)
63	local parser = ucl.parser()
64	local f = io.open(p .. "/network_data.json")
65	if not f then
66		-- silently return no network configuration is provided
67		return
68	end
69	f:close()
70	local res,err = parser:parse_file(p .. "/network_data.json")
71	if not res then
72		nuage.warn("nuageinit: error parsing network_data.json: " .. err)
73		return
74	end
75	local obj = parser:get_object()
76
77	local ifaces = get_ifaces()
78	if not ifaces then
79		nuage.warn("nuageinit: no network interfaces found")
80		return
81	end
82	local mylinks = {}
83	for _,v in pairs(obj["links"]) do
84		local s = v["ethernet_mac_address"]:lower()
85		mylinks[v["id"]] = ifaces[s]
86	end
87
88	nuage.mkdir_p(root .. "/etc/rc.conf.d")
89	local network = open_config("network")
90	local routing = open_config("routing")
91	local ipv6 = {}
92	local ipv6_routes = {}
93	local ipv4 = {}
94	for _,v in pairs(obj["networks"]) do
95		local interface = mylinks[v["link"]]
96		if v["type"] == "ipv4_dhcp" then
97			network:write("ifconfig_"..interface.."=\"DHCP\"\n")
98		end
99		if v["type"] == "ipv4" then
100			network:write("ifconfig_"..interface.."=\"inet "..v["ip_address"].." netmask " .. v["netmask"] .. "\"\n")
101			if v["gateway"] then
102				routing:write("defaultrouter=\""..v["gateway"].."\"\n")
103			end
104			if v["routes"] then
105				for i,r in ipairs(v["routes"]) do
106					local rname = "cloudinit" .. i .. "_" .. interface
107					if v["gateway"] and v["gateway"] == r["gateway"] then goto next end
108					if r["network"] == "0.0.0.0" then
109						routing:write("defaultrouter=\""..r["gateway"].."\"\n")
110						goto next
111					end
112					routing:write("route_".. rname .. "=\"-net ".. r["network"] .. " ")
113					routing:write(r["gateway"] .. " " .. r["netmask"] .. "\"\n")
114					ipv4[#ipv4 + 1] = rname
115					::next::
116				end
117			end
118		end
119		if v["type"] == "ipv6" then
120			ipv6[#ipv6+1] = interface
121			ipv6_routes[#ipv6_routes+1] = interface
122			network:write("ifconfig_"..interface.."_ipv6=\"inet6 "..v["ip_address"].."\"\n")
123			if v["gateway"] then
124				routing:write("ipv6_defaultrouter=\""..v["gateway"].."\"\n")
125				routing:write("ipv6_route_"..interface.."=\""..v["gateway"])
126				routing:write(" -prefixlen 128 -interface "..interface.."\"\n")
127			end
128			-- TODO compute the prefixlen for the routes
129			--if v["routes"] then
130			--	for i,r in ipairs(v["routes"]) do
131			--	local rname = "cloudinit" .. i .. "_" .. mylinks[v["link"]]
132			--		-- skip all the routes which are already covered by the default gateway, some provider
133			--		-- still list plenty of them.
134			--		if v["gateway"] == r["gateway"] then goto next end
135			--		routing:write("ipv6_route_" .. rname .. "\"\n")
136			--		ipv6_routes[#ipv6_routes+1] = rname
137			--		::next::
138			--	end
139			--end
140		end
141	end
142	if #ipv4 > 0 then
143		routing:write("static_routes=\"")
144		routing:write(table.concat(ipv4, " ") .. "\"\n")
145	end
146	if #ipv6 > 0 then
147		network:write("ipv6_network_interfaces=\"")
148		network:write(table.concat(ipv6, " ") .. "\"\n")
149		network:write("ipv6_default_interface=\""..ipv6[1].."\"\n")
150	end
151	if #ipv6_routes > 0 then
152		routing:write("ipv6_static_routes=\"")
153		routing:write(table.concat(ipv6, " ") .. "\"\n")
154	end
155	network:close()
156	routing:close()
157end
158
159if citype == "config-2" then
160	local parser = ucl.parser()
161	local res,err = parser:parse_file(path..'/meta_data.json')
162
163	if not res then
164		nuage.err("nuageinit: error parsing config-2: meta_data.json: " .. err)
165	end
166	local obj = parser:get_object()
167	nuage.sethostname(obj["hostname"])
168
169	-- network
170	config2_network(path)
171elseif citype == "nocloud" then
172	local f,err = io.open(path.."/meta-data")
173	if err then
174		nuage.err("nuageinit: error parsing nocloud meta-data: ".. err)
175	end
176	local obj = yaml.eval(f:read("*a"))
177	f:close()
178	if not obj then
179		nuage.err("nuageinit: error parsing nocloud meta-data")
180	end
181	local hostname = obj['local-hostname']
182	if not hostname then
183		hostname = obj['hostname']
184	end
185	if hostname then
186		nuage.sethostname(hostname)
187	end
188else
189	nuage.err("Unknown cloud init type: ".. citype)
190end
191
192-- deal with user-data
193local ud = nil
194local f = nil
195userdatas = { "user-data", "user_data" }
196for _,v in pairs(userdatas) do
197	f = io.open(path..'/' .. v, "r")
198	if f then
199		ud = v
200		break
201	end
202end
203if not f then
204	os.exit(0)
205end
206local line = f:read('*l')
207f:close()
208if line == "#cloud-config" then
209	f = io.open(path.."/" .. ud)
210	local obj = yaml.eval(f:read("*a"))
211	f:close()
212	if not obj then
213		nuage.err("nuageinit: error parsing cloud-config file: " .. ud)
214	end
215	if obj.groups then
216		for n,g in pairs(obj.groups) do
217			if (type(g) == "string") then
218				local r = nuage.addgroup({name = g})
219				if not r then
220					nuage.warn("nuageinit: failed to add group: ".. g)
221				end
222			elseif type(g) == "table" then
223				for k,v in pairs(g) do
224					nuage.addgroup({name = k, members = v})
225				end
226			else
227				nuage.warn("nuageinit: invalid type : "..type(g).." for users entry number "..n);
228			end
229		end
230	end
231	if obj.users then
232		for n,u in pairs(obj.users) do
233			if type(u) == "string" then
234				if u == "default" then
235					nuage.adduser(default_user)
236				else
237					nuage.adduser({name = u})
238				end
239			elseif type(u) == "table" then
240				-- ignore users without a username
241				if u.name == nil then
242					goto unext
243				end
244				local homedir = nuage.adduser(u)
245				if u.ssh_authorized_keys then
246					for _,v in ipairs(u.ssh_authorized_keys) do
247						nuage.addsshkey(homedir, v)
248					end
249				end
250			else
251				nuage.warn("nuageinit: invalid type : "..type(u).." for users entry number "..n);
252			end
253			::unext::
254		end
255	else
256	-- default user if none are defined
257		nuage.adduser(default_user)
258	end
259	if obj.ssh_authorized_keys then
260		local homedir = nuage.adduser(default_user)
261		for _,k in ipairs(obj.ssh_authorized_keys) do
262			nuage.addsshkey(homedir, k)
263		end
264	end
265	if obj.network then
266		local ifaces = get_ifaces()
267		nuage.mkdir_p(root .. "/etc/rc.conf.d")
268		local network = open_config("network")
269		local routing = open_config("routing")
270		local ipv6={}
271		for _,v in pairs(obj.network.ethernets) do
272			if not v.match then goto next end
273			if not v.match.macaddress then goto next end
274			if not ifaces[v.match.macaddress] then
275				nuage.warn("nuageinit: not interface matching: "..v.match.macaddress)
276				goto next
277			end
278			local interface = ifaces[v.match.macaddress]
279			if v.dhcp4 then
280				network:write("ifconfig_"..interface.."=\"DHCP\"\n")
281			elseif v.addresses then
282				for _,a in pairs(v.addresses) do
283					if a:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)") then
284						network:write("ifconfig_"..interface.."=\"inet "..a.."\"\n")
285					else
286						network:write("ifconfig_"..interface.."_ipv6=\"inet6 "..a.."\"\n")
287						ipv6[#ipv6 +1] = interface
288					end
289				end
290			end
291			if v.gateway4 then
292				routing:write("defaultrouter=\""..v.gateway4.."\"\n")
293			end
294			if v.gateway6 then
295				routing:write("ipv6_defaultrouter=\""..v.gateway6.."\"\n")
296				routing:write("ipv6_route_"..interface.."=\""..v.gateway6)
297				routing:write(" -prefixlen 128 -interface "..interface.."\"\n")
298			end
299			::next::
300		end
301		if #ipv6 > 0 then
302			network:write("ipv6_network_interfaces=\"")
303			network:write(table.concat(ipv6, " ") .. "\"\n")
304			network:write("ipv6_default_interface=\""..ipv6[1].."\"\n")
305		end
306		network:close()
307		routing:close()
308	end
309else
310	local res,err = os.execute(path..'/' .. ud)
311	if not res then
312		nuage.err("nuageinit: error executing user-data script: ".. err)
313	end
314end
315