The branch main has been updated by dtxdf: URL: https://cgit.FreeBSD.org/src/commit/?id=ba5df7a2d03cd5624b1825ca8d4c39dcaace7796
commit ba5df7a2d03cd5624b1825ca8d4c39dcaace7796 Author: Jesús Daniel Colmenares Oviedo <dt...@freebsd.org> AuthorDate: 2025-08-22 18:14:18 +0000 Commit: Jesús Daniel Colmenares Oviedo <dt...@freebsd.org> CommitDate: 2025-08-22 18:40:36 +0000 nuageinit: Improvements for nuageinit - Fix 'pkg update' usage: - The function 'nuage:run_pkg_cmd(...)' adds the flag '-y', which does not make sense with some commands such as 'pkg update', causing an error when updating the repository catalogs. - Fix typo 'ssh-authorized-keys -> ssh_authorized_keys' in 'nuageinit(7)' man page. - Document 'ssh_authorized_keys' parameter. - Use device configuration ID when no 'match' rule is specified: - This is the default behavior of cloud-init when no match rule is specified, so the device is configured anyway (even if it does not exist). This greatly simplifies things, since in many cases 'if_vtnet(4)' is used, so there is no need to perform a comparison with the MAC address. - Document 'network' parameter: - Add example to 'EXAMPLES' section. - Set 'gateway[46]' only when 'addresses' is specified: - To comply with the cloud-init specification, 'gateway4' and 'gateway6' must only take effect when 'addresses' (or static configuration) is specified. - Use a separate function to check 'match' rules: - This way, we can easily add new logic to new types of rules. - Implement 'network.ethernets.{id}.match.name' parameter: - But unlike cloud-init, which works with glob expressions (although it depends on the network backend), this implementation takes advantage of Lua pattern-matching expressions. Also note that previously we were only concerned with one interface matching, however, to be cloud-init-compliant, we need to configure the matching interfaces (one or more). - Set default router only once. - Implement 'network.ethernets.{id}.wakeonlan' parameter. - Implement 'network.ethernets.{id}.set-name' parameter. - Implement 'network.ethernets.{id}.match.driver' parameter: - Rename 'get_ifaces(...)' function as 'get_ifaces_by_mac(...)'. - Add get_ifaces_by_driver(...) function. - Implement 'network.ethernets.{id}.mtu' parameter. - Implement 'nameservers' parameter. - Use 'resolvconf(8)' to manipulate 'resolv.conf(5)'. - Use 'tzsetup(8)' to set time zone. Reviewed by: bapt@ Approved by: bapt@ Differential Revision: https://reviews.freebsd.org/D51643 --- libexec/nuageinit/nuage.lua | 20 ++- libexec/nuageinit/nuageinit | 238 ++++++++++++++++++++++++++++---- libexec/nuageinit/nuageinit.7 | 92 +++++++++++- libexec/nuageinit/tests/Makefile | 1 + libexec/nuageinit/tests/nuage.sh | 9 ++ libexec/nuageinit/tests/nuageinit.sh | 4 +- libexec/nuageinit/tests/settimezone.lua | 5 + 7 files changed, 335 insertions(+), 34 deletions(-) diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua index 493ae11d6ca7..48f54b120615 100644 --- a/libexec/nuageinit/nuage.lua +++ b/libexec/nuageinit/nuage.lua @@ -451,6 +451,23 @@ local function chpasswd(obj) 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 @@ -480,7 +497,7 @@ local function install_package(package) end local function run_pkg_cmd(subcmd) - local cmd = "pkg " .. subcmd .. " -y" + local cmd = "env ASSUME_ALWAYS_YES=yes pkg " .. subcmd if os.getenv("NUAGE_RUN_TESTS") then print(cmd) return true @@ -556,6 +573,7 @@ local n = { dirname = dirname, mkdir_p = mkdir_p, sethostname = sethostname, + settimezone = settimezone, adduser = adduser, addgroup = addgroup, addsshkey = addsshkey, diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit index 0fcdc7274db3..70b27cb33d87 100755 --- a/libexec/nuageinit/nuageinit +++ b/libexec/nuageinit/nuageinit @@ -46,7 +46,15 @@ local function open_config(name) return openat("/etc/rc.conf.d", name) end -local function get_ifaces() +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") @@ -77,6 +85,10 @@ local function sethostname(obj) end end +local function settimezone(obj) + nuage.settimezone(obj.timezone) +end + local function groups(obj) if obj.groups == nil then return end @@ -171,6 +183,59 @@ local function ssh_authorized_keys(obj) 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") @@ -187,6 +252,85 @@ local function install_packages(packages) 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 @@ -210,41 +354,76 @@ end 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 + 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 - if not v.match.macaddress then - goto next + local extra_opts = "" + if v.wakeonlan then + extra_opts = extra_opts .. " wol" end - if not ifaces[v.match.macaddress] then - nuage.warn("not interface matching: " .. v.match.macaddress) - goto next + 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 - 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 + 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 - 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 @@ -316,7 +495,7 @@ local function config2_network(p) end local obj = parser:get_object() - local ifaces = get_ifaces() + local ifaces = get_ifaces_by_mac() if not ifaces then nuage.warn("no network interfaces found") return @@ -468,6 +647,7 @@ f:close() if line == "#cloud-config" then local pre_network_calls = { sethostname, + settimezone, groups, create_default_user, ssh_keys, diff --git a/libexec/nuageinit/nuageinit.7 b/libexec/nuageinit/nuageinit.7 index 327ce160e151..604d8a2221ca 100644 --- a/libexec/nuageinit/nuageinit.7 +++ b/libexec/nuageinit/nuageinit.7 @@ -143,6 +143,11 @@ Specify a fully qualified domain name for the instance. 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 @@ -176,6 +181,81 @@ boolean which determines the value of the configuration in .Pa /etc/ssh/sshd_config .It Ic network +Network configuration parameters. +.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 @@ -186,7 +266,7 @@ Update the remote package metadata. 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" +.Bl -tag -width "ssh_authorized_keys" .It Ic name Name of the user. .It Ic gecos @@ -201,6 +281,8 @@ The list of other groups the user should belong to. 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 @@ -287,7 +369,7 @@ users: - name: user gecos: Foo B. Bar sudo: ALL=(ALL) NOPASSWD:ALL - ssh-authorized-keys: + ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr... packages: - neovim @@ -303,6 +385,12 @@ ssh_keys: ... -----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 , diff --git a/libexec/nuageinit/tests/Makefile b/libexec/nuageinit/tests/Makefile index c69bc28a4c86..dc8997717b59 100644 --- a/libexec/nuageinit/tests/Makefile +++ b/libexec/nuageinit/tests/Makefile @@ -15,6 +15,7 @@ ${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 diff --git a/libexec/nuageinit/tests/nuage.sh b/libexec/nuageinit/tests/nuage.sh index 56651c8c5bb7..b709d25532ff 100644 --- a/libexec/nuageinit/tests/nuage.sh +++ b/libexec/nuageinit/tests/nuage.sh @@ -7,12 +7,21 @@ 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 diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh index 849f1c258b62..98593f7d75b0 100644 --- a/libexec/nuageinit/tests/nuageinit.sh +++ b/libexec/nuageinit/tests/nuageinit.sh @@ -815,7 +815,7 @@ config2_userdata_update_packages_body() 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 + atf_check -o inline:"env ASSUME_ALWAYS_YES=yes pkg update\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_upgrade_packages_body() @@ -829,7 +829,7 @@ config2_userdata_upgrade_packages_body() 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 + atf_check -o inline:"env ASSUME_ALWAYS_YES=yes pkg upgrade\n" /usr/libexec/nuageinit "${PWD}"/media/nuageinit postnet } config2_userdata_shebang_body() 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")