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 f = io.open(path..'/user-data', "r")
194if not f then
195	os.exit(0)
196end
197local line = f:read('*l')
198f:close()
199if line == "#cloud-config" then
200	f = io.open(path.."/user-data")
201	local obj = yaml.eval(f:read("*a"))
202	f:close()
203	if not obj then
204		nuage.err("nuageinit: error parsing cloud-config file: user-data")
205	end
206	if obj.groups then
207		for n,g in pairs(obj.groups) do
208			if (type(g) == "string") then
209				local r = nuage.addgroup({name = g})
210				if not r then
211					nuage.warn("nuageinit: failed to add group: ".. g)
212				end
213			elseif type(g) == "table" then
214				for k,v in pairs(g) do
215					nuage.addgroup({name = k, members = v})
216				end
217			else
218				nuage.warn("nuageinit: invalid type : "..type(g).." for users entry number "..n);
219			end
220		end
221	end
222	if obj.users then
223		for n,u in pairs(obj.users) do
224			if type(u) == "string" then
225				if u == "default" then
226					nuage.adduser(default_user)
227				else
228					nuage.adduser({name = u})
229				end
230			elseif type(u) == "table" then
231				-- ignore users without a username
232				if u.name == nil then
233					goto unext
234				end
235				local homedir = nuage.adduser(u)
236				if u.ssh_authorized_keys then
237					for _,v in ipairs(u.ssh_authorized_keys) do
238						nuage.addsshkey(homedir, v)
239					end
240				end
241			else
242				nuage.warn("nuageinit: invalid type : "..type(u).." for users entry number "..n);
243			end
244			::unext::
245		end
246	else
247	-- default user if none are defined
248		nuage.adduser(default_user)
249	end
250	if obj.ssh_authorized_keys then
251		local homedir = nuage.adduser(default_user)
252		for _,k in ipairs(obj.ssh_authorized_keys) do
253			nuage.addsshkey(homedir, k)
254		end
255	end
256	if obj.network then
257		local ifaces = get_ifaces()
258		nuage.mkdir_p(root .. "/etc/rc.conf.d")
259		local network = open_config("network")
260		local routing = open_config("routing")
261		local ipv6={}
262		for _,v in pairs(obj.network.ethernets) do
263			if not v.match then goto next end
264			if not v.match.macaddress then goto next end
265			if not ifaces[v.match.macaddress] then
266				nuage.warn("nuageinit: not interface matching: "..v.match.macaddress)
267				goto next
268			end
269			local interface = ifaces[v.match.macaddress]
270			if v.dhcp4 then
271				network:write("ifconfig_"..interface.."=\"DHCP\"\n")
272			elseif v.addresses then
273				for _,a in pairs(v.addresses) do
274					if a:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)") then
275						network:write("ifconfig_"..interface.."=\"inet "..a.."\"\n")
276					else
277						network:write("ifconfig_"..interface.."_ipv6=\"inet6 "..a.."\"\n")
278						ipv6[#ipv6 +1] = interface
279					end
280				end
281			end
282			if v.gateway4 then
283				routing:write("defaultrouter=\""..v.gateway4.."\"\n")
284			end
285			if v.gateway6 then
286				routing:write("ipv6_defaultrouter=\""..v.gateway6.."\"\n")
287				routing:write("ipv6_route_"..interface.."=\""..v.gateway6)
288				routing:write(" -prefixlen 128 -interface "..interface.."\"\n")
289			end
290			::next::
291		end
292		if #ipv6 > 0 then
293			network:write("ipv6_network_interfaces=\"")
294			network:write(table.concat(ipv6, " ") .. "\"\n")
295			network:write("ipv6_default_interface=\""..ipv6[1].."\"\n")
296		end
297		network:close()
298		routing:close()
299	end
300else
301	local res,err = os.execute(path..'/user-data')
302	if not res then
303		nuage.err("nuageinit: error executing user-data script: ".. err)
304	end
305end
306