xref: /freebsd/libexec/nuageinit/nuage.lua (revision db33c6f3ae9d1231087710068ee4ea5398aacca7)
1---
2-- SPDX-License-Identifier: BSD-2-Clause
3--
4-- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
5
6local unistd = require("posix.unistd")
7local sys_stat = require("posix.sys.stat")
8local lfs = require("lfs")
9
10local function warnmsg(str, prepend)
11	if not str then
12		return
13	end
14	local tag = ""
15	if prepend ~= false then
16		tag = "nuageinit: "
17	end
18	io.stderr:write(tag .. str .. "\n")
19end
20
21local function errmsg(str, prepend)
22	warnmsg(str, prepend)
23	os.exit(1)
24end
25
26local function dirname(oldpath)
27	if not oldpath then
28		return nil
29	end
30	local path = oldpath:gsub("[^/]+/*$", "")
31	if path == "" then
32		return nil
33	end
34	return path
35end
36
37local function mkdir_p(path)
38	if lfs.attributes(path, "mode") ~= nil then
39		return true
40	end
41	local r, err = mkdir_p(dirname(path))
42	if not r then
43		return nil, err .. " (creating " .. path .. ")"
44	end
45	return lfs.mkdir(path)
46end
47
48local function sethostname(hostname)
49	if hostname == nil then
50		return
51	end
52	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
53	if not root then
54		root = ""
55	end
56	local hostnamepath = root .. "/etc/rc.conf.d/hostname"
57
58	mkdir_p(dirname(hostnamepath))
59	local f, err = io.open(hostnamepath, "w")
60	if not f then
61		warnmsg("Impossible to open " .. hostnamepath .. ":" .. err)
62		return
63	end
64	f:write('hostname="' .. hostname .. '"\n')
65	f:close()
66end
67
68local function splitlist(list)
69	local ret = {}
70	if type(list) == "string" then
71		for str in list:gmatch("([^, ]+)") do
72			ret[#ret + 1] = str
73		end
74	elseif type(list) == "table" then
75		ret = list
76	else
77		warnmsg("Invalid type " .. type(list) .. ", expecting table or string")
78	end
79	return ret
80end
81
82local function adduser(pwd)
83	if (type(pwd) ~= "table") then
84		warnmsg("Argument should be a table")
85		return nil
86	end
87	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
88	local cmd = "pw "
89	if root then
90		cmd = cmd .. "-R " .. root .. " "
91	end
92	local f = io.popen(cmd .. " usershow " .. pwd.name .. " -7 2> /dev/null")
93	local pwdstr = f:read("*a")
94	f:close()
95	if pwdstr:len() ~= 0 then
96		return pwdstr:match("%a+:.+:%d+:%d+:.*:(.*):.*")
97	end
98	if not pwd.gecos then
99		pwd.gecos = pwd.name .. " User"
100	end
101	if not pwd.homedir then
102		pwd.homedir = "/home/" .. pwd.name
103	end
104	local extraargs = ""
105	if pwd.groups then
106		local list = splitlist(pwd.groups)
107		extraargs = " -G " .. table.concat(list, ",")
108	end
109	-- pw will automatically create a group named after the username
110	-- do not add a -g option in this case
111	if pwd.primary_group and pwd.primary_group ~= pwd.name then
112		extraargs = extraargs .. " -g " .. pwd.primary_group
113	end
114	if not pwd.no_create_home then
115		extraargs = extraargs .. " -m "
116	end
117	if not pwd.shell then
118		pwd.shell = "/bin/sh"
119	end
120	local precmd = ""
121	local postcmd = ""
122	local input = nil
123	if pwd.passwd then
124		input = pwd.passwd
125		postcmd = " -H 0"
126	elseif pwd.plain_text_passwd then
127		input = pwd.plain_text_passwd
128		postcmd = " -h 0"
129	end
130	cmd = precmd .. "pw "
131	if root then
132		cmd = cmd .. "-R " .. root .. " "
133	end
134	cmd = cmd .. "useradd -n " .. pwd.name .. " -M 0755 -w none "
135	cmd = cmd .. extraargs .. " -c '" .. pwd.gecos
136	cmd = cmd .. "' -d '" .. pwd.homedir .. "' -s " .. pwd.shell .. postcmd
137
138	local f = io.popen(cmd, "w")
139	if input then
140		f:write(input)
141	end
142	local r = f:close(cmd)
143	if not r then
144		warnmsg("fail to add user " .. pwd.name)
145		warnmsg(cmd)
146		return nil
147	end
148	if pwd.locked then
149		cmd = "pw "
150		if root then
151			cmd = cmd .. "-R " .. root .. " "
152		end
153		cmd = cmd .. "lock " .. pwd.name
154		os.execute(cmd)
155	end
156	return pwd.homedir
157end
158
159local function addgroup(grp)
160	if (type(grp) ~= "table") then
161		warnmsg("Argument should be a table")
162		return false
163	end
164	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
165	local cmd = "pw "
166	if root then
167		cmd = cmd .. "-R " .. root .. " "
168	end
169	local f = io.popen(cmd .. " groupshow " .. grp.name .. " 2> /dev/null")
170	local grpstr = f:read("*a")
171	f:close()
172	if grpstr:len() ~= 0 then
173		return true
174	end
175	local extraargs = ""
176	if grp.members then
177		local list = splitlist(grp.members)
178		extraargs = " -M " .. table.concat(list, ",")
179	end
180	cmd = "pw "
181	if root then
182		cmd = cmd .. "-R " .. root .. " "
183	end
184	cmd = cmd .. "groupadd -n " .. grp.name .. extraargs
185	local r = os.execute(cmd)
186	if not r then
187		warnmsg("fail to add group " .. grp.name)
188		warnmsg(cmd)
189		return false
190	end
191	return true
192end
193
194local function addsshkey(homedir, key)
195	local chownak = false
196	local chowndotssh = false
197	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
198	if root then
199		homedir = root .. "/" .. homedir
200	end
201	local ak_path = homedir .. "/.ssh/authorized_keys"
202	local dotssh_path = homedir .. "/.ssh"
203	local dirattrs = lfs.attributes(ak_path)
204	if dirattrs == nil then
205		chownak = true
206		dirattrs = lfs.attributes(dotssh_path)
207		if dirattrs == nil then
208			assert(lfs.mkdir(dotssh_path))
209			chowndotssh = true
210			dirattrs = lfs.attributes(homedir)
211		end
212	end
213
214	local f = io.open(ak_path, "a")
215	if not f then
216		warnmsg("impossible to open " .. ak_path)
217		return
218	end
219	f:write(key .. "\n")
220	f:close()
221	if chownak then
222		sys_stat.chmod(ak_path, 384)
223		unistd.chown(ak_path, dirattrs.uid, dirattrs.gid)
224	end
225	if chowndotssh then
226		sys_stat.chmod(dotssh_path, 448)
227		unistd.chown(dotssh_path, dirattrs.uid, dirattrs.gid)
228	end
229end
230
231local n = {
232	warn = warnmsg,
233	err = errmsg,
234	dirname = dirname,
235	mkdir_p = mkdir_p,
236	sethostname = sethostname,
237	adduser = adduser,
238	addgroup = addgroup,
239	addsshkey = addsshkey
240}
241
242return n
243