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