aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBaptiste Daroussin <bapt@FreeBSD.org>2022-11-23 19:00:39 +0000
committerBaptiste Daroussin <bapt@FreeBSD.org>2024-04-11 11:52:52 +0000
commit16a6da44e28d607f2383be7c7c325090979f531f (patch)
treede938203ebf1b28b72dfd8d496c4ad19b4fe7040
parent7ced57106295ac92a0584ea03e0a05b66973ed73 (diff)
downloadsrc-16a6da44e28d.tar.gz
src-16a6da44e28d.zip
nuageinit: add basic support for cloudinit.
this is a very early script to support cloudinit, it does not intend to be a full featured cloudinit client, but will support a good enough subset to be viable in most case. It support nocloud and openstack config-2 config drive mode (iso9660 or msdosfs) The following features are currently supported: - adding users (including a default user named 'freebsd' with password 'freebsd' - adding groups - adding ssh keys - static ipv4, static ipv6, dynamic ipv4 With this one is able to use the 'bring your own image feature" out of box. It is expected that the script grows the support of other clouds supporting cloud-init, contributions are welcomed. It is designed to be only run once via the firstboot mecanism. Sponsored by: OVHCloud Differential Revision: https://reviews.freebsd.org/D44141 (cherry picked from commit a42d6f76018e4ed8324e319ab48aac904bda437c) (cherry picked from commit c051f22bce42d920abba61bd7cf4ef5b6a270ffa) (cherry picked from commit b8c053c9a612651d4909f7a323088f3e92485b7b) (cherry picked from commit 9eae9233fdcc946945f4191e1413f548adfa2943)
-rw-r--r--etc/mtree/BSD.tests.dist2
-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
-rw-r--r--release/packages/Makefile.package2
-rw-r--r--share/mk/src.opts.mk1
-rw-r--r--tools/build/mk/OptionalObsoleteFiles.inc21
-rw-r--r--tools/build/options/WITHOUT_NUAGEINIT1
23 files changed, 1704 insertions, 0 deletions
diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index a3c5cae09567..81cd5baca513 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -435,6 +435,8 @@
atf-sh
..
..
+ nuageinit
+ ..
rc
..
rtld-elf
diff --git a/libexec/Makefile b/libexec/Makefile
index bd1571edcfe4..204c7ef31bdb 100644
--- a/libexec/Makefile
+++ b/libexec/Makefile
@@ -29,6 +29,7 @@ SUBDIR= ${_atf} \
${_rshd} \
${_rtld-elf} \
save-entropy \
+ ${_nuageinit} \
${_smrsh} \
${_tests} \
${_tftp-proxy} \
@@ -121,6 +122,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..977b44e465fe
--- /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/[cC][oO][nN][fF][iI][gG]-2"
+ if [ -e $drive ]; then
+ citype=config-2
+ break
+ fi
+ drive="/dev/$f/[cC][iI][dD][aA][tT][aA]"
+ 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"
diff --git a/release/packages/Makefile.package b/release/packages/Makefile.package
index d9f202a602d1..ed3da78665be 100644
--- a/release/packages/Makefile.package
+++ b/release/packages/Makefile.package
@@ -112,6 +112,8 @@ newsyslog_COMMENT= Newsyslog Utility
newsyslog_DESC= Newsyslog Utility
nfs_COMMENT= NFS Utilities
nfs_DESC= NFS Utilities
+nuageinit_COMMENT= CloudInit support scripts
+nuageinit_DESC= CloudInit support scripts
nvme-tools_COMMENT= NVME Utilities
nvme-tools_DESC= NVME Utilities
openssl_COMMENT= OpenSSL Utility
diff --git a/share/mk/src.opts.mk b/share/mk/src.opts.mk
index 7b3e8a441229..a8b33fcdc6d6 100644
--- a/share/mk/src.opts.mk
+++ b/share/mk/src.opts.mk
@@ -149,6 +149,7 @@ __DEFAULT_YES_OPTIONS = \
NLS_CATALOGS \
NS_CACHING \
NTP \
+ NUAGEINIT \
NVME \
OFED \
OPENSSL \
diff --git a/tools/build/mk/OptionalObsoleteFiles.inc b/tools/build/mk/OptionalObsoleteFiles.inc
index 86b068a09b82..8fdd1dff768a 100644
--- a/tools/build/mk/OptionalObsoleteFiles.inc
+++ b/tools/build/mk/OptionalObsoleteFiles.inc
@@ -7362,6 +7362,27 @@ OLD_DIRS+=var/spool/clientmqueue
OLD_FILES+=var/db/services.db
.endif
+.if ${MK_NUAGEINIT} == no
+OLD_FILES+=etc/rc.d/nuageinit
+OLD_FILES+=usr/libexec/nuageinit
+OLD_FILES+=usr/share/flua/nuage.lua
+OLD_FILES+=usr/share/flua/yaml.lua
+OLD_FILES+=usr/tests
+OLD_FILES+=usr/tests/libexec/nuageinit/utils
+OLD_FILES+=usr/tests/libexec/nuageinit/nuage
+OLD_FILES+=usr/tests/libexec/nuageinit/warn.lua
+OLD_FILES+=usr/tests/libexec/nuageinit/dirname.lua
+OLD_FILES+=usr/tests/libexec/nuageinit/err.lua
+OLD_FILES+=usr/tests/libexec/nuageinit/adduser.lua
+OLD_FILES+=usr/tests/libexec/nuageinit/addsshkeys.lua
+OLD_FILES+=usr/tests/libexec/nuageinit/Kyuafile
+OLD_FILES+=usr/tests/libexec/nuageinit/ktrace.out
+OLD_FILES+=usr/tests/libexec/nuageinit/addgroup.lua
+OLD_FILES+=usr/tests/libexec/nuageinit/sethostname.lua
+OLD_FILES+=usr/tests/libexec/nuageinit/nuageinit
+OLD_FILES+=usr/tests/libexec/nuageinit/addsshkey.lua
+.endif
+
.if ${MK_SHAREDOCS} == no
OLD_FILES+=usr/share/doc/pjdfstest/README
OLD_DIRS+=usr/share/doc/pjdfstest
diff --git a/tools/build/options/WITHOUT_NUAGEINIT b/tools/build/options/WITHOUT_NUAGEINIT
new file mode 100644
index 000000000000..14a17ce0ed5b
--- /dev/null
+++ b/tools/build/options/WITHOUT_NUAGEINIT
@@ -0,0 +1 @@
+Do not install the limited cloud init support scripts.