mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-28 11:25:21 +01:00
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
1238 lines
32 KiB
Plaintext
Executable File
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
|