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