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