aboutsummaryrefslogtreecommitdiff
path: root/libexec/nuageinit
diff options
context:
space:
mode:
Diffstat (limited to 'libexec/nuageinit')
-rw-r--r--libexec/nuageinit/Makefile12
-rw-r--r--libexec/nuageinit/Makefile.depend10
-rw-r--r--libexec/nuageinit/nuage.lua651
-rwxr-xr-xlibexec/nuageinit/nuageinit719
-rw-r--r--libexec/nuageinit/nuageinit.7431
-rw-r--r--libexec/nuageinit/tests/Makefile22
-rw-r--r--libexec/nuageinit/tests/Makefile.depend10
-rw-r--r--libexec/nuageinit/tests/addfile.lua71
-rw-r--r--libexec/nuageinit/tests/addgroup.lua16
-rw-r--r--libexec/nuageinit/tests/addsshkey.lua5
-rw-r--r--libexec/nuageinit/tests/adduser.lua16
-rw-r--r--libexec/nuageinit/tests/adduser_passwd.lua20
-rw-r--r--libexec/nuageinit/tests/dirname.lua11
-rw-r--r--libexec/nuageinit/tests/err.lua5
-rw-r--r--libexec/nuageinit/tests/nuage.sh101
-rw-r--r--libexec/nuageinit/tests/nuageinit.sh947
-rw-r--r--libexec/nuageinit/tests/sethostname.lua5
-rw-r--r--libexec/nuageinit/tests/settimezone.lua5
-rw-r--r--libexec/nuageinit/tests/utils.sh32
-rw-r--r--libexec/nuageinit/tests/warn.lua5
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")