diff options
author | Baptiste Daroussin <bapt@FreeBSD.org> | 2022-11-23 19:00:39 +0000 |
---|---|---|
committer | Baptiste Daroussin <bapt@FreeBSD.org> | 2024-03-15 08:22:16 +0000 |
commit | a42d6f76018e4ed8324e319ab48aac904bda437c (patch) | |
tree | c382f13d72fcf14f0c3ea63e270b8ade3d508442 | |
parent | 3705d679a6344c957cae7a1b6372a8bfb8c44f0e (diff) | |
download | src-a42d6f76018e4ed8324e319ab48aac904bda437c.tar.gz src-a42d6f76018e4ed8324e319ab48aac904bda437c.zip |
-rw-r--r-- | libexec/Makefile | 5 | ||||
-rw-r--r-- | libexec/nuageinit/Makefile | 11 | ||||
-rw-r--r-- | libexec/nuageinit/nuage.lua | 214 | ||||
-rwxr-xr-x | libexec/nuageinit/nuageinit | 312 | ||||
-rw-r--r-- | libexec/nuageinit/tests/Makefile | 13 | ||||
-rw-r--r-- | libexec/nuageinit/tests/addgroup.lua | 15 | ||||
-rw-r--r-- | libexec/nuageinit/tests/addsshkey.lua | 2 | ||||
-rw-r--r-- | libexec/nuageinit/tests/adduser.lua | 15 | ||||
-rw-r--r-- | libexec/nuageinit/tests/dirname.lua | 8 | ||||
-rw-r--r-- | libexec/nuageinit/tests/err.lua | 4 | ||||
-rw-r--r-- | libexec/nuageinit/tests/nuage.sh | 52 | ||||
-rw-r--r-- | libexec/nuageinit/tests/nuageinit.sh | 338 | ||||
-rw-r--r-- | libexec/nuageinit/tests/sethostname.lua | 4 | ||||
-rw-r--r-- | libexec/nuageinit/tests/utils.sh | 21 | ||||
-rw-r--r-- | libexec/nuageinit/tests/warn.lua | 4 | ||||
-rw-r--r-- | libexec/nuageinit/yaml.lua | 586 | ||||
-rw-r--r-- | libexec/rc/rc.d/Makefile | 6 | ||||
-rwxr-xr-x | libexec/rc/rc.d/nuageinit | 67 | ||||
-rw-r--r-- | share/mk/src.opts.mk | 1 | ||||
-rw-r--r-- | tools/build/mk/OptionalObsoleteFiles.inc | 21 | ||||
-rw-r--r-- | tools/build/options/WITHOUT_NUAGEINIT | 1 |
21 files changed, 1700 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" diff --git a/share/mk/src.opts.mk b/share/mk/src.opts.mk index 3666094bfc40..99218febf476 100644 --- a/share/mk/src.opts.mk +++ b/share/mk/src.opts.mk @@ -148,6 +148,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 680bf02fa783..896f3e5ec43d 100644 --- a/tools/build/mk/OptionalObsoleteFiles.inc +++ b/tools/build/mk/OptionalObsoleteFiles.inc @@ -7347,6 +7347,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. |