Files
freebsd/libexec/nuageinit/nuageinit
T
Navdeep Parhar a209aac868 nuageinit: root should be allowed to log in when disable_root is false
PermitRootLogin is "no" by default and that stopped root from logging in
even though disable_root was set to false during initialization.

Reviewed by:	bapt
Sponsored by:	Chelsio Communications
Differential Revision:	https://reviews.freebsd.org/D59101
2026-08-21 23:58:28 -07:00

1238 lines
32 KiB
Plaintext
Executable File

#!/usr/libexec/flua
---
-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
--
-- Copyright(c) 2022-2025 Baptiste Daroussin <[email protected]>
-- Copyright(c) 2025 Jesús Daniel Colmenares Oviedo <[email protected]>
local nuage = require("nuage")
local lfs = require("lfs")
local ucl = require("ucl")
local yaml = require("lyaml")
if #arg ~= 2 then
nuage.err("Usage: " .. arg[0] .. " <cloud-init-directory> (<config-2> | <nocloud>)", false)
end
local ni_path = arg[1]
local citype = arg[2]
local function default_user(obj, metadata)
local ssh_authorized_keys = {}
if (not obj or obj.allow_public_ssh_keys ~= false) and
type(metadata.public_keys) == "table" then
for _, k in pairs(metadata.public_keys) do
table.insert(ssh_authorized_keys, k)
end
end
if obj and type(obj.ssh_authorized_keys) == "table" then
for _, k in ipairs(obj.ssh_authorized_keys) do
table.insert(ssh_authorized_keys, k)
end
end
return {
name = "freebsd",
homedir = "/home/freebsd",
groups = "wheel",
gecos = "FreeBSD User",
shell = "/bin/sh",
plain_text_passwd = "freebsd",
ssh_authorized_keys = ssh_authorized_keys
}
end
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()
local path_dir = root .. "/etc"
local path_name = path_dir .. "/resolvconf.conf"
nuage.mkdir_p(path_dir)
local f, err = io.open(path_name, "a")
if not f then
nuage.err("unable to open " .. path_name .. ": " .. err)
end
return f, path_name
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 output: " .. err)
return nil
end
local ifaces = parser:get_object()
local myifaces = {}
for _, iface in pairs(ifaces["statistics"]["interface"]) do
if iface["network"]:match("<Link#%d>") then
local s = iface["address"]
myifaces[s:lower()] = iface["name"]
end
end
return myifaces
end
local function resolve_hostname(obj, metadata)
if obj and obj.fqdn then
return obj.fqdn
elseif obj and obj.hostname then
return obj.hostname
elseif metadata["local-hostname"] then
return metadata["local-hostname"]
elseif metadata["hostname"] then
return metadata["hostname"]
end
return nil
end
local function sethostname(obj, metadata)
local hostname = resolve_hostname(obj, metadata)
if hostname then
nuage.sethostname(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, metadata)
local function need_default_user()
-- no user data
if not obj then
return true
end
if not obj.users then
-- default user if "users" is undefined
return true
end
-- create default user if "default" is in the users list
for _, u in pairs(obj.users) do
if type(u) == "string" and u == "default" then
return true
end
end
return false
end
if need_default_user() then
local du = default_user(obj, metadata)
local homedir = nuage.adduser(du)
if du.ssh_authorized_keys then
for _, k in ipairs(du.ssh_authorized_keys) do
nuage.addsshkey(homedir, k)
end
end
end
end
local function users(obj, metadata)
if obj.users == nil then return end
for n, u in pairs(obj.users) do
if type(u) == "string" then
if u == "default" then
-- already done during create_default_user
nuage.adduser(default_user(obj, metadata))
else
nuage.adduser({name = u})
end
elseif type(u) == "table" then
-- ignore users without a username
if u.name == nil then
goto unext
end
local homedir = nuage.adduser(u)
if u.ssh_authorized_keys then
for _, v in ipairs(u.ssh_authorized_keys) do
nuage.addsshkey(homedir, v)
end
end
if u.sudo then
nuage.addsudo(u)
end
if u.doas then
nuage.adddoas(u)
end
else
nuage.warn("invalid type : " .. type(u) .. " for users entry number " .. n)
end
::unext::
end
end
local function ssh_keys(obj)
if obj.ssh_keys == nil then return end
if type(obj.ssh_keys) ~= "table" then
nuage.warn("Invalid type for ssh_keys")
return
end
for key, val in pairs(obj.ssh_keys) do
for keyname, keytype in key:gmatch("(%w+)_(%w+)") do
local sshkn = nil
if keytype == "public" then
sshkn = "ssh_host_" .. keyname .. "_key.pub"
elseif keytype == "private" then
sshkn = "ssh_host_" .. keyname .. "_key"
end
if sshkn then
local sshkey, path = open_ssh_key(sshkn)
if sshkey then
sshkey:write(val .. "\n")
sshkey:close()
end
if keytype == "private" then
nuage.chmod(path, "0600")
end
end
end
end
end
local function nameservers(interface, obj)
local resolvconf_conf_handler = open_resolvconf_conf()
if obj.search then
resolvconf_conf_handler:write("search_domains=" .. nuage.shell_escape(table.concat(obj.search, " ")) .. "\n")
end
if obj.addresses then
resolvconf_conf_handler:write("name_servers=" .. nuage.shell_escape(table.concat(obj.addresses, " ")) .. "\n")
end
resolvconf_conf_handler:close()
local resolv_conf = root .. "/etc/resolv.conf"
local resolv_conf_attr = lfs.attributes(resolv_conf)
if resolv_conf_attr == nil then
local resolv_conf_handler = open_resolv_conf()
resolv_conf_handler:close()
end
-- Only call resolvconf with interface if interface is provided
local resolvconf_command
if interface then
resolvconf_command = "resolvconf -a " .. nuage.shell_escape(interface) .. " < " .. resolv_conf
else
resolvconf_command = "resolvconf -u"
end
if not os.execute(resolvconf_command) 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 interface
local interfaces = {}
if rules.macaddress then
local ifaces = get_ifaces_by_mac()
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_deferred(obj)
write_files(obj.write_files, false)
end
local function write_files_deferred(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
local mtu
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
if not v["set-name"]:match("^[a-zA-Z0-9]+$") then
nuage.warn("set-name contains invalid characters, ignoring: " .. v["set-name"])
else
network:write("ifconfig_" .. interface .. "_name=" .. nuage.shell_escape(v["set-name"]) .. "\n")
interface = v["set-name"]
end
end
end
if v.dhcp4 then
network:write("ifconfig_" .. interface .. "=" .. nuage.shell_escape("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 .. "=" .. nuage.shell_escape("inet " .. a .. extra_opts) .. "\n")
else
network:write("ifconfig_" .. interface .. "_ipv6=" .. nuage.shell_escape("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=" .. nuage.shell_escape(v.gateway4) .. "\n")
end
if v.gateway6 then
if set_defaultrouter6 then
set_defaultrouter6 = false
routing:write("ipv6_defaultrouter=" .. nuage.shell_escape(v.gateway6) .. "\n")
end
routing:write("ipv6_route_" .. interface .. "=" .. nuage.shell_escape(v.gateway6 .. " -prefixlen 128 -interface " .. interface) .. "\n")
end
end
end
::next::
end
if #ipv6 > 0 then
network:write("ipv6_network_interfaces=" .. nuage.shell_escape(table.concat(ipv6, " ")) .. "\n")
network:write("ipv6_default_interface=" .. nuage.shell_escape(ipv6[1]) .. "\n")
end
network:close()
routing:close()
end
local function ssh_pwauth(obj)
if obj.ssh_pwauth == nil then return end
local value
if type(obj.ssh_pwauth) == "boolean" then
value = obj.ssh_pwauth and "yes" or "no"
elseif type(obj.ssh_pwauth) == "string" then
local s = obj.ssh_pwauth:lower()
if s == "yes" or s == "true" or s == "1" or s == "on" then
value = "yes"
elseif s == "no" or s == "false" or s == "0" or s == "off" then
value = "no"
elseif s == "unchanged" then
return
else
nuage.warn("ssh_pwauth: unrecognized value '" ..
obj.ssh_pwauth .. "', leaving unchanged")
return
end
else
nuage.warn("ssh_pwauth: invalid type " .. type(obj.ssh_pwauth))
return
end
nuage.update_sshd_config("PasswordAuthentication", value)
end
local function ssh_authkey_fingerprints(obj)
if obj.ssh_authkey_fingerprints ~= true then return end
local ssh_dir = root .. "/etc/ssh"
local attrs = lfs.attributes(ssh_dir)
if not attrs or attrs.mode ~= "directory" then
return
end
for entry in lfs.dir(ssh_dir) do
if entry:match("^ssh_host_.*_key$") then
local keypath = ssh_dir .. "/" .. entry
local f = io.popen("ssh-keygen -l -f " ..
nuage.shell_escape(keypath), "r")
if f then
local fingerprint = f:read("*a")
f:close()
if fingerprint and fingerprint ~= "" then
io.stderr:write(fingerprint)
end
end
end
end
end
local function ntp(obj)
if obj.ntp == nil then return end
if type(obj.ntp) == "table" and obj.ntp.enabled == false then
return
end
local servers = {}
if type(obj.ntp) == "table" then
if obj.ntp[1] then
servers = obj.ntp
else
for _, s in ipairs(obj.ntp.servers or {}) do
table.insert(servers, "server " .. s .. " iburst")
end
for _, p in ipairs(obj.ntp.pools or {}) do
table.insert(servers, "pool " .. p .. " iburst")
end
end
elseif type(obj.ntp) == "string" then
table.insert(servers, "server " .. obj.ntp .. " iburst")
end
if #servers == 0 then return end
local path = root .. "/etc/ntp.conf"
local f = io.open(path, "w")
if not f then
warnmsg("unable to open " .. path .. " for writing")
return
end
for _, line in ipairs(servers) do
f:write(line .. "\n")
end
f:write("leapfile /var/db/ntpd.leap-seconds.list\n")
f:write("restrict default limited kod nomodify notrap nopeer noquery\n")
f:write("restrict source limited kod nomodify notrap noquery\n")
f:close()
end
local function ca_certs(obj)
if obj.ca_certs == nil then return end
local trusted = obj.ca_certs.trusted
if trusted == nil or #trusted == 0 then return end
local certdir = root .. "/usr/share/certs/trusted"
nuage.mkdir_p(certdir)
for i, cert in ipairs(trusted) do
local certpath = certdir .. "/nuageinit-" .. i .. ".pem"
local f = io.open(certpath, "w")
if f then
f:write(cert .. "\n")
f:close()
else
warnmsg("unable to write " .. certpath)
end
end
os.execute("certctl rehash 2>/dev/null")
end
local function ssh_deletekeys(obj)
if obj.ssh_deletekeys == nil then return end
if obj.ssh_deletekeys then
nuage.delete_ssh_host_keys(root)
end
end
local DISABLE_USER_OPTS = 'no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command="echo \'Please login as the user \\"$USER\\" rather than the user \\"$DISABLE_USER\\".\';echo;sleep 10;exit 142"'
local function disable_root(obj, metadata)
-- cloud-init semantics: restrict root's authorized_keys options
local disable = true
if obj.disable_root ~= nil then
disable = obj.disable_root
end
local opts = ""
if disable then
opts = obj.disable_root_opts or DISABLE_USER_OPTS
if type(opts) == "table" then
opts = opts[1]
end
opts = opts:gsub("%$USER", "freebsd")
opts = opts:gsub("%$DISABLE_USER", "root")
else
nuage.update_sshd_config("PermitRootLogin", "prohibit-password")
end
local keys = {}
if obj.allow_public_ssh_keys ~= false and
type(metadata.public_keys) == "table" then
for _, k in pairs(metadata.public_keys) do
table.insert(keys, k)
end
end
if obj and type(obj.ssh_authorized_keys) == "table" then
for _, k in ipairs(obj.ssh_authorized_keys) do
table.insert(keys, k)
end
end
for _, k in ipairs(keys) do
nuage.addsshkey("/root", k, opts)
end
end
local function resolv_conf(obj)
if obj.resolv_conf == nil then return end
nuage.write_resolv_conf(root, obj.resolv_conf)
end
local function keyboard(obj)
if obj.keyboard == nil then return end
local layout = obj.keyboard.layout
local variant = obj.keyboard.variant
if layout == nil then
warnmsg("keyboard: missing layout")
return
end
local keymap = layout
if variant then
keymap = layout .. "." .. variant
end
local path = root .. "/etc/rc.conf.d/keymap"
nuage.mkdir_p(root .. "/etc/rc.conf.d")
local f = io.open(path, "w")
if not f then
warnmsg("unable to open " .. path .. " for writing")
return
end
f:write("keymap=" .. nuage.shell_escape(keymap) .. "\n")
f:close()
end
local function locale(obj)
if obj.locale == nil then return end
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
if not root then root = "" end
local profile_path = root .. "/etc/profile"
local f = io.open(profile_path, "a")
if not f then
nuage.warn("unable to open " .. profile_path .. " for writing")
return
end
if type(obj.locale) == "string" then
f:write("export LANG=" .. nuage.shell_escape(obj.locale) .. "\n")
elseif type(obj.locale) == "table" then
for k, v in pairs(obj.locale) do
if not k:match("^[a-zA-Z_][a-zA-Z0-9_]*$") then
nuage.warn("locale: invalid variable name '" .. k .. "', skipping")
else
f:write("export " .. k .. "=" .. nuage.shell_escape(v) .. "\n")
end
end
else
nuage.warn("locale: invalid type " .. type(obj.locale) .. ", expecting string or object")
end
f:close()
end
local function mounts(obj)
if obj.mounts == nil then return end
for _, m in ipairs(obj.mounts) do
local device, mount_point, fstype, options, dump_freq, passno
if type(m) == "table" then
if m[1] then
-- List format: [device, mount_point, fstype, options, dump_freq, passno]
device = m[1]
mount_point = m[2]
fstype = m[3]
options = m[4]
dump_freq = tonumber(m[5]) or 0
passno = tonumber(m[6]) or 0
else
-- Dict format
device = m.name or m.device or m.spec
mount_point = m.mount_point or m.mountpoint
fstype = m.type or m.filesystem or m.fstype
options = m.options or m.opts
dump_freq = tonumber(m.dump) or 0
passno = tonumber(m.passno) or tonumber(m.pass_no) or 0
end
end
if device and mount_point and fstype then
nuage.mkdir_p(root .. mount_point)
nuage.add_fstab_entry(root, device, mount_point, fstype, options, dump_freq, passno)
else
warnmsg("Invalid mount entry, skipping")
end
end
end
local function bootcmd(obj)
if obj.bootcmd == nil then return end
local f = nil
for _, c in ipairs(obj.bootcmd) do
if f == nil then
nuage.mkdir_p(root .. "/var/cache/nuageinit")
f = assert(io.open(root .. "/var/cache/nuageinit/bootcmds", "w"))
f:write("#!/bin/sh\n")
end
f:write(c .. "\n")
end
if f ~= nil then
f:close()
nuage.chmod(root .. "/var/cache/nuageinit/bootcmds", "0755")
end
end
local function manage_etc_hosts(obj, metadata)
if obj.manage_etc_hosts == false then
return
end
local hostname = resolve_hostname(obj, metadata)
if hostname then
nuage.update_etc_hosts(root, hostname)
end
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 power_state_change(obj)
if obj.power_state == nil then return end
local ps = obj.power_state
local delay = ps.delay or "now"
local mode = ps.mode or "poweroff"
local message = ps.message
local condition = ps.condition
if condition == nil then condition = true end
-- Evaluate condition
if condition == false then return end
if type(condition) == "string" then
local ret = os.execute(condition)
if not ret then return end
end
-- Map mode to shutdown flag
local mode_map = {poweroff = "p", reboot = "r", halt = "h"}
local flag = mode_map[mode]
if not flag then
nuage.warn("power_state: invalid mode '" .. mode .. "', using poweroff")
flag = "p"
end
-- Build shutdown command
local cmd = "shutdown -" .. flag .. " " .. nuage.shell_escape(delay)
if message then
cmd = cmd .. " " .. nuage.shell_escape(message)
end
if os.getenv("NUAGE_RUN_TESTS") then
print(cmd)
return
end
os.execute(cmd)
end
local function get_uptime()
-- Get system uptime in seconds
local f = io.popen("sysctl -n kern.boottime 2>/dev/null")
local boottime = f:read("*a")
f:close()
local sec = boottime:match("sec = (%d+)")
if sec then
return os.time() - tonumber(sec)
end
return 0
end
local function final_message(obj)
if obj.final_message == nil then return end
local msg = obj.final_message
if type(msg) ~= "string" then
nuage.warn("final_message must be a string")
return
end
local uptime = tostring(math.floor(get_uptime()))
msg = msg:gsub("%$UPTIME", uptime)
nuage.warn(msg, false)
end
local function phone_home(obj, metadata)
if obj.phone_home == nil then return end
local ph = obj.phone_home
if type(ph) ~= "table" then
nuage.warn("phone_home must be an object")
return
end
if not ph.url then
nuage.warn("phone_home.url is required")
return
end
local url = ph.url
local tries = ph.tries or 1
if type(tries) ~= "number" or tries < 1 then
tries = 1
end
-- Collect data to post
local data = {}
local post = ph.post
if post == "all" then
post = {"pub_key_rsa", "pub_key_ecdsa", "pub_key_ed25519",
"instance_id", "hostname", "fqdn"}
end
if type(post) == "table" then
for _, key in ipairs(post) do
if key == "hostname" then
if metadata.hostname then
table.insert(data, "hostname=" .. metadata.hostname)
end
elseif key == "fqdn" then
if metadata.hostname then
table.insert(data, "fqdn=" .. metadata.hostname)
end
elseif key == "instance_id" then
if metadata.uuid then
table.insert(data, "instance_id=" .. metadata.uuid)
end
elseif key:match("^pub_key_") then
local algo = key:match("^pub_key_(.+)$")
if metadata.public_keys then
for _, k in ipairs(metadata.public_keys) do
if algo == "rsa" and k:match("^ssh%-rsa ") then
table.insert(data, key .. "=" .. nuage.encode_base64(k))
elseif algo == "ecdsa" and k:match("^ecdsa%-") then
table.insert(data, key .. "=" .. nuage.encode_base64(k))
elseif algo == "ed25519" and k:match("^ssh%-ed25519 ") then
table.insert(data, key .. "=" .. nuage.encode_base64(k))
end
end
end
end
end
end
local post_data = table.concat(data, "&")
local cmd = "fetch -q -o /dev/null --post-data " .. nuage.shell_escape(post_data)
cmd = cmd .. " " .. nuage.shell_escape(url)
if os.getenv("NUAGE_RUN_TESTS") then
print(cmd)
return
end
for i = 1, tries do
if os.execute(cmd) then
break
end
if i < tries then
-- wait 1 second before retrying
os.execute("sleep 1")
end
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 .. "=" .. nuage.shell_escape("DHCP") .. "\n")
end
if v["type"] == "ipv4" then
network:write(
"ifconfig_" .. interface .. "=" .. nuage.shell_escape("inet " .. v["ip_address"] .. " netmask " .. v["netmask"]) .. "\n"
)
if v["gateway"] then
routing:write("defaultrouter=" .. nuage.shell_escape(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=" .. nuage.shell_escape(r["gateway"]) .. "\n")
goto next
end
routing:write("route_" .. rname .. "=" .. nuage.shell_escape("-net " .. r["network"] .. " " .. 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=" .. nuage.shell_escape("inet6 " .. v["ip_address"]) .. "\n")
if v["gateway"] then
routing:write("ipv6_defaultrouter=" .. nuage.shell_escape(v["gateway"]) .. "\n")
routing:write("ipv6_route_" .. interface .. "=" .. nuage.shell_escape(v["gateway"] .. " -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
-- Handle global nameservers from services section
if obj["services"] then
local dns_servers = {}
for _, service in pairs(obj["services"]) do
if service["type"] == "dns" then
table.insert(dns_servers, service["address"])
end
end
if #dns_servers > 0 then
-- Use nameservers() function for global services
local nameserver_config = {addresses = dns_servers}
nameservers(nil, nameserver_config)
end
end
if #ipv4 > 0 then
routing:write("static_routes=" .. nuage.shell_escape(table.concat(ipv4, " ")) .. "\n")
end
if #ipv6 > 0 then
network:write("ipv6_network_interfaces=" .. nuage.shell_escape(table.concat(ipv6, " ")) .. "\n")
network:write("ipv6_default_interface=" .. nuage.shell_escape(ipv6[1]) .. "\n")
end
if #ipv6_routes > 0 then
routing:write("ipv6_static_routes=" .. nuage.shell_escape(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
local function load_metadata(citype)
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()
return obj
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
return obj
elseif citype == "postnet" then
-- reload metadata: try config-2 format first, then nocloud
local parser = ucl.parser()
local res, err = parser:parse_file(ni_path .. "/meta_data.json")
if res then
return parser:get_object()
end
local f = io.open(ni_path .. "/meta-data")
if f then
local obj = yaml.load(f:read("*a"))
f:close()
if obj then return obj end
end
return {}
else
nuage.err("Unknown cloud init type: " .. citype)
end
end
local function load_userdata()
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
return nil, nil
end
local line = f:read("*l")
if not line or #string.gsub(line, "^%s*(.-)%s*$", "%1") == 0 then
f:close()
return
end
if line:match("^Content%-Type: multipart/") then
local rest = f:read("*a")
f:close()
local full = line .. "\n" .. rest
local parts = nuage.parse_mime_multipart(full)
if parts then
local cc_body = nil
for _, p in ipairs(parts) do
if p.content_type == "text/cloud-config" then
cc_body = p.body
elseif p.content_type:match("x%-shellscript") or p.content_type:match("x%-sh") then
if citype ~= "postnet" then
nuage.mkdir_p(root .. "/var/cache/nuageinit")
local spath = root .. "/var/cache/nuageinit/multipart_script"
local sf = io.open(spath, "w")
if sf then
sf:write(p.body .. "\n")
sf:close()
nuage.chmod(spath, "0755")
end
end
end
end
if cc_body then
local obj = yaml.load(cc_body)
if obj then
if citype ~= "postnet" then
nuage.mkdir_p(root .. "/var/cache/nuageinit")
local tof = assert(io.open(root .. "/var/cache/nuageinit/user_data", "w"))
tof:write("#cloud-config\n" .. cc_body)
tof:close()
end
return "#cloud-config", obj
end
end
end
return nil, nil
end
if citype ~= "postnet" then
local content = f:read("*a")
if not content or #string.gsub(content, "^%s*(.-)%s*$", "%1") == 0 then
f:close()
return
end
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()
local obj = nil
if ud and line == "#cloud-config" then
f = io.open(ni_path .. "/" .. ud)
obj = yaml.load(f:read("*a"))
f:close()
if not obj then
nuage.err("error parsing cloud-config file: " .. ud)
end
end
return line, obj
end
-- Clean up stale resolvconf.conf from previous boot
if citype ~= "postnet" then
os.remove(root .. "/etc/resolvconf.conf")
end
if citype == "config-2" then
-- network
config2_network(ni_path)
end
local metadata = load_metadata(citype)
local line, obj = load_userdata()
-- No user-data
if line == nil then
local calls_table = {
sethostname,
create_default_user,
}
for i = 1, #calls_table do
calls_table[i](obj, metadata)
end
-- YAML user-data
elseif line == "#cloud-config" then
local pre_network_calls = {
ca_certs,
mounts,
bootcmd,
sethostname,
manage_etc_hosts,
settimezone,
groups,
create_default_user,
ssh_deletekeys,
ssh_keys,
ssh_authkey_fingerprints,
network_config,
resolv_conf,
keyboard,
locale,
disable_root,
ssh_pwauth,
runcmd,
write_files_not_deferred,
}
local post_network_calls = {
ntp,
packages,
users,
chpasswd,
write_files_deferred,
phone_home,
final_message,
power_state_change,
}
local calls_table = pre_network_calls
if citype == "postnet" then
calls_table = post_network_calls
end
for i = 1, #calls_table do
if citype == "nocloud" and calls_table[i] == network_config then
local netobj = parse_network_config() or obj
network_config(netobj)
else
calls_table[i](obj, metadata)
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