diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua index deb441ee25ba..cdc0fc6cf2a7 100644 --- a/libexec/nuageinit/nuage.lua +++ b/libexec/nuageinit/nuage.lua @@ -1,462 +1,548 @@ --- -- SPDX-License-Identifier: BSD-2-Clause -- -- Copyright(c) 2022-2025 Baptiste Daroussin local unistd = require("posix.unistd") local sys_stat = require("posix.sys.stat") local lfs = require("lfs") +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 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 sys_stat.chmod(ak_path, 384) unistd.chown(ak_path, dirattrs.uid, dirattrs.gid) end if chowndotssh then sys_stat.chmod(dotssh_path, 448) unistd.chown(dotssh_path, dirattrs.uid, dirattrs.gid) end end local function addsudo(pwd) local chmodsudoersd = false local chmodsudoers = false local root = os.getenv("NUAGE_FAKE_ROOTDIR") local sudoers_dir = "/usr/local/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 f:write(pwd.name .. " " .. pwd.sudo .. "\n") f:close() if chmodsudoers then sys_stat.chmod(sudoers, 416) end if chmodsudoersd then sys_stat.chmod(sudoers, 480) 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 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 = "pkg " .. subcmd .. " -y" 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 + -- convert from octal to decimal + local perm = tonumber(file.permissions, 8) + sys_stat.chmod(file.path, perm) + end + if file.owner then + local owner, group = string.match(file.owner, "([^:]+):([^:]+)") + unistd.chown(file.path, owner, group) + end + return true +end + local n = { warn = warnmsg, err = errmsg, dirname = dirname, mkdir_p = mkdir_p, sethostname = sethostname, 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 + addsudo = addsudo, + addfile = addfile } return n diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index 5af1b84c1848..84133d4373c5 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -1,486 +1,507 @@ #!/usr/libexec/flua --- -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD -- -- Copyright(c) 2022-2025 Baptiste Daroussin local nuage = require("nuage") local ucl = require("ucl") local yaml = require("lyaml") local sys_stat = require("posix.sys.stat") if #arg ~= 2 then nuage.err("Usage: " .. arg[0] .. " ( | )", false) end local ni_path = arg[1] local citype = arg[2] local default_user = { name = "freebsd", homedir = "/home/freebsd", groups = "wheel", gecos = "FreeBSD User", shell = "/bin/sh", plain_text_passwd = "freebsd" } local 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 get_ifaces() 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("") 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 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 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 sys_stat.chmod(path, 384) 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 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 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 ifaces = get_ifaces() local network = open_config("network") local routing = open_config("routing") local ipv6 = {} for _, v in pairs(obj.network.ethernets) do if not v.match then goto next end if not v.match.macaddress then goto next end if not ifaces[v.match.macaddress] then nuage.warn("not interface matching: " .. v.match.macaddress) goto next end local interface = ifaces[v.match.macaddress] if v.dhcp4 then network:write("ifconfig_" .. interface .. '="DHCP"\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 .. '"\n') else network:write("ifconfig_" .. interface .. '_ipv6="inet6 ' .. a .. '"\n') ipv6[#ipv6 + 1] = interface end end end if v.gateway4 then routing:write('defaultrouter="' .. v.gateway4 .. '"\n') end if v.gateway6 then routing:write('ipv6_defaultrouter="' .. v.gateway6 .. '"\n') routing:write("ipv6_route_" .. interface .. '="' .. v.gateway6) routing:write(" -prefixlen 128 -interface " .. interface .. '"\n') 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() sys_stat.chmod(root .. "/var/cache/nuageinit/runcmds", 493) 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() 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 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, groups, create_default_user, ssh_keys, ssh_authorized_keys, network_config, ssh_pwauth, - runcmd + runcmd, + write_files_not_defered, } local post_network_calls = { packages, users, - chpasswd + 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 calls_table[i](obj) end elseif line:sub(1, 2) == "#!" then -- delay for execution at rc.local time -- sys_stat.chmod(root .. "/var/cache/nuageinit/user_data", 493) end diff --git a/libexec/nuageinit/nuageinit.7 b/libexec/nuageinit/nuageinit.7 index 1d2f83fe62e0..3bb440ebac95 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -1,286 +1,322 @@ .\" SPDX-License-Identifier: BSD-2-Clause .\" .\" Copyright (c) 2025 Baptiste Daroussin .\" -.Dd June 16, 2025 +.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 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 .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 "plain_text_passwd" .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 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 groups The list of other groups the user should belong to. .It Ic locked Boolean to determine if the user account should be locked. .It Ic sudo An entry which should be appended to .Pa /usr/local/etc/sudoers.d/90-nuageinit-users .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 +Specifiy 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+ .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 index bb2f0d7c747e..c69bc28a4c86 100644 --- a/libexec/nuageinit/tests/Makefile +++ b/libexec/nuageinit/tests/Makefile @@ -1,20 +1,21 @@ 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+= warn.lua +${PACKAGE}FILES+= addfile.lua .include 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/nuage.sh b/libexec/nuageinit/tests/nuage.sh index f2753d6d91e6..56651c8c5bb7 100644 --- a/libexec/nuageinit/tests/nuage.sh +++ b/libexec/nuageinit/tests/nuage.sh @@ -1,83 +1,91 @@ #- -# Copyright (c) 2022 Baptiste Daroussin +# Copyright (c) 2022-2025 Baptiste Daroussin # # SPDX-License-Identifier: BSD-2-Clause # export NUAGE_FAKE_ROOTDIR="$PWD" atf_test_case sethostname atf_test_case addsshkey atf_test_case adduser atf_test_case adduser_passwd atf_test_case addgroup +atf_test_case addfile 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 index 44830f67e4c8..639c87181f95 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -1,895 +1,930 @@ #- -# Copyright (c) 2022 Baptiste Daroussin +# Copyright (c) 2022-2025 Baptiste Daroussin # # 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 ( | )\n" /usr/libexec/nuageinit atf_check -s exit:1 -e inline:"Usage: /usr/libexec/nuageinit ( | )\n" /usr/libexec/nuageinit bla atf_check -s exit:1 -e inline:"Usage: /usr/libexec/nuageinit ( | )\n" /usr/libexec/nuageinit bla meh plop atf_check -s exit:1 -e inline:"nuageinit: Unknown cloud init type: meh\n" /usr/libexec/nuageinit bla meh } nocloud_body() { mkdir -p media/nuageinit 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 groups: users passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/ 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: 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 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 atf_check -o inline:"foobar ALL=(ALL) NOPASSWD:ALL\n" cat ${PWD}/usr/local/etc/sudoers.d/90-nuageinit-users } 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:"pkg update -y\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:"pkg upgrade -y\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_shebang_body() { mkdir -p media/nuageinit setup_test_adduser printf "{}" > media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/user_data < media/nuageinit/meta_data.json + cat > media/nuageinit/user_data < media/nuageinit/meta_data.json cat > media/nuageinit/user_data < media/nuageinit/user_data <