diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index 9aaec1b87a6a..40e19f98de6c 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -1,749 +1,775 @@ #!/usr/libexec/flua --- -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD -- -- Copyright(c) 2022-2025 Baptiste Daroussin -- Copyright(c) 2025 Jesús Daniel Colmenares Oviedo local nuage = require("nuage") local lfs = require("lfs") local ucl = require("ucl") local yaml = require("lyaml") if #arg ~= 2 then nuage.err("Usage: " .. arg[0] .. " ( | )", false) end local ni_path = arg[1] local citype = arg[2] -local default_user = { - name = "freebsd", - homedir = "/home/freebsd", - groups = "wheel", - gecos = "FreeBSD User", - shell = "/bin/sh", - plain_text_passwd = "freebsd" -} +local function default_user(obj, metadata) + local ssh_authorized_keys = {} + if type(metadata.public_keys) == "table" then + for _, k in pairs(metadata.public_keys) do + table.insert(ssh_authorized_keys, k) + end + end + if type(obj.ssh_authorized_keys) == "table" then + for _, k in ipairs(obj.ssh_authorized_keys) do + table.insert(ssh_authorized_keys, k) + end + end + return { + name = "freebsd", + homedir = "/home/freebsd", + groups = "wheel", + gecos = "FreeBSD User", + shell = "/bin/sh", + plain_text_passwd = "freebsd", + ssh_authorized_keys = ssh_authorized_keys + } +end local root = os.getenv("NUAGE_FAKE_ROOTDIR") if not root then root = "" end local function openat(dir, name) local path_dir = root .. dir local path_name = path_dir .. "/" .. name nuage.mkdir_p(path_dir) local f, err = io.open(path_name, "w") if not f then nuage.err("unable to open " .. path_name .. ": " .. err) end return f, path_name end local function open_ssh_key(name) return openat("/etc/ssh", name) end local function open_config(name) return openat("/etc/rc.conf.d", name) end local function open_resolv_conf() return openat("/etc", "resolv.conf") end local function open_resolvconf_conf() return openat("/etc", "resolvconf.conf") end local function get_ifaces_by_mac() local parser = ucl.parser() -- grab ifaces local ns = io.popen("netstat -i --libxo json") local netres = ns:read("*a") ns:close() local res, err = parser:parse_string(netres) if not res then nuage.warn("Error parsing netstat -i --libxo json output: " .. err) return nil end local ifaces = parser:get_object() local myifaces = {} for _, iface in pairs(ifaces["statistics"]["interface"]) do if iface["network"]:match("") then local s = iface["address"] myifaces[s:lower()] = iface["name"] end end return myifaces end -local function sethostname(obj) +local function sethostname(obj, metadata) -- always prefer fqdn if specified over hostname if obj.fqdn then nuage.sethostname(obj.fqdn) elseif obj.hostname then nuage.sethostname(obj.hostname) + elseif metadata["local-hostname"] then + nuage.sethostname(metadata["local-hostname"]) + elseif metadata["local"] then + nuage.sethostname(metadata["hostname"]) end end local function settimezone(obj) nuage.settimezone(obj.timezone) end local function groups(obj) if obj.groups == nil then return end for n, g in pairs(obj.groups) do if (type(g) == "string") then local r = nuage.addgroup({name = g}) if not r then nuage.warn("failed to add group: " .. g) end elseif type(g) == "table" then for k, v in pairs(g) do nuage.addgroup({name = k, members = v}) end else nuage.warn("invalid type: " .. type(g) .. " for users entry number " .. n) end end end -local function create_default_user(obj) - if not obj.users then - -- default user if none are defined - nuage.adduser(default_user) +local function create_default_user(obj, metadata) + local function need_default_user() + if not obj.users then + -- default user if "users" is undefined + return true + end + -- create default user if "default" is in the users list + for _, u in pairs(obj.users) do + if type(u) == "string" and u == "default" then + return true + end + end + return false + end + + if need_default_user() then + local du = default_user(obj, metadata) + local homedir = nuage.adduser(du) + if du.ssh_authorized_keys then + for _, k in ipairs(du.ssh_authorized_keys) do + nuage.addsshkey(homedir, k) + end + end end end -local function users(obj) +local function users(obj, metadata) if obj.users == nil then return end for n, u in pairs(obj.users) do if type(u) == "string" then if u == "default" then - nuage.adduser(default_user) + -- already done during create_default_user + nuage.adduser(default_user(obj, metadata)) else nuage.adduser({name = u}) end elseif type(u) == "table" then -- ignore users without a username if u.name == nil then goto unext end local homedir = nuage.adduser(u) if u.ssh_authorized_keys then for _, v in ipairs(u.ssh_authorized_keys) do nuage.addsshkey(homedir, v) end end if u.sudo then nuage.addsudo(u) end if u.doas then nuage.adddoas(u) end else nuage.warn("invalid type : " .. type(u) .. " for users entry number " .. n) end ::unext:: end end local function ssh_keys(obj) if obj.ssh_keys == nil then return end if type(obj.ssh_keys) ~= "table" then nuage.warn("Invalid type for ssh_keys") return end for key, val in pairs(obj.ssh_keys) do for keyname, keytype in key:gmatch("(%w+)_(%w+)") do local sshkn = nil if keytype == "public" then sshkn = "ssh_host_" .. keyname .. "_key.pub" elseif keytype == "private" then sshkn = "ssh_host_" .. keyname .. "_key" end if sshkn then local sshkey, path = open_ssh_key(sshkn) if sshkey then sshkey:write(val .. "\n") sshkey:close() end if keytype == "private" then nuage.chmod(path, "0600") end end end end end -local function ssh_authorized_keys(obj) - if obj.ssh_authorized_keys == nil then return end - local homedir = nuage.adduser(default_user) - for _, k in ipairs(obj.ssh_authorized_keys) do - nuage.addsshkey(homedir, k) - end -end - local function nameservers(interface, obj) local resolvconf_conf_handler = open_resolvconf_conf() if obj.search then local with_space = false resolvconf_conf_handler:write('search_domains="') for _, d in ipairs(obj.search) do if with_space then resolvconf_conf_handler:write(" " .. d) else resolvconf_conf_handler:write(d) with_space = true end end resolvconf_conf_handler:write('"\n') end if obj.addresses then local with_space = false resolvconf_conf_handler:write('name_servers="') for _, a in ipairs(obj.addresses) do if with_space then resolvconf_conf_handler:write(" " .. a) else resolvconf_conf_handler:write(a) with_space = true end end resolvconf_conf_handler:write('"\n') end resolvconf_conf_handler:close() local resolv_conf = root .. "/etc/resolv.conf" local resolv_conf_attr = lfs.attributes(resolv_conf) if resolv_conf_attr == nil then local resolv_conf_handler = open_resolv_conf() resolv_conf_handler:close() end -- Only call resolvconf with interface if interface is provided if interface then resolvconf_command = "resolvconf -a " .. interface .. " < " .. resolv_conf else resolvconf_command = "resolvconf -u" end if not os.execute(resolvconf_command) then nuage.warn("Failed to execute resolvconf(8)") end end local function install_packages(packages) if not nuage.pkg_bootstrap() then nuage.warn("Failed to bootstrap pkg, skip installing packages") return end for n, p in pairs(packages) do if type(p) == "string" then if not nuage.install_package(p) then nuage.warn("Failed to install : " .. p) end else nuage.warn("Invalid type: " .. type(p) .. " for packages entry number " .. n) end end end local function list_ifaces() local proc = io.popen("ifconfig -l") local raw_ifaces = proc:read("*a") proc:close() local ifaces = {} for i in raw_ifaces:gmatch("[^%s]+") do table.insert(ifaces, i) end return ifaces end local function get_ifaces_by_driver() local proc = io.popen("ifconfig -D") local drivers = {} local last_interface = nil for line in proc:lines() do - local interface = line:match("^([%S]+): ") + local interface = line:match("^([%S]+): ") - if interface then + if interface then last_interface = interface - end + end - local driver = line:match("^[%s]+drivername: ([%S]+)$") + local driver = line:match("^[%s]+drivername: ([%S]+)$") - if driver then + if driver then drivers[driver] = last_interface - end + end end proc:close() return drivers end local function match_rules(rules) -- To comply with the cloud-init specification, all rules must match and a table -- with the matching interfaces must be returned. This changes the way we initially -- thought about our implementation, since at first we only needed one interface, -- but cloud-init performs actions on a group of matching interfaces. local interface local interfaces = {} if rules.macaddress then local ifaces = get_ifaces_by_mac() interface = ifaces[rules.macaddress] if not interface then nuage.warn("not interface matching by MAC address: " .. rules.macaddress) return end interfaces[interface] = 1 end if rules.name then local match = false for _, i in pairs(list_ifaces()) do if i:match(rules.name) then match = true interfaces[i] = 1 end end if not match then nuage.warn("not interface matching by name: " .. rules.name) return end end if rules.driver then local match = false local drivers = get_ifaces_by_driver() for d in pairs(drivers) do if d:match(rules.driver) then match = true interface = drivers[d] interfaces[interface] = 1 end end if not match then nuage.warn("not interface matching by driver: " .. rules.driver) return end end return interfaces end local function write_files(files, defer) if not files then return end for n, file in pairs(files) do local r, errstr = nuage.addfile(file, defer) if not r then nuage.warn("Skipping write_files entry number " .. n .. ": " .. errstr) end end end local function write_files_not_deferred(obj) write_files(obj.write_files, false) end local function write_files_deferred(obj) write_files(obj.write_files, true) end -- Set network configuration from user_data local function network_config(obj) if obj.network == nil then return end local network = open_config("network") local routing = open_config("routing") local ipv6 = {} local set_defaultrouter = true local set_defaultrouter6 = true local set_nameservers = true for i, v in pairs(obj.network.ethernets) do local interfaces = {} if v.match then interfaces = match_rules(v.match) if next(interfaces) == nil then goto next end else interfaces[i] = 1 end local extra_opts = "" if v.wakeonlan then extra_opts = extra_opts .. " wol" end if v.mtu then local mtu if type(v.mtu) == "number" then mtu = tostring(v.mtu) else mtu = v.mtu end if mtu:match("%d") then extra_opts = extra_opts .. " mtu " .. mtu else nuage.warn("MTU is not set because the specified value is invalid: " .. mtu) end end for interface in pairs(interfaces) do if v.match and v.match.macaddress and v["set-name"] then local ifaces = get_ifaces_by_mac() local matched = ifaces[v.match.macaddress] if matched and matched == interface then network:write("ifconfig_" .. interface .. '_name=' .. v["set-name"] .. '\n') interface = v["set-name"] end end if v.dhcp4 then network:write("ifconfig_" .. interface .. '="DHCP"' .. extra_opts .. '\n') elseif v.addresses then for _, a in pairs(v.addresses) do if a:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)") then network:write("ifconfig_" .. interface .. '="inet ' .. a .. extra_opts .. '"\n') else network:write("ifconfig_" .. interface .. '_ipv6="inet6 ' .. a .. extra_opts .. '"\n') ipv6[#ipv6 + 1] = interface end end if set_nameservers and v.nameservers then set_nameservers = false nameservers(interface, v.nameservers) end if set_defaultrouter and v.gateway4 then set_defaultrouter = false routing:write('defaultrouter="' .. v.gateway4 .. '"\n') end if v.gateway6 then if set_defaultrouter6 then set_defaultrouter6 = false routing:write('ipv6_defaultrouter="' .. v.gateway6 .. '"\n') end routing:write("ipv6_route_" .. interface .. '="' .. v.gateway6) routing:write(" -prefixlen 128 -interface " .. interface .. '"\n') end end end ::next:: end if #ipv6 > 0 then network:write('ipv6_network_interfaces="') network:write(table.concat(ipv6, " ") .. '"\n') network:write('ipv6_default_interface="' .. ipv6[1] .. '"\n') end network:close() routing:close() end local function ssh_pwauth(obj) if obj.ssh_pwauth == nil then return end local value = "no" if obj.ssh_pwauth then value = "yes" end nuage.update_sshd_config("PasswordAuthentication", value) end local function runcmd(obj) if obj.runcmd == nil then return end local f = nil for _, c in ipairs(obj.runcmd) do if f == nil then nuage.mkdir_p(root .. "/var/cache/nuageinit") f = assert(io.open(root .. "/var/cache/nuageinit/runcmds", "w")) f:write("#!/bin/sh\n") end f:write(c .. "\n") end if f ~= nil then f:close() nuage.chmod(root .. "/var/cache/nuageinit/runcmds", "0755") end end local function packages(obj) if obj.package_update then nuage.update_packages() end if obj.package_upgrade then nuage.upgrade_packages() end if obj.packages then install_packages(obj.packages) end end local function chpasswd(obj) if obj.chpasswd == nil then return end nuage.chpasswd(obj.chpasswd) end local function config2_network(p) local parser = ucl.parser() local f = io.open(p .. "/network_data.json") if not f then -- silently return no network configuration is provided return end f:close() local res, err = parser:parse_file(p .. "/network_data.json") if not res then nuage.warn("error parsing network_data.json: " .. err) return end local obj = parser:get_object() local ifaces = get_ifaces_by_mac() if not ifaces then nuage.warn("no network interfaces found") return end local mylinks = {} for _, v in pairs(obj["links"]) do local s = v["ethernet_mac_address"]:lower() mylinks[v["id"]] = ifaces[s] end local network = open_config("network") local routing = open_config("routing") local ipv6 = {} local ipv6_routes = {} local ipv4 = {} for _, v in pairs(obj["networks"]) do local interface = mylinks[v["link"]] if v["type"] == "ipv4_dhcp" then network:write("ifconfig_" .. interface .. '="DHCP"\n') end if v["type"] == "ipv4" then network:write( "ifconfig_" .. interface .. '="inet ' .. v["ip_address"] .. " netmask " .. v["netmask"] .. '"\n' ) if v["gateway"] then routing:write('defaultrouter="' .. v["gateway"] .. '"\n') end if v["routes"] then for i, r in ipairs(v["routes"]) do local rname = "cloudinit" .. i .. "_" .. interface if v["gateway"] and v["gateway"] == r["gateway"] then goto next end if r["network"] == "0.0.0.0" then routing:write('defaultrouter="' .. r["gateway"] .. '"\n') goto next end routing:write("route_" .. rname .. '="-net ' .. r["network"] .. " ") routing:write(r["gateway"] .. " " .. r["netmask"] .. '"\n') ipv4[#ipv4 + 1] = rname ::next:: end end end if v["type"] == "ipv6" then ipv6[#ipv6 + 1] = interface ipv6_routes[#ipv6_routes + 1] = interface network:write("ifconfig_" .. interface .. '_ipv6="inet6 ' .. v["ip_address"] .. '"\n') if v["gateway"] then routing:write('ipv6_defaultrouter="' .. v["gateway"] .. '"\n') routing:write("ipv6_route_" .. interface .. '="' .. v["gateway"]) routing:write(" -prefixlen 128 -interface " .. interface .. '"\n') end -- TODO compute the prefixlen for the routes --if v["routes"] then -- for i, r in ipairs(v["routes"]) do -- local rname = "cloudinit" .. i .. "_" .. mylinks[v["link"]] -- -- skip all the routes which are already covered by the default gateway, some provider -- -- still list plenty of them. -- if v["gateway"] == r["gateway"] then -- goto next -- end -- routing:write("ipv6_route_" .. rname .. '"\n') -- ipv6_routes[#ipv6_routes + 1] = rname -- ::next:: -- end --end end end -- Handle global nameservers from services section if obj["services"] then local dns_servers = {} for _, service in pairs(obj["services"]) do if service["type"] == "dns" then table.insert(dns_servers, service["address"]) end end if #dns_servers > 0 then -- Use nameservers() function for global services local nameserver_config = {addresses = dns_servers} nameservers(nil, nameserver_config) end end if #ipv4 > 0 then routing:write('static_routes="') routing:write(table.concat(ipv4, " ") .. '"\n') end if #ipv6 > 0 then network:write('ipv6_network_interfaces="') network:write(table.concat(ipv6, " ") .. '"\n') network:write('ipv6_default_interface="' .. ipv6[1] .. '"\n') end if #ipv6_routes > 0 then routing:write('ipv6_static_routes="') routing:write(table.concat(ipv6, " ") .. '"\n') end network:close() routing:close() end local function parse_network_config() local nc_file = ni_path .. "/network-config" local nc_file_attr = lfs.attributes(nc_file) if nc_file_attr == nil then return end local f, err = io.open(nc_file) if err then nuage.err("error parsing nocloud network-config: " .. err) end local obj = yaml.load(f:read("*a")) f:close() if not obj then nuage.err("error parsing nocloud network-config") end local netobj = {} netobj["network"] = obj return netobj end -if citype == "config-2" then - local parser = ucl.parser() - local res, err = parser:parse_file(ni_path .. "/meta_data.json") +local function load_metadata() + if citype == "config-2" then + local parser = ucl.parser() + local res, err = parser:parse_file(ni_path .. "/meta_data.json") - if not res then - nuage.err("error parsing config-2 meta_data.json: " .. err) - end - local obj = parser:get_object() - if obj.public_keys then - local homedir = nuage.adduser(default_user) - for _,v in pairs(obj.public_keys) do - nuage.addsshkey(homedir, v) + if not res then + nuage.err("error parsing config-2 meta_data.json: " .. err) + end + local obj = parser:get_object() + + return obj + elseif citype == "nocloud" then + local f, err = io.open(ni_path .. "/meta-data") + if err then + nuage.err("error parsing nocloud meta-data: " .. err) end + local obj = yaml.load(f:read("*a")) + f:close() + if not obj then + nuage.err("error parsing nocloud meta-data") + end + return obj + elseif citype ~= "postnet" then + nuage.err("Unknown cloud init type: " .. citype) end - nuage.sethostname(obj["hostname"]) + return {} +end + +if citype == "config-2" then -- network config2_network(ni_path) -elseif citype == "nocloud" then - local f, err = io.open(ni_path .. "/meta-data") - if err then - nuage.err("error parsing nocloud meta-data: " .. err) - end - local obj = yaml.load(f:read("*a")) - f:close() - if not obj then - nuage.err("error parsing nocloud meta-data") - end - local hostname = obj["local-hostname"] - if not hostname then - hostname = obj["hostname"] - end - if hostname then - nuage.sethostname(hostname) - end -elseif citype ~= "postnet" then - nuage.err("Unknown cloud init type: " .. citype) end +local metadata = load_metadata() + -- deal with user-data local ud = nil local f = nil local userdatas = {"user-data", "user_data"} for _, v in pairs(userdatas) do f = io.open(ni_path .. "/" .. v, "r") if f then ud = v break end end if not f then os.exit(0) end local line = f:read("*l") if not line or #string.gsub(line, "^%s*(.-)%s*$", "%1") == 0 then f:close() os.exit(0) end if citype ~= "postnet" then local content = f:read("*a") if not content or #string.gsub(content, "^%s*(.-)%s*$", "%1") == 0 then f:close() os.exit(0) end nuage.mkdir_p(root .. "/var/cache/nuageinit") local tof = assert(io.open(root .. "/var/cache/nuageinit/user_data", "w")) tof:write(line .. "\n" .. content) tof:close() end f:close() if line == "#cloud-config" then local pre_network_calls = { sethostname, settimezone, groups, create_default_user, ssh_keys, - ssh_authorized_keys, network_config, ssh_pwauth, runcmd, write_files_not_deferred, } local post_network_calls = { packages, users, chpasswd, write_files_deferred, } f = io.open(ni_path .. "/" .. ud) local obj = yaml.load(f:read("*a")) f:close() if not obj then nuage.err("error parsing cloud-config file: " .. ud) end local calls_table = pre_network_calls if citype == "postnet" then calls_table = post_network_calls end for i = 1, #calls_table do if citype == "nocloud" and calls_table[i] == network_config then local netobj = parse_network_config() or obj network_config(netobj) else - calls_table[i](obj) + calls_table[i](obj, metadata) end end elseif line:sub(1, 2) == "#!" then -- delay for execution at rc.local time -- nuage.chmod(root .. "/var/cache/nuageinit/user_data", "0755") end