aboutsummaryrefslogtreecommitdiff
path: root/libexec
diff options
context:
space:
mode:
Diffstat (limited to 'libexec')
-rw-r--r--libexec/Makefile5
-rw-r--r--libexec/nuageinit/Makefile11
-rw-r--r--libexec/nuageinit/nuage.lua214
-rwxr-xr-xlibexec/nuageinit/nuageinit312
-rw-r--r--libexec/nuageinit/tests/Makefile13
-rw-r--r--libexec/nuageinit/tests/addgroup.lua15
-rw-r--r--libexec/nuageinit/tests/addsshkey.lua2
-rw-r--r--libexec/nuageinit/tests/adduser.lua15
-rw-r--r--libexec/nuageinit/tests/dirname.lua8
-rw-r--r--libexec/nuageinit/tests/err.lua4
-rw-r--r--libexec/nuageinit/tests/nuage.sh52
-rw-r--r--libexec/nuageinit/tests/nuageinit.sh338
-rw-r--r--libexec/nuageinit/tests/sethostname.lua4
-rw-r--r--libexec/nuageinit/tests/utils.sh21
-rw-r--r--libexec/nuageinit/tests/warn.lua4
-rw-r--r--libexec/nuageinit/yaml.lua586
-rw-r--r--libexec/rc/rc.d/Makefile6
-rwxr-xr-xlibexec/rc/rc.d/nuageinit67
18 files changed, 1677 insertions, 0 deletions
diff --git a/libexec/Makefile b/libexec/Makefile
index ee354fa60e79..8287690eeb3c 100644
--- a/libexec/Makefile
+++ b/libexec/Makefile
@@ -27,6 +27,7 @@ SUBDIR= ${_atf} \
${_rshd} \
${_rtld-elf} \
save-entropy \
+ ${_nuageinit} \
${_smrsh} \
${_tests} \
${_tftp-proxy} \
@@ -119,6 +120,10 @@ _atf= atf
_tests= tests
.endif
+.if ${MK_NUAGEINIT} != "no"
+_nuageinit= nuageinit
+.endif
+
.include <bsd.arch.inc.mk>
.include <bsd.subdir.mk>
diff --git a/libexec/nuageinit/Makefile b/libexec/nuageinit/Makefile
new file mode 100644
index 000000000000..64c5ec316f3d
--- /dev/null
+++ b/libexec/nuageinit/Makefile
@@ -0,0 +1,11 @@
+PACKAGE= nuageinit
+SCRIPTS= nuageinit
+FILES= nuage.lua yaml.lua
+FILESDIR= ${SHAREDIR}/flua
+
+.include <src.opts.mk>
+
+HAS_TESTS=
+SUBDIR.${MK_TESTS}+= tests
+
+.include <bsd.prog.mk>
diff --git a/libexec/nuageinit/nuage.lua b/libexec/nuageinit/nuage.lua
new file mode 100644
index 000000000000..55486ae2b122
--- /dev/null
+++ b/libexec/nuageinit/nuage.lua
@@ -0,0 +1,214 @@
+-- SPDX-License-Identifier: BSD-2-Clause
+--
+-- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
+
+local pu = require("posix.unistd")
+
+local function warnmsg(str)
+ io.stderr:write(str.."\n")
+end
+
+local function errmsg(str)
+ io.stderr:write(str.."\n")
+ os.exit(1)
+end
+
+local function dirname(oldpath)
+ if not oldpath then
+ return nil
+ end
+ local path = oldpath:gsub("[^/]+/*$", "")
+ if path == "" then
+ return nil
+ end
+ return path
+end
+
+local function mkdir_p(path)
+ if lfs.attributes(path, "mode") ~= nil then
+ return true
+ end
+ local r,err = mkdir_p(dirname(path))
+ if not r then
+ return nil,err.." (creating "..path..")"
+ end
+ return lfs.mkdir(path)
+end
+
+local function sethostname(hostname)
+ if hostname == nil then return end
+ local root = os.getenv("NUAGE_FAKE_ROOTDIR")
+ if not root then
+ root = ""
+ end
+ local hostnamepath = root .. "/etc/rc.conf.d/hostname"
+
+ mkdir_p(dirname(hostnamepath))
+ local f,err = io.open(hostnamepath, "w")
+ if not f then
+ warnmsg("Impossible to open "..hostnamepath .. ":" ..err)
+ return
+ end
+ f:write("hostname=\""..hostname.."\"\n")
+ f:close()
+end
+
+local function splitlist(list)
+ local ret = {}
+ if type(list) == "string" then
+ for str in list:gmatch("([^, ]+)") do
+ ret[#ret + 1] = str
+ end
+ elseif type(list) == "table" then
+ ret = list
+ else
+ warnmsg("Invalid type ".. type(list) ..", expecting table or string")
+ end
+ return ret
+end
+
+local function adduser(pwd)
+ if (type(pwd) ~= "table") then
+ warnmsg("Argument should be a table")
+ return nil
+ end
+ local f = io.popen("getent passwd "..pwd.name)
+ local pwdstr = f:read("*a")
+ f:close()
+ if pwdstr:len() ~= 0 then
+ return pwdstr:match("%a+:.+:%d+:%d+:.*:(.*):.*")
+ end
+ if not pwd.gecos then
+ pwd.gecos = pwd.name .. " User"
+ end
+ if not pwd.home then
+ pwd.home = "/home/" .. pwd.name
+ end
+ local extraargs=""
+ if pwd.groups then
+ local list = splitlist(pwd.groups)
+ extraargs = " -G ".. table.concat(list, ',')
+ end
+ -- pw will automatically create a group named after the username
+ -- do not add a -g option in this case
+ if pwd.primary_group and pwd.primary_group ~= pwd.name then
+ extraargs = extraargs .. " -g " .. pwd.primary_group
+ end
+ if not pwd.no_create_home then
+ extraargs = extraargs .. " -m "
+ end
+ if not pwd.shell then
+ pwd.shell = "/bin/sh"
+ end
+ local precmd = ""
+ local postcmd = ""
+ if pwd.passwd then
+ precmd = "echo "..pwd.passwd .. "| "
+ postcmd = " -H 0 "
+ elseif pwd.plain_text_passwd then
+ precmd = "echo "..pwd.plain_text_passwd .. "| "
+ postcmd = " -H 0 "
+ end
+ local root = os.getenv("NUAGE_FAKE_ROOTDIR")
+ local cmd = precmd .. "pw "
+ if root then
+ cmd = cmd .. "-R " .. root .. " "
+ end
+ cmd = cmd .. "useradd -n ".. pwd.name .. " -M 0755 -w none "
+ cmd = cmd .. extraargs .. " -c '".. pwd.gecos
+ cmd = cmd .. "' -d '" .. pwd.home .. "' -s "..pwd.shell .. postcmd
+
+ local r = os.execute(cmd)
+ if not r then
+ warnmsg("nuageinit: fail to add user "..pwd.name);
+ warnmsg(cmd)
+ return nil
+ end
+ if pwd.locked then
+ cmd = "pw "
+ if root then
+ cmd = cmd .. "-R " .. root .. " "
+ end
+ cmd = cmd .. "lock " .. pwd.name
+ os.execute(cmd)
+ end
+ return pwd.home
+end
+
+local function addgroup(grp)
+ if (type(grp) ~= "table") then
+ warnmsg("Argument should be a table")
+ return false
+ end
+ local f = io.popen("getent group "..grp.name)
+ local grpstr = f:read("*a")
+ f:close()
+ if grpstr:len() ~= 0 then
+ return true
+ end
+ local extraargs = ""
+ if grp.members then
+ local list = splitlist(grp.members)
+ extraargs = " -M " .. table.concat(list, ',')
+ end
+ local root = os.getenv("NUAGE_FAKE_ROOTDIR")
+ local cmd = "pw "
+ if root then
+ cmd = cmd .. "-R " .. root .. " "
+ end
+ cmd = cmd .. "groupadd -n ".. grp.name .. extraargs
+ local r = os.execute(cmd)
+ if not r then
+ warnmsg("nuageinit: fail to add group ".. grp.name);
+ warnmsg(cmd)
+ return false
+ end
+ return true
+end
+
+local function addsshkey(homedir, key)
+ local chownak = false
+ local chowndotssh = false
+ local ak_path = homedir .. "/.ssh/authorized_keys"
+ local dotssh_path = homedir .. "/.ssh"
+ local dirattrs = lfs.attributes(ak_path)
+ if dirattrs == nil then
+ chownak = true
+ dirattrs = lfs.attributes(dotssh_path)
+ if dirattrs == nil then
+ if not lfs.mkdir(dotssh_path) then
+ warnmsg("nuageinit: impossible to create ".. dotssh_path)
+ return
+ end
+ chowndotssh = true
+ dirattrs = lfs.attributes(homedir)
+ end
+ end
+
+ local f = io.open(ak_path, "a")
+ if not f then
+ warnmsg("nuageinit: impossible to open "..ak_path)
+ return
+ end
+ f:write(key .. "\n")
+ f:close()
+ if chownak then
+ pu.chown(ak_path, dirattrs.uid, dirattrs.gid)
+ end
+ if chowndotssh then
+ pu.chown(dotssh_path, dirattrs.uid, dirattrs.gid)
+ end
+end
+
+local n = {
+ warn = warnmsg,
+ err = errmsg,
+ sethostname = sethostname,
+ adduser = adduser,
+ addgroup = addgroup,
+ addsshkey = addsshkey,
+ dirname = dirname,
+ mkdir_p = mkdir_p,
+}
+
+return n
diff --git a/libexec/nuageinit/nuageinit b/libexec/nuageinit/nuageinit
new file mode 100755
index 000000000000..08224061d1b1
--- /dev/null
+++ b/libexec/nuageinit/nuageinit
@@ -0,0 +1,312 @@
+#!/usr/libexec/flua
+
+-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+--
+-- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
+
+local nuage = require("nuage")
+local yaml = require("yaml")
+
+if #arg ~= 2 then
+ nuage.err("Usage ".. arg[0] .." <cloud-init directory> [config-2|nocloud]")
+end
+local path = arg[1]
+local citype = arg[2]
+local ucl = require("ucl")
+
+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 open_config(name)
+ nuage.mkdir_p(root .. "/etc/rc.conf.d")
+ local f,err = io.open(root .. "/etc/rc.conf.d/" .. name, "w")
+ if not f then
+ nuage.err("nuageinit: unable to open "..name.." config: " .. err)
+ end
+ return f
+end
+
+local function get_ifaces()
+ local parser = ucl.parser()
+ -- grab ifaces
+ local ns = io.popen('netstat -i --libxo json')
+ local netres = ns:read("*a")
+ ns:close()
+ local res,err = parser:parse_string(netres)
+ if not res then
+ nuage.warn("Error parsing netstat -i --libxo json outout: " .. err)
+ return nil
+ end
+ local ifaces = parser:get_object()
+ local myifaces = {}
+ for _,iface in pairs(ifaces["statistics"]["interface"]) do
+ if iface["network"]:match("<Link#%d>") then
+ local s = iface["address"]
+ myifaces[s:lower()] = iface["name"]
+ end
+ end
+ return myifaces
+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("nuageinit: error parsing network_data.json: " .. err)
+ return
+ end
+ local obj = parser:get_object()
+
+ local ifaces = get_ifaces()
+ if not ifaces then
+ nuage.warn("nuageinit: 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
+
+ nuage.mkdir_p(root .. "/etc/rc.conf.d")
+ local network = open_config("network")
+ local routing = open_config("routing")
+ local ipv6 = {}
+ local ipv6_routes = {}
+ local ipv4 = {}
+ for _,v in pairs(obj["networks"]) do
+ local interface = mylinks[v["link"]]
+ if v["type"] == "ipv4_dhcp" then
+ network:write("ifconfig_"..interface.."=\"DHCP\"\n")
+ end
+ if v["type"] == "ipv4" then
+ network:write("ifconfig_"..interface.."=\"inet "..v["ip_address"].." netmask " .. v["netmask"] .. "\"\n")
+ if v["gateway"] then
+ routing:write("defaultrouter=\""..v["gateway"].."\"\n")
+ end
+ if v["routes"] then
+ for i,r in ipairs(v["routes"]) do
+ local rname = "cloudinit" .. i .. "_" .. interface
+ if v["gateway"] and v["gateway"] == r["gateway"] then goto next end
+ if r["network"] == "0.0.0.0" then
+ routing:write("defaultrouter=\""..r["gateway"].."\"\n")
+ goto next
+ end
+ routing:write("route_".. rname .. "=\"-net ".. r["network"] .. " ")
+ routing:write(r["gateway"] .. " " .. r["netmask"] .. "\"\n")
+ ipv4[#ipv4 + 1] = rname
+ ::next::
+ end
+ end
+ end
+ if v["type"] == "ipv6" then
+ ipv6[#ipv6+1] = interface
+ ipv6_routes[#ipv6_routes+1] = interface
+ network:write("ifconfig_"..interface.."_ipv6=\"inet6 "..v["ip_address"].."\"\n")
+ if v["gateway"] then
+ routing:write("ipv6_defaultrouter=\""..v["gateway"].."\"\n")
+ routing:write("ipv6_route_"..interface.."=\""..v["gateway"])
+ routing:write(" -prefixlen 128 -interface "..interface.."\"\n")
+ end
+ -- TODO compute the prefixlen for the routes
+ --if v["routes"] then
+ -- for i,r in ipairs(v["routes"]) do
+ -- local rname = "cloudinit" .. i .. "_" .. mylinks[v["link"]]
+ -- -- skip all the routes which are already covered by the default gateway, some provider
+ -- -- still list plenty of them.
+ -- if v["gateway"] == r["gateway"] then goto next end
+ -- routing:write("ipv6_route_" .. rname .. "\"\n")
+ -- ipv6_routes[#ipv6_routes+1] = rname
+ -- ::next::
+ -- end
+ --end
+ end
+ end
+ if #ipv4 > 0 then
+ routing:write("static_routes=\"")
+ routing:write(table.concat(ipv4, " ") .. "\"\n")
+ end
+ if #ipv6 > 0 then
+ network:write("ipv6_network_interfaces=\"")
+ network:write(table.concat(ipv6, " ") .. "\"\n")
+ network:write("ipv6_default_interface=\""..ipv6[1].."\"\n")
+ end
+ if #ipv6_routes > 0 then
+ routing:write("ipv6_static_routes=\"")
+ routing:write(table.concat(ipv6, " ") .. "\"\n")
+ end
+ network:close()
+ routing:close()
+end
+
+if citype == "config-2" then
+ local parser = ucl.parser()
+ local res,err = parser:parse_file(path..'/meta_data.json')
+
+ if not res then
+ nuage.err("nuageinit: error parsing config-2: meta_data.json: " .. err)
+ end
+ local obj = parser:get_object()
+ local sshkeys = obj["public_keys"]
+ if sshkeys then
+ local homedir = nuage.adduser(default_user)
+ for _,v in pairs(sshkeys) do
+ nuage.addsshkey(root .. homedir, v)
+ end
+ end
+ nuage.sethostname(obj["hostname"])
+
+ -- network
+ config2_network(path)
+elseif citype == "nocloud" then
+ local f,err = io.open(path.."/meta-data")
+ if err then
+ nuage.err("nuageinit: error parsing nocloud meta-data: ".. err)
+ end
+ local obj = yaml.eval(f:read("*a"))
+ f:close()
+ if not obj then
+ nuage.err("nuageinit: 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
+else
+ nuage.err("Unknown cloud init type: ".. citype)
+end
+
+-- deal with user-data
+local f = io.open(path..'/user-data', "r")
+if not f then
+ os.exit(0)
+end
+local line = f:read('*l')
+f:close()
+if line == "#cloud-config" then
+ f = io.open(path.."/user-data")
+ local obj = yaml.eval(f:read("*a"))
+ f:close()
+ if not obj then
+ nuage.err("nuageinit: error parsing cloud-config file: user-data")
+ end
+ if obj.groups then
+ 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("nuageinit: 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("nuageinit: invalid type : "..type(g).." for users entry number "..n);
+ end
+ end
+ end
+ if obj.users then
+ 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
+ else
+ nuage.warn("nuageinit: invalid type : "..type(u).." for users entry number "..n);
+ end
+ ::unext::
+ end
+ else
+ -- default user if none are defined
+ nuage.adduser(default_user)
+ end
+ if obj.ssh_authorized_keys then
+ local homedir = nuage.adduser(default_user)
+ for _,k in ipairs(obj.ssh_authorized_keys) do
+ nuage.addsshkey(homedir, k)
+ end
+ end
+ if obj.network then
+ local ifaces = get_ifaces()
+ nuage.mkdir_p(root .. "/etc/rc.conf.d")
+ local network = open_config("network")
+ local routing = open_config("routing")
+ local ipv6={}
+ for _,v in pairs(obj.network.ethernets) do
+ if not v.match then goto next end
+ if not v.match.macaddress then goto next end
+ if not ifaces[v.match.macaddress] then
+ nuage.warn("nuageinit: not interface matching: "..v.match.macaddress)
+ goto next
+ end
+ local interface = ifaces[v.match.macaddress]
+ if v.dhcp4 then
+ network:write("ifconfig_"..interface.."=\"DHCP\"\n")
+ elseif v.addresses then
+ for _,a in pairs(v.addresses) do
+ if a:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)") then
+ network:write("ifconfig_"..interface.."=\"inet "..a.."\"\n")
+ else
+ network:write("ifconfig_"..interface.."_ipv6=\"inet6 "..a.."\"\n")
+ ipv6[#ipv6 +1] = interface
+ end
+ end
+ end
+ if v.gateway4 then
+ routing:write("defaultrouter=\""..v.gateway4.."\"\n")
+ end
+ if v.gateway6 then
+ routing:write("ipv6_defaultrouter=\""..v.gateway6.."\"\n")
+ routing:write("ipv6_route_"..interface.."=\""..v.gateway6)
+ routing:write(" -prefixlen 128 -interface "..interface.."\"\n")
+ end
+ ::next::
+ end
+ if #ipv6 > 0 then
+ network:write("ipv6_network_interfaces=\"")
+ network:write(table.concat(ipv6, " ") .. "\"\n")
+ network:write("ipv6_default_interface=\""..ipv6[1].."\"\n")
+ end
+ network:close()
+ routing:close()
+ end
+else
+ local res,err = os.execute(path..'/user-data')
+ if not res then
+ nuage.err("nuageinit: error executing user-data script: ".. err)
+ end
+end
diff --git a/libexec/nuageinit/tests/Makefile b/libexec/nuageinit/tests/Makefile
new file mode 100644
index 000000000000..d5b3bd9dcc82
--- /dev/null
+++ b/libexec/nuageinit/tests/Makefile
@@ -0,0 +1,13 @@
+PACKAGE= tests
+
+ATF_TESTS_SH= nuage utils nuageinit
+
+${PACKAGE}FILES+= warn.lua
+${PACKAGE}FILES+= err.lua
+${PACKAGE}FILES+= dirname.lua
+${PACKAGE}FILES+= sethostname.lua
+${PACKAGE}FILES+= addsshkey.lua
+${PACKAGE}FILES+= adduser.lua
+${PACKAGE}FILES+= addgroup.lua
+
+.include <bsd.test.mk>
diff --git a/libexec/nuageinit/tests/addgroup.lua b/libexec/nuageinit/tests/addgroup.lua
new file mode 100644
index 000000000000..60a0d8346793
--- /dev/null
+++ b/libexec/nuageinit/tests/addgroup.lua
@@ -0,0 +1,15 @@
+#!/usr/libexec/flua
+
+local n = require("nuage")
+if n.addgroup() then
+ n.err("addgroup should not accept empty value")
+end
+if n.addgroup("plop") then
+ n.err("addgroup should not accept empty value")
+end
+local gr = {}
+gr.name = "impossible_groupname"
+local res = n.addgroup(gr)
+if not res then
+ n.err("valid addgroup should return a path")
+end
diff --git a/libexec/nuageinit/tests/addsshkey.lua b/libexec/nuageinit/tests/addsshkey.lua
new file mode 100644
index 000000000000..3aa5f7619ec2
--- /dev/null
+++ b/libexec/nuageinit/tests/addsshkey.lua
@@ -0,0 +1,2 @@
+local n = require("nuage")
+n.addsshkey(".", "mykey")
diff --git a/libexec/nuageinit/tests/adduser.lua b/libexec/nuageinit/tests/adduser.lua
new file mode 100644
index 000000000000..9366d2abd0f4
--- /dev/null
+++ b/libexec/nuageinit/tests/adduser.lua
@@ -0,0 +1,15 @@
+#!/usr/libexec/flua
+
+local n = require("nuage")
+if n.adduser() then
+ n.err("adduser should not accept empty value")
+end
+if n.adduser("plop") then
+ n.err("adduser should not accept empty value")
+end
+local pw = {}
+pw.name = "impossible_username"
+local res = n.adduser(pw)
+if not res then
+ n.err("valid adduser should return a path")
+end
diff --git a/libexec/nuageinit/tests/dirname.lua b/libexec/nuageinit/tests/dirname.lua
new file mode 100644
index 000000000000..d1268e48575c
--- /dev/null
+++ b/libexec/nuageinit/tests/dirname.lua
@@ -0,0 +1,8 @@
+local n = require("nuage")
+print(n.dirname("/my/path/path1"))
+if n.dirname("path") then
+ nuage.err("Expecting nil for n.dirname(\"path\")")
+end
+if n.dirname() then
+ nuage.err("Expecting nil for n.dirname")
+end
diff --git a/libexec/nuageinit/tests/err.lua b/libexec/nuageinit/tests/err.lua
new file mode 100644
index 000000000000..c62fa1098f09
--- /dev/null
+++ b/libexec/nuageinit/tests/err.lua
@@ -0,0 +1,4 @@
+#!/usr/libexec/flua
+
+local n = require("nuage")
+n.err("plop")
diff --git a/libexec/nuageinit/tests/nuage.sh b/libexec/nuageinit/tests/nuage.sh
new file mode 100644
index 000000000000..bbf306eae51f
--- /dev/null
+++ b/libexec/nuageinit/tests/nuage.sh
@@ -0,0 +1,52 @@
+atf_test_case sethostname
+atf_test_case addsshkey
+atf_test_case adduser
+atf_test_case addgroup
+
+sethostname_body() {
+ export NUAGE_FAKE_ROOTDIR="$(pwd)"
+ atf_check /usr/libexec/flua $(atf_get_srcdir)/sethostname.lua
+ if [ ! -f etc/rc.conf.d/hostname ]; then
+ atf_fail "hostname not written"
+ fi
+ atf_check -o inline:"hostname=\"myhostname\"\n" cat etc/rc.conf.d/hostname
+}
+
+addsshkey_body() {
+ atf_check /usr/libexec/flua $(atf_get_srcdir)/addsshkey.lua
+ if [ ! -f .ssh/authorized_keys ]; then
+ atf_fail "ssh key not added"
+ fi
+ atf_check -o inline:"mykey\n" cat .ssh/authorized_keys
+ atf_check /usr/libexec/flua $(atf_get_srcdir)/addsshkey.lua
+ atf_check -o inline:"mykey\nmykey\n" cat .ssh/authorized_keys
+}
+
+adduser_body() {
+ export NUAGE_FAKE_ROOTDIR="$(pwd)"
+ if [ $(id -u) -ne 0 ]; then
+ atf_skip "root required"
+ fi
+ mkdir etc
+ printf "root:*:0:0::0:0:Charlie &:/root:/bin/csh\n" > etc/master.passwd
+ pwd_mkdb -d etc etc/master.passwd
+ printf "wheel:*:0:root\n" > etc/group
+ atf_check -e inline:"Argument should be a table\nArgument should be a table\n" /usr/libexec/flua $(atf_get_srcdir)/adduser.lua
+ test -d home/impossible_username || atf_fail "home not created"
+ atf_check -o inline:"impossible_username::1001:1001::0:0:impossible_username User:/home/impossible_username:/bin/sh\n" grep impossible_username etc/master.passwd
+}
+
+addgroup_body() {
+ export NUAGE_FAKE_ROOTDIR="$(pwd)"
+ mkdir etc
+ printf "wheel:*:0:root\n" > etc/group
+ atf_check -e inline:"Argument should be a table\nArgument should be a table\n" /usr/libexec/flua $(atf_get_srcdir)/addgroup.lua
+ atf_check -o inline:"impossible_groupname:*:1001:\n" grep impossible_groupname etc/group
+}
+
+atf_init_test_cases() {
+ atf_add_test_case sethostname
+ atf_add_test_case addsshkey
+ atf_add_test_case adduser
+ atf_add_test_case addgroup
+}
diff --git a/libexec/nuageinit/tests/nuageinit.sh b/libexec/nuageinit/tests/nuageinit.sh
new file mode 100644
index 000000000000..926233bcf66d
--- /dev/null
+++ b/libexec/nuageinit/tests/nuageinit.sh
@@ -0,0 +1,338 @@
+atf_test_case args
+atf_test_case nocloud
+atf_test_case nocloud_userdata_script
+atf_test_case nocloud_userdata_cloudconfig
+atf_test_case nocloud_userdata_cloudconfig_users
+atf_test_case nocloud_network
+atf_test_case config2
+atf_test_case config2_pubkeys
+atf_test_case config2_network
+atf_test_case config2_network_static_v4
+
+
+args_body()
+{
+ atf_check -s exit:1 -e inline:"Usage /usr/libexec/nuageinit <cloud-init directory> [config-2|nocloud]\n" /usr/libexec/nuageinit
+ atf_check -s exit:1 -e inline:"Usage /usr/libexec/nuageinit <cloud-init directory> [config-2|nocloud]\n" /usr/libexec/nuageinit bla
+ atf_check -s exit:1 -e inline:"Usage /usr/libexec/nuageinit <cloud-init directory> [config-2|nocloud]\n" /usr/libexec/nuageinit bla meh plop
+ atf_check -s exit:1 -e inline:"Unknown cloud init type: meh\n" /usr/libexec/nuageinit bla meh
+}
+
+nocloud_body()
+{
+ here=$(pwd)
+ mkdir -p media/nuageinit
+ atf_check -s exit:1 -e match:"nuageinit: error parsing nocloud.*" /usr/libexec/nuageinit ${here}/media/nuageinit/ nocloud
+ export NUAGE_FAKE_ROOTDIR=$(pwd)
+ printf "instance-id: iid-local01\nlocal-hostname: cloudimg\n" > ${here}/media/nuageinit/meta-data
+ atf_check -s exit:0 /usr/libexec/nuageinit ${here}/media/nuageinit nocloud
+ atf_check -o inline:"hostname=\"cloudimg\"\n" cat etc/rc.conf.d/hostname
+ cat > media/nuageinit/meta-data << EOF
+instance-id: iid-local01
+hostname: myhost
+EOF
+ atf_check -s exit:0 /usr/libexec/nuageinit ${here}/media/nuageinit nocloud
+ atf_check -o inline:"hostname=\"myhost\"\n" cat etc/rc.conf.d/hostname
+}
+
+nocloud_userdata_script_body()
+{
+ here=$(pwd)
+ mkdir -p media/nuageinit
+ printf "instance-id: iid-local01\n" > ${here}/media/nuageinit/meta-data
+ printf "#!/bin/sh\necho "yeah"\n" > ${here}/media/nuageinit/user-data
+ chmod 755 ${here}/media/nuageinit/user-data
+ atf_check -s exit:0 -o inline:"yeah\n" /usr/libexec/nuageinit ${here}/media/nuageinit nocloud
+}
+
+nocloud_userdata_cloudconfig_users_body()
+{
+ here=$(pwd)
+ export NUAGE_FAKE_ROOTDIR=$(pwd)
+ if [ $(id -u) -ne 0 ]; then
+ atf_skip "root required"
+ fi
+ mkdir -p media/nuageinit
+ printf "instance-id: iid-local01\n" > ${here}/media/nuageinit/meta-data
+ mkdir -p etc
+ cat > etc/master.passwd <<EOF
+root:*:0:0::0:0:Charlie &:/root:/bin/csh
+sys:*:1:0::0:0:Sys:/home/sys:/bin/csh
+EOF
+ pwd_mkdb -d etc ${here}/etc/master.passwd
+ cat > etc/group <<EOF
+wheel:*:0:root
+users:*:1:
+EOF
+ cat > media/nuageinit/user-data <<EOF
+#cloud-config
+groups:
+ - admingroup: [root,sys]
+ - cloud-users
+users:
+ - default
+ - name: foobar
+ gecos: Foo B. Bar
+ primary_group: foobar
+ groups: users
+ passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
+EOF
+ atf_check /usr/libexec/nuageinit ${here}/media/nuageinit nocloud
+ cat > expectedgroup << EOF
+wheel:*:0:root,freebsd
+users:*:1:foobar
+admingroup:*:1001:root,sys
+cloud-users:*:1002:
+freebsd:*:1003:
+foobar:*:1004:
+EOF
+ cat > expectedpasswd << EOF
+root:*:0:0::0:0:Charlie &:/root:/bin/csh
+sys:*:1:0::0:0:Sys:/home/sys:/bin/csh
+freebsd:freebsd:1001:1003::0:0:FreeBSD User:/home/freebsd:/bin/sh
+foobar:H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/:1002:1004::0:0:Foo B. Bar:/home/foobar:/bin/sh
+EOF
+ atf_check -o file:expectedpasswd cat ${here}/etc/master.passwd
+ atf_check -o file:expectedgroup cat ${here}/etc/group
+}
+
+nocloud_network_body()
+{
+ here=$(pwd)
+ mkdir -p media/nuageinit
+ mkdir -p etc
+ cat > etc/master.passwd <<EOF
+root:*:0:0::0:0:Charlie &:/root:/bin/csh
+sys:*:1:0::0:0:Sys:/home/sys:/bin/csh
+EOF
+ pwd_mkdb -d etc ${here}/etc/master.passwd
+ cat > etc/group <<EOF
+wheel:*:0:root
+users:*:1:
+EOF
+ if [ $(id -u) -ne 0 ]; then
+ atf_skip "root required"
+ fi
+ mynetworks=$(ifconfig -l ether)
+ if [ -z "$mynetworks" ]; then
+ atf_skip "a network interface is needed"
+ fi
+ set -- $mynetworks
+ myiface=$1
+ myaddr=$(ifconfig $myiface ether | awk '/ether/ { print $2 }')
+ printf "instance-id: iid-local01\n" > ${here}/media/nuageinit/meta-data
+ cat > media/nuageinit/user-data <<EOF
+#cloud-config
+#
+network:
+ version: 2
+ ethernets:
+ # opaque ID for physical interfaces, only referred to by other stanzas
+ id0:
+ match:
+ macaddress: '${myaddr}'
+ addresses:
+ - 192.168.14.2/24
+ - 2001:1::1/64
+ gateway4: 192.168.14.1
+ gateway6: 2001:1::2
+EOF
+ export NUAGE_FAKE_ROOTDIR=$(pwd)
+ atf_check /usr/libexec/nuageinit ${here}/media/nuageinit nocloud
+ cat > network <<EOF
+ifconfig_${myiface}="inet 192.168.14.2/24"
+ifconfig_${myiface}_ipv6="inet6 2001:1::1/64"
+ipv6_network_interfaces="${myiface}"
+ipv6_default_interface="${myiface}"
+EOF
+ cat > routing <<EOF
+defaultrouter="192.168.14.1"
+ipv6_defaultrouter="2001:1::2"
+ipv6_route_${myiface}="2001:1::2 -prefixlen 128 -interface ${myiface}"
+EOF
+ atf_check -o file:network cat ${here}/etc/rc.conf.d/network
+ atf_check -o file:routing cat ${here}/etc/rc.conf.d/routing
+}
+config2_body()
+{
+ here=$(pwd)
+ mkdir -p media/nuageinit
+ atf_check -s exit:1 -e match:"nuageinit: error parsing config-2: meta_data.json.*" /usr/libexec/nuageinit ${here}/media/nuageinit config-2
+ printf "{}" > media/nuageinit/meta_data.json
+ atf_check /usr/libexec/nuageinit ${here}/media/nuageinit config-2
+ cat > media/nuageinit/meta_data.json << EOF
+{
+ "hostname": "cloudimg",
+}
+EOF
+ export NUAGE_FAKE_ROOTDIR=$(pwd)
+ atf_check /usr/libexec/nuageinit ${here}/media/nuageinit config-2
+ atf_check -o inline:"hostname=\"cloudimg\"\n" cat etc/rc.conf.d/hostname
+}
+
+config2_pubkeys_body()
+{
+ here=$(pwd)
+ if [ $(id -u) -ne 0 ]; then
+ atf_skip "root required"
+ fi
+ mkdir -p media/nuageinit
+ cat > media/nuageinit/meta_data.json << EOF
+{
+ "public_keys": {
+ "mykey": "ssh-rsa AAAAB3NzaC1y...== Generated by Nova"
+ },
+}
+EOF
+ mkdir -p etc
+ cat > etc/master.passwd <<EOF
+root:*:0:0::0:0:Charlie &:/root:/bin/csh
+sys:*:1:0::0:0:Sys:/home/sys:/bin/csh
+EOF
+ pwd_mkdb -d etc ${here}/etc/master.passwd
+ cat > etc/group <<EOF
+wheel:*:0:root
+users:*:1:
+EOF
+ export NUAGE_FAKE_ROOTDIR=$(pwd)
+ atf_check /usr/libexec/nuageinit ${here}/media/nuageinit config-2
+ atf_check -o inline:"ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n" cat home/freebsd/.ssh/authorized_keys
+}
+
+config2_network_body() {
+ here=$(pwd)
+ mkdir -p media/nuageinit
+ printf "{}" > media/nuageinit/meta_data.json
+ mynetworks=$(ifconfig -l ether)
+ if [ -z "$mynetworks" ]; then
+ atf_skip "a network interface is needed"
+ fi
+ set -- $mynetworks
+ myiface=$1
+ myaddr=$(ifconfig $myiface ether | awk '/ether/ { print $2 }')
+cat > media/nuageinit/network_data.json <<EOF
+{
+ "links": [
+ {
+ "ethernet_mac_address": "$myaddr",
+ "id": "iface0",
+ "mtu": null,
+ }
+ ],
+ "networks": [
+ {
+ "id": "network0",
+ "link": "iface0",
+ "type": "ipv4_dhcp"
+ },
+ { // IPv6
+ "id": "private-ipv4",
+ "type": "ipv6",
+ "link": "iface0",
+ // supports condensed IPv6 with CIDR netmask
+ "ip_address": "2001:cdba::3257:9652/24",
+ "gateway": "fd00::1"
+ "routes": [
+ {
+ "network": "::",
+ "netmask": "::",
+ "gateway": "fd00::1"
+ },
+ {
+ "network": "::",
+ "netmask": "ffff:ffff:ffff::",
+ "gateway": "fd00::1:1"
+ },
+ ],
+ "network_id": "da5bb487-5193-4a65-a3df-4a0055a8c0d8"
+},
+ ],
+}
+EOF
+ export NUAGE_FAKE_ROOTDIR=$(pwd)
+ atf_check /usr/libexec/nuageinit ${here}/media/nuageinit config-2
+ cat > network <<EOF
+ifconfig_${myiface}="DHCP"
+ifconfig_${myiface}_ipv6="inet6 2001:cdba::3257:9652/24"
+ipv6_network_interfaces="${myiface}"
+ipv6_default_interface="${myiface}"
+EOF
+ cat > routing <<EOF
+ipv6_defaultrouter="fd00::1"
+ipv6_route_${myiface}="fd00::1 -prefixlen 128 -interface ${myiface}"
+ipv6_static_routes="${myiface}"
+EOF
+ atf_check -o file:network cat ${here}/etc/rc.conf.d/network
+ atf_check -o file:routing cat ${here}/etc/rc.conf.d/routing
+}
+
+config2_network_static_v4_body() {
+ here=$(pwd)
+ mkdir -p media/nuageinit
+ printf "{}" > media/nuageinit/meta_data.json
+ mynetworks=$(ifconfig -l ether)
+ if [ -z "$mynetworks" ]; then
+ atf_skip "a network interface is needed"
+ fi
+ set -- $mynetworks
+ myiface=$1
+ myaddr=$(ifconfig $myiface ether | awk '/ether/ { print $2 }')
+cat > media/nuageinit/network_data.json <<EOF
+{
+ "links": [
+ {
+ "ethernet_mac_address": "$myaddr",
+ "id": "iface0",
+ "mtu": null,
+ }
+ ],
+ "networks": [
+ {
+ "id": "network0",
+ "link": "iface0",
+ "type": "ipv4"
+ "ip_address": "10.184.0.244",
+ "netmask": "255.255.240.0",
+ "routes": [
+ {
+ "network": "10.0.0.0",
+ "netmask": "255.0.0.0",
+ "gateway": "11.0.0.1"
+ },
+ {
+ "network": "0.0.0.0",
+ "netmask": "0.0.0.0",
+ "gateway": "23.253.157.1"
+ }
+ ]
+ }
+]
+}
+EOF
+
+ export NUAGE_FAKE_ROOTDIR=$(pwd)
+ atf_check /usr/libexec/nuageinit ${here}/media/nuageinit config-2
+ cat > network <<EOF
+ifconfig_${myiface}="inet 10.184.0.244 netmask 255.255.240.0"
+EOF
+ cat > routing <<EOF
+route_cloudinit1_${myiface}="-net 10.0.0.0 11.0.0.1 255.0.0.0"
+defaultrouter="23.253.157.1"
+static_routes="cloudinit1_${myiface}"
+EOF
+ atf_check -o file:network cat ${here}/etc/rc.conf.d/network
+ atf_check -o file:routing cat ${here}/etc/rc.conf.d/routing
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case args
+ atf_add_test_case nocloud
+ atf_add_test_case nocloud_userdata_script
+ atf_add_test_case nocloud_userdata_cloudconfig_users
+ atf_add_test_case nocloud_network
+ atf_add_test_case config2
+ atf_add_test_case config2_pubkeys
+ atf_add_test_case config2_network
+ atf_add_test_case config2_network_static_v4
+}
diff --git a/libexec/nuageinit/tests/sethostname.lua b/libexec/nuageinit/tests/sethostname.lua
new file mode 100644
index 000000000000..01434403934b
--- /dev/null
+++ b/libexec/nuageinit/tests/sethostname.lua
@@ -0,0 +1,4 @@
+#!/usr/libexec/flua
+
+local n = require("nuage")
+n.sethostname("myhostname")
diff --git a/libexec/nuageinit/tests/utils.sh b/libexec/nuageinit/tests/utils.sh
new file mode 100644
index 000000000000..192ccfb565cb
--- /dev/null
+++ b/libexec/nuageinit/tests/utils.sh
@@ -0,0 +1,21 @@
+atf_test_case warn
+atf_test_case err
+atf_test_case dirname
+
+warn_body() {
+ atf_check -e "inline:plop\n" -s exit:0 /usr/libexec/flua $(atf_get_srcdir)/warn.lua
+}
+
+err_body() {
+ atf_check -e "inline:plop\n" -s exit:1 /usr/libexec/flua $(atf_get_srcdir)/err.lua
+}
+
+dirname_body() {
+ atf_check -o "inline:/my/path/\n" -s exit:0 /usr/libexec/flua $(atf_get_srcdir)/dirname.lua
+}
+
+atf_init_test_cases() {
+ atf_add_test_case warn
+ atf_add_test_case err
+ atf_add_test_case dirname
+}
diff --git a/libexec/nuageinit/tests/warn.lua b/libexec/nuageinit/tests/warn.lua
new file mode 100644
index 000000000000..8575ae92c071
--- /dev/null
+++ b/libexec/nuageinit/tests/warn.lua
@@ -0,0 +1,4 @@
+#!/usr/libexec/flua
+
+local n = require("nuage")
+n.warn("plop")
diff --git a/libexec/nuageinit/yaml.lua b/libexec/nuageinit/yaml.lua
new file mode 100644
index 000000000000..8ab17b15eed7
--- /dev/null
+++ b/libexec/nuageinit/yaml.lua
@@ -0,0 +1,586 @@
+-- SPDX-License-Identifier: MIT
+--
+-- Copyright (c) 2017 Dominic Letz dominicletz@exosite.com
+
+local table_print_value
+table_print_value = function(value, indent, done)
+ indent = indent or 0
+ done = done or {}
+ if type(value) == "table" and not done [value] then
+ done [value] = true
+
+ local list = {}
+ for key in pairs (value) do
+ list[#list + 1] = key
+ end
+ table.sort(list, function(a, b) return tostring(a) < tostring(b) end)
+ local last = list[#list]
+
+ local rep = "{\n"
+ local comma
+ for _, key in ipairs (list) do
+ if key == last then
+ comma = ''
+ else
+ comma = ','
+ end
+ local keyRep
+ if type(key) == "number" then
+ keyRep = key
+ else
+ keyRep = string.format("%q", tostring(key))
+ end
+ rep = rep .. string.format(
+ "%s[%s] = %s%s\n",
+ string.rep(" ", indent + 2),
+ keyRep,
+ table_print_value(value[key], indent + 2, done),
+ comma
+ )
+ end
+
+ rep = rep .. string.rep(" ", indent) -- indent it
+ rep = rep .. "}"
+
+ done[value] = false
+ return rep
+ elseif type(value) == "string" then
+ return string.format("%q", value)
+ else
+ return tostring(value)
+ end
+end
+
+local table_print = function(tt)
+ print('return '..table_print_value(tt))
+end
+
+local table_clone = function(t)
+ local clone = {}
+ for k,v in pairs(t) do
+ clone[k] = v
+ end
+ return clone
+end
+
+local string_trim = function(s, what)
+ what = what or " "
+ return s:gsub("^[" .. what .. "]*(.-)["..what.."]*$", "%1")
+end
+
+local push = function(stack, item)
+ stack[#stack + 1] = item
+end
+
+local pop = function(stack)
+ local item = stack[#stack]
+ stack[#stack] = nil
+ return item
+end
+
+local context = function (str)
+ if type(str) ~= "string" then
+ return ""
+ end
+
+ str = str:sub(0,25):gsub("\n","\\n"):gsub("\"","\\\"");
+ return ", near \"" .. str .. "\""
+end
+
+local Parser = {}
+function Parser.new (self, tokens)
+ self.tokens = tokens
+ self.parse_stack = {}
+ self.refs = {}
+ self.current = 0
+ return self
+end
+
+local exports = {version = "1.2"}
+
+local word = function(w) return "^("..w..")([%s$%c])" end
+
+local tokens = {
+ {"comment", "^#[^\n]*"},
+ {"indent", "^\n( *)"},
+ {"space", "^ +"},
+ {"true", word("enabled"), const = true, value = true},
+ {"true", word("true"), const = true, value = true},
+ {"true", word("yes"), const = true, value = true},
+ {"true", word("on"), const = true, value = true},
+ {"false", word("disabled"), const = true, value = false},
+ {"false", word("false"), const = true, value = false},
+ {"false", word("no"), const = true, value = false},
+ {"false", word("off"), const = true, value = false},
+ {"null", word("null"), const = true, value = nil},
+ {"null", word("Null"), const = true, value = nil},
+ {"null", word("NULL"), const = true, value = nil},
+ {"null", word("~"), const = true, value = nil},
+ {"id", "^\"([^\"]-)\" *(:[%s%c])"},
+ {"id", "^'([^']-)' *(:[%s%c])"},
+ {"string", "^\"([^\"]-)\"", force_text = true},
+ {"string", "^'([^']-)'", force_text = true},
+ {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)"},
+ {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)"},
+ {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)"},
+ {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)"},
+ {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)"},
+ {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)"},
+ {"doc", "^%-%-%-[^%c]*"},
+ {",", "^,"},
+ {"string", "^%b{} *[^,%c]+", noinline = true},
+ {"{", "^{"},
+ {"}", "^}"},
+ {"string", "^%b[] *[^,%c]+", noinline = true},
+ {"[", "^%["},
+ {"]", "^%]"},
+ {"-", "^%-", noinline = true},
+ {":", "^:"},
+ {"pipe", "^(|)(%d*[+%-]?)", sep = "\n"},
+ {"pipe", "^(>)(%d*[+%-]?)", sep = " "},
+ {"id", "^([%w][%w %-_]*)(:[%s%c])"},
+ {"string", "^[^%c]+", noinline = true},
+ {"string", "^[^,%]}%c ]+"}
+};
+exports.tokenize = function (str)
+ local token
+ local row = 0
+ local ignore
+ local indents = 0
+ local lastIndents
+ local stack = {}
+ local indentAmount = 0
+ local inline = false
+ str = str:gsub("\r\n","\010")
+
+ while #str > 0 do
+ for i in ipairs(tokens) do
+ local captures = {}
+ if not inline or tokens[i].noinline == nil then
+ captures = {str:match(tokens[i][2])}
+ end
+
+ if #captures > 0 then
+ captures.input = str:sub(0, 25)
+ token = table_clone(tokens[i])
+ token[2] = captures
+ local str2 = str:gsub(tokens[i][2], "", 1)
+ token.raw = str:sub(1, #str - #str2)
+ str = str2
+
+ if token[1] == "{" or token[1] == "[" then
+ inline = true
+ elseif token.const then
+ -- Since word pattern contains last char we're re-adding it
+ str = token[2][2] .. str
+ token.raw = token.raw:sub(1, #token.raw - #token[2][2])
+ elseif token[1] == "id" then
+ -- Since id pattern contains last semi-colon we're re-adding it
+ str = token[2][2] .. str
+ token.raw = token.raw:sub(1, #token.raw - #token[2][2])
+ -- Trim
+ token[2][1] = string_trim(token[2][1])
+ elseif token[1] == "string" then
+ -- Finding numbers
+ local snip = token[2][1]
+ if not token.force_text then
+ if snip:match("^(-?%d+%.%d+)$") or snip:match("^(-?%d+)$") then
+ token[1] = "number"
+ end
+ end
+
+ elseif token[1] == "comment" then
+ ignore = true;
+ elseif token[1] == "indent" then
+ row = row + 1
+ inline = false
+ lastIndents = indents
+ if indentAmount == 0 then
+ indentAmount = #token[2][1]
+ end
+
+ if indentAmount ~= 0 then
+ indents = (#token[2][1] / indentAmount);
+ else
+ indents = 0
+ end
+
+ if indents == lastIndents then
+ ignore = true;
+ elseif indents > lastIndents + 2 then
+ error("SyntaxError: invalid indentation, got " .. tostring(indents)
+ .. " instead of " .. tostring(lastIndents) .. context(token[2].input))
+ elseif indents > lastIndents + 1 then
+ push(stack, token)
+ elseif indents < lastIndents then
+ local input = token[2].input
+ token = {"dedent", {"", input = ""}}
+ token.input = input
+ while lastIndents > indents + 1 do
+ lastIndents = lastIndents - 1
+ push(stack, token)
+ end
+ end
+ end -- if token[1] == XXX
+ token.row = row
+ break
+ end -- if #captures > 0
+ end
+
+ if not ignore then
+ if token then
+ push(stack, token)
+ token = nil
+ else
+ error("SyntaxError " .. context(str))
+ end
+ end
+
+ ignore = false;
+ end
+
+ return stack
+end
+
+Parser.peek = function (self, offset)
+ offset = offset or 1
+ return self.tokens[offset + self.current]
+end
+
+Parser.advance = function (self)
+ self.current = self.current + 1
+ return self.tokens[self.current]
+end
+
+Parser.advanceValue = function (self)
+ return self:advance()[2][1]
+end
+
+Parser.accept = function (self, type)
+ if self:peekType(type) then
+ return self:advance()
+ end
+end
+
+Parser.expect = function (self, type, msg)
+ return self:accept(type) or
+ error(msg .. context(self:peek()[1].input))
+end
+
+Parser.expectDedent = function (self, msg)
+ return self:accept("dedent") or (self:peek() == nil) or
+ error(msg .. context(self:peek()[2].input))
+end
+
+Parser.peekType = function (self, val, offset)
+ return self:peek(offset) and self:peek(offset)[1] == val
+end
+
+Parser.ignore = function (self, items)
+ local advanced
+ repeat
+ advanced = false
+ for _,v in pairs(items) do
+ if self:peekType(v) then
+ self:advance()
+ advanced = true
+ end
+ end
+ until advanced == false
+end
+
+Parser.ignoreSpace = function (self)
+ self:ignore{"space"}
+end
+
+Parser.ignoreWhitespace = function (self)
+ self:ignore{"space", "indent", "dedent"}
+end
+
+Parser.parse = function (self)
+
+ local ref = nil
+ if self:peekType("string") and not self:peek().force_text then
+ local char = self:peek()[2][1]:sub(1,1)
+ if char == "&" then
+ ref = self:peek()[2][1]:sub(2)
+ self:advanceValue()
+ self:ignoreSpace()
+ elseif char == "*" then
+ ref = self:peek()[2][1]:sub(2)
+ return self.refs[ref]
+ end
+ end
+
+ local result
+ local c = {
+ indent = self:accept("indent") and 1 or 0,
+ token = self:peek()
+ }
+ push(self.parse_stack, c)
+
+ if c.token[1] == "doc" then
+ result = self:parseDoc()
+ elseif c.token[1] == "-" then
+ result = self:parseList()
+ elseif c.token[1] == "{" then
+ result = self:parseInlineHash()
+ elseif c.token[1] == "[" then
+ result = self:parseInlineList()
+ elseif c.token[1] == "id" then
+ result = self:parseHash()
+ elseif c.token[1] == "string" then
+ result = self:parseString("\n")
+ elseif c.token[1] == "timestamp" then
+ result = self:parseTimestamp()
+ elseif c.token[1] == "number" then
+ result = tonumber(self:advanceValue())
+ elseif c.token[1] == "pipe" then
+ result = self:parsePipe()
+ elseif c.token.const == true then
+ self:advanceValue();
+ result = c.token.value
+ else
+ error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input))
+ end
+
+ pop(self.parse_stack)
+ while c.indent > 0 do
+ c.indent = c.indent - 1
+ local term = "term "..c.token[1]..": '"..c.token[2][1].."'"
+ self:expectDedent("last ".. term .." is not properly dedented")
+ end
+
+ if ref then
+ self.refs[ref] = result
+ end
+ return result
+end
+
+Parser.parseDoc = function (self)
+ self:accept("doc")
+ return self:parse()
+end
+
+Parser.inline = function (self)
+ local current = self:peek(0)
+ if not current then
+ return {}, 0
+ end
+
+ local inline = {}
+ local i = 0
+
+ while self:peek(i) and not self:peekType("indent", i) and current.row == self:peek(i).row do
+ inline[self:peek(i)[1]] = true
+ i = i - 1
+ end
+ return inline, -i
+end
+
+Parser.isInline = function (self)
+ local _, i = self:inline()
+ return i > 0
+end
+
+Parser.parent = function(self, level)
+ level = level or 1
+ return self.parse_stack[#self.parse_stack - level]
+end
+
+Parser.parentType = function(self, type, level)
+ return self:parent(level) and self:parent(level).token[1] == type
+end
+
+Parser.parseString = function (self)
+ if self:isInline() then
+ local result = self:advanceValue()
+
+ --[[
+ - a: this looks
+ flowing: but is
+ no: string
+ --]]
+ local types = self:inline()
+ if types["id"] and types["-"] then
+ if not self:peekType("indent") or not self:peekType("indent", 2) then
+ return result
+ end
+ end
+
+ --[[
+ a: 1
+ b: this is
+ a flowing string
+ example
+ c: 3
+ --]]
+ if self:peekType("indent") then
+ self:expect("indent", "text block needs to start with indent")
+ local addtl = self:accept("indent")
+
+ result = result .. "\n" .. self:parseTextBlock("\n")
+
+ self:expectDedent("text block ending dedent missing")
+ if addtl then
+ self:expectDedent("text block ending dedent missing")
+ end
+ end
+ return result
+ else
+ --[[
+ a: 1
+ b:
+ this is also
+ a flowing string
+ example
+ c: 3
+ --]]
+ return self:parseTextBlock("\n")
+ end
+end
+
+Parser.parsePipe = function (self)
+ local pipe = self:expect("pipe")
+ self:expect("indent", "text block needs to start with indent")
+ local result = self:parseTextBlock(pipe.sep)
+ self:expectDedent("text block ending dedent missing")
+ return result
+end
+
+Parser.parseTextBlock = function (self, sep)
+ local token = self:advance()
+ local result = string_trim(token.raw, "\n")
+ local indents = 0
+ while self:peek() ~= nil and ( indents > 0 or not self:peekType("dedent") ) do
+ local newtoken = self:advance()
+ while token.row < newtoken.row do
+ result = result .. sep
+ token.row = token.row + 1
+ end
+ if newtoken[1] == "indent" then
+ indents = indents + 1
+ elseif newtoken[1] == "dedent" then
+ indents = indents - 1
+ else
+ result = result .. string_trim(newtoken.raw, "\n")
+ end
+ end
+ return result
+end
+
+Parser.parseHash = function (self, hash)
+ hash = hash or {}
+ local indents = 0
+
+ if self:isInline() then
+ local id = self:advanceValue()
+ self:expect(":", "expected semi-colon after id")
+ self:ignoreSpace()
+ if self:accept("indent") then
+ indents = indents + 1
+ hash[id] = self:parse()
+ else
+ hash[id] = self:parse()
+ if self:accept("indent") then
+ indents = indents + 1
+ end
+ end
+ self:ignoreSpace();
+ end
+
+ while self:peekType("id") do
+ local id = self:advanceValue()
+ self:expect(":","expected semi-colon after id")
+ self:ignoreSpace()
+ hash[id] = self:parse()
+ self:ignoreSpace();
+ end
+
+ while indents > 0 do
+ self:expectDedent("expected dedent")
+ indents = indents - 1
+ end
+
+ return hash
+end
+
+Parser.parseInlineHash = function (self)
+ local id
+ local hash = {}
+ local i = 0
+
+ self:accept("{")
+ while not self:accept("}") do
+ self:ignoreSpace()
+ if i > 0 then
+ self:expect(",","expected comma")
+ end
+
+ self:ignoreWhitespace()
+ if self:peekType("id") then
+ id = self:advanceValue()
+ if id then
+ self:expect(":","expected semi-colon after id")
+ self:ignoreSpace()
+ hash[id] = self:parse()
+ self:ignoreWhitespace()
+ end
+ end
+
+ i = i + 1
+ end
+ return hash
+end
+
+Parser.parseList = function (self)
+ local list = {}
+ while self:accept("-") do
+ self:ignoreSpace()
+ list[#list + 1] = self:parse()
+
+ self:ignoreSpace()
+ end
+ return list
+end
+
+Parser.parseInlineList = function (self)
+ local list = {}
+ local i = 0
+ self:accept("[")
+ while not self:accept("]") do
+ self:ignoreSpace()
+ if i > 0 then
+ self:expect(",","expected comma")
+ end
+
+ self:ignoreSpace()
+ list[#list + 1] = self:parse()
+ self:ignoreSpace()
+ i = i + 1
+ end
+
+ return list
+end
+
+Parser.parseTimestamp = function (self)
+ local capture = self:advance()[2]
+
+ return os.time{
+ year = capture[1],
+ month = capture[2],
+ day = capture[3],
+ hour = capture[4] or 0,
+ min = capture[5] or 0,
+ sec = capture[6] or 0,
+ isdst = false,
+ } - os.time{year=1970, month=1, day=1, hour=8}
+end
+
+exports.eval = function (str)
+ return Parser:new(exports.tokenize(str)):parse()
+end
+
+exports.dump = table_print
+
+return exports
diff --git a/libexec/rc/rc.d/Makefile b/libexec/rc/rc.d/Makefile
index 4f4bcabd449c..320408d5d62c 100644
--- a/libexec/rc/rc.d/Makefile
+++ b/libexec/rc/rc.d/Makefile
@@ -313,6 +313,12 @@ SMRCD= sendmail
SMRCDPACKAGE= sendmail
.endif
+.if ${MK_NUAGEINIT} != "no"
+CONFGROUPS+= NIUAGEINIT
+NIUAGEINIT= nuageinit
+NIUAGEINITPACKAGE= nuageinit
+.endif
+
.if ${MK_UNBOUND} != "no"
CONFGROUPS+= UNBOUND
UNBOUND+= local_unbound
diff --git a/libexec/rc/rc.d/nuageinit b/libexec/rc/rc.d/nuageinit
new file mode 100755
index 000000000000..9c914c340015
--- /dev/null
+++ b/libexec/rc/rc.d/nuageinit
@@ -0,0 +1,67 @@
+#!/bin/sh
+#
+
+# PROVIDE: nuageinit
+# REQUIRE: mountcritlocal
+# BEFORE: NETWORKING
+# KEYWORD: firstboot
+
+. /etc/rc.subr
+
+name="nuageinit"
+desc="Limited Cloud Init configuration"
+start_cmd="nuageinit_start"
+stop_cmd=":"
+rcvar="nuageinit_enable"
+
+nuageinit_start()
+{
+ local citype
+ # detect cloud init provider
+ # according to the specification of the config drive
+ # it either formatted in vfat or iso9660 and labeled
+ # config-2
+ for f in iso9660 msdosfs; do
+ drive=/dev/$f/config-2
+ if [ -e $drive ]; then
+ citype=config-2
+ break
+ fi
+ drive=/dev/$f/cidata
+ if [ -e $drive ]; then
+ citype=nocloud
+ break
+ fi
+ unset drive
+ done
+ if [ -z "$drive" ]; then
+ # try to detect networked based instance
+ err 1 "Impossible to find a cloud init provider"
+ fi
+ mkdir -p /media/nuageinit
+ fs=$(fstyp $drive)
+ mount -t $fs $drive /media/nuageinit
+ # according to the specification, the content is either
+ # in the openstack or ec2 directory
+ case "$citype" in
+ config-2)
+ for d in openstack ec2; do
+ dir=/media/nuageinit/$d/latest
+ if [ -d $dir ]; then
+ /usr/libexec/nuageinit $dir $citype
+ break
+ fi
+ done
+ ;;
+ nocloud)
+ /usr/libexec/nuageinit /media/nuageinit $citype
+ ;;
+ esac
+ if [ -n "$drive" ]; then
+ umount /media/nuageinit
+ fi
+ rmdir /media/nuageinit
+}
+
+load_rc_config $name
+run_rc_command "$1"