diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua index cbd842460e55..3a9ac253fbcb 100644 --- a/libexec/nuageinit/nuage.lua +++ b/libexec/nuageinit/nuage.lua @@ -1,1005 +1,1009 @@ --- -- SPDX-License-Identifier: BSD-2-Clause -- -- Copyright(c) 2022-2025 Baptiste Daroussin -- Copyright(c) 2025 Jesús Daniel Colmenares Oviedo local unistd = require("posix.unistd") local sys_stat = require("posix.sys.stat") local lfs = require("lfs") local function getlocalbase() local f = io.popen("sysctl -in user.localbase 2> /dev/null") local localbase = f:read("*l") f:close() if localbase == nil or localbase:len() == 0 then -- fallback localbase = "/usr/local" end return localbase end local function decode_base64(input) if input == nil or #input == 0 then return "" end local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' input = string.gsub(input, '[^'..b..'=]', '') local result = {} local bits = '' -- convert all characters in bits for i = 1, #input do local x = input:sub(i, i) if x == '=' then break end local f = b:find(x) - 1 for j = 6, 1, -1 do bits = bits .. (f % 2^j - f % 2^(j-1) > 0 and '1' or '0') end end for i = 1, #bits, 8 do local byte = bits:sub(i, i + 7) if #byte == 8 then local c = 0 for j = 1, 8 do c = c + (byte:sub(j, j) == '1' and 2^(8 - j) or 0) end table.insert(result, string.char(c)) end end return table.concat(result) end local function encode_base64(input) if input == nil or #input == 0 then return "" end local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' local result = {} local pos = 1 local padding = "" while pos <= #input do local a = string.byte(input, pos) local bb = pos + 1 <= #input and string.byte(input, pos + 1) or 0 local c = pos + 2 <= #input and string.byte(input, pos + 2) or 0 table.insert(result, string.sub(b, math.floor(a / 4) + 1, math.floor(a / 4) + 1)) table.insert(result, string.sub(b, math.floor(a % 4 * 16 + bb / 16) + 1, math.floor(a % 4 * 16 + bb / 16) + 1)) if pos + 1 <= #input then table.insert(result, string.sub(b, math.floor(bb % 16 * 4 + c / 64) + 1, math.floor(bb % 16 * 4 + c / 64) + 1)) else table.insert(result, "=") end if pos + 2 <= #input then table.insert(result, string.sub(b, math.floor(c % 64) + 1, math.floor(c % 64) + 1)) else table.insert(result, "=") end pos = pos + 3 end return table.concat(result) end local function shell_escape(s) return "'" .. string.gsub(s, "'", "'\\''") .. "'" end local function warnmsg(str, prepend) if not str then return end local tag = "" if prepend ~= false then tag = "nuageinit: " end io.stderr:write(tag .. str .. "\n") end local function errmsg(str, prepend) warnmsg(str, prepend) os.exit(1) end local function chmod(path, mode) mode = tonumber(mode, 8) local _, err, msg = sys_stat.chmod(path, mode) if err then errmsg("chmod(" .. path .. ", " .. mode .. ") failed: " .. msg) end end local function chown(path, owner, group) local _, err, msg = unistd.chown(path, owner, group) if err then errmsg("chown(" .. path .. ", " .. owner .. ", " .. group .. ") failed: " .. msg) end end local function dirname(oldpath) if not oldpath then return nil end local path = oldpath:gsub("[^/]+/*$", "") if path == "" then if oldpath:sub(1, 1) == "/" then return "/" end return nil end return path end local function mkdir_p(path) if lfs.attributes(path, "mode") ~= nil then return true end local r, err = mkdir_p(dirname(path)) if not r then return nil, err .. " (creating " .. path .. ")" end return lfs.mkdir(path) end local function sethostname(hostname) if hostname == nil then return end -- Basic hostname validation (RFC 952/1123) if #hostname == 0 then warnmsg("hostname is empty, ignoring") return end if #hostname > 253 then warnmsg("hostname too long (" .. #hostname .. " > 253), ignoring") return end if hostname:match("[^a-zA-Z0-9%.%-]") then warnmsg("hostname contains invalid characters: " .. hostname) return end if hostname:match("^[%.%-]") or hostname:match("[%.%-]$") then warnmsg("hostname must not start or end with a dot or hyphen: " .. hostname) return end for label in hostname:gmatch("[^.]+") do if #label > 63 then warnmsg("hostname label too long (" .. #label .. " > 63): " .. label) return end if label:match("^-") or label:match("-$") then warnmsg("hostname label starts or ends with hyphen: " .. label) return end end local root = os.getenv("NUAGE_FAKE_ROOTDIR") if not root then root = "" end local hostnamepath = root .. "/etc/rc.conf.d/hostname" mkdir_p(dirname(hostnamepath)) local f, err = io.open(hostnamepath, "w") if not f then warnmsg("Impossible to open " .. hostnamepath .. ":" .. err) return end f:write("hostname=" .. shell_escape(hostname) .. "\n") f:close() end local function update_etc_hosts(root, hostname) if hostname == nil or hostname == "" then return end local hosts_path = root .. "/etc/hosts" local lines = {} local already_present = false local f = io.open(hosts_path, "r") if not f then -- File doesn't exist, create a minimal one local nf = io.open(hosts_path, "w") if not nf then warnmsg("unable to create " .. hosts_path) return end nf:write("::1\t\tlocalhost " .. hostname .. "\n") nf:write("127.0.0.1\t\tlocalhost " .. hostname .. "\n") nf:close() return end for line in f:lines() do if line:find(hostname, 1, true) then already_present = true end table.insert(lines, line) end f:close() if already_present then return end -- Not present, append to localhost lines local new_lines = {} local found_localhost = false for _, line in ipairs(lines) do if (line:match("^127%.0%.0%.1%s") or line:match("^::1%s")) and line:find("localhost", 1, true) then table.insert(new_lines, line .. " " .. hostname) found_localhost = true else table.insert(new_lines, line) end end if not found_localhost then table.insert(new_lines, "127.0.0.1\t\tlocalhost " .. hostname) end f = io.open(hosts_path, "w") if not f then warnmsg("unable to open " .. hosts_path .. " for writing") return end for _, line in ipairs(new_lines) do f:write(line .. "\n") end f:close() end local function splitlist(list) local ret = {} if type(list) == "string" then for str in list:gmatch("([^, ]+)") do ret[#ret + 1] = str end elseif type(list) == "table" then ret = list else warnmsg("Invalid type " .. type(list) .. ", expecting table or string") end return ret end local function splitlines(s) local ret = {} for line in string.gmatch(s, "[^\n]+") do ret[#ret + 1] = line end return ret end local function getgroups() local root = os.getenv("NUAGE_FAKE_ROOTDIR") local cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end local f = io.popen(cmd .. "groupshow -a 2> /dev/null | cut -d: -f1") local groups = f:read("*a") f:close() return splitlines(groups) end local function purge_group(groups) local existing = getgroups() local ret = {} for _, group in ipairs(groups) do local found = false for _, eg in ipairs(existing) do if group == eg then found = true break end end if found then ret[#ret + 1] = group else warnmsg("ignoring non-existent group '" .. group .. "'") end end return ret end local function adduser(pwd) if (type(pwd) ~= "table") then warnmsg("Argument should be a table") return nil end local root = os.getenv("NUAGE_FAKE_ROOTDIR") local cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end local f = io.popen(cmd .. " usershow " .. shell_escape(pwd.name) .. " -7 2> /dev/null") local pwdstr = f:read("*a") f:close() if pwdstr:len() ~= 0 then return pwdstr:match("%a+:.+:%d+:%d+:.*:(.*):.*") end if not pwd.gecos then pwd.gecos = pwd.name .. " User" end if not pwd.homedir then pwd.homedir = "/home/" .. pwd.name end local extraargs = "" if pwd.groups then local list = splitlist(pwd.groups) -- pw complains if the group does not exist, so if the user -- specifies one that cannot be found, nuageinit will generate -- an exception and exit, unlike cloud-init, which only issues -- a warning but creates the user anyway. list = purge_group(list) if #list > 0 then local escaped_list = {} for _, g in ipairs(list) do table.insert(escaped_list, shell_escape(g)) end extraargs = " -G " .. table.concat(escaped_list, ",") end end -- pw will automatically create a group named after the username -- do not add a -g option in this case if pwd.primary_group and pwd.primary_group ~= pwd.name then extraargs = extraargs .. " -g " .. shell_escape(pwd.primary_group) end if not pwd.no_create_home then extraargs = extraargs .. " -m " end if not pwd.shell then pwd.shell = "/bin/sh" end local postcmd = "" local input = nil if pwd.passwd then input = pwd.passwd postcmd = " -H 0" elseif pwd.plain_text_passwd then input = pwd.plain_text_passwd postcmd = " -h 0" end cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end cmd = cmd .. "useradd -n " .. shell_escape(pwd.name) .. " -M 0755 -w none " cmd = cmd .. extraargs .. " -c " .. shell_escape(pwd.gecos) cmd = cmd .. " -d " .. shell_escape(pwd.homedir) .. " -s " .. shell_escape(pwd.shell) .. postcmd f = io.popen(cmd, "w") if input then f:write(input) end local r = f:close() if not r then warnmsg("fail to add user " .. pwd.name) warnmsg(cmd) return nil end if pwd.locked then cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end cmd = cmd .. "lock " .. shell_escape(pwd.name) os.execute(cmd) end return pwd.homedir end local function addgroup(grp) if (type(grp) ~= "table") then warnmsg("Argument should be a table") return false end local root = os.getenv("NUAGE_FAKE_ROOTDIR") local cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end local f = io.popen(cmd .. " groupshow " .. shell_escape(grp.name) .. " 2> /dev/null") local grpstr = f:read("*a") f:close() if grpstr:len() ~= 0 then return true end local extraargs = "" if grp.members then local list = splitlist(grp.members) local escaped_list = {} for _, m in ipairs(list) do table.insert(escaped_list, shell_escape(m)) end extraargs = " -M " .. table.concat(escaped_list, ",") end cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end cmd = cmd .. "groupadd -n " .. shell_escape(grp.name) .. extraargs local r = os.execute(cmd) if not r then warnmsg("fail to add group " .. grp.name) warnmsg(cmd) return false end return true end -local function addsshkey(homedir, key) +local function addsshkey(homedir, key, options) local root = os.getenv("NUAGE_FAKE_ROOTDIR") if root then homedir = root .. "/" .. homedir end local ak_path = homedir .. "/.ssh/authorized_keys" local dotssh_path = homedir .. "/.ssh" -- Check what already exists before creating anything local ak_exists = lfs.attributes(ak_path) ~= nil local dotssh_exists = lfs.attributes(dotssh_path) ~= nil -- Ensure .ssh directory exists if not dotssh_exists then local r, err = mkdir_p(dotssh_path) if not r then warnmsg("cannot create " .. dotssh_path .. ": " .. err) return end end -- Get homedir attributes for ownership local dirattrs = lfs.attributes(homedir) if not dirattrs then warnmsg("cannot get attributes for " .. homedir) return end local f = io.open(ak_path, "a") if not f then warnmsg("impossible to open " .. ak_path) return end - f:write(key .. "\n") + if options and options ~= "" then + f:write(options .. " " .. key .. "\n") + else + f:write(key .. "\n") + end f:close() -- Set permissions and ownership on newly created files/dirs if not ak_exists then chmod(ak_path, "0600") chown(ak_path, dirattrs.uid, dirattrs.gid) end if not dotssh_exists then chmod(dotssh_path, "0700") chown(dotssh_path, dirattrs.uid, dirattrs.gid) end end local function adddoas(pwd) local root = os.getenv("NUAGE_FAKE_ROOTDIR") local localbase = getlocalbase() local etcdir = localbase .. "/etc" if root then etcdir= root .. etcdir end local doasconf = etcdir .. "/doas.conf" local doasconf_exists = lfs.attributes(doasconf) ~= nil local etcdir_exists = lfs.attributes(etcdir) ~= nil -- Ensure etc directory exists if not etcdir_exists then local r, err = mkdir_p(etcdir) if not r then warnmsg("cannot create " .. etcdir .. ": " .. err) return end end local f = io.open(doasconf, "a") if not f then warnmsg("impossible to open " .. doasconf) return end if type(pwd.doas) == "string" then local rule = pwd.doas rule = rule:gsub("%%u", pwd.name) f:write(rule .. "\n") elseif type(pwd.doas) == "table" then for _, str in ipairs(pwd.doas) do local rule = str rule = rule:gsub("%%u", pwd.name) f:write(rule .. "\n") end end f:close() -- Set permissions on newly created files/dirs if not doasconf_exists then chmod(doasconf, "0640") end if not etcdir_exists then chmod(etcdir, "0755") end end local function addsudo(pwd) local root = os.getenv("NUAGE_FAKE_ROOTDIR") local localbase = getlocalbase() local sudoers_dir = localbase .. "/etc/sudoers.d" if root then sudoers_dir= root .. sudoers_dir end local sudoers = sudoers_dir .. "/90-nuageinit-users" local sudoers_exists = lfs.attributes(sudoers) ~= nil local sudoers_dir_exists = lfs.attributes(sudoers_dir) ~= nil -- Ensure sudoers.d directory exists if not sudoers_dir_exists then local r, err = mkdir_p(sudoers_dir) if not r then warnmsg("cannot create " .. sudoers_dir .. ": " .. err) return end end local f = io.open(sudoers, "a") if not f then warnmsg("impossible to open " .. sudoers) return end if type(pwd.sudo) == "string" then f:write(pwd.name .. " " .. pwd.sudo .. "\n") elseif type(pwd.sudo) == "table" then for _, str in ipairs(pwd.sudo) do f:write(pwd.name .. " " .. str .. "\n") end end f:close() -- Set permissions on newly created files/dirs if not sudoers_exists then chmod(sudoers, "0440") end if not sudoers_dir_exists then chmod(sudoers_dir, "0750") end end local function update_sshd_config(key, value) local sshd_config = "/etc/ssh/sshd_config" local root = os.getenv("NUAGE_FAKE_ROOTDIR") if root then sshd_config = root .. sshd_config end local f = io.open(sshd_config, "r") if not f then -- File does not exist, create it with the given key/value f = io.open(sshd_config, "w") if not f then warnmsg("Unable to open " .. sshd_config .. " for writing") return end f:write(key .. " " .. value .. "\n") f:close() return end -- Read existing content local lines = {} local found = false local pattern = "^%s*"..key:lower().."%s+(%w+)%s*#?.*$" for line in f:lines() do local _, _, val = line:lower():find(pattern) if val then found = true if val ~= value then table.insert(lines, key .. " " .. value) else table.insert(lines, line) end else table.insert(lines, line) end end f:close() if not found then table.insert(lines, key .. " " .. value) end -- Write back f = io.open(sshd_config .. ".nuageinit", "w") if not f then warnmsg("Unable to open " .. sshd_config .. ".nuageinit for writing") return end for _, l in ipairs(lines) do f:write(l .. "\n") end f:close() os.rename(sshd_config .. ".nuageinit", sshd_config) end local function delete_ssh_host_keys(root) 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") or entry:match("^ssh_host_.*key%.pub") then os.remove(ssh_dir .. "/" .. entry) end end end local function exec_change_password(user, password, type, expire) local root = os.getenv("NUAGE_FAKE_ROOTDIR") local cmd = "pw " if root then cmd = cmd .. "-R " .. root .. " " end local postcmd = " -H 0" local input = password if type ~= nil and type == "text" then postcmd = " -h 0" else if password == "RANDOM" then input = nil postcmd = " -w random" end end cmd = cmd .. "usermod " .. shell_escape(user) .. postcmd if expire then cmd = cmd .. " -p 1" else cmd = cmd .. " -p 0" end local f = io.popen(cmd .. " >/dev/null", "w") if input then f:write(input) end -- ignore stdout to avoid printing the password in case of random password local r = f:close() if not r then warnmsg("fail to change user password ".. user) warnmsg(cmd) end end local function change_password_from_line(line, expire) local user, password = line:match("%s*(%w+):(%S+)%s*") local type = nil if user and password then if password == "R" then password = "RANDOM" end if not password:match("^%$%d+%$%w+%$") then if password ~= "RANDOM" then type = "text" end end exec_change_password(user, password, type, expire) end end local function chpasswd(obj) if type(obj) ~= "table" then warnmsg("Invalid chpasswd entry, expecting an object") return end local expire = false if obj.expire ~= nil then if type(obj.expire) == "boolean" then expire = obj.expire else warnmsg("Invalid type for chpasswd.expire, expecting a boolean, got a ".. type(obj.expire)) end end if obj.users ~= nil then if type(obj.users) ~= "table" then warnmsg("Invalid type for chpasswd.users, expecting a list, got a ".. type(obj.users)) else for _, u in ipairs(obj.users) do if type(u) ~= "table" then warnmsg("Invalid chpasswd.users entry, expecting an object, got a " .. type(u)) elseif not u.name then warnmsg("Invalid entry for chpasswd.users: missing 'name'") elseif not u.password then warnmsg("Invalid entry for chpasswd.users: missing 'password'") else exec_change_password(u.name, u.password, u.type, expire) end end end end if obj.list ~= nil then warnmsg("chpasswd.list is deprecated consider using chpasswd.users") if type(obj.list) == "string" then for line in obj.list:gmatch("[^\n]+") do change_password_from_line(line, expire) end elseif type(obj.list) == "table" then for _, u in ipairs(obj.list) do change_password_from_line(u, expire) end end end end local function settimezone(timezone) if timezone == nil then return end local root = os.getenv("NUAGE_FAKE_ROOTDIR") if not root then root = "/" end local f, _, rc = os.execute("tzsetup -s -C " .. shell_escape(root) .. " " .. shell_escape(timezone)) if not f then warnmsg("Impossible to configure time zone ( rc = " .. rc .. " )") return end end local function pkg_bootstrap() if os.getenv("NUAGE_RUN_TESTS") then return true end if os.execute("pkg -N 2>/dev/null") then return true end print("Bootstrapping pkg") return os.execute("env ASSUME_ALWAYS_YES=YES pkg bootstrap") end local function install_package(package) if package == nil then return true end local install_cmd = "pkg install -y " .. shell_escape(package) local test_cmd = "pkg info -q " .. shell_escape(package) if os.getenv("NUAGE_RUN_TESTS") then print(install_cmd) print(test_cmd) return true end if os.execute(test_cmd) then return true end return os.execute(install_cmd) end local function run_pkg_cmd(subcmd) local cmd = "env ASSUME_ALWAYS_YES=yes pkg " .. subcmd if os.getenv("NUAGE_RUN_TESTS") then print(cmd) return true end return os.execute(cmd) end local function update_packages() return run_pkg_cmd("update") end local function upgrade_packages() return run_pkg_cmd("upgrade") end local function addfile(file, defer) if type(file) ~= "table" then return false, "Invalid object" end if defer and not file.defer then return true end if not defer and file.defer then return true end if not file.path then return false, "No path provided for the file to write" end local content = nil if file.content then if file.encoding then if file.encoding == "b64" or file.encoding == "base64" then content = decode_base64(file.content) else return false, "Unsupported encoding: " .. file.encoding end else content = file.content end end local mode = "w" if file.append then mode = "a" end local root = os.getenv("NUAGE_FAKE_ROOTDIR") if not root then root = "" end local filepath = root .. file.path mkdir_p(dirname(filepath)) local f = assert(io.open(filepath, mode)) if content then f:write(content) end f:close() if file.permissions then chmod(filepath, file.permissions) end if file.owner then local owner, group = string.match(file.owner, "([^:]+):([^:]+)") if not owner then owner = file.owner end chown(filepath, owner, group) end return true end local function add_fstab_entry(root, device, mount_point, fstype, options, dump_freq, passno) local fstab_path = root .. "/etc/fstab" local f = io.open(fstab_path, "a") if not f then warnmsg("unable to open " .. fstab_path .. " for writing") return false end options = options or "rw" dump_freq = dump_freq or 0 passno = passno or 0 f:write(string.format("%s\t\t%s\t\t%s\t\t%s\t\t%d\t\t%d\n", device, mount_point, fstype, options, dump_freq, passno)) f:close() return true end local function write_resolv_conf(root, config) local path = root .. "/etc/resolv.conf" local f = io.open(path, "w") if not f then warnmsg("unable to open " .. path .. " for writing") return end if config.domain then f:write("domain " .. config.domain .. "\n") end if config.searchdomains then f:write("search " .. table.concat(config.searchdomains, " ") .. "\n") end if config.sortlist then f:write("sortlist " .. table.concat(config.sortlist, " ") .. "\n") end if config.options then local opts = {} for k, v in pairs(config.options) do table.insert(opts, k .. ":" .. v) end f:write("options " .. table.concat(opts, " ") .. "\n") end if config.nameservers then for _, ns in ipairs(config.nameservers) do f:write("nameserver " .. ns .. "\n") end end f:close() end local function remove_fstab_entry(root, mount_point) local fstab_path = root .. "/etc/fstab" local f = io.open(fstab_path, "r") if not f then return end local lines = {} for line in f:lines() do local fields = {} for field in line:gmatch("%S+") do table.insert(fields, field) end if fields[2] ~= mount_point then table.insert(lines, line) end end f:close() local nf = io.open(fstab_path, "w") if not nf then warnmsg("unable to open " .. fstab_path .. " for writing") return end for _, line in ipairs(lines) do nf:write(line .. "\n") end nf:close() end local function parse_mime_multipart(data) local boundary = data:match("boundary=\"([^\"]+)\"") if not boundary then boundary = data:match("boundary=([^%s;]+)") end if not boundary then return nil end local parts = {} local pos = data:find("\n") or 1 local first = data:find("--" .. boundary, pos, true) if not first then return nil end pos = data:find("\n", first) if not pos then return nil end pos = pos + 1 while true do local nextb = data:find("--" .. boundary, pos, true) if not nextb then break end local part = data:sub(pos, nextb - 1) part = part:gsub("^\r?\n", ""):gsub("\r?\n$", "") local header_end = part:find("\r?\n\r?\n") local headers_str, body if header_end then headers_str = part:sub(1, header_end - 1) body = part:sub(header_end + 2):gsub("^\r?\n", ""):gsub("\r?\n$", "") else body = part end local ct = "text/plain" if headers_str then local m = headers_str:match("[Cc]ontent%-[Tt]ype:%s*([^%s;]+)") if m then ct = m:lower() end end table.insert(parts, {content_type = ct, body = body}) local after = data:sub(nextb + 2 + #boundary, nextb + 3 + #boundary) if after == "--" then break end pos = data:find("\n", nextb) or nextb if pos then pos = pos + 1 end end return parts end local n = { shell_escape = shell_escape, warn = warnmsg, err = errmsg, chmod = chmod, chown = chown, dirname = dirname, mkdir_p = mkdir_p, sethostname = sethostname, settimezone = settimezone, adduser = adduser, addgroup = addgroup, addsshkey = addsshkey, update_sshd_config = update_sshd_config, delete_ssh_host_keys = delete_ssh_host_keys, update_etc_hosts = update_etc_hosts, chpasswd = chpasswd, pkg_bootstrap = pkg_bootstrap, install_package = install_package, update_packages = update_packages, upgrade_packages = upgrade_packages, addsudo = addsudo, adddoas = adddoas, addfile = addfile, decode_base64 = decode_base64, encode_base64 = encode_base64, add_fstab_entry = add_fstab_entry, remove_fstab_entry = remove_fstab_entry, write_resolv_conf = write_resolv_conf, parse_mime_multipart = parse_mime_multipart, } return n diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index ba26f504effb..d67ac6ce4229 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -1,1199 +1,1217 @@ #!/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 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 = "no" if obj.ssh_pwauth then value = "yes" 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 function disable_root(obj) - if obj.disable_root == nil then return end - if obj.disable_root then - local value = "no" - if obj.disable_root_opts then - if type(obj.disable_root_opts) == "string" then - value = obj.disable_root_opts - elseif type(obj.disable_root_opts) == "table" then - value = obj.disable_root_opts[1] - 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 + for _, k in pairs(metadata.public_keys) do + table.insert(keys, k) end - nuage.update_sshd_config("PermitRootLogin", value) + 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 a3d9da2415d9..c9c9096eee72 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -1,702 +1,711 @@ .\" 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 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 , -sets -.Qq Ic PermitRootLogin -to -.Ar no -.Pq or the value specified in Ic disable_root_opts -in -.Pa /etc/ssh/sshd_config . +.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 set the value of -.Qq Ic PermitRootLogin -in -.Pa /etc/ssh/sshd_config , +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 . +.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 locked Boolean to determine if the user account should be locked. .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 ab9e697076e1..1fb5f7b4c967 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -1,1494 +1,1495 @@ #- # 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_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_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 } 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/user_data < etc/ssh/sshd_config atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 - atf_check -o inline:"PermitRootLogin yes\n" cat etc/ssh/sshd_config + atf_check -o inline:"ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n" cat root/.ssh/authorized_keys } config2_userdata_bootcmd_head() { atf_set "require.user" root } config2_userdata_bootcmd_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/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_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_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 }