Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/libexec/nuageinit/nuage.lua
34856 views
1
---
2
-- SPDX-License-Identifier: BSD-2-Clause
3
--
4
-- Copyright(c) 2022-2025 Baptiste Daroussin <[email protected]>
5
6
local unistd = require("posix.unistd")
7
local sys_stat = require("posix.sys.stat")
8
local lfs = require("lfs")
9
10
local function decode_base64(input)
11
local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
12
input = string.gsub(input, '[^'..b..'=]', '')
13
14
local result = {}
15
local bits = ''
16
17
-- convert all characters in bits
18
for i = 1, #input do
19
local x = input:sub(i, i)
20
if x == '=' then
21
break
22
end
23
local f = b:find(x) - 1
24
for j = 6, 1, -1 do
25
bits = bits .. (f % 2^j - f % 2^(j-1) > 0 and '1' or '0')
26
end
27
end
28
29
for i = 1, #bits, 8 do
30
local byte = bits:sub(i, i + 7)
31
if #byte == 8 then
32
local c = 0
33
for j = 1, 8 do
34
c = c + (byte:sub(j, j) == '1' and 2^(8 - j) or 0)
35
end
36
table.insert(result, string.char(c))
37
end
38
end
39
40
return table.concat(result)
41
end
42
43
local function warnmsg(str, prepend)
44
if not str then
45
return
46
end
47
local tag = ""
48
if prepend ~= false then
49
tag = "nuageinit: "
50
end
51
io.stderr:write(tag .. str .. "\n")
52
end
53
54
local function errmsg(str, prepend)
55
warnmsg(str, prepend)
56
os.exit(1)
57
end
58
59
local function chmod(path, mode)
60
local mode = tonumber(mode, 8)
61
local _, err, msg = sys_stat.chmod(path, mode)
62
if err then
63
errmsg("chmod(" .. path .. ", " .. mode .. ") failed: " .. msg)
64
end
65
end
66
67
local function chown(path, owner, group)
68
local _, err, msg = unistd.chown(path, owner, group)
69
if err then
70
errmsg("chown(" .. path .. ", " .. owner .. ", " .. group .. ") failed: " .. msg)
71
end
72
end
73
74
local function dirname(oldpath)
75
if not oldpath then
76
return nil
77
end
78
local path = oldpath:gsub("[^/]+/*$", "")
79
if path == "" then
80
return nil
81
end
82
return path
83
end
84
85
local function mkdir_p(path)
86
if lfs.attributes(path, "mode") ~= nil then
87
return true
88
end
89
local r, err = mkdir_p(dirname(path))
90
if not r then
91
return nil, err .. " (creating " .. path .. ")"
92
end
93
return lfs.mkdir(path)
94
end
95
96
local function sethostname(hostname)
97
if hostname == nil then
98
return
99
end
100
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
101
if not root then
102
root = ""
103
end
104
local hostnamepath = root .. "/etc/rc.conf.d/hostname"
105
106
mkdir_p(dirname(hostnamepath))
107
local f, err = io.open(hostnamepath, "w")
108
if not f then
109
warnmsg("Impossible to open " .. hostnamepath .. ":" .. err)
110
return
111
end
112
f:write('hostname="' .. hostname .. '"\n')
113
f:close()
114
end
115
116
local function splitlist(list)
117
local ret = {}
118
if type(list) == "string" then
119
for str in list:gmatch("([^, ]+)") do
120
ret[#ret + 1] = str
121
end
122
elseif type(list) == "table" then
123
ret = list
124
else
125
warnmsg("Invalid type " .. type(list) .. ", expecting table or string")
126
end
127
return ret
128
end
129
130
local function adduser(pwd)
131
if (type(pwd) ~= "table") then
132
warnmsg("Argument should be a table")
133
return nil
134
end
135
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
136
local cmd = "pw "
137
if root then
138
cmd = cmd .. "-R " .. root .. " "
139
end
140
local f = io.popen(cmd .. " usershow " .. pwd.name .. " -7 2> /dev/null")
141
local pwdstr = f:read("*a")
142
f:close()
143
if pwdstr:len() ~= 0 then
144
return pwdstr:match("%a+:.+:%d+:%d+:.*:(.*):.*")
145
end
146
if not pwd.gecos then
147
pwd.gecos = pwd.name .. " User"
148
end
149
if not pwd.homedir then
150
pwd.homedir = "/home/" .. pwd.name
151
end
152
local extraargs = ""
153
if pwd.groups then
154
local list = splitlist(pwd.groups)
155
extraargs = " -G " .. table.concat(list, ",")
156
end
157
-- pw will automatically create a group named after the username
158
-- do not add a -g option in this case
159
if pwd.primary_group and pwd.primary_group ~= pwd.name then
160
extraargs = extraargs .. " -g " .. pwd.primary_group
161
end
162
if not pwd.no_create_home then
163
extraargs = extraargs .. " -m "
164
end
165
if not pwd.shell then
166
pwd.shell = "/bin/sh"
167
end
168
local precmd = ""
169
local postcmd = ""
170
local input = nil
171
if pwd.passwd then
172
input = pwd.passwd
173
postcmd = " -H 0"
174
elseif pwd.plain_text_passwd then
175
input = pwd.plain_text_passwd
176
postcmd = " -h 0"
177
end
178
cmd = precmd .. "pw "
179
if root then
180
cmd = cmd .. "-R " .. root .. " "
181
end
182
cmd = cmd .. "useradd -n " .. pwd.name .. " -M 0755 -w none "
183
cmd = cmd .. extraargs .. " -c '" .. pwd.gecos
184
cmd = cmd .. "' -d '" .. pwd.homedir .. "' -s " .. pwd.shell .. postcmd
185
186
f = io.popen(cmd, "w")
187
if input then
188
f:write(input)
189
end
190
local r = f:close(cmd)
191
if not r then
192
warnmsg("fail to add user " .. pwd.name)
193
warnmsg(cmd)
194
return nil
195
end
196
if pwd.locked then
197
cmd = "pw "
198
if root then
199
cmd = cmd .. "-R " .. root .. " "
200
end
201
cmd = cmd .. "lock " .. pwd.name
202
os.execute(cmd)
203
end
204
return pwd.homedir
205
end
206
207
local function addgroup(grp)
208
if (type(grp) ~= "table") then
209
warnmsg("Argument should be a table")
210
return false
211
end
212
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
213
local cmd = "pw "
214
if root then
215
cmd = cmd .. "-R " .. root .. " "
216
end
217
local f = io.popen(cmd .. " groupshow " .. grp.name .. " 2> /dev/null")
218
local grpstr = f:read("*a")
219
f:close()
220
if grpstr:len() ~= 0 then
221
return true
222
end
223
local extraargs = ""
224
if grp.members then
225
local list = splitlist(grp.members)
226
extraargs = " -M " .. table.concat(list, ",")
227
end
228
cmd = "pw "
229
if root then
230
cmd = cmd .. "-R " .. root .. " "
231
end
232
cmd = cmd .. "groupadd -n " .. grp.name .. extraargs
233
local r = os.execute(cmd)
234
if not r then
235
warnmsg("fail to add group " .. grp.name)
236
warnmsg(cmd)
237
return false
238
end
239
return true
240
end
241
242
local function addsshkey(homedir, key)
243
local chownak = false
244
local chowndotssh = false
245
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
246
if root then
247
homedir = root .. "/" .. homedir
248
end
249
local ak_path = homedir .. "/.ssh/authorized_keys"
250
local dotssh_path = homedir .. "/.ssh"
251
local dirattrs = lfs.attributes(ak_path)
252
if dirattrs == nil then
253
chownak = true
254
dirattrs = lfs.attributes(dotssh_path)
255
if dirattrs == nil then
256
assert(lfs.mkdir(dotssh_path))
257
chowndotssh = true
258
dirattrs = lfs.attributes(homedir)
259
end
260
end
261
262
local f = io.open(ak_path, "a")
263
if not f then
264
warnmsg("impossible to open " .. ak_path)
265
return
266
end
267
f:write(key .. "\n")
268
f:close()
269
if chownak then
270
chmod(ak_path, "0600")
271
chown(ak_path, dirattrs.uid, dirattrs.gid)
272
end
273
if chowndotssh then
274
chmod(dotssh_path, "0700")
275
chown(dotssh_path, dirattrs.uid, dirattrs.gid)
276
end
277
end
278
279
local function addsudo(pwd)
280
local chmodsudoersd = false
281
local chmodsudoers = false
282
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
283
local sudoers_dir = "/usr/local/etc/sudoers.d"
284
if root then
285
sudoers_dir= root .. sudoers_dir
286
end
287
local sudoers = sudoers_dir .. "/90-nuageinit-users"
288
local sudoers_attr = lfs.attributes(sudoers)
289
if sudoers_attr == nil then
290
chmodsudoers = true
291
local dirattrs = lfs.attributes(sudoers_dir)
292
if dirattrs == nil then
293
local r, err = mkdir_p(sudoers_dir)
294
if not r then
295
return nil, err .. " (creating " .. sudoers_dir .. ")"
296
end
297
chmodsudoersd = true
298
end
299
end
300
local f = io.open(sudoers, "a")
301
if not f then
302
warnmsg("impossible to open " .. sudoers)
303
return
304
end
305
if type(pwd.sudo) == "string" then
306
f:write(pwd.name .. " " .. pwd.sudo .. "\n")
307
elseif type(pwd.sudo) == "table" then
308
for _, str in ipairs(pwd.sudo) do
309
f:write(pwd.name .. " " .. str .. "\n")
310
end
311
end
312
f:close()
313
if chmodsudoers then
314
chmod(sudoers, "0640")
315
end
316
if chmodsudoersd then
317
chmod(sudoers, "0740")
318
end
319
end
320
321
local function update_sshd_config(key, value)
322
local sshd_config = "/etc/ssh/sshd_config"
323
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
324
if root then
325
sshd_config = root .. sshd_config
326
end
327
local f = assert(io.open(sshd_config, "r+"))
328
local tgt = assert(io.open(sshd_config .. ".nuageinit", "w"))
329
local found = false
330
local pattern = "^%s*"..key:lower().."%s+(%w+)%s*#?.*$"
331
while true do
332
local line = f:read()
333
if line == nil then break end
334
local _, _, val = line:lower():find(pattern)
335
if val then
336
found = true
337
if val == value then
338
assert(tgt:write(line .. "\n"))
339
else
340
assert(tgt:write(key .. " " .. value .. "\n"))
341
end
342
else
343
assert(tgt:write(line .. "\n"))
344
end
345
end
346
if not found then
347
assert(tgt:write(key .. " " .. value .. "\n"))
348
end
349
assert(f:close())
350
assert(tgt:close())
351
os.rename(sshd_config .. ".nuageinit", sshd_config)
352
end
353
354
local function exec_change_password(user, password, type, expire)
355
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
356
local cmd = "pw "
357
if root then
358
cmd = cmd .. "-R " .. root .. " "
359
end
360
local postcmd = " -H 0"
361
local input = password
362
if type ~= nil and type == "text" then
363
postcmd = " -h 0"
364
else
365
if password == "RANDOM" then
366
input = nil
367
postcmd = " -w random"
368
end
369
end
370
cmd = cmd .. "usermod " .. user .. postcmd
371
if expire then
372
cmd = cmd .. " -p 1"
373
else
374
cmd = cmd .. " -p 0"
375
end
376
local f = io.popen(cmd .. " >/dev/null", "w")
377
if input then
378
f:write(input)
379
end
380
-- ignore stdout to avoid printing the password in case of random password
381
local r = f:close(cmd)
382
if not r then
383
warnmsg("fail to change user password ".. user)
384
warnmsg(cmd)
385
end
386
end
387
388
local function change_password_from_line(line, expire)
389
local user, password = line:match("%s*(%w+):(%S+)%s*")
390
local type = nil
391
if user and password then
392
if password == "R" then
393
password = "RANDOM"
394
end
395
if not password:match("^%$%d+%$%w+%$") then
396
if password ~= "RANDOM" then
397
type = "text"
398
end
399
end
400
exec_change_password(user, password, type, expire)
401
end
402
end
403
404
local function chpasswd(obj)
405
if type(obj) ~= "table" then
406
warnmsg("Invalid chpasswd entry, expecting an object")
407
return
408
end
409
local expire = false
410
if obj.expire ~= nil then
411
if type(obj.expire) == "boolean" then
412
expire = obj.expire
413
else
414
warnmsg("Invalid type for chpasswd.expire, expecting a boolean, got a ".. type(obj.expire))
415
end
416
end
417
if obj.users ~= nil then
418
if type(obj.users) ~= "table" then
419
warnmsg("Invalid type for chpasswd.users, expecting a list, got a ".. type(obj.users))
420
goto list
421
end
422
for _, u in ipairs(obj.users) do
423
if type(u) ~= "table" then
424
warnmsg("Invalid chpasswd.users entry, expecting an object, got a " .. type(u))
425
goto next
426
end
427
if not u.name then
428
warnmsg("Invalid entry for chpasswd.users: missing 'name'")
429
goto next
430
end
431
if not u.password then
432
warnmsg("Invalid entry for chpasswd.users: missing 'password'")
433
goto next
434
end
435
exec_change_password(u.name, u.password, u.type, expire)
436
::next::
437
end
438
end
439
::list::
440
if obj.list ~= nil then
441
warnmsg("chpasswd.list is deprecated consider using chpasswd.users")
442
if type(obj.list) == "string" then
443
for line in obj.list:gmatch("[^\n]+") do
444
change_password_from_line(line, expire)
445
end
446
elseif type(obj.list) == "table" then
447
for _, u in ipairs(obj.list) do
448
change_password_from_line(u, expire)
449
end
450
end
451
end
452
end
453
454
local function settimezone(timezone)
455
if timezone == nil then
456
return
457
end
458
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
459
if not root then
460
root = "/"
461
end
462
463
f, _, rc = os.execute("tzsetup -s -C " .. root .. " " .. timezone)
464
465
if not f then
466
warnmsg("Impossible to configure time zone ( rc = " .. rc .. " )")
467
return
468
end
469
end
470
471
local function pkg_bootstrap()
472
if os.getenv("NUAGE_RUN_TESTS") then
473
return true
474
end
475
if os.execute("pkg -N 2>/dev/null") then
476
return true
477
end
478
print("Bootstrapping pkg")
479
return os.execute("env ASSUME_ALWAYS_YES=YES pkg bootstrap")
480
end
481
482
local function install_package(package)
483
if package == nil then
484
return true
485
end
486
local install_cmd = "pkg install -y " .. package
487
local test_cmd = "pkg info -q " .. package
488
if os.getenv("NUAGE_RUN_TESTS") then
489
print(install_cmd)
490
print(test_cmd)
491
return true
492
end
493
if os.execute(test_cmd) then
494
return true
495
end
496
return os.execute(install_cmd)
497
end
498
499
local function run_pkg_cmd(subcmd)
500
local cmd = "env ASSUME_ALWAYS_YES=yes pkg " .. subcmd
501
if os.getenv("NUAGE_RUN_TESTS") then
502
print(cmd)
503
return true
504
end
505
return os.execute(cmd)
506
end
507
local function update_packages()
508
return run_pkg_cmd("update")
509
end
510
511
local function upgrade_packages()
512
return run_pkg_cmd("upgrade")
513
end
514
515
local function addfile(file, defer)
516
if type(file) ~= "table" then
517
return false, "Invalid object"
518
end
519
if defer and not file.defer then
520
return true
521
end
522
if not defer and file.defer then
523
return true
524
end
525
if not file.path then
526
return false, "No path provided for the file to write"
527
end
528
local content = nil
529
if file.content then
530
if file.encoding then
531
if file.encoding == "b64" or file.encoding == "base64" then
532
content = decode_base64(file.content)
533
else
534
return false, "Unsupported encoding: " .. file.encoding
535
end
536
else
537
content = file.content
538
end
539
end
540
local mode = "w"
541
if file.append then
542
mode = "a"
543
end
544
545
local root = os.getenv("NUAGE_FAKE_ROOTDIR")
546
if not root then
547
root = ""
548
end
549
local filepath = root .. file.path
550
local f = assert(io.open(filepath, mode))
551
if content then
552
f:write(content)
553
end
554
f:close()
555
if file.permissions then
556
chmod(filepath, file.permissions)
557
end
558
if file.owner then
559
local owner, group = string.match(file.owner, "([^:]+):([^:]+)")
560
if not owner then
561
owner = file.owner
562
end
563
chown(filepath, owner, group)
564
end
565
return true
566
end
567
568
local n = {
569
warn = warnmsg,
570
err = errmsg,
571
chmod = chmod,
572
chown = chown,
573
dirname = dirname,
574
mkdir_p = mkdir_p,
575
sethostname = sethostname,
576
settimezone = settimezone,
577
adduser = adduser,
578
addgroup = addgroup,
579
addsshkey = addsshkey,
580
update_sshd_config = update_sshd_config,
581
chpasswd = chpasswd,
582
pkg_bootstrap = pkg_bootstrap,
583
install_package = install_package,
584
update_packages = update_packages,
585
upgrade_packages = upgrade_packages,
586
addsudo = addsudo,
587
addfile = addfile
588
}
589
590
return n
591
592