]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - libexec/nuageinit/nuageinit
nuageinit: add basic support for cloudinit.
[FreeBSD/FreeBSD.git] / libexec / nuageinit / nuageinit
1 #!/usr/libexec/flua
2
3 -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
4 --
5 -- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
6
7 local nuage = require("nuage")
8 local yaml = require("yaml")
9
10 if #arg ~= 2 then
11         nuage.err("Usage ".. arg[0] .." <cloud-init directory> [config-2|nocloud]")
12 end
13 local path = arg[1]
14 local citype = arg[2]
15 local ucl = require("ucl")
16
17 local 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
26 local root = os.getenv("NUAGE_FAKE_ROOTDIR")
27 if not root then
28         root = ""
29 end
30
31 local 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
38 end
39
40 local 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
60 end
61
62 local 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()
157 end
158
159 if 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)
178 elseif 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
195 else
196         nuage.err("Unknown cloud init type: ".. citype)
197 end
198
199 -- deal with user-data
200 local f = io.open(path..'/user-data', "r")
201 if not f then
202         os.exit(0)
203 end
204 local line = f:read('*l')
205 f:close()
206 if 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
307 else
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
312 end