diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index 536739c8a0bf..31daff5c71b9 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -1,1233 +1,1235 @@ #!/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 function default_user(obj, metadata) local ssh_authorized_keys = {} - if type(metadata.public_keys) == "table" then + if (not obj or obj.allow_public_ssh_keys ~= false) and + type(metadata.public_keys) == "table" then for _, k in pairs(metadata.public_keys) do table.insert(ssh_authorized_keys, k) end end if obj and 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() local path_dir = root .. "/etc" local path_name = path_dir .. "/resolvconf.conf" nuage.mkdir_p(path_dir) local f, err = io.open(path_name, "a") if not f then nuage.err("unable to open " .. path_name .. ": " .. err) end return f, path_name 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 resolve_hostname(obj, metadata) if obj and obj.fqdn then return obj.fqdn elseif obj and obj.hostname then return obj.hostname elseif metadata["local-hostname"] then return metadata["local-hostname"] elseif metadata["hostname"] then return metadata["hostname"] end return nil end local function sethostname(obj, metadata) local hostname = resolve_hostname(obj, metadata) if hostname then nuage.sethostname(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, metadata) local function need_default_user() -- no user data if not obj then return true end 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, 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 -- 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 nameservers(interface, obj) local resolvconf_conf_handler = open_resolvconf_conf() if obj.search then resolvconf_conf_handler:write("search_domains=" .. nuage.shell_escape(table.concat(obj.search, " ")) .. "\n") end if obj.addresses then resolvconf_conf_handler:write("name_servers=" .. nuage.shell_escape(table.concat(obj.addresses, " ")) .. "\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 local resolvconf_command if interface then resolvconf_command = "resolvconf -a " .. nuage.shell_escape(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]+): ") if interface then last_interface = interface end local driver = line:match("^[%s]+drivername: ([%S]+)$") if driver then drivers[driver] = last_interface 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 if not v["set-name"]:match("^[a-zA-Z0-9]+$") then nuage.warn("set-name contains invalid characters, ignoring: " .. v["set-name"]) else network:write("ifconfig_" .. interface .. "_name=" .. nuage.shell_escape(v["set-name"]) .. "\n") interface = v["set-name"] end end end if v.dhcp4 then network:write("ifconfig_" .. interface .. "=" .. nuage.shell_escape("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 .. "=" .. nuage.shell_escape("inet " .. a .. extra_opts) .. "\n") else network:write("ifconfig_" .. interface .. "_ipv6=" .. nuage.shell_escape("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=" .. nuage.shell_escape(v.gateway4) .. "\n") end if v.gateway6 then if set_defaultrouter6 then set_defaultrouter6 = false routing:write("ipv6_defaultrouter=" .. nuage.shell_escape(v.gateway6) .. "\n") end routing:write("ipv6_route_" .. interface .. "=" .. nuage.shell_escape(v.gateway6 .. " -prefixlen 128 -interface " .. interface) .. "\n") end end end ::next:: end if #ipv6 > 0 then network:write("ipv6_network_interfaces=" .. nuage.shell_escape(table.concat(ipv6, " ")) .. "\n") network:write("ipv6_default_interface=" .. nuage.shell_escape(ipv6[1]) .. "\n") end network:close() routing:close() end local function ssh_pwauth(obj) if obj.ssh_pwauth == nil then return end local value if type(obj.ssh_pwauth) == "boolean" then value = obj.ssh_pwauth and "yes" or "no" elseif type(obj.ssh_pwauth) == "string" then local s = obj.ssh_pwauth:lower() if s == "yes" or s == "true" or s == "1" or s == "on" then value = "yes" elseif s == "no" or s == "false" or s == "0" or s == "off" then value = "no" elseif s == "unchanged" then return else nuage.warn("ssh_pwauth: unrecognized value '" .. obj.ssh_pwauth .. "', leaving unchanged") return end else nuage.warn("ssh_pwauth: invalid type " .. type(obj.ssh_pwauth)) return end nuage.update_sshd_config("PasswordAuthentication", value) end local function ssh_authkey_fingerprints(obj) if obj.ssh_authkey_fingerprints ~= true then return end local ssh_dir = root .. "/etc/ssh" local attrs = lfs.attributes(ssh_dir) if not attrs or attrs.mode ~= "directory" then return end for entry in lfs.dir(ssh_dir) do if entry:match("^ssh_host_.*_key$") then local keypath = ssh_dir .. "/" .. entry local f = io.popen("ssh-keygen -l -f " .. nuage.shell_escape(keypath), "r") if f then local fingerprint = f:read("*a") f:close() if fingerprint and fingerprint ~= "" then io.stderr:write(fingerprint) end end end end end local function ntp(obj) if obj.ntp == nil then return end if type(obj.ntp) == "table" and obj.ntp.enabled == false then return end local servers = {} if type(obj.ntp) == "table" then if obj.ntp[1] then servers = obj.ntp else for _, s in ipairs(obj.ntp.servers or {}) do table.insert(servers, "server " .. s .. " iburst") end for _, p in ipairs(obj.ntp.pools or {}) do table.insert(servers, "pool " .. p .. " iburst") end end elseif type(obj.ntp) == "string" then table.insert(servers, "server " .. obj.ntp .. " iburst") end if #servers == 0 then return end local path = root .. "/etc/ntp.conf" local f = io.open(path, "w") if not f then warnmsg("unable to open " .. path .. " for writing") return end for _, line in ipairs(servers) do f:write(line .. "\n") end f:write("leapfile /var/db/ntpd.leap-seconds.list\n") f:write("restrict default limited kod nomodify notrap nopeer noquery\n") f:write("restrict source limited kod nomodify notrap noquery\n") f:close() end local function ca_certs(obj) if obj.ca_certs == nil then return end local trusted = obj.ca_certs.trusted if trusted == nil or #trusted == 0 then return end local certdir = root .. "/usr/share/certs/trusted" nuage.mkdir_p(certdir) for i, cert in ipairs(trusted) do local certpath = certdir .. "/nuageinit-" .. i .. ".pem" local f = io.open(certpath, "w") if f then f:write(cert .. "\n") f:close() else warnmsg("unable to write " .. certpath) end end os.execute("certctl rehash 2>/dev/null") end local function ssh_deletekeys(obj) if obj.ssh_deletekeys == nil then return end if obj.ssh_deletekeys then nuage.delete_ssh_host_keys(root) end end local DISABLE_USER_OPTS = 'no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command="echo \'Please login as the user \\"$USER\\" rather than the user \\"$DISABLE_USER\\".\';echo;sleep 10;exit 142"' local function disable_root(obj, metadata) -- cloud-init semantics: restrict root's authorized_keys options local disable = true if obj.disable_root ~= nil then disable = obj.disable_root end local opts = "" if disable then opts = obj.disable_root_opts or DISABLE_USER_OPTS if type(opts) == "table" then opts = opts[1] end opts = opts:gsub("%$USER", "freebsd") opts = opts:gsub("%$DISABLE_USER", "root") end local keys = {} - if type(metadata.public_keys) == "table" then + if obj.allow_public_ssh_keys ~= false and + type(metadata.public_keys) == "table" then for _, k in pairs(metadata.public_keys) do table.insert(keys, k) end end if obj and type(obj.ssh_authorized_keys) == "table" then for _, k in ipairs(obj.ssh_authorized_keys) do table.insert(keys, k) end end for _, k in ipairs(keys) do nuage.addsshkey("/root", k, opts) end end local function resolv_conf(obj) if obj.resolv_conf == nil then return end nuage.write_resolv_conf(root, obj.resolv_conf) end local function keyboard(obj) if obj.keyboard == nil then return end local layout = obj.keyboard.layout local variant = obj.keyboard.variant if layout == nil then warnmsg("keyboard: missing layout") return end local keymap = layout if variant then keymap = layout .. "." .. variant end local path = root .. "/etc/rc.conf.d/keymap" nuage.mkdir_p(root .. "/etc/rc.conf.d") local f = io.open(path, "w") if not f then warnmsg("unable to open " .. path .. " for writing") return end f:write("keymap=" .. nuage.shell_escape(keymap) .. "\n") f:close() end local function locale(obj) if obj.locale == nil then return end local root = os.getenv("NUAGE_FAKE_ROOTDIR") if not root then root = "" end local profile_path = root .. "/etc/profile" local f = io.open(profile_path, "a") if not f then nuage.warn("unable to open " .. profile_path .. " for writing") return end if type(obj.locale) == "string" then f:write("export LANG=" .. nuage.shell_escape(obj.locale) .. "\n") elseif type(obj.locale) == "table" then for k, v in pairs(obj.locale) do if not k:match("^[a-zA-Z_][a-zA-Z0-9_]*$") then nuage.warn("locale: invalid variable name '" .. k .. "', skipping") else f:write("export " .. k .. "=" .. nuage.shell_escape(v) .. "\n") end end else nuage.warn("locale: invalid type " .. type(obj.locale) .. ", expecting string or object") end f:close() end local function mounts(obj) if obj.mounts == nil then return end for _, m in ipairs(obj.mounts) do local device, mount_point, fstype, options, dump_freq, passno if type(m) == "table" then if m[1] then -- List format: [device, mount_point, fstype, options, dump_freq, passno] device = m[1] mount_point = m[2] fstype = m[3] options = m[4] dump_freq = tonumber(m[5]) or 0 passno = tonumber(m[6]) or 0 else -- Dict format device = m.name or m.device or m.spec mount_point = m.mount_point or m.mountpoint fstype = m.type or m.filesystem or m.fstype options = m.options or m.opts dump_freq = tonumber(m.dump) or 0 passno = tonumber(m.passno) or tonumber(m.pass_no) or 0 end end if device and mount_point and fstype then nuage.mkdir_p(root .. mount_point) nuage.add_fstab_entry(root, device, mount_point, fstype, options, dump_freq, passno) else warnmsg("Invalid mount entry, skipping") end end end local function bootcmd(obj) if obj.bootcmd == nil then return end local f = nil for _, c in ipairs(obj.bootcmd) do if f == nil then nuage.mkdir_p(root .. "/var/cache/nuageinit") f = assert(io.open(root .. "/var/cache/nuageinit/bootcmds", "w")) f:write("#!/bin/sh\n") end f:write(c .. "\n") end if f ~= nil then f:close() nuage.chmod(root .. "/var/cache/nuageinit/bootcmds", "0755") end end local function manage_etc_hosts(obj, metadata) if obj.manage_etc_hosts == false then return end local hostname = resolve_hostname(obj, metadata) if hostname then nuage.update_etc_hosts(root, hostname) end 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 power_state_change(obj) if obj.power_state == nil then return end local ps = obj.power_state local delay = ps.delay or "now" local mode = ps.mode or "poweroff" local message = ps.message local condition = ps.condition if condition == nil then condition = true end -- Evaluate condition if condition == false then return end if type(condition) == "string" then local ret = os.execute(condition) if not ret then return end end -- Map mode to shutdown flag local mode_map = {poweroff = "p", reboot = "r", halt = "h"} local flag = mode_map[mode] if not flag then nuage.warn("power_state: invalid mode '" .. mode .. "', using poweroff") flag = "p" end -- Build shutdown command local cmd = "shutdown -" .. flag .. " " .. nuage.shell_escape(delay) if message then cmd = cmd .. " " .. nuage.shell_escape(message) end if os.getenv("NUAGE_RUN_TESTS") then print(cmd) return end os.execute(cmd) end local function get_uptime() -- Get system uptime in seconds local f = io.popen("sysctl -n kern.boottime 2>/dev/null") local boottime = f:read("*a") f:close() local sec = boottime:match("sec = (%d+)") if sec then return os.time() - tonumber(sec) end return 0 end local function final_message(obj) if obj.final_message == nil then return end local msg = obj.final_message if type(msg) ~= "string" then nuage.warn("final_message must be a string") return end local uptime = tostring(math.floor(get_uptime())) msg = msg:gsub("%$UPTIME", uptime) nuage.warn(msg, false) end local function phone_home(obj, metadata) if obj.phone_home == nil then return end local ph = obj.phone_home if type(ph) ~= "table" then nuage.warn("phone_home must be an object") return end if not ph.url then nuage.warn("phone_home.url is required") return end local url = ph.url local tries = ph.tries or 1 if type(tries) ~= "number" or tries < 1 then tries = 1 end -- Collect data to post local data = {} local post = ph.post if post == "all" then post = {"pub_key_rsa", "pub_key_ecdsa", "pub_key_ed25519", "instance_id", "hostname", "fqdn"} end if type(post) == "table" then for _, key in ipairs(post) do if key == "hostname" then if metadata.hostname then table.insert(data, "hostname=" .. metadata.hostname) end elseif key == "fqdn" then if metadata.hostname then table.insert(data, "fqdn=" .. metadata.hostname) end elseif key == "instance_id" then if metadata.uuid then table.insert(data, "instance_id=" .. metadata.uuid) end elseif key:match("^pub_key_") then local algo = key:match("^pub_key_(.+)$") if metadata.public_keys then for _, k in ipairs(metadata.public_keys) do if algo == "rsa" and k:match("^ssh%-rsa ") then table.insert(data, key .. "=" .. nuage.encode_base64(k)) elseif algo == "ecdsa" and k:match("^ecdsa%-") then table.insert(data, key .. "=" .. nuage.encode_base64(k)) elseif algo == "ed25519" and k:match("^ssh%-ed25519 ") then table.insert(data, key .. "=" .. nuage.encode_base64(k)) end end end end end end local post_data = table.concat(data, "&") local cmd = "fetch -q -o /dev/null --post-data " .. nuage.shell_escape(post_data) cmd = cmd .. " " .. nuage.shell_escape(url) if os.getenv("NUAGE_RUN_TESTS") then print(cmd) return end for i = 1, tries do if os.execute(cmd) then break end if i < tries then -- wait 1 second before retrying os.execute("sleep 1") end 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 .. "=" .. nuage.shell_escape("DHCP") .. "\n") end if v["type"] == "ipv4" then network:write( "ifconfig_" .. interface .. "=" .. nuage.shell_escape("inet " .. v["ip_address"] .. " netmask " .. v["netmask"]) .. "\n" ) if v["gateway"] then routing:write("defaultrouter=" .. nuage.shell_escape(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=" .. nuage.shell_escape(r["gateway"]) .. "\n") goto next end routing:write("route_" .. rname .. "=" .. nuage.shell_escape("-net " .. r["network"] .. " " .. 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=" .. nuage.shell_escape("inet6 " .. v["ip_address"]) .. "\n") if v["gateway"] then routing:write("ipv6_defaultrouter=" .. nuage.shell_escape(v["gateway"]) .. "\n") routing:write("ipv6_route_" .. interface .. "=" .. nuage.shell_escape(v["gateway"] .. " -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=" .. nuage.shell_escape(table.concat(ipv4, " ")) .. "\n") end if #ipv6 > 0 then network:write("ipv6_network_interfaces=" .. nuage.shell_escape(table.concat(ipv6, " ")) .. "\n") network:write("ipv6_default_interface=" .. nuage.shell_escape(ipv6[1]) .. "\n") end if #ipv6_routes > 0 then routing:write("ipv6_static_routes=" .. nuage.shell_escape(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 local function load_metadata(citype) 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() 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 -- reload metadata: try config-2 format first, then nocloud local parser = ucl.parser() local res, err = parser:parse_file(ni_path .. "/meta_data.json") if res then return parser:get_object() end local f = io.open(ni_path .. "/meta-data") if f then local obj = yaml.load(f:read("*a")) f:close() if obj then return obj end end return {} else nuage.err("Unknown cloud init type: " .. citype) end end local function load_userdata() 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 return nil, nil end local line = f:read("*l") if not line or #string.gsub(line, "^%s*(.-)%s*$", "%1") == 0 then f:close() return end if line:match("^Content%-Type: multipart/") then local rest = f:read("*a") f:close() local full = line .. "\n" .. rest local parts = nuage.parse_mime_multipart(full) if parts then local cc_body = nil for _, p in ipairs(parts) do if p.content_type == "text/cloud-config" then cc_body = p.body elseif p.content_type:match("x%-shellscript") or p.content_type:match("x%-sh") then if citype ~= "postnet" then nuage.mkdir_p(root .. "/var/cache/nuageinit") local spath = root .. "/var/cache/nuageinit/multipart_script" local sf = io.open(spath, "w") if sf then sf:write(p.body .. "\n") sf:close() nuage.chmod(spath, "0755") end end end end if cc_body then local obj = yaml.load(cc_body) if obj then if citype ~= "postnet" then nuage.mkdir_p(root .. "/var/cache/nuageinit") local tof = assert(io.open(root .. "/var/cache/nuageinit/user_data", "w")) tof:write("#cloud-config\n" .. cc_body) tof:close() end return "#cloud-config", obj end end end return nil, nil end if citype ~= "postnet" then local content = f:read("*a") if not content or #string.gsub(content, "^%s*(.-)%s*$", "%1") == 0 then f:close() return 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() local obj = nil if ud and line == "#cloud-config" then f = io.open(ni_path .. "/" .. ud) obj = yaml.load(f:read("*a")) f:close() if not obj then nuage.err("error parsing cloud-config file: " .. ud) end end return line, obj end -- Clean up stale resolvconf.conf from previous boot if citype ~= "postnet" then os.remove(root .. "/etc/resolvconf.conf") end if citype == "config-2" then -- network config2_network(ni_path) end local metadata = load_metadata(citype) local line, obj = load_userdata() -- No user-data if line == nil then local calls_table = { sethostname, create_default_user, } for i = 1, #calls_table do calls_table[i](obj, metadata) end -- YAML user-data elseif line == "#cloud-config" then local pre_network_calls = { ca_certs, mounts, bootcmd, sethostname, manage_etc_hosts, settimezone, groups, create_default_user, ssh_deletekeys, ssh_keys, ssh_authkey_fingerprints, network_config, resolv_conf, keyboard, locale, disable_root, ssh_pwauth, runcmd, write_files_not_deferred, } local post_network_calls = { ntp, packages, users, chpasswd, write_files_deferred, phone_home, final_message, power_state_change, } 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, 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 diff --git a/libexec/nuageinit/nuageinit.7 b/libexec/nuageinit/nuageinit.7 index 7e6c9154f799..6a72d322cca4 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -1,714 +1,724 @@ .\" SPDX-License-Identifier: BSD-2-Clause .\" .\" Copyright (c) 2025 Baptiste Daroussin .\" Copyright (c) 2025 Jesús Daniel Colmenares Oviedo .\" .Dd June 8, 2026 .Dt NUAGEINIT 7 .Os .Sh NAME .Nm nuageinit .Nd initialize a cloud-init environment .Sh DESCRIPTION The .Nm program is used to initialize instances in a cloud environment. .Nm runs at the first boot after the system installation. It is composed of three .Xr rc 8 scripts: .Bl -tag -width "nuageinit" .It Cm nuageinit This script detects the type of cloud environment and gathers the configuration data accordingly. The following cloud environments are supported right now: .Bl -tag -width "OpenStack" .It ondisk A cloud agnostic environment where the disk is provided to the system with the configuration data on it. The disk must be formatted using one of the following filesystems: .Xr cd9660 4 or .Xr msdosfs 4 and be labelled (via filesystem label) either .Ar config-2 or .Ar cidata . .It OpenStack The system is running in an .Lk https://www.openstack.org/ OpenStack environment . It is detected via the .Ar smbios.system.product .Xr smbios 4 description available in .Xr kenv 2 . .El .Pp Depending on the cloud environment above, .Nm will attempt to configure the instance. This script executes early after all the local filesystem are mounted but before the network is configured. .It Cm nuageinit_post_net This script is responsible for processing the configurations that are network dependent: .Bl -bullet .It dealing with packages .It dealing with users (which can depend on shell provided by packages) .El .It Cm nuageinit_user_data_script This script is responsible for executing everything which would have been passed via the configuration to be executed, via the configuration or because the user_data provided is a script. .El .Pp The default user for nuageinit is a user named .Va freebsd with a password set to .Va freebsd and a login shell set to .Va /bin/sh . .Sh CONFIGURATION The configuration of .Nm is typically provided as metadata by the cloud provider. The metadata is presented to nuageinit in different forms depending on the provider: .Bl -tag -width "config-2" .It nocloud If the data is provided via a disk labelled .Va cidata , then the metadata is provided in the form of a file named .Pa meta-data in YAML format. .Nm will configure the hostname of the instance according to the value of the following variables .Va local-hostname or .Va hostname . .It config-2 If the data is provided via a disk labelled .Va config-2 or if it is fetched from OpenStack, the metadata is expected in two json files: .Pp The .Pa meta_data.json file supports the following keys: .Bl -tag -width "public_keys" .It Ic hostname Set the hostname of the instance. .It Ic public_keys Append each entry of the array to .Nm default user which will be created. .El .Pp The .Pa network_data.json file supports the following keys: .Bl -tag -width "public_keys" .It Ic links Array of network interfaces to be configured. .It Ic networks Array of network configurations to be set. .It Ic services Array of service configurations to be set (e.g: DNS). .El .El .Pp Along with the metadata, a user data file is provided, either named .Pa user_data or .Pa user-data . If this file starts with a .Qq #! , it will be executed at the end of the boot via .Cm nuageinit_user_data_script . If this file starts with .Qq #cloud-config , it will be parsed as a YAML configuration file. All other cases will be ignored. .Pp The .Qq #cloud-config configuration entries supported by .Nm : .Bl -tag -width "config-2" .It Ic fqdn Specify a fully qualified domain name for the instance. .It Ic hostname Specify the hostname of the instance if .Qq Ic fqdn is not set. .It Ic manage_etc_hosts Boolean which determines whether the hostname should be added to .Pa /etc/hosts on the .Qq 127.0.0.1 and .Qq ::1 localhost lines. Defaults to .Ar true . Set to .Ar false to skip this behaviour. .It Ic mounts A list of mount points to configure. Each entry is written to .Pa /etc/fstab and the mount point directory is created. .Pp Each entry can be specified as a list: .Bd -literal -offset indent [ device, mountpoint, fstype ] .Ed .Pp or as an object: .Bd -literal -offset indent { device: "...", mountpoint: "...", type: "...", options: "..." } .Ed .Pp The following keys are recognized: .Bl -tag -width "options" .It device (or name, spec) The device to mount. .It mountpoint (or mount_point) The mount point directory. .It type (or fstype, filesystem) The filesystem type. .It options (or opts) The mount options, defaults to .Qq rw . .It dump The dump frequency for .Xr dump 8 , defaults to 0. .It passno The pass number for .Xr fsck 8 , defaults to 0. .El .It Ic resolv_conf An object configuring the content of .Pa /etc/resolv.conf . .Pp The following keys are recognized: .Bl -tag -width "nameservers" .It nameservers A list of IP addresses for DNS name servers. .It searchdomains A list of search domains. .It domain A single domain name. .It sortlist A list of IP/netmask sortlist entries. .It options A dictionary of resolver options. .El .It Ic locale Set the system locale by appending .Qq Cm export statements to .Pa /etc/profile . .Pp If the value is a string, it is used as the .Dq Cm LANG value. If the value is an object mapping, each key-value pair is exported. .It Ic keyboard An object configuring the keyboard layout. .Pp Sets the .Va keymap variable in .Pa /etc/rc.conf.d/keymap . .Pp The following keys are recognized: .Bl -tag -width "variant" .It layout The keyboard layout (e.g., .Qq fr , .Qq de , .Qq us ) . .It variant The keyboard variant (e.g., .Qq acc , .Qq nodeadkeys ) . .El .It Ic ssh_authkey_fingerprints Boolean which determines whether fingerprints of SSH host keys should be logged to the console. Defaults to .Ar false . .It Ic ntp An object configuring the NTP daemon by writing .Pa /etc/ntp.conf . .Pp The following keys are recognized: .Bl -tag -width "enabled" .It servers A list of NTP server addresses. .It pools A list of NTP pool addresses. .It enabled Boolean, defaults to .Ar true . Set to .Ar false to skip NTP configuration. .El .Pp Alternatively, .Ic ntp can be a list of server addresses (legacy format). .It Ic ca_certs An object managing CA certificates. .Pp The following keys are recognized: .Bl -tag -width "remove_defaults" .It trusted A list of PEM-encoded CA certificates to add to the system trust store. Certificates are written to .Pa /usr/share/certs/trusted/ and .Xr certctl 8 rehash is executed. .It remove_defaults Boolean, if .Ar true , remove the default CA certificates. .El .It Ic timezone Sets the system timezone based on the value provided. .Pp See also .Xr tzfile 5 . .It Ic groups An array of strings or objects to be created: .Bl -bullet .It If the entry is a string, a group using this string as a name will be created. .It if the entry is an object, the .Qq Ar key will be used as the name of the group, the .Qq Ar value is expected to be a list of members (array), specified by name. .El .It Ic ssh_deletekeys Boolean which determines if the existing SSH host keys in .Pa /etc/ssh should be removed on first boot. New host keys will be generated automatically by .Xr sshd 8 . .It Ic ssh_keys An object of multiple key/values, .Qq Cm keys being in the form .Ar algo_private or .Ar algo_public , .Qq Cm values being the actual content of the files in .Pa /etc/ssh . .It Ic ssh_authorized_keys Append each entry of the array to .Nm default user which will be created. +.It Ic allow_public_ssh_keys +Boolean which determines if the public SSH keys from the datasource +metadata are imported to the default user and root. +If set to +.Ar false , +only the keys specified in +.Ic ssh_authorized_keys +are used. +Defaults to +.Ar true . .It Ic ssh_pwauth boolean which determines the value of the .Qq Ic PasswordAuthentication configuration in .Pa /etc/ssh/sshd_config .It Ic disable_root Boolean which determines if root login via SSH should be disabled. If set to .Ar true .Pq the default , the public SSH keys are written to .Pa /root/.ssh/authorized_keys with the options specified in .Ic disable_root_opts , which by default redirects the login to the default user. If set to .Ar false , the public SSH keys are written to .Pa /root/.ssh/authorized_keys without any restriction. .It Ic disable_root_opts String or array of options used to prefix the public SSH keys in .Pa /root/.ssh/authorized_keys when .Ic disable_root is set to .Ar true . The .Ar $USER and .Ar $DISABLE_USER placeholders are replaced with the default user name and .Ar root respectively. If not specified, defaults to .Ar no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command="echo 'Please login as the user "$USER" rather than the user "$DISABLE_USER".';echo;sleep 10;exit 142" . .Pp Only the first value is used when an array is provided. .It Ic network Network configuration parameters. .Pp Specifying the following parameters from a file named .Pa network-config takes precedence over their specification from the .Ic network parameter of .Pa user-data . .Bl -tag -width "ethernets" .It Ic ethernets Mapping representing a generic configuration for existing network interfaces. .Pp Each key is an interface name that is only used when no .Sy match rule is specified. If .Sy match rules are specified, an arbitrary name can be used .Po e.g.: id0 Pc . .Bl -tag -width "nameservers" .It Ic match This selects a subset of available physical devices by various hardware properties. The following configuration will then apply to all matching devices, as soon as they appear. All specified properties must match. The following properties for creating matches are supported: .Bl -tag -width "macaddress" .It Ic macaddress .No Device's MAC address in the form Sy xx:xx:xx:xx:xx:xx . Letters should be lowercase. .It Ic name Current interface name. Lua pattern-matching expressions are supported. .It Ic driver Interface driver name and unit number of the interface. Lua pattern-natching expressions are supported. .El .It Ic set-name When matching on unique properties such as MAC, match rules can be written so that they match only one device. Then this property can be used to give that device a more specific/desirable/nicer name than the default. .Pp While multiple properties can be used in a match, .Sy macaddress is required for nuageinit to perform the rename. .It Ic mtu The MTU key represents a device's Maximum Transmission Unit, the largest size packet or frame. .It Ic wakeonlan Enable wake on LAN. Off by default. .It Ic dhcp4 Configure the interface to use DHCP. .Pp This takes precedence over .Sy addresses when both are specified. .It Ic addresses List of strings representing IPv4 or IPv6 addresses. .It Ic gateway4 Set default gateway for IPv4, for manual address configuration. This requires setting .Sy addresses too. .Pp Since only one default router can be configured at a time, this parameter is applied when processing the first entry, and any others are silently ignored. .It Ic gateway6 Set default gateway for IPv6, for manual address configuration. This requires setting .Sy addresses too. .Pp Since only one default router can be configured at a time, this parameter is applied when processing the first entry, and any others are silently ignored. .It Ic nameservers Set DNS servers and search domains, for manual address configuration. .Pp There are two supported fields: .Bl -tag -width "addresses" .It Ic search Search list for host-name lookup. .It Ic addresses List of IPv4 or IPv6 name server addresses that the resolver should query. .El .El .El .It Ic runcmd An array of commands to be run at the end of the boot process .It Ic packages List of packages to be installed. .It Ic package_update Update the remote package metadata. .It Ic package_upgrade Upgrade the packages installed to their latest version. .It Ic power_state An object controlling the power state of the instance after configuration. The following keys are recognized: .Bl -tag -width "condition" .It Ic delay Time to wait before the action. Can be .Qq now or a time accepted by .Xr shutdown 8 (e.g., .Qq +5 for five minutes). Defaults to .Qq now . .It Ic mode The action to take: .Qq poweroff , .Qq reboot , or .Qq halt . Defaults to .Qq poweroff . .It Ic message Optional message to display to users. .It Ic timeout Not supported on .Fx , silently ignored. .It Ic condition Boolean or command string. If .Qq false , the action is skipped. If a string, it is run as a command; the action is only taken if the command succeeds. Defaults to .Qq true . .El .It Ic phone_home An object configuring the posting of instance data to a URL. .Pp The following keys are recognized: .Bl -tag -width "tries" .It Ic url The target URL to which the data will be posted. .It Ic post A list of data items to send, or the string .Qq all to send all supported items. Supported items are: .Qq hostname , .Qq fqdn , .Qq instance_id , .Qq pub_key_rsa , .Qq pub_key_ecdsa , and .Qq pub_key_ed25519 . .It Ic tries Number of retry attempts. Defaults to 1. .El .Pp The data is posted using .Xr fetch 1 . .It Ic final_message A string message to be displayed at the end of the boot process. The placeholder .Qq $UPTIME is replaced with the system uptime in seconds. .It Ic users Specify a list of users to be created: .Bl -tag -width "ssh_authorized_keys" .It Ic name Name of the user. .It Ic gecos GECOS for the user. .It Ic homedir The path of the home directory for the user. .It Ic primary_group The main group the user should belong to. .It Ic groups The list of other groups the user should belong to. .It Ic no_create_home A boolean which determines if the home directory should be created or not. .It Ic shell The shell that should be used for the user. .It Ic ssh_authorized_keys List of SSH keys for the user. .It Ic passwd The encrypted password for the user. .It Ic plain_text_passwd The password in plain text for the user. Ignored if an encrypted password is already provided. .It Ic lock_passwd Boolean to determine if the user account should be locked. The legacy .Ic locked key is also accepted. .It Ic sudo A string or an array of strings which should be appended to .Pa ${LOCALBASE}/etc/sudoers.d/90-nuageinit-users .It Ic doas A string or an array of strings which should be appended to .Pa ${LOCALBASE}/etc/doas.conf .Pp Instead of hardcoding the username, you can use .Sy %u , which will be replaced by the current username. .El .Pp A special case exist: if the entry is a simple string with the value .Qq default , then the default user is created. Configuring .Nm sudo or .Nm doas does not automatically install them. Ensure the relevant command is listed in your .Nm packages section. .It Ic bootcmd An array of commands to be run early in the boot process, before the hostname is set and before the network is configured. .It Ic chpasswd Change the passwords for users, it accepts the following keys: .Bl -tag -width "expire" .It Ic expire Boolean to force the user to change their password on first login. .It Ic users An array of objects: .Bl -tag -width "password" .It Ic name Specify the user whose password will be changed. .It Ic password Specify a text line with the new password or .Qq Cm RANDOM to assign the password randomly. If the textline starts with .Qq Cm $x$ where x is a number, then the password is considered encrypted, otherwise the password is considered plaintext. .El .El .It Ic write_files An array of objects representing files to be created at first boot. The files are being created before the installation of any packages and the creation of the users. The only mandatory field is: .Ic path . It accepts the following keys for each objects: .Bl -tag -width "permissions" .It Ic content The content to be written to the file. If this key is not existing then an empty file will be created. .It Ic encoding Specify the encoding used for content. If not specified, then plain text is considered. Only .Ar b64 and .Ar base64 are supported for now. .It Ic path The path of the file to be created. .Pq Note intermerdiary directories will be created . .It Ic permissions A string representing the permission of the file in octal. .It Ic owner A string representing the owner, two forms are possible: .Ar user or .Ar user:group . .It Ic append A boolean to specify the content should be appended to the file if the file exists. .It Ic defer A boolean to specify that the files should be created after the packages are installed and the users are created. .El .El .Pp Additionally, user-data can be provided as a MIME multipart message with content type .Qq multipart/mixed . Each part is handled according to its .Qq Content-Type header. Supported part types: .Bl -tag -width "text/x-shellscript" .It text/cloud-config Processed as a cloud-config YAML document. .It text/x-shellscript Saved as an executable script for later execution. .El .Sh EXAMPLES Here is an example of a YAML configuration for .Nm : .Bd -literal #cloud-config bootcmd: - kldload if_bridge fqdn: myhost.mynetwork.tld users: - default - name: user gecos: Foo B. Bar sudo: ALL=(ALL) NOPASSWD:ALL ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr... packages: - neovim - git-lite - sudo package_update: true package_upgrade: true runcmd: - logger -t nuageinit "boot finished" ssh_deletekeys: true disable_root: true ssh_keys: ed25519_private: | -----BEGIN OPENSSH PRIVATE KEY----- blabla ... -----END OPENSSH PRIVATE KEY----- ed25519_public: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK+MH4E8KO32N5CXRvXVqvyZVl0+6ue4DobdhU0FqFd+ network: ethernets: vtnet0: addresses: - 192.168.8.2/24 gateway4: 192.168.8.1 .Ed .Sh SEE ALSO .Xr kenv 2 , .Xr cd9660 4 , .Xr msdosfs 4 , .Xr smbios 4 , .Xr ssh_config 5 , .Xr rc 8 .Sh STANDARDS .Nm is believed to conform to the .Lk https://cloud-init.io/ Cloud Init specification. .Sh HISTORY .Nm appeared in .Fx 14.1 diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh index 4803ddf941ab..bfc7e9f13c88 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -1,1543 +1,1584 @@ #- # Copyright (c) 2022-2025 Baptiste Daroussin # Copyright (c) 2025 Jesús Daniel Colmenares Oviedo # # SPDX-License-Identifier: BSD-2-Clause # export NUAGE_FAKE_ROOTDIR="$PWD" atf_test_case args atf_test_case nocloud atf_test_case nocloud_userdata_script atf_test_case nocloud_user_data_script atf_test_case nocloud_userdata_cloudconfig_users atf_test_case nocloud_network atf_test_case config2 atf_test_case config2_pubkeys atf_test_case config2_pubkeys_user_data atf_test_case config2_pubkeys_meta_data atf_test_case config2_network atf_test_case config2_network_static_v4 atf_test_case config2_network_dns atf_test_case config2_ssh_keys atf_test_case nocloud_userdata_cloudconfig_ssh_pwauth atf_test_case nocloud_userdata_cloudconfig_chpasswd atf_test_case nocloud_userdata_cloudconfig_chpasswd_list_string atf_test_case nocloud_userdata_cloudconfig_chpasswd_list_list atf_test_case config2_userdata_runcmd atf_test_case config2_userdata_packages atf_test_case config2_userdata_update_packages atf_test_case config2_userdata_upgrade_packages atf_test_case config2_userdata_shebang atf_test_case config2_userdata_ssh_deletekeys atf_test_case config2_userdata_disable_root atf_test_case config2_userdata_bootcmd atf_test_case config2_userdata_manage_etc_hosts atf_test_case config2_userdata_mounts atf_test_case config2_userdata_resolv_conf atf_test_case config2_userdata_keyboard atf_test_case config2_userdata_ssh_authkey_fingerprints atf_test_case config2_userdata_ntp atf_test_case config2_userdata_ca_certs atf_test_case config2_userdata_multipart atf_test_case config2_userdata_power_state atf_test_case config2_userdata_locale atf_test_case config2_userdata_fqdn_and_hostname atf_test_case config2_userdata_write_files atf_test_case config2_userdata_encode_base64 atf_test_case config2_userdata_final_message atf_test_case config2_userdata_phone_home setup_test_adduser() { here=$(pwd) export NUAGE_FAKE_ROOTDIR=$(pwd) mkdir -p etc/ssh cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/csh sys:*:1:0::0:0:Sys:/home/sys:/bin/csh EOF pwd_mkdb -d etc ${here}/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF } args_body() { atf_check -s exit:1 -e inline:"Usage: /usr/libexec/nuageinit ( | )\n" /usr/libexec/nuageinit atf_check -s exit:1 -e inline:"Usage: /usr/libexec/nuageinit ( | )\n" /usr/libexec/nuageinit bla atf_check -s exit:1 -e inline:"Usage: /usr/libexec/nuageinit ( | )\n" /usr/libexec/nuageinit bla meh plop atf_check -s exit:1 -e inline:"nuageinit: Unknown cloud init type: meh\n" /usr/libexec/nuageinit bla meh } nocloud_body() { mkdir -p media/nuageinit setup_test_adduser atf_check -s exit:1 -e match:"nuageinit: error parsing nocloud.*" /usr/libexec/nuageinit "${PWD}"/media/nuageinit/ nocloud printf "instance-id: iid-local01\nlocal-hostname: cloudimg\n" > "${PWD}"/media/nuageinit/meta-data atf_check -s exit:0 /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:"hostname='cloudimg'\n" cat etc/rc.conf.d/hostname cat > media/nuageinit/meta-data << EOF instance-id: iid-local01 hostname: myhost EOF atf_check -s exit:0 /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:"hostname='myhost'\n" cat etc/rc.conf.d/hostname } nocloud_userdata_script_body() { mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data # ensure this is an invalid when parsed with the yaml parser printf "#!/bin/sh\n: ${test:-yes}\necho $test\n" > "${PWD}"/media/nuageinit/user-data chmod 644 "${PWD}"/media/nuageinit/user-data atf_check -s exit:0 /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check test -x var/cache/nuageinit/user_data atf_check -o inline:"#!/bin/sh\n: ${test:-yes}\necho $test\n" cat var/cache/nuageinit/user_data } nocloud_user_data_script_body() { mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data printf "#!/bin/sh\necho yeah\n" > "${PWD}"/media/nuageinit/user_data chmod 755 "${PWD}"/media/nuageinit/user_data atf_check -s exit:0 /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:"#!/bin/sh\necho yeah\n" cat var/cache/nuageinit/user_data } nocloud_userdata_cloudconfig_users_head() { atf_set "require.user" root } nocloud_userdata_cloudconfig_users_body() { mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data mkdir -p etc cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF cat > media/nuageinit/user-data << 'EOF' #cloud-config groups: - admingroup: [root,sys] - cloud-users users: - default - name: foobar gecos: Foo B. Bar primary_group: foobar sudo: ALL=(ALL) NOPASSWD:ALL doas: permit persist %u as root groups: users passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/ - name: bla sudo: - "ALL=(ALL) NOPASSWD:/usr/sbin/pw" - "ALL=(ALL) ALL" doas: - "deny %u as foobar" - "permit persist %u as root cmd whoami" EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet cat > expectedgroup << EOF wheel:*:0:root,freebsd users:*:1:foobar admingroup:*:1001:root,sys cloud-users:*:1002: freebsd:*:1003: foobar:*:1004: bla:*:1005: EOF cat > expectedpasswd << 'EOF' root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh freebsd:freebsd:1001:1003::0:0:FreeBSD User:/home/freebsd:/bin/sh foobar:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1002:1004::0:0:Foo B. Bar:/home/foobar:/bin/sh bla::1003:1005::0:0:bla User:/home/bla:/bin/sh EOF sed -i "" "s/freebsd:.*:1001/freebsd:freebsd:1001/" "${PWD}"/etc/master.passwd atf_check -o file:expectedpasswd cat "${PWD}"/etc/master.passwd atf_check -o file:expectedgroup cat "${PWD}"/etc/group localbase=`sysctl -ni user.localbase 2> /dev/null` if [ -z "${localbase}" ]; then # fallback localbase="/usr/local" fi atf_check -o inline:"foobar ALL=(ALL) NOPASSWD:ALL\nbla ALL=(ALL) NOPASSWD:/usr/sbin/pw\nbla ALL=(ALL) ALL\n" cat "${PWD}/${localbase}/etc/sudoers.d/90-nuageinit-users" atf_check -o inline:"permit persist foobar as root\ndeny bla as foobar\npermit persist bla as root cmd whoami\n" cat "${PWD}/${localbase}/etc/doas.conf" } nocloud_userdata_cloudconfig_users_lock_passwd_head() { atf_set "require.user" root } nocloud_userdata_cloudconfig_users_lock_passwd_body() { mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data mkdir -p etc cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF cat > media/nuageinit/user-data << 'EOF' #cloud-config users: - name: lockeduser lock_passwd: true EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o match:'lockeduser:\*LOCKED\*:' cat "${PWD}"/etc/master.passwd } nocloud_network_head() { atf_set "require.user" root } nocloud_network_body() { mkdir -p media/nuageinit mkdir -p etc cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF mynetworks=$(ifconfig -l ether) if [ -z "$mynetworks" ]; then atf_skip "a network interface is needed" fi set -- $mynetworks myiface=$1 myaddr=$(ifconfig $myiface ether | awk '/ether/ { print $2 }') printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data cat > media/nuageinit/user-data << EOF #cloud-config network: version: 2 ethernets: # opaque ID for physical interfaces, only referred to by other stanzas id0: match: macaddress: "$myaddr" addresses: - 192.0.2.2/24 - 2001:db8::2/64 gateway4: 192.0.2.1 gateway6: 2001:db8::1 EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud cat > network << EOF ifconfig_${myiface}='inet 192.0.2.2/24' ifconfig_${myiface}_ipv6='inet6 2001:db8::2/64' ipv6_network_interfaces='${myiface}' ipv6_default_interface='${myiface}' EOF cat > routing << EOF defaultrouter='192.0.2.1' ipv6_defaultrouter='2001:db8::1' ipv6_route_${myiface}='2001:db8::1 -prefixlen 128 -interface ${myiface}' EOF atf_check -o file:network cat "${PWD}"/etc/rc.conf.d/network atf_check -o file:routing cat "${PWD}"/etc/rc.conf.d/routing } config2_body() { mkdir -p media/nuageinit setup_test_adduser atf_check -s exit:1 -e match:"nuageinit: error parsing config-2 meta_data.json:.*" /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 printf "{}" > media/nuageinit/meta_data.json atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 cat > media/nuageinit/meta_data.json << EOF { "hostname": "cloudimg" } EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 atf_check -o inline:"hostname='cloudimg'\n" cat etc/rc.conf.d/hostname } config2_pubkeys_head() { atf_set "require.user" root } config2_pubkeys_body() { mkdir -p media/nuageinit touch media/nuageinit/meta_data.json cat > media/nuageinit/user-data << EOF #cloud-config ssh_authorized_keys: - "ssh-rsa AAAAB3NzaC1y...== Generated by Nova" EOF mkdir -p etc mkdir -p root cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 atf_check -o inline:"ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n" cat home/freebsd/.ssh/authorized_keys } config2_pubkeys_user_data_head() { atf_set "require.user" root } config2_pubkeys_user_data_body() { mkdir -p media/nuageinit touch media/nuageinit/meta_data.json cat > media/nuageinit/user_data << EOF #cloud-config ssh_authorized_keys: - "ssh-rsa AAAAB3NzaC1y...== Generated by Nova" EOF mkdir -p etc mkdir -p root cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 atf_check -o inline:"ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n" cat home/freebsd/.ssh/authorized_keys } config2_pubkeys_meta_data_body() { here=$(pwd) export NUAGE_FAKE_ROOTDIR=$(pwd) if [ $(id -u) -ne 0 ]; then atf_skip "root required" fi mkdir -p media/nuageinit cat > media/nuageinit/meta_data.json << EOF { "uuid": "uuid_for_this_instance", "admin_pass": "a_generated_password", "public_keys": { "tdb": "ssh-ed25519 my_key_id tdb@host" }, "keys": [ { "name": "tdb", "type": "ssh", "data": "ssh-ed25519 my_key_id tdb@host" } ], "hostname": "freebsd-14-test.novalocal", "name": "freebsd-14-test", "launch_index": 0, "availability_zone": "nova", "random_seed": "long_random_seed", "project_id": "my_project_id", "devices": [], "dedicated_cpus": [] } EOF mkdir -p etc mkdir -p root cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/csh sys:*:1:0::0:0:Sys:/home/sys:/bin/csh EOF pwd_mkdb -d etc ${here}/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF atf_check /usr/libexec/nuageinit ${here}/media/nuageinit config-2 atf_check -o inline:"ssh-ed25519 my_key_id tdb@host\n" cat home/freebsd/.ssh/authorized_keys } +config2_pubkeys_allow_public_ssh_keys_head() +{ + atf_set "require.user" root +} +config2_pubkeys_allow_public_ssh_keys_body() +{ + here=$(pwd) + export NUAGE_FAKE_ROOTDIR=$(pwd) + if [ $(id -u) -ne 0 ]; then + atf_skip "root required" + fi + mkdir -p media/nuageinit + cat > media/nuageinit/meta_data.json << EOF +{ + "uuid": "uuid_for_this_instance", + "public_keys": { + "tdb": "ssh-ed25519 my_key_id tdb@host" + } +} +EOF + cat > media/nuageinit/user_data << EOF +#cloud-config +allow_public_ssh_keys: false +EOF + mkdir -p etc + mkdir -p root + cat > etc/master.passwd << EOF +root:*:0:0::0:0:Charlie &:/root:/bin/csh +sys:*:1:0::0:0:Sys:/home/sys:/bin/csh +EOF + pwd_mkdb -d etc ${here}/etc/master.passwd + cat > etc/group << EOF +wheel:*:0:root +users:*:1: +EOF + atf_check /usr/libexec/nuageinit ${here}/media/nuageinit config-2 + # no public key should be imported + atf_check -s exit:1 test -e home/freebsd/.ssh/authorized_keys +} + config2_network_body() { mkdir -p media/nuageinit setup_test_adduser printf "{}" > media/nuageinit/meta_data.json mynetworks=$(ifconfig -l ether) if [ -z "$mynetworks" ]; then atf_skip "a network interface is needed" fi set -- $mynetworks myiface=$1 myaddr=$(ifconfig $myiface ether | awk '/ether/ { print $2 }') cat > media/nuageinit/network_data.json << EOF { "links": [ { "ethernet_mac_address": "$myaddr", "id": "iface0", "mtu": null } ], "networks": [ { "id": "network0", "link": "iface0", "type": "ipv4_dhcp" }, { // IPv6 "id": "private-ipv4", "type": "ipv6", "link": "iface0", // supports condensed IPv6 with CIDR netmask "ip_address": "2001:db8::3257:9652/64", "gateway": "fd00::1", "routes": [ { "network": "::", "netmask": "::", "gateway": "fd00::1" }, { "network": "::", "netmask": "ffff:ffff:ffff::", "gateway": "fd00::1:1" } ], "network_id": "da5bb487-5193-4a65-a3df-4a0055a8c0d8" } ] } EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 cat > network << EOF ifconfig_${myiface}='DHCP' ifconfig_${myiface}_ipv6='inet6 2001:db8::3257:9652/64' ipv6_network_interfaces='${myiface}' ipv6_default_interface='${myiface}' EOF cat > routing << EOF ipv6_defaultrouter='fd00::1' ipv6_route_${myiface}='fd00::1 -prefixlen 128 -interface ${myiface}' ipv6_static_routes='${myiface}' EOF atf_check -o file:network cat "${PWD}"/etc/rc.conf.d/network atf_check -o file:routing cat "${PWD}"/etc/rc.conf.d/routing } config2_network_static_v4_body() { mkdir -p media/nuageinit setup_test_adduser printf "{}" > media/nuageinit/meta_data.json mynetworks=$(ifconfig -l ether) if [ -z "$mynetworks" ]; then atf_skip "a network interface is needed" fi set -- $mynetworks myiface=$1 myaddr=$(ifconfig $myiface ether | awk '/ether/ { print $2 }') cat > media/nuageinit/network_data.json << EOF { "links": [ { "ethernet_mac_address": "$myaddr", "id": "iface0", "mtu": null } ], "networks": [ { "id": "network0", "link": "iface0", "type": "ipv4", "ip_address": "10.184.0.244", "netmask": "255.255.240.0", "routes": [ { "network": "10.0.0.0", "netmask": "255.0.0.0", "gateway": "11.0.0.1" }, { "network": "0.0.0.0", "netmask": "0.0.0.0", "gateway": "23.253.157.1" } ] } ] } EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 cat > network << EOF ifconfig_${myiface}='inet 10.184.0.244 netmask 255.255.240.0' EOF cat > routing << EOF route_cloudinit1_${myiface}='-net 10.0.0.0 11.0.0.1 255.0.0.0' defaultrouter='23.253.157.1' static_routes='cloudinit1_${myiface}' EOF atf_check -o file:network cat "${PWD}"/etc/rc.conf.d/network atf_check -o file:routing cat "${PWD}"/etc/rc.conf.d/routing } config2_network_dns_body() { mkdir -p media/nuageinit setup_test_adduser printf "{}" > media/nuageinit/meta_data.json mynetworks=$(ifconfig -l ether) if [ -z "$mynetworks" ]; then atf_skip "a network interface is needed" fi set -- $mynetworks myiface=$1 myaddr=$(ifconfig $myiface ether | awk '/ether/ { print $2 }') cat > media/nuageinit/network_data.json << EOF { "links": [ { "ethernet_mac_address": "$myaddr", "id": "iface0", "mtu": null } ], "networks": [ { "id": "network0", "link": "iface0", "type": "ipv4_dhcp" } ], "services": [ { "type": "dns", "address": "9.9.9.9" }, { "type": "dns", "address": "149.112.112.112" } ] } EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 atf_check -o inline:"name_servers='9.9.9.9 149.112.112.112'\n" \ cat "${PWD}"/etc/resolvconf.conf } config2_ssh_keys_head() { atf_set "require.user" root } config2_ssh_keys_body() { here=$(pwd) export NUAGE_FAKE_ROOTDIR=$(pwd) mkdir -p media/nuageinit touch media/nuageinit/meta_data.json cat > media/nuageinit/user-data << EOF #cloud-config ssh_keys: rsa_private: | -----BEGIN RSA PRIVATE KEY----- MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco ... -----END RSA PRIVATE KEY----- rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ... ed25519_private: | -----BEGIN OPENSSH PRIVATE KEY----- blabla ... -----END OPENSSH PRIVATE KEY----- ed25519_public: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK+MH4E8KO32N5CXRvXVqvyZVl0+6ue4DobdhU0FqFd+ EOF mkdir -p etc/ssh cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/csh sys:*:1:0::0:0:Sys:/home/sys:/bin/csh EOF pwd_mkdb -d etc ${here}/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 _expected="-----BEGIN RSA PRIVATE KEY----- MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco ... -----END RSA PRIVATE KEY----- " atf_check -o inline:"${_expected}" cat ${PWD}/etc/ssh/ssh_host_rsa_key _expected="ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ...\n" atf_check -o inline:"${_expected}" cat ${PWD}/etc/ssh/ssh_host_rsa_key.pub _expected="-----BEGIN OPENSSH PRIVATE KEY----- blabla ... -----END OPENSSH PRIVATE KEY----- " atf_check -o inline:"${_expected}" cat ${PWD}/etc/ssh/ssh_host_ed25519_key _expected="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK+MH4E8KO32N5CXRvXVqvyZVl0+6ue4DobdhU0FqFd+\n" atf_check -o inline:"${_expected}" cat ${PWD}/etc/ssh/ssh_host_ed25519_key.pub } nocloud_userdata_cloudconfig_ssh_pwauth_head() { atf_set "require.user" root } nocloud_userdata_cloudconfig_ssh_pwauth_body() { mkdir -p etc cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data cat > media/nuageinit/user-data << 'EOF' #cloud-config ssh_pwauth: true EOF mkdir -p etc/ssh/ touch etc/ssh/sshd_config atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:"PasswordAuthentication yes\n" cat etc/ssh/sshd_config # Same value we don't touch anything printf " PasswordAuthentication yes # I want password\n" > etc/ssh/sshd_config atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:" PasswordAuthentication yes # I want password\n" cat etc/ssh/sshd_config printf " PasswordAuthentication no # Should change\n" > etc/ssh/sshd_config atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:"PasswordAuthentication yes\n" cat etc/ssh/sshd_config cat > media/nuageinit/user-data << 'EOF' #cloud-config ssh_pwauth: false EOF printf " PasswordAuthentication no # no passwords\n" > etc/ssh/sshd_config atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:" PasswordAuthentication no # no passwords\n" cat etc/ssh/sshd_config printf " PasswordAuthentication yes # Should change\n" > etc/ssh/sshd_config atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:"PasswordAuthentication no\n" cat etc/ssh/sshd_config cat > media/nuageinit/user-data << 'EOF' #cloud-config ssh_pwauth: "no" EOF printf " PasswordAuthentication yes # Should change\n" > etc/ssh/sshd_config atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:"PasswordAuthentication no\n" cat etc/ssh/sshd_config cat > media/nuageinit/user-data << 'EOF' #cloud-config ssh_pwauth: "unchanged" EOF printf " PasswordAuthentication yes # keep\n" > etc/ssh/sshd_config atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o inline:" PasswordAuthentication yes # keep\n" cat etc/ssh/sshd_config } nocloud_userdata_cloudconfig_chpasswd_head() { atf_set "require.user" root } nocloud_userdata_cloudconfig_chpasswd_body() { mkdir -p etc cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh user:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: true users: - { user: "sys", password: RANDOM } EOF atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o empty -e inline:"nuageinit: Invalid entry for chpasswd.users: missing 'name'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet # nothing modified atf_check -o inline:"sys:*:1:0::0:0:Sys:/home/sys:/bin/sh\n" pw -R $(pwd) usershow sys cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: true users: - { name: "sys", pwd: RANDOM } EOF atf_check -o empty -e inline:"nuageinit: Invalid entry for chpasswd.users: missing 'password'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet # nothing modified atf_check -o inline:"sys:*:1:0::0:0:Sys:/home/sys:/bin/sh\n" pw -R $(pwd) usershow sys cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: false users: - { name: "sys", password: RANDOM } EOF # not empty because the password is printed to stdout atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o match:'sys:\$.*:1:0::0:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: true users: - { name: "sys", password: RANDOM } EOF # not empty because the password is printed to stdout atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o match:'sys:\$.*:1:0::1:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: true users: - { name: "user", password: "$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/" } EOF # not empty because the password is printed to stdout atf_check -o empty -e empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o inline:'user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1:0::1:0:Sys:/home/sys:/bin/sh\n' pw -R $(pwd) usershow user } nocloud_userdata_cloudconfig_chpasswd_list_string_head() { atf_set "require.user" root } nocloud_userdata_cloudconfig_chpasswd_list_string_body() { mkdir -p etc cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh user:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: true list: | sys:RANDOM EOF atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o empty -e inline:"nuageinit: chpasswd.list is deprecated consider using chpasswd.users\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o match:'sys:\$.*:1:0::1:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: false list: | sys:plop user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/ root:R EOF atf_check -o empty -e ignore /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o match:'sys:\$.*:1:0::0:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys atf_check -o inline:'user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1:0::0:0:Sys:/home/sys:/bin/sh\n' pw -R $(pwd) usershow user atf_check -o match:'root:\$.*:0:0::0:0:Charlie &:/root:/bin/sh$' pw -R $(pwd) usershow root } nocloud_userdata_cloudconfig_chpasswd_list_list_head() { atf_set "require.user" root } nocloud_userdata_cloudconfig_chpasswd_list_list_body() { mkdir -p etc cat > etc/master.passwd << EOF root:*:0:0::0:0:Charlie &:/root:/bin/sh sys:*:1:0::0:0:Sys:/home/sys:/bin/sh user:*:1:0::0:0:Sys:/home/sys:/bin/sh EOF pwd_mkdb -d etc "${PWD}"/etc/master.passwd cat > etc/group << EOF wheel:*:0:root users:*:1: EOF mkdir -p media/nuageinit printf "instance-id: iid-local01\n" > "${PWD}"/media/nuageinit/meta-data cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: true list: - sys:RANDOM EOF atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit nocloud atf_check -o empty -e inline:"nuageinit: chpasswd.list is deprecated consider using chpasswd.users\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o match:'sys:\$.*:1:0::1:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys cat > media/nuageinit/user-data << 'EOF' #cloud-config chpasswd: expire: false list: - sys:plop - user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/ - root:R EOF atf_check -o empty -e ignore /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet atf_check -o match:'sys:\$.*:1:0::0:0:Sys:/home/sys:/bin/sh$' pw -R $(pwd) usershow sys atf_check -o inline:'user:$6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1:0::0:0:Sys:/home/sys:/bin/sh\n' pw -R $(pwd) usershow user atf_check -o match:'root:\$.*:0:0::0:0:Charlie &:/root:/bin/sh$' pw -R $(pwd) usershow root } config2_userdata_runcmd_head() { atf_set "require.user" root } config2_userdata_runcmd_body() { mkdir -p media/nuageinit setup_test_adduser printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data << 'EOF' #cloud-config runcmd: EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 cat > media/nuageinit/user_data << 'EOF' #cloud-config runcmd: - plop EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check -s exit:0 /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 test -f var/cache/nuageinit/runcmds || atf_fail "File not created" test -x var/cache/nuageinit/runcmds || atf_fail "Missing execution permission" atf_check -o inline:"#!/bin/sh\nplop\n" cat var/cache/nuageinit/runcmds cat > media/nuageinit/user_data << 'EOF' #cloud-config runcmd: - echo "yeah!" - uname -s EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 atf_check -o inline:"#!/bin/sh\necho \"yeah!\"\nuname -s\n" cat var/cache/nuageinit/runcmds } config2_userdata_packages_head() { atf_set "require.user" root } config2_userdata_packages_body() { mkdir -p media/nuageinit setup_test_adduser export NUAGE_RUN_TESTS=1 printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data << 'EOF' #cloud-config packages: EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet cat > media/nuageinit/user_data << 'EOF' #cloud-config packages: - yeah/plop EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check -s exit:0 -o inline:"pkg install -y 'yeah/plop'\npkg info -q 'yeah/plop'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet cat > media/nuageinit/user_data << 'EOF' #cloud-config packages: - curl EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check -o inline:"pkg install -y 'curl'\npkg info -q 'curl'\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet cat > media/nuageinit/user_data << 'EOF' #cloud-config packages: - curl - meh: bla EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check -o inline:"pkg install -y 'curl'\npkg info -q 'curl'\n" -e inline:"nuageinit: Invalid type: table for packages entry number 2\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_update_packages_body() { mkdir -p media/nuageinit setup_test_adduser export NUAGE_RUN_TESTS=1 printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data << 'EOF' #cloud-config package_update: true EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check -o inline:"env ASSUME_ALWAYS_YES=yes pkg update\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_upgrade_packages_body() { mkdir -p media/nuageinit setup_test_adduser export NUAGE_RUN_TESTS=1 printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data << 'EOF' #cloud-config package_upgrade: true EOF chmod 755 "${PWD}"/media/nuageinit/user_data atf_check -o inline:"env ASSUME_ALWAYS_YES=yes pkg upgrade\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_shebang_body() { mkdir -p media/nuageinit setup_test_adduser printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/meta_data.json # Test 1: manage_etc_hosts adds hostname when /etc/hosts does not exist cat > media/nuageinit/user_data < etc/hosts < media/nuageinit/user_data < etc/hosts < media/nuageinit/user_data < etc/hosts < media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/meta_data.json mkdir -p etc/ssh ssh-keygen -t ed25519 -f etc/ssh/ssh_host_ed25519_key -N "" -q 2>/dev/null cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data <<'EOF' #cloud-config ca_certs: trusted: - | -----BEGIN CERTIFICATE----- dGVzdGNlcnQx -----END CERTIFICATE----- - | -----BEGIN CERTIFICATE----- dGVzdGNlcnQy -----END CERTIFICATE----- EOF atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 atf_check -o match:"dGVzdGNlcnQx" cat usr/share/certs/trusted/nuageinit-1.pem atf_check -o match:"dGVzdGNlcnQy" cat usr/share/certs/trusted/nuageinit-2.pem true } config2_userdata_multipart_head() { atf_set "require.user" root } config2_userdata_multipart_body() { mkdir -p media/nuageinit setup_test_adduser printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data <<'EOF' Content-Type: multipart/mixed; boundary="==BOUNDARY==" --==BOUNDARY== Content-Type: text/cloud-config; charset="us-ascii" #cloud-config hostname: multipart-host --==BOUNDARY== Content-Type: text/x-shellscript #!/bin/sh echo "multipart script executed" --==BOUNDARY==-- EOF atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 atf_check -o inline:"hostname='multipart-host'\n" cat etc/rc.conf.d/hostname atf_check -o inline:"#!/bin/sh\necho \"multipart script executed\"\n" cat var/cache/nuageinit/multipart_script test -x var/cache/nuageinit/multipart_script || atf_fail "multipart_script not executable" true } config2_userdata_power_state_head() { atf_set "require.user" root } config2_userdata_power_state_body() { mkdir -p media/nuageinit setup_test_adduser export NUAGE_RUN_TESTS=1 printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data << 'EOF' #cloud-config final_message: "System ready after $UPTIME seconds" EOF atf_check -e match:"System ready after [0-9]+ seconds" \ /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_phone_home_body() { mkdir -p media/nuageinit setup_test_adduser export NUAGE_RUN_TESTS=1 printf '{"hostname": "myhost", "uuid": "abc-123", "public_keys": ["ssh-rsa AAAAB...", "ssh-ed25519 AAAAC..."]}' > media/nuageinit/meta_data.json cat > media/nuageinit/user_data << 'EOF' #cloud-config phone_home: url: "http://example.com/endpoint" post: - hostname - instance_id tries: 1 EOF atf_check -o match:"fetch -q -o /dev/null --post-data 'hostname=myhost&instance_id=abc-123' 'http://example.com/endpoint'" \ /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet # Test "all" post printf '{"hostname": "myhost"}' > media/nuageinit/meta_data.json cat > media/nuageinit/user_data << 'EOF' #cloud-config phone_home: url: "http://example.com/endpoint" post: all tries: 1 EOF atf_check -o match:"fetch -q -o /dev/null --post-data 'hostname=myhost&fqdn=myhost' 'http://example.com/endpoint'" \ /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } atf_init_test_cases() { atf_add_test_case args atf_add_test_case nocloud atf_add_test_case nocloud_userdata_script atf_add_test_case nocloud_user_data_script atf_add_test_case nocloud_userdata_cloudconfig_users atf_add_test_case nocloud_userdata_cloudconfig_users_lock_passwd atf_add_test_case nocloud_network atf_add_test_case config2 atf_add_test_case config2_pubkeys atf_add_test_case config2_pubkeys_user_data atf_add_test_case config2_pubkeys_meta_data + atf_add_test_case config2_pubkeys_allow_public_ssh_keys atf_add_test_case config2_network atf_add_test_case config2_network_static_v4 atf_add_test_case config2_network_dns atf_add_test_case config2_ssh_keys atf_add_test_case nocloud_userdata_cloudconfig_ssh_pwauth atf_add_test_case nocloud_userdata_cloudconfig_chpasswd atf_add_test_case nocloud_userdata_cloudconfig_chpasswd_list_string atf_add_test_case nocloud_userdata_cloudconfig_chpasswd_list_list atf_add_test_case config2_userdata_runcmd atf_add_test_case config2_userdata_packages atf_add_test_case config2_userdata_update_packages atf_add_test_case config2_userdata_upgrade_packages atf_add_test_case config2_userdata_shebang atf_add_test_case config2_userdata_ssh_deletekeys atf_add_test_case config2_userdata_disable_root atf_add_test_case config2_userdata_bootcmd atf_add_test_case config2_userdata_manage_etc_hosts atf_add_test_case config2_userdata_mounts atf_add_test_case config2_userdata_resolv_conf atf_add_test_case config2_userdata_keyboard atf_add_test_case config2_userdata_ssh_authkey_fingerprints atf_add_test_case config2_userdata_ntp atf_add_test_case config2_userdata_ca_certs atf_add_test_case config2_userdata_multipart atf_add_test_case config2_userdata_power_state atf_add_test_case config2_userdata_locale atf_add_test_case config2_userdata_fqdn_and_hostname atf_add_test_case config2_userdata_write_files atf_add_test_case config2_userdata_encode_base64 atf_add_test_case config2_userdata_final_message atf_add_test_case config2_userdata_phone_home }