diff options
Diffstat (limited to 'libexec/nuageinit')
-rw-r--r-- | libexec/nuageinit/Makefile | 12 | ||||
-rw-r--r-- | libexec/nuageinit/Makefile.depend | 10 | ||||
-rw-r--r-- | libexec/nuageinit/nuage.lua | 651 | ||||
-rwxr-xr-x | libexec/nuageinit/nuageinit | 719 | ||||
-rw-r--r-- | libexec/nuageinit/nuageinit.7 | 431 | ||||
-rw-r--r-- | libexec/nuageinit/tests/Makefile | 22 | ||||
-rw-r--r-- | libexec/nuageinit/tests/Makefile.depend | 10 | ||||
-rw-r--r-- | libexec/nuageinit/tests/addfile.lua | 71 | ||||
-rw-r--r-- | libexec/nuageinit/tests/addgroup.lua | 16 | ||||
-rw-r--r-- | libexec/nuageinit/tests/addsshkey.lua | 5 | ||||
-rw-r--r-- | libexec/nuageinit/tests/adduser.lua | 16 | ||||
-rw-r--r-- | libexec/nuageinit/tests/adduser_passwd.lua | 20 | ||||
-rw-r--r-- | libexec/nuageinit/tests/dirname.lua | 11 | ||||
-rw-r--r-- | libexec/nuageinit/tests/err.lua | 5 | ||||
-rw-r--r-- | libexec/nuageinit/tests/nuage.sh | 101 | ||||
-rw-r--r-- | libexec/nuageinit/tests/nuageinit.sh | 947 | ||||
-rw-r--r-- | libexec/nuageinit/tests/sethostname.lua | 5 | ||||
-rw-r--r-- | libexec/nuageinit/tests/settimezone.lua | 5 | ||||
-rw-r--r-- | libexec/nuageinit/tests/utils.sh | 32 | ||||
-rw-r--r-- | libexec/nuageinit/tests/warn.lua | 5 |
20 files changed, 3094 insertions, 0 deletions
diff --git a/libexec/nuageinit/Makefile b/libexec/nuageinit/Makefile new file mode 100644 index 000000000000..755ecb7ff418 --- /dev/null +++ b/libexec/nuageinit/Makefile @@ -0,0 +1,12 @@ +PACKAGE= nuageinit +SCRIPTS= nuageinit +FILES= nuage.lua +FILESDIR= ${SHAREDIR}/flua +MAN= nuageinit.7 + +.include <src.opts.mk> + +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + +.include <bsd.prog.mk> diff --git a/libexec/nuageinit/Makefile.depend b/libexec/nuageinit/Makefile.depend new file mode 100644 index 000000000000..11aba52f82cf --- /dev/null +++ b/libexec/nuageinit/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua new file mode 100644 index 000000000000..ef3cfd994fe1 --- /dev/null +++ b/libexec/nuageinit/nuage.lua @@ -0,0 +1,651 @@ +--- +-- SPDX-License-Identifier: BSD-2-Clause +-- +-- Copyright(c) 2022-2025 Baptiste Daroussin <bapt@FreeBSD.org> +-- Copyright(c) 2025 Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org> + +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) + 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 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) + local 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 + 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 + 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="' .. hostname .. '"\n') + 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 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 " .. 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) + extraargs = " -G " .. table.concat(list, ",") + 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 " .. 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 precmd = "" + 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 = precmd .. "pw " + if root then + cmd = cmd .. "-R " .. root .. " " + end + cmd = cmd .. "useradd -n " .. pwd.name .. " -M 0755 -w none " + cmd = cmd .. extraargs .. " -c '" .. pwd.gecos + cmd = cmd .. "' -d '" .. pwd.homedir .. "' -s " .. pwd.shell .. postcmd + + f = io.popen(cmd, "w") + if input then + f:write(input) + end + local r = f:close(cmd) + 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 " .. 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 " .. 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) + extraargs = " -M " .. table.concat(list, ",") + end + cmd = "pw " + if root then + cmd = cmd .. "-R " .. root .. " " + end + cmd = cmd .. "groupadd -n " .. 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 chownak = false + local chowndotssh = false + 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" + local dirattrs = lfs.attributes(ak_path) + if dirattrs == nil then + chownak = true + dirattrs = lfs.attributes(dotssh_path) + if dirattrs == nil then + assert(lfs.mkdir(dotssh_path)) + chowndotssh = true + dirattrs = lfs.attributes(homedir) + end + end + + local f = io.open(ak_path, "a") + if not f then + warnmsg("impossible to open " .. ak_path) + return + end + f:write(key .. "\n") + f:close() + if chownak then + chmod(ak_path, "0600") + chown(ak_path, dirattrs.uid, dirattrs.gid) + end + if chowndotssh then + chmod(dotssh_path, "0700") + chown(dotssh_path, dirattrs.uid, dirattrs.gid) + end +end + +local function adddoas(pwd) + local chmodetcdir = false + local chmoddoasconf = false + 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_attr = lfs.attributes(doasconf) + if doasconf_attr == nil then + chmoddoasconf = true + local dirattrs = lfs.attributes(etcdir) + if dirattrs == nil then + local r, err = mkdir_p(etcdir) + if not r then + return nil, err .. " (creating " .. etcdir .. ")" + end + chmodetcdir = true + 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() + if chmoddoasconf then + chmod(doasconf, "0640") + end + if chmodetcdir then + chmod(etcdir, "0755") + end +end + +local function addsudo(pwd) + local chmodsudoersd = false + local chmodsudoers = false + 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_attr = lfs.attributes(sudoers) + if sudoers_attr == nil then + chmodsudoers = true + local dirattrs = lfs.attributes(sudoers_dir) + if dirattrs == nil then + local r, err = mkdir_p(sudoers_dir) + if not r then + return nil, err .. " (creating " .. sudoers_dir .. ")" + end + chmodsudoersd = true + 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() + if chmodsudoers then + chmod(sudoers, "0440") + end + if chmodsudoersd 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 = assert(io.open(sshd_config, "r+")) + local tgt = assert(io.open(sshd_config .. ".nuageinit", "w")) + local found = false + local pattern = "^%s*"..key:lower().."%s+(%w+)%s*#?.*$" + while true do + local line = f:read() + if line == nil then break end + local _, _, val = line:lower():find(pattern) + if val then + found = true + if val == value then + assert(tgt:write(line .. "\n")) + else + assert(tgt:write(key .. " " .. value .. "\n")) + end + else + assert(tgt:write(line .. "\n")) + end + end + if not found then + assert(tgt:write(key .. " " .. value .. "\n")) + end + assert(f:close()) + assert(tgt:close()) + os.rename(sshd_config .. ".nuageinit", sshd_config) +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 " .. 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(cmd) + 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)) + goto list + end + for _, u in ipairs(obj.users) do + if type(u) ~= "table" then + warnmsg("Invalid chpasswd.users entry, expecting an object, got a " .. type(u)) + goto next + end + if not u.name then + warnmsg("Invalid entry for chpasswd.users: missing 'name'") + goto next + end + if not u.password then + warnmsg("Invalid entry for chpasswd.users: missing 'password'") + goto next + end + exec_change_password(u.name, u.password, u.type, expire) + ::next:: + end + end + ::list:: + 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 + + f, _, rc = os.execute("tzsetup -s -C " .. root .. " " .. 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 " .. package + local test_cmd = "pkg info -q " .. 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 + 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 n = { + 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, + chpasswd = chpasswd, + pkg_bootstrap = pkg_bootstrap, + install_package = install_package, + update_packages = update_packages, + upgrade_packages = upgrade_packages, + addsudo = addsudo, + adddoas = adddoas, + addfile = addfile +} + +return n diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit new file mode 100755 index 000000000000..29340a3d91ea --- /dev/null +++ b/libexec/nuageinit/nuageinit @@ -0,0 +1,719 @@ +#!/usr/libexec/flua +--- +-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD +-- +-- Copyright(c) 2022-2025 Baptiste Daroussin <bapt@FreeBSD.org> +-- Copyright(c) 2025 Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org> + +local nuage = require("nuage") +local ucl = require("ucl") +local yaml = require("lyaml") + +if #arg ~= 2 then + nuage.err("Usage: " .. arg[0] .. " <cloud-init-directory> (<config-2> | <nocloud>)", false) +end +local ni_path = arg[1] +local citype = arg[2] + +local default_user = { + name = "freebsd", + homedir = "/home/freebsd", + groups = "wheel", + gecos = "FreeBSD User", + shell = "/bin/sh", + plain_text_passwd = "freebsd" +} + +local root = os.getenv("NUAGE_FAKE_ROOTDIR") +if not root then + root = "" +end + +local function openat(dir, name) + local path_dir = root .. dir + local path_name = path_dir .. "/" .. name + nuage.mkdir_p(path_dir) + local f, err = io.open(path_name, "w") + if not f then + nuage.err("unable to open " .. path_name .. ": " .. err) + end + return f, path_name +end +local function open_ssh_key(name) + return openat("/etc/ssh", name) +end + +local function open_config(name) + return openat("/etc/rc.conf.d", name) +end + +local function open_resolv_conf() + return openat("/etc", "resolv.conf") +end + +local function open_resolvconf_conf() + return openat("/etc", "resolvconf.conf") +end + +local function get_ifaces_by_mac() + local parser = ucl.parser() + -- grab ifaces + local ns = io.popen("netstat -i --libxo json") + local netres = ns:read("*a") + ns:close() + local res, err = parser:parse_string(netres) + if not res then + nuage.warn("Error parsing netstat -i --libxo json outout: " .. err) + return nil + end + local ifaces = parser:get_object() + local myifaces = {} + for _, iface in pairs(ifaces["statistics"]["interface"]) do + if iface["network"]:match("<Link#%d>") then + local s = iface["address"] + myifaces[s:lower()] = iface["name"] + end + end + return myifaces +end + +local function sethostname(obj) + -- always prefer fqdn if specified over hostname + if obj.fqdn then + nuage.sethostname(obj.fqdn) + elseif obj.hostname then + nuage.sethostname(obj.hostname) + end +end + +local function settimezone(obj) + nuage.settimezone(obj.timezone) +end + +local function groups(obj) + if obj.groups == nil then return end + + for n, g in pairs(obj.groups) do + if (type(g) == "string") then + local r = nuage.addgroup({name = g}) + if not r then + nuage.warn("failed to add group: " .. g) + end + elseif type(g) == "table" then + for k, v in pairs(g) do + nuage.addgroup({name = k, members = v}) + end + else + nuage.warn("invalid type: " .. type(g) .. " for users entry number " .. n) + end + end +end + +local function create_default_user(obj) + if not obj.users then + -- default user if none are defined + nuage.adduser(default_user) + end +end + +local function users(obj) + if obj.users == nil then return end + + for n, u in pairs(obj.users) do + if type(u) == "string" then + if u == "default" then + nuage.adduser(default_user) + else + nuage.adduser({name = u}) + end + elseif type(u) == "table" then + -- ignore users without a username + if u.name == nil then + goto unext + end + local homedir = nuage.adduser(u) + if u.ssh_authorized_keys then + for _, v in ipairs(u.ssh_authorized_keys) do + nuage.addsshkey(homedir, v) + end + end + if u.sudo then + nuage.addsudo(u) + end + if u.doas then + nuage.adddoas(u) + end + else + nuage.warn("invalid type : " .. type(u) .. " for users entry number " .. n) + end + ::unext:: + end +end + +local function ssh_keys(obj) + if obj.ssh_keys == nil then return end + if type(obj.ssh_keys) ~= "table" then + nuage.warn("Invalid type for ssh_keys") + return + end + + for key, val in pairs(obj.ssh_keys) do + for keyname, keytype in key:gmatch("(%w+)_(%w+)") do + local sshkn = nil + if keytype == "public" then + sshkn = "ssh_host_" .. keyname .. "_key.pub" + elseif keytype == "private" then + sshkn = "ssh_host_" .. keyname .. "_key" + end + if sshkn then + local sshkey, path = open_ssh_key(sshkn) + if sshkey then + sshkey:write(val .. "\n") + sshkey:close() + end + if keytype == "private" then + nuage.chmod(path, "0600") + end + end + end + end +end + +local function ssh_authorized_keys(obj) + if obj.ssh_authorized_keys == nil then return end + local homedir = nuage.adduser(default_user) + for _, k in ipairs(obj.ssh_authorized_keys) do + nuage.addsshkey(homedir, k) + end +end + +local function nameservers(interface, obj) + local resolvconf_conf_handler = open_resolvconf_conf() + + if obj.search then + local with_space = false + + resolvconf_conf_handler:write('search_domains="') + + for _, d in ipairs(obj.search) do + if with_space then + resolvconf_conf_handler:write(" " .. d) + else + resolvconf_conf_handler:write(d) + with_space = true + end + end + + resolvconf_conf_handler:write('"\n') + end + + if obj.addresses then + local with_space = false + + resolvconf_conf_handler:write('name_servers="') + + for _, a in ipairs(obj.addresses) do + if with_space then + resolvconf_conf_handler:write(" " .. a) + else + resolvconf_conf_handler:write(a) + with_space = true + end + end + + resolvconf_conf_handler:write('"\n') + end + + resolvconf_conf_handler:close() + + local resolv_conf = root .. "/etc/resolv.conf" + + resolv_conf_attr = lfs.attributes(resolv_conf) + + if resolv_conf_attr == nil then + resolv_conf_handler = open_resolv_conf() + resolv_conf_handler:close() + end + + if not os.execute("resolvconf -a " .. interface .. " < " .. resolv_conf) 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 interfaces = {} + if rules.macaddress then + local ifaces = get_ifaces_by_mac() + local 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_defered(obj) + write_files(obj.write_files, false) +end + +local function write_files_defered(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 + if type(v.mtu) == "number" then + mtu = tostring(v.mtu) + else + mtu = v.mtu + end + if mtu:match("%d") then + extra_opts = extra_opts .. " mtu " .. mtu + else + nuage.warn("MTU is not set because the specified value is invalid: " .. mtu) + end + end + for interface in pairs(interfaces) do + if v.match and v.match.macaddress and v["set-name"] then + local ifaces = get_ifaces_by_mac() + local matched = ifaces[v.match.macaddress] + if matched and matched == interface then + network:write("ifconfig_" .. interface .. '_name=' .. v["set-name"] .. '\n') + interface = v["set-name"] + end + end + if v.dhcp4 then + network:write("ifconfig_" .. interface .. '="DHCP"' .. extra_opts .. '\n') + elseif v.addresses then + for _, a in pairs(v.addresses) do + if a:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)") then + network:write("ifconfig_" .. interface .. '="inet ' .. a .. extra_opts .. '"\n') + else + network:write("ifconfig_" .. interface .. '_ipv6="inet6 ' .. a .. extra_opts .. '"\n') + ipv6[#ipv6 + 1] = interface + end + end + if set_nameservers and v.nameservers then + set_nameservers = false + nameservers(interface, v.nameservers) + end + if set_defaultrouter and v.gateway4 then + set_defaultrouter = false + routing:write('defaultrouter="' .. v.gateway4 .. '"\n') + end + if v.gateway6 then + if set_defaultrouter6 then + set_defaultrouter6 = false + routing:write('ipv6_defaultrouter="' .. v.gateway6 .. '"\n') + end + routing:write("ipv6_route_" .. interface .. '="' .. v.gateway6) + routing:write(" -prefixlen 128 -interface " .. interface .. '"\n') + end + end + end + ::next:: + end + if #ipv6 > 0 then + network:write('ipv6_network_interfaces="') + network:write(table.concat(ipv6, " ") .. '"\n') + network:write('ipv6_default_interface="' .. ipv6[1] .. '"\n') + end + network:close() + routing:close() +end + +local function ssh_pwauth(obj) + if obj.ssh_pwauth == nil then return end + + local value = "no" + if obj.ssh_pwauth then + value = "yes" + end + nuage.update_sshd_config("PasswordAuthentication", value) +end + +local function runcmd(obj) + if obj.runcmd == nil then return end + local f = nil + for _, c in ipairs(obj.runcmd) do + if f == nil then + nuage.mkdir_p(root .. "/var/cache/nuageinit") + f = assert(io.open(root .. "/var/cache/nuageinit/runcmds", "w")) + f:write("#!/bin/sh\n") + end + f:write(c .. "\n") + end + if f ~= nil then + f:close() + nuage.chmod(root .. "/var/cache/nuageinit/runcmds", "0755") + end +end + +local function packages(obj) + if obj.package_update then + nuage.update_packages() + end + if obj.package_upgrade then + nuage.upgrade_packages() + end + if obj.packages then + install_packages(obj.packages) + end +end + +local function chpasswd(obj) + if obj.chpasswd == nil then return end + nuage.chpasswd(obj.chpasswd) +end + +local function config2_network(p) + local parser = ucl.parser() + local f = io.open(p .. "/network_data.json") + if not f then + -- silently return no network configuration is provided + return + end + f:close() + local res, err = parser:parse_file(p .. "/network_data.json") + if not res then + nuage.warn("error parsing network_data.json: " .. err) + return + end + local obj = parser:get_object() + + local ifaces = get_ifaces_by_mac() + if not ifaces then + nuage.warn("no network interfaces found") + return + end + local mylinks = {} + for _, v in pairs(obj["links"]) do + local s = v["ethernet_mac_address"]:lower() + mylinks[v["id"]] = ifaces[s] + end + + local network = open_config("network") + local routing = open_config("routing") + local ipv6 = {} + local ipv6_routes = {} + local ipv4 = {} + for _, v in pairs(obj["networks"]) do + local interface = mylinks[v["link"]] + if v["type"] == "ipv4_dhcp" then + network:write("ifconfig_" .. interface .. '="DHCP"\n') + end + if v["type"] == "ipv4" then + network:write( + "ifconfig_" .. interface .. '="inet ' .. v["ip_address"] .. " netmask " .. v["netmask"] .. '"\n' + ) + if v["gateway"] then + routing:write('defaultrouter="' .. v["gateway"] .. '"\n') + end + if v["routes"] then + for i, r in ipairs(v["routes"]) do + local rname = "cloudinit" .. i .. "_" .. interface + if v["gateway"] and v["gateway"] == r["gateway"] then + goto next + end + if r["network"] == "0.0.0.0" then + routing:write('defaultrouter="' .. r["gateway"] .. '"\n') + goto next + end + routing:write("route_" .. rname .. '="-net ' .. r["network"] .. " ") + routing:write(r["gateway"] .. " " .. r["netmask"] .. '"\n') + ipv4[#ipv4 + 1] = rname + ::next:: + end + end + end + if v["type"] == "ipv6" then + ipv6[#ipv6 + 1] = interface + ipv6_routes[#ipv6_routes + 1] = interface + network:write("ifconfig_" .. interface .. '_ipv6="inet6 ' .. v["ip_address"] .. '"\n') + if v["gateway"] then + routing:write('ipv6_defaultrouter="' .. v["gateway"] .. '"\n') + routing:write("ipv6_route_" .. interface .. '="' .. v["gateway"]) + routing:write(" -prefixlen 128 -interface " .. interface .. '"\n') + end + -- TODO compute the prefixlen for the routes + --if v["routes"] then + -- for i, r in ipairs(v["routes"]) do + -- local rname = "cloudinit" .. i .. "_" .. mylinks[v["link"]] + -- -- skip all the routes which are already covered by the default gateway, some provider + -- -- still list plenty of them. + -- if v["gateway"] == r["gateway"] then + -- goto next + -- end + -- routing:write("ipv6_route_" .. rname .. '"\n') + -- ipv6_routes[#ipv6_routes + 1] = rname + -- ::next:: + -- end + --end + end + end + if #ipv4 > 0 then + routing:write('static_routes="') + routing:write(table.concat(ipv4, " ") .. '"\n') + end + if #ipv6 > 0 then + network:write('ipv6_network_interfaces="') + network:write(table.concat(ipv6, " ") .. '"\n') + network:write('ipv6_default_interface="' .. ipv6[1] .. '"\n') + end + if #ipv6_routes > 0 then + routing:write('ipv6_static_routes="') + routing:write(table.concat(ipv6, " ") .. '"\n') + end + network:close() + routing:close() +end + +local function parse_network_config() + local nc_file = ni_path .. "/network-config" + local nc_file_attr = lfs.attributes(nc_file) + if nc_file_attr == nil then + return + end + local f, err = io.open(nc_file) + if err then + nuage.err("error parsing nocloud network-config: " .. err) + end + local obj = yaml.load(f:read("*a")) + f:close() + if not obj then + nuage.err("error parsing nocloud network-config") + end + local netobj = {} + netobj["network"] = obj + return netobj +end + +if citype == "config-2" then + local parser = ucl.parser() + local res, err = parser:parse_file(ni_path .. "/meta_data.json") + + if not res then + nuage.err("error parsing config-2 meta_data.json: " .. err) + end + local obj = parser:get_object() + if obj.public_keys then + local homedir = nuage.adduser(default_user) + for _,v in pairs(obj.public_keys) do + nuage.addsshkey(homedir, v) + end + end + nuage.sethostname(obj["hostname"]) + + -- network + config2_network(ni_path) +elseif citype == "nocloud" then + local f, err = io.open(ni_path .. "/meta-data") + if err then + nuage.err("error parsing nocloud meta-data: " .. err) + end + local obj = yaml.load(f:read("*a")) + f:close() + if not obj then + nuage.err("error parsing nocloud meta-data") + end + local hostname = obj["local-hostname"] + if not hostname then + hostname = obj["hostname"] + end + if hostname then + nuage.sethostname(hostname) + end +elseif citype ~= "postnet" then + nuage.err("Unknown cloud init type: " .. citype) +end + +-- deal with user-data +local ud = nil +local f = nil +local userdatas = {"user-data", "user_data"} +for _, v in pairs(userdatas) do + f = io.open(ni_path .. "/" .. v, "r") + if f then + ud = v + break + end +end +if not f then + os.exit(0) +end +local line = f:read("*l") +if citype ~= "postnet" then + local content = f:read("*a") + nuage.mkdir_p(root .. "/var/cache/nuageinit") + local tof = assert(io.open(root .. "/var/cache/nuageinit/user_data", "w")) + tof:write(line .. "\n" .. content) + tof:close() +end +f:close() +if line == "#cloud-config" then + local pre_network_calls = { + sethostname, + settimezone, + groups, + create_default_user, + ssh_keys, + ssh_authorized_keys, + network_config, + ssh_pwauth, + runcmd, + write_files_not_defered, + } + + local post_network_calls = { + packages, + users, + chpasswd, + write_files_defered, + } + + f = io.open(ni_path .. "/" .. ud) + local obj = yaml.load(f:read("*a")) + f:close() + if not obj then + nuage.err("error parsing cloud-config file: " .. ud) + end + + local calls_table = pre_network_calls + if citype == "postnet" then + calls_table = post_network_calls + end + + for i = 1, #calls_table do + if citype == "nocloud" and calls_table[i] == network_config then + netobj = parse_network_config() + if netobj == nil then + network_config(obj) + else + network_config(netobj) + end + else + calls_table[i](obj) + 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 new file mode 100644 index 000000000000..b527c984970c --- /dev/null +++ b/libexec/nuageinit/nuageinit.7 @@ -0,0 +1,431 @@ +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2025 Baptiste Daroussin <bapt@FreeBSD.org> +.\" Copyright (c) 2025 Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org> +.\" +.Dd June 26, 2025 +.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. +.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 timezone +Sets the system timezone based on the value provided. +.Pp +See also +.Xr tzfile 3 Ns . +.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_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 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 Ns . +.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 Ns . +.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 Ns . +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 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 Ns , +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. +.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 user +Specify the user whose password will be changed. +.It Ic password +Specify a text line with the new password or +specify the user whose password will be changed. +.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 not 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 +.Sh EXAMPLES +Here is an example of a YAML configuration for +.Nm : +.Bd -literal +#cloud-config +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 +package_update: true +package_upgrade: true +runcmd: + - logger -t nuageinit "boot finished" +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/Makefile b/libexec/nuageinit/tests/Makefile new file mode 100644 index 000000000000..dc8997717b59 --- /dev/null +++ b/libexec/nuageinit/tests/Makefile @@ -0,0 +1,22 @@ +PACKAGE= tests +.PATH: ${SRCTOP}/usr.sbin/pw/tests + +BINDIR= ${TESTSDIR} + +PROGS= crypt +LIBADD= crypt + +ATF_TESTS_SH= nuage utils nuageinit + +${PACKAGE}FILES+= addgroup.lua +${PACKAGE}FILES+= addsshkey.lua +${PACKAGE}FILES+= adduser.lua +${PACKAGE}FILES+= adduser_passwd.lua +${PACKAGE}FILES+= dirname.lua +${PACKAGE}FILES+= err.lua +${PACKAGE}FILES+= sethostname.lua +${PACKAGE}FILES+= settimezone.lua +${PACKAGE}FILES+= warn.lua +${PACKAGE}FILES+= addfile.lua + +.include <bsd.test.mk> diff --git a/libexec/nuageinit/tests/Makefile.depend b/libexec/nuageinit/tests/Makefile.depend new file mode 100644 index 000000000000..11aba52f82cf --- /dev/null +++ b/libexec/nuageinit/tests/Makefile.depend @@ -0,0 +1,10 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/libexec/nuageinit/tests/addfile.lua b/libexec/nuageinit/tests/addfile.lua new file mode 100644 index 000000000000..98d020e557c0 --- /dev/null +++ b/libexec/nuageinit/tests/addfile.lua @@ -0,0 +1,71 @@ +#!/bin/libexec/flua + +local n = require("nuage") +local lfs = require("lfs") + +local f = { + content = "plop" +} + +local r, err = n.addfile(f, false) +if r or err ~= "No path provided for the file to write" then + n.err("addfile should not accept a file to write without a path") +end + +local function addfile_and_getres(file) + local r, err = n.addfile(file, false) + if not r then + n.err(err) + end + local root = os.getenv("NUAGE_FAKE_ROOTDIR") + if not root then + root = "" + end + local filepath = root .. file.path + local resf = assert(io.open(filepath, "r")) + local str = resf:read("*all") + resf:close() + return str +end + +-- simple file +f.path="/tmp/testnuage" +local str = addfile_and_getres(f) +if str ~= f.content then + n.err("Invalid file content") +end + +-- the file is overwriten +f.content = "test" + +str = addfile_and_getres(f) +if str ~= f.content then + n.err("Invalid file content, not overwritten") +end + +-- try to append now +f.content = "more" +f.append = true + +str = addfile_and_getres(f) +if str ~= "test" .. f.content then + n.err("Invalid file content, not appended") +end + +-- base64 +f.content = "YmxhCg==" +f.encoding = "base64" +f.append = false + +str = addfile_and_getres(f) +if str ~= "bla\n" then + n.err("Invalid file content, base64 decode") +end + +-- b64 +f.encoding = "b64" +str = addfile_and_getres(f) +if str ~= "bla\n" then + n.err("Invalid file content, b64 decode") + print("==>" .. str .. "<==") +end diff --git a/libexec/nuageinit/tests/addgroup.lua b/libexec/nuageinit/tests/addgroup.lua new file mode 100644 index 000000000000..a36a5e24c7b3 --- /dev/null +++ b/libexec/nuageinit/tests/addgroup.lua @@ -0,0 +1,16 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +if n.addgroup() then + n.err("addgroup should not accept empty value") +end +if n.addgroup("plop") then + n.err("addgroup should not accept empty value") +end +local gr = {} +gr.name = "impossible_groupname" +local res = n.addgroup(gr) +if not res then + n.err("valid addgroup should return a path") +end diff --git a/libexec/nuageinit/tests/addsshkey.lua b/libexec/nuageinit/tests/addsshkey.lua new file mode 100644 index 000000000000..47e102c162a9 --- /dev/null +++ b/libexec/nuageinit/tests/addsshkey.lua @@ -0,0 +1,5 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +n.addsshkey(".", "mykey") diff --git a/libexec/nuageinit/tests/adduser.lua b/libexec/nuageinit/tests/adduser.lua new file mode 100644 index 000000000000..cef6be0c0e0c --- /dev/null +++ b/libexec/nuageinit/tests/adduser.lua @@ -0,0 +1,16 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +if n.adduser() then + n.err("adduser should not accept empty value") +end +if n.adduser("plop") then + n.err("adduser should not accept empty value") +end +local pw = {} +pw.name = "impossible_username" +local res = n.adduser(pw) +if not res then + n.err("valid adduser should return a path") +end diff --git a/libexec/nuageinit/tests/adduser_passwd.lua b/libexec/nuageinit/tests/adduser_passwd.lua new file mode 100644 index 000000000000..e2d9395d679d --- /dev/null +++ b/libexec/nuageinit/tests/adduser_passwd.lua @@ -0,0 +1,20 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +local pw = {} +pw.name = "foo" +pw.plain_text_passwd = "bar" +local res = n.adduser(pw) +if not res then + n.err("valid user should return a path") +end + +local pw2 = {} +pw2.name = "foocrypted" +-- barcrypted +pw2.passwd = "$6$ZY8faYcEfyoEZnNX$FuAZA2SKhIfYLebhEtbmjptQNrenr6mJhji35Ru.zqdaa6G/gkKiHoQuh0vYZTKrjaykyohR8W4Q5ZF56yt8u1" +res = n.adduser(pw2) +if not res then + n.err("valid user should return a path") +end diff --git a/libexec/nuageinit/tests/dirname.lua b/libexec/nuageinit/tests/dirname.lua new file mode 100644 index 000000000000..7e3a2c835502 --- /dev/null +++ b/libexec/nuageinit/tests/dirname.lua @@ -0,0 +1,11 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +print(n.dirname("/my/path/path1")) +if n.dirname("path") then + n.err('Expecting nil for n.dirname("path")') +end +if n.dirname() then + n.err("Expecting nil for n.dirname") +end diff --git a/libexec/nuageinit/tests/err.lua b/libexec/nuageinit/tests/err.lua new file mode 100644 index 000000000000..567d4f2df66e --- /dev/null +++ b/libexec/nuageinit/tests/err.lua @@ -0,0 +1,5 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +n.err("plop") diff --git a/libexec/nuageinit/tests/nuage.sh b/libexec/nuageinit/tests/nuage.sh new file mode 100644 index 000000000000..57d83b62928a --- /dev/null +++ b/libexec/nuageinit/tests/nuage.sh @@ -0,0 +1,101 @@ +#- +# Copyright (c) 2022-2025 Baptiste Daroussin <bapt@FreeBSD.org> +# Copyright (c) 2025 Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org> +# +# SPDX-License-Identifier: BSD-2-Clause +# + +export NUAGE_FAKE_ROOTDIR="$PWD" + +atf_test_case sethostname +atf_test_case settimezone +atf_test_case addsshkey +atf_test_case adduser +atf_test_case adduser_passwd +atf_test_case addgroup +atf_test_case addfile + +settimezone_body() +{ + atf_check /usr/libexec/flua $(atf_get_srcdir)/settimezone.lua + if [ ! -f etc/localtime ]; then + atf_fail "localtime not written" + fi +} + +sethostname_body() +{ + atf_check /usr/libexec/flua $(atf_get_srcdir)/sethostname.lua + if [ ! -f etc/rc.conf.d/hostname ]; then + atf_fail "hostname not written" + fi + atf_check -o inline:"hostname=\"myhostname\"\n" cat etc/rc.conf.d/hostname +} + +addsshkey_body() +{ + atf_check /usr/libexec/flua $(atf_get_srcdir)/addsshkey.lua + if [ ! -f .ssh/authorized_keys ]; then + atf_fail "ssh key not added" + fi + atf_check -o inline:"40700\n" stat -f %p .ssh + atf_check -o inline:"100600\n" stat -f %p .ssh/authorized_keys + atf_check -o inline:"mykey\n" cat .ssh/authorized_keys + atf_check /usr/libexec/flua $(atf_get_srcdir)/addsshkey.lua + atf_check -o inline:"mykey\nmykey\n" cat .ssh/authorized_keys +} + +adduser_head() +{ + atf_set "require.user" root +} +adduser_body() +{ + mkdir etc + printf "root:*:0:0::0:0:Charlie &:/root:/bin/sh\n" > etc/master.passwd + pwd_mkdb -d etc etc/master.passwd + printf "wheel:*:0:root\n" > etc/group + atf_check -e inline:"nuageinit: Argument should be a table\nnuageinit: Argument should be a table\n" /usr/libexec/flua $(atf_get_srcdir)/adduser.lua + test -d home/impossible_username || atf_fail "home not created" + atf_check -o inline:"impossible_username::1001:1001::0:0:impossible_username User:/home/impossible_username:/bin/sh\n" grep impossible_username etc/master.passwd +} + +adduser_passwd_body() +{ + mkdir etc + printf "root:*:0:0::0:0:Charlie &:/root:/bin/sh\n" > etc/master.passwd + pwd_mkdb -d etc etc/master.passwd + printf "wheel:*:0:root\n" > etc/group + atf_check /usr/libexec/flua $(atf_get_srcdir)/adduser_passwd.lua + test -d home/foo || atf_fail "home not created" + passhash=`awk -F ':' '/^foo:/ {print $2}' etc/master.passwd` + atf_check -s exit:0 -o inline:$passhash \ + $(atf_get_srcdir)/crypt $passhash "bar" + passhash=`awk -F ':' '/^foocrypted:/ {print $2}' etc/master.passwd` + atf_check -s exit:0 -o inline:$passhash \ + $(atf_get_srcdir)/crypt $passhash "barcrypted" +} + +addgroup_body() +{ + mkdir etc + printf "wheel:*:0:root\n" > etc/group + atf_check -e inline:"nuageinit: Argument should be a table\nnuageinit: Argument should be a table\n" /usr/libexec/flua $(atf_get_srcdir)/addgroup.lua + atf_check -o inline:"impossible_groupname:*:1001:\n" grep impossible_groupname etc/group +} + +addfile_body() +{ + mkdir tmp + atf_check /usr/libexec/flua $(atf_get_srcdir)/addfile.lua +} + +atf_init_test_cases() +{ + atf_add_test_case sethostname + atf_add_test_case addsshkey + atf_add_test_case adduser + atf_add_test_case adduser_passwd + atf_add_test_case addgroup + atf_add_test_case addfile +} diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh new file mode 100644 index 000000000000..2b7c5226c97a --- /dev/null +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -0,0 +1,947 @@ +#- +# Copyright (c) 2022-2025 Baptiste Daroussin <bapt@FreeBSD.org> +# Copyright (c) 2025 Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org> +# +# 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_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_fqdn_and_hostname +atf_test_case config2_userdata_write_files + +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 <cloud-init-directory> (<config-2> | <nocloud>)\n" /usr/libexec/nuageinit + atf_check -s exit:1 -e inline:"Usage: /usr/libexec/nuageinit <cloud-init-directory> (<config-2> | <nocloud>)\n" /usr/libexec/nuageinit bla + atf_check -s exit:1 -e inline:"Usage: /usr/libexec/nuageinit <cloud-init-directory> (<config-2> | <nocloud>)\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 + 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 + 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_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 + 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 + 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 + 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 + 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 + 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 + 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_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 <<EOF +#!/we/dont/care +anything +EOF + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 + test -f var/cache/nuageinit/user_data || atf_fail "File not created" + test -x var/cache/nuageinit/user_data || atf_fail "Missing execution permission" + atf_check -o inline:"#!/we/dont/care\nanything\n" cat var/cache/nuageinit/user_data + cat > media/nuageinit/user_data <<EOF +/we/dont/care +EOF + rm var/cache/nuageinit/user_data + if [ -f var/cache/nuageinit/user_data ]; then + atf_fail "File should not have been created" + fi +} + +config2_userdata_write_files_body() +{ + mkdir -p media/nuageinit + setup_test_adduser + printf "{}" > media/nuageinit/meta_data.json + cat > media/nuageinit/user_data <<EOF +#cloud-config +write_files: +- content: "plop" + path: /file1 +- path: /emptyfile +- content: !!binary | + YmxhCg== + path: /file_base64 + encoding: b64 + permissions: '0755' + owner: nobody +- content: "bob" + path: "/foo" + defer: true +EOF + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 + atf_check -o inline:"plop" cat file1 + atf_check -o inline:"" cat emptyfile + atf_check -o inline:"bla\n" cat file_base64 + test -f foo && atf_fail "foo creation should have been defered" + atf_check -o match:"^-rwxr-xr-x.*nobody" ls -l file_base64 + rm file1 emptyfile file_base64 + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet + test -f file1 -o -f emptyfile -o -f file_base64 && atf_fail "defer not working properly" + atf_check -o inline:"bob" cat foo +} + +config2_userdata_fqdn_and_hostname_body() +{ + mkdir -p media/nuageinit + setup_test_adduser + printf "{}" > media/nuageinit/meta_data.json + cat > media/nuageinit/user_data <<EOF +#cloud-config +fqdn: host.domain.tld +hostname: host +EOF + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 + atf_check -o inline:"hostname=\"host.domain.tld\"\n" cat ${PWD}/etc/rc.conf.d/hostname + cat > media/nuageinit/user_data <<EOF +#cloud-config +hostname: host +EOF + atf_check -o empty /usr/libexec/nuageinit "${PWD}"/media/nuageinit config-2 + atf_check -o inline:"hostname=\"host\"\n" cat ${PWD}/etc/rc.conf.d/hostname +} + +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_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_fqdn_and_hostname + atf_add_test_case config2_userdata_write_files +} diff --git a/libexec/nuageinit/tests/sethostname.lua b/libexec/nuageinit/tests/sethostname.lua new file mode 100644 index 000000000000..47632497b545 --- /dev/null +++ b/libexec/nuageinit/tests/sethostname.lua @@ -0,0 +1,5 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +n.sethostname("myhostname") diff --git a/libexec/nuageinit/tests/settimezone.lua b/libexec/nuageinit/tests/settimezone.lua new file mode 100644 index 000000000000..a8cacf09f4e7 --- /dev/null +++ b/libexec/nuageinit/tests/settimezone.lua @@ -0,0 +1,5 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +n.settimezone("UTC") diff --git a/libexec/nuageinit/tests/utils.sh b/libexec/nuageinit/tests/utils.sh new file mode 100644 index 000000000000..76cd7e045473 --- /dev/null +++ b/libexec/nuageinit/tests/utils.sh @@ -0,0 +1,32 @@ +#- +# Copyright (c) 2022 Baptiste Daroussin <bapt@FreeBSD.org> +# Copyright (c) 2025 Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org> +# +# SPDX-License-Identifier: BSD-2-Clause +# + +atf_test_case warn +atf_test_case err +atf_test_case dirname + +warn_body() +{ + atf_check -e "inline:nuageinit: plop\n" -s exit:0 /usr/libexec/flua $(atf_get_srcdir)/warn.lua +} + +err_body() +{ + atf_check -e "inline:nuageinit: plop\n" -s exit:1 /usr/libexec/flua $(atf_get_srcdir)/err.lua +} + +dirname_body() +{ + atf_check -o "inline:/my/path/\n" -s exit:0 /usr/libexec/flua $(atf_get_srcdir)/dirname.lua +} + +atf_init_test_cases() +{ + atf_add_test_case warn + atf_add_test_case err + atf_add_test_case dirname +} diff --git a/libexec/nuageinit/tests/warn.lua b/libexec/nuageinit/tests/warn.lua new file mode 100644 index 000000000000..ce2b63a8dbf0 --- /dev/null +++ b/libexec/nuageinit/tests/warn.lua @@ -0,0 +1,5 @@ +#!/usr/libexec/flua + +local n = require("nuage") + +n.warn("plop") |