diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index c94eb822ba0f..5541f6d0f164 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -1,687 +1,716 @@ #!/usr/libexec/flua --- -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD -- -- Copyright(c) 2022-2025 Baptiste Daroussin -- Copyright(c) 2025 Jesús Daniel Colmenares Oviedo local nuage = require("nuage") local ucl = require("ucl") local yaml = require("lyaml") if #arg ~= 2 then nuage.err("Usage: " .. arg[0] .. " ( | )", false) end local ni_path = arg[1] local citype = arg[2] local 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("") 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 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 - calls_table[i](obj) + 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 index f02829618f44..8d9aac3d3809 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -1,409 +1,416 @@ .\" SPDX-License-Identifier: BSD-2-Clause .\" .\" Copyright (c) 2025 Baptiste Daroussin .\" Copyright (c) 2025 Jesús Daniel Colmenares Oviedo .\" .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 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+ 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