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