1--- 2-- SPDX-License-Identifier: BSD-2-Clause 3-- 4-- Copyright(c) 2022-2025 Baptiste Daroussin <bapt@FreeBSD.org> 5-- Copyright(c) 2025 Jesús Daniel Colmenares Oviedo <dtxdf@FreeBSD.org> 6 7local unistd = require("posix.unistd") 8local sys_stat = require("posix.sys.stat") 9local lfs = require("lfs") 10 11local function getlocalbase() 12 local f = io.popen("sysctl -in user.localbase 2> /dev/null") 13 local localbase = f:read("*l") 14 f:close() 15 if localbase == nil or localbase:len() == 0 then 16 -- fallback 17 localbase = "/usr/local" 18 end 19 return localbase 20end 21 22local function decode_base64(input) 23 if input == nil or #input == 0 then 24 return "" 25 end 26 local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 27 input = string.gsub(input, '[^'..b..'=]', '') 28 29 local result = {} 30 local bits = '' 31 32 -- convert all characters in bits 33 for i = 1, #input do 34 local x = input:sub(i, i) 35 if x == '=' then 36 break 37 end 38 local f = b:find(x) - 1 39 for j = 6, 1, -1 do 40 bits = bits .. (f % 2^j - f % 2^(j-1) > 0 and '1' or '0') 41 end 42 end 43 44 for i = 1, #bits, 8 do 45 local byte = bits:sub(i, i + 7) 46 if #byte == 8 then 47 local c = 0 48 for j = 1, 8 do 49 c = c + (byte:sub(j, j) == '1' and 2^(8 - j) or 0) 50 end 51 table.insert(result, string.char(c)) 52 end 53 end 54 55 return table.concat(result) 56end 57 58local function encode_base64(input) 59 if input == nil or #input == 0 then 60 return "" 61 end 62 local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 63 local result = {} 64 local pos = 1 65 local padding = "" 66 while pos <= #input do 67 local a = string.byte(input, pos) 68 local bb = pos + 1 <= #input and string.byte(input, pos + 1) or 0 69 local c = pos + 2 <= #input and string.byte(input, pos + 2) or 0 70 table.insert(result, string.sub(b, math.floor(a / 4) + 1, math.floor(a / 4) + 1)) 71 table.insert(result, string.sub(b, math.floor(a % 4 * 16 + bb / 16) + 1, math.floor(a % 4 * 16 + bb / 16) + 1)) 72 if pos + 1 <= #input then 73 table.insert(result, string.sub(b, math.floor(bb % 16 * 4 + c / 64) + 1, math.floor(bb % 16 * 4 + c / 64) + 1)) 74 else 75 table.insert(result, "=") 76 end 77 if pos + 2 <= #input then 78 table.insert(result, string.sub(b, math.floor(c % 64) + 1, math.floor(c % 64) + 1)) 79 else 80 table.insert(result, "=") 81 end 82 pos = pos + 3 83 end 84 return table.concat(result) 85end 86 87local function shell_escape(s) 88 return "'" .. string.gsub(s, "'", "'\\''") .. "'" 89end 90 91local function warnmsg(str, prepend) 92 if not str then 93 return 94 end 95 local tag = "" 96 if prepend ~= false then 97 tag = "nuageinit: " 98 end 99 io.stderr:write(tag .. str .. "\n") 100end 101 102local function errmsg(str, prepend) 103 warnmsg(str, prepend) 104 os.exit(1) 105end 106 107local function chmod(path, mode) 108 mode = tonumber(mode, 8) 109 local _, err, msg = sys_stat.chmod(path, mode) 110 if err then 111 errmsg("chmod(" .. path .. ", " .. mode .. ") failed: " .. msg) 112 end 113end 114 115local function chown(path, owner, group) 116 local _, err, msg = unistd.chown(path, owner, group) 117 if err then 118 errmsg("chown(" .. path .. ", " .. owner .. ", " .. group .. ") failed: " .. msg) 119 end 120end 121 122local function dirname(oldpath) 123 if not oldpath then 124 return nil 125 end 126 local path = oldpath:gsub("[^/]+/*$", "") 127 if path == "" then 128 if oldpath:sub(1, 1) == "/" then 129 return "/" 130 end 131 return nil 132 end 133 return path 134end 135 136local function mkdir_p(path) 137 if lfs.attributes(path, "mode") ~= nil then 138 return true 139 end 140 local r, err = mkdir_p(dirname(path)) 141 if not r then 142 return nil, err .. " (creating " .. path .. ")" 143 end 144 return lfs.mkdir(path) 145end 146 147local function sethostname(hostname) 148 if hostname == nil then 149 return 150 end 151 -- Basic hostname validation (RFC 952/1123) 152 if #hostname == 0 then 153 warnmsg("hostname is empty, ignoring") 154 return 155 end 156 if #hostname > 253 then 157 warnmsg("hostname too long (" .. #hostname .. " > 253), ignoring") 158 return 159 end 160 if hostname:match("[^a-zA-Z0-9%.%-]") then 161 warnmsg("hostname contains invalid characters: " .. hostname) 162 return 163 end 164 if hostname:match("^[%.%-]") or hostname:match("[%.%-]$") then 165 warnmsg("hostname must not start or end with a dot or hyphen: " .. hostname) 166 return 167 end 168 for label in hostname:gmatch("[^.]+") do 169 if #label > 63 then 170 warnmsg("hostname label too long (" .. #label .. " > 63): " .. label) 171 return 172 end 173 if label:match("^-") or label:match("-$") then 174 warnmsg("hostname label starts or ends with hyphen: " .. label) 175 return 176 end 177 end 178 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 179 if not root then 180 root = "" 181 end 182 local hostnamepath = root .. "/etc/rc.conf.d/hostname" 183 184 mkdir_p(dirname(hostnamepath)) 185 local f, err = io.open(hostnamepath, "w") 186 if not f then 187 warnmsg("Impossible to open " .. hostnamepath .. ":" .. err) 188 return 189 end 190 f:write("hostname=" .. shell_escape(hostname) .. "\n") 191 f:close() 192end 193 194local function update_etc_hosts(root, hostname) 195 if hostname == nil or hostname == "" then 196 return 197 end 198 local hosts_path = root .. "/etc/hosts" 199 local lines = {} 200 local already_present = false 201 202 local f = io.open(hosts_path, "r") 203 if not f then 204 -- File doesn't exist, create a minimal one 205 local nf = io.open(hosts_path, "w") 206 if not nf then 207 warnmsg("unable to create " .. hosts_path) 208 return 209 end 210 nf:write("::1\t\tlocalhost " .. hostname .. "\n") 211 nf:write("127.0.0.1\t\tlocalhost " .. hostname .. "\n") 212 nf:close() 213 return 214 end 215 216 for line in f:lines() do 217 if line:find(hostname, 1, true) then 218 already_present = true 219 end 220 table.insert(lines, line) 221 end 222 f:close() 223 224 if already_present then 225 return 226 end 227 228 -- Not present, append to localhost lines 229 local new_lines = {} 230 local found_localhost = false 231 for _, line in ipairs(lines) do 232 if (line:match("^127%.0%.0%.1%s") or line:match("^::1%s")) and line:find("localhost", 1, true) then 233 table.insert(new_lines, line .. " " .. hostname) 234 found_localhost = true 235 else 236 table.insert(new_lines, line) 237 end 238 end 239 240 if not found_localhost then 241 table.insert(new_lines, "127.0.0.1\t\tlocalhost " .. hostname) 242 end 243 244 f = io.open(hosts_path, "w") 245 if not f then 246 warnmsg("unable to open " .. hosts_path .. " for writing") 247 return 248 end 249 for _, line in ipairs(new_lines) do 250 f:write(line .. "\n") 251 end 252 f:close() 253end 254 255local function splitlist(list) 256 local ret = {} 257 if type(list) == "string" then 258 for str in list:gmatch("([^, ]+)") do 259 ret[#ret + 1] = str 260 end 261 elseif type(list) == "table" then 262 ret = list 263 else 264 warnmsg("Invalid type " .. type(list) .. ", expecting table or string") 265 end 266 return ret 267end 268 269local function splitlines(s) 270 local ret = {} 271 272 for line in string.gmatch(s, "[^\n]+") do 273 ret[#ret + 1] = line 274 end 275 276 return ret 277end 278 279local function getgroups() 280 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 281 local cmd = "pw " 282 if root then 283 cmd = cmd .. "-R " .. root .. " " 284 end 285 286 local f = io.popen(cmd .. "groupshow -a 2> /dev/null | cut -d: -f1") 287 local groups = f:read("*a") 288 f:close() 289 290 return splitlines(groups) 291end 292 293local function purge_group(groups) 294 local existing = getgroups() 295 local ret = {} 296 297 for _, group in ipairs(groups) do 298 local found = false 299 for _, eg in ipairs(existing) do 300 if group == eg then 301 found = true 302 break 303 end 304 end 305 if found then 306 ret[#ret + 1] = group 307 else 308 warnmsg("ignoring non-existent group '" .. group .. "'") 309 end 310 end 311 312 return ret 313end 314 315local function adduser(pwd) 316 if (type(pwd) ~= "table") then 317 warnmsg("Argument should be a table") 318 return nil 319 end 320 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 321 local cmd = "pw " 322 if root then 323 cmd = cmd .. "-R " .. root .. " " 324 end 325 local f = io.popen(cmd .. " usershow " .. shell_escape(pwd.name) .. " -7 2> /dev/null") 326 local pwdstr = f:read("*a") 327 f:close() 328 if pwdstr:len() ~= 0 then 329 return pwdstr:match("%a+:.+:%d+:%d+:.*:(.*):.*") 330 end 331 if not pwd.gecos then 332 pwd.gecos = pwd.name .. " User" 333 end 334 if not pwd.homedir then 335 pwd.homedir = "/home/" .. pwd.name 336 end 337 local extraargs = "" 338 if pwd.groups then 339 local list = splitlist(pwd.groups) 340 -- pw complains if the group does not exist, so if the user 341 -- specifies one that cannot be found, nuageinit will generate 342 -- an exception and exit, unlike cloud-init, which only issues 343 -- a warning but creates the user anyway. 344 list = purge_group(list) 345 if #list > 0 then 346 local escaped_list = {} 347 for _, g in ipairs(list) do 348 table.insert(escaped_list, shell_escape(g)) 349 end 350 extraargs = " -G " .. table.concat(escaped_list, ",") 351 end 352 end 353 -- pw will automatically create a group named after the username 354 -- do not add a -g option in this case 355 if pwd.primary_group and pwd.primary_group ~= pwd.name then 356 extraargs = extraargs .. " -g " .. shell_escape(pwd.primary_group) 357 end 358 if not pwd.no_create_home then 359 extraargs = extraargs .. " -m " 360 end 361 if not pwd.shell then 362 pwd.shell = "/bin/sh" 363 end 364 local postcmd = "" 365 local input = nil 366 if pwd.passwd then 367 input = pwd.passwd 368 postcmd = " -H 0" 369 elseif pwd.plain_text_passwd then 370 input = pwd.plain_text_passwd 371 postcmd = " -h 0" 372 end 373 cmd = "pw " 374 if root then 375 cmd = cmd .. "-R " .. root .. " " 376 end 377 cmd = cmd .. "useradd -n " .. shell_escape(pwd.name) .. " -M 0755 -w none " 378 cmd = cmd .. extraargs .. " -c " .. shell_escape(pwd.gecos) 379 cmd = cmd .. " -d " .. shell_escape(pwd.homedir) .. " -s " .. shell_escape(pwd.shell) .. postcmd 380 381 f = io.popen(cmd, "w") 382 if input then 383 f:write(input) 384 end 385 local r = f:close() 386 if not r then 387 warnmsg("fail to add user " .. pwd.name) 388 warnmsg(cmd) 389 return nil 390 end 391 if pwd.lock_passwd or pwd.locked then 392 cmd = "pw " 393 if root then 394 cmd = cmd .. "-R " .. root .. " " 395 end 396 cmd = cmd .. "lock " .. shell_escape(pwd.name) 397 os.execute(cmd) 398 end 399 return pwd.homedir 400end 401 402local function addgroup(grp) 403 if (type(grp) ~= "table") then 404 warnmsg("Argument should be a table") 405 return false 406 end 407 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 408 local cmd = "pw " 409 if root then 410 cmd = cmd .. "-R " .. root .. " " 411 end 412 local f = io.popen(cmd .. " groupshow " .. shell_escape(grp.name) .. " 2> /dev/null") 413 local grpstr = f:read("*a") 414 f:close() 415 if grpstr:len() ~= 0 then 416 return true 417 end 418 local extraargs = "" 419 if grp.members then 420 local list = splitlist(grp.members) 421 local escaped_list = {} 422 for _, m in ipairs(list) do 423 table.insert(escaped_list, shell_escape(m)) 424 end 425 extraargs = " -M " .. table.concat(escaped_list, ",") 426 end 427 cmd = "pw " 428 if root then 429 cmd = cmd .. "-R " .. root .. " " 430 end 431 cmd = cmd .. "groupadd -n " .. shell_escape(grp.name) .. extraargs 432 local r = os.execute(cmd) 433 if not r then 434 warnmsg("fail to add group " .. grp.name) 435 warnmsg(cmd) 436 return false 437 end 438 return true 439end 440 441local function addsshkey(homedir, key, options) 442 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 443 if root then 444 homedir = root .. "/" .. homedir 445 end 446 local ak_path = homedir .. "/.ssh/authorized_keys" 447 local dotssh_path = homedir .. "/.ssh" 448 449 -- Check what already exists before creating anything 450 local ak_exists = lfs.attributes(ak_path) ~= nil 451 local dotssh_exists = lfs.attributes(dotssh_path) ~= nil 452 453 -- Ensure .ssh directory exists 454 if not dotssh_exists then 455 local r, err = mkdir_p(dotssh_path) 456 if not r then 457 warnmsg("cannot create " .. dotssh_path .. ": " .. err) 458 return 459 end 460 end 461 462 -- Get homedir attributes for ownership 463 local dirattrs = lfs.attributes(homedir) 464 if not dirattrs then 465 warnmsg("cannot get attributes for " .. homedir) 466 return 467 end 468 469 local f = io.open(ak_path, "a") 470 if not f then 471 warnmsg("impossible to open " .. ak_path) 472 return 473 end 474 if options and options ~= "" then 475 f:write(options .. " " .. key .. "\n") 476 else 477 f:write(key .. "\n") 478 end 479 f:close() 480 481 -- Set permissions and ownership on newly created files/dirs 482 if not ak_exists then 483 chmod(ak_path, "0600") 484 chown(ak_path, dirattrs.uid, dirattrs.gid) 485 end 486 if not dotssh_exists then 487 chmod(dotssh_path, "0700") 488 chown(dotssh_path, dirattrs.uid, dirattrs.gid) 489 end 490end 491 492local function adddoas(pwd) 493 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 494 local localbase = getlocalbase() 495 local etcdir = localbase .. "/etc" 496 if root then 497 etcdir= root .. etcdir 498 end 499 local doasconf = etcdir .. "/doas.conf" 500 501 local doasconf_exists = lfs.attributes(doasconf) ~= nil 502 local etcdir_exists = lfs.attributes(etcdir) ~= nil 503 504 -- Ensure etc directory exists 505 if not etcdir_exists then 506 local r, err = mkdir_p(etcdir) 507 if not r then 508 warnmsg("cannot create " .. etcdir .. ": " .. err) 509 return 510 end 511 end 512 513 local f = io.open(doasconf, "a") 514 if not f then 515 warnmsg("impossible to open " .. doasconf) 516 return 517 end 518 if type(pwd.doas) == "string" then 519 local rule = pwd.doas 520 rule = rule:gsub("%%u", pwd.name) 521 f:write(rule .. "\n") 522 elseif type(pwd.doas) == "table" then 523 for _, str in ipairs(pwd.doas) do 524 local rule = str 525 rule = rule:gsub("%%u", pwd.name) 526 f:write(rule .. "\n") 527 end 528 end 529 f:close() 530 531 -- Set permissions on newly created files/dirs 532 if not doasconf_exists then 533 chmod(doasconf, "0640") 534 end 535 if not etcdir_exists then 536 chmod(etcdir, "0755") 537 end 538end 539 540local function addsudo(pwd) 541 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 542 local localbase = getlocalbase() 543 local sudoers_dir = localbase .. "/etc/sudoers.d" 544 if root then 545 sudoers_dir= root .. sudoers_dir 546 end 547 local sudoers = sudoers_dir .. "/90-nuageinit-users" 548 549 local sudoers_exists = lfs.attributes(sudoers) ~= nil 550 local sudoers_dir_exists = lfs.attributes(sudoers_dir) ~= nil 551 552 -- Ensure sudoers.d directory exists 553 if not sudoers_dir_exists then 554 local r, err = mkdir_p(sudoers_dir) 555 if not r then 556 warnmsg("cannot create " .. sudoers_dir .. ": " .. err) 557 return 558 end 559 end 560 561 local f = io.open(sudoers, "a") 562 if not f then 563 warnmsg("impossible to open " .. sudoers) 564 return 565 end 566 if type(pwd.sudo) == "string" then 567 f:write(pwd.name .. " " .. pwd.sudo .. "\n") 568 elseif type(pwd.sudo) == "table" then 569 for _, str in ipairs(pwd.sudo) do 570 f:write(pwd.name .. " " .. str .. "\n") 571 end 572 end 573 f:close() 574 575 -- Set permissions on newly created files/dirs 576 if not sudoers_exists then 577 chmod(sudoers, "0440") 578 end 579 if not sudoers_dir_exists then 580 chmod(sudoers_dir, "0750") 581 end 582end 583 584local function update_sshd_config(key, value) 585 local sshd_config = "/etc/ssh/sshd_config" 586 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 587 if root then 588 sshd_config = root .. sshd_config 589 end 590 local f = io.open(sshd_config, "r") 591 if not f then 592 -- File does not exist, create it with the given key/value 593 f = io.open(sshd_config, "w") 594 if not f then 595 warnmsg("Unable to open " .. sshd_config .. " for writing") 596 return 597 end 598 f:write(key .. " " .. value .. "\n") 599 f:close() 600 return 601 end 602 -- Read existing content 603 local lines = {} 604 local found = false 605 local pattern = "^%s*"..key:lower().."%s+(%w+)%s*#?.*$" 606 for line in f:lines() do 607 local _, _, val = line:lower():find(pattern) 608 if val then 609 found = true 610 if val ~= value then 611 table.insert(lines, key .. " " .. value) 612 else 613 table.insert(lines, line) 614 end 615 else 616 table.insert(lines, line) 617 end 618 end 619 f:close() 620 if not found then 621 table.insert(lines, key .. " " .. value) 622 end 623 -- Write back 624 f = io.open(sshd_config .. ".nuageinit", "w") 625 if not f then 626 warnmsg("Unable to open " .. sshd_config .. ".nuageinit for writing") 627 return 628 end 629 for _, l in ipairs(lines) do 630 f:write(l .. "\n") 631 end 632 f:close() 633 os.rename(sshd_config .. ".nuageinit", sshd_config) 634end 635 636local function delete_ssh_host_keys(root) 637 local ssh_dir = root .. "/etc/ssh" 638 local attrs = lfs.attributes(ssh_dir) 639 if not attrs or attrs.mode ~= "directory" then 640 return 641 end 642 for entry in lfs.dir(ssh_dir) do 643 if entry:match("^ssh_host_.*key") or entry:match("^ssh_host_.*key%.pub") then 644 os.remove(ssh_dir .. "/" .. entry) 645 end 646 end 647end 648 649local function exec_change_password(user, password, type, expire) 650 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 651 local cmd = "pw " 652 if root then 653 cmd = cmd .. "-R " .. root .. " " 654 end 655 local postcmd = " -H 0" 656 local input = password 657 if type ~= nil and type == "text" then 658 postcmd = " -h 0" 659 else 660 if password == "RANDOM" then 661 input = nil 662 postcmd = " -w random" 663 end 664 end 665 cmd = cmd .. "usermod " .. shell_escape(user) .. postcmd 666 if expire then 667 cmd = cmd .. " -p 1" 668 else 669 cmd = cmd .. " -p 0" 670 end 671 local f = io.popen(cmd .. " >/dev/null", "w") 672 if input then 673 f:write(input) 674 end 675 -- ignore stdout to avoid printing the password in case of random password 676 local r = f:close() 677 if not r then 678 warnmsg("fail to change user password ".. user) 679 warnmsg(cmd) 680 end 681end 682 683local function change_password_from_line(line, expire) 684 local user, password = line:match("%s*(%w+):(%S+)%s*") 685 local type = nil 686 if user and password then 687 if password == "R" then 688 password = "RANDOM" 689 end 690 if not password:match("^%$%d+%$%w+%$") then 691 if password ~= "RANDOM" then 692 type = "text" 693 end 694 end 695 exec_change_password(user, password, type, expire) 696 end 697end 698 699local function chpasswd(obj) 700 if type(obj) ~= "table" then 701 warnmsg("Invalid chpasswd entry, expecting an object") 702 return 703 end 704 local expire = false 705 if obj.expire ~= nil then 706 if type(obj.expire) == "boolean" then 707 expire = obj.expire 708 else 709 warnmsg("Invalid type for chpasswd.expire, expecting a boolean, got a ".. type(obj.expire)) 710 end 711 end 712 if obj.users ~= nil then 713 if type(obj.users) ~= "table" then 714 warnmsg("Invalid type for chpasswd.users, expecting a list, got a ".. type(obj.users)) 715 else 716 for _, u in ipairs(obj.users) do 717 if type(u) ~= "table" then 718 warnmsg("Invalid chpasswd.users entry, expecting an object, got a " .. type(u)) 719 elseif not u.name then 720 warnmsg("Invalid entry for chpasswd.users: missing 'name'") 721 elseif not u.password then 722 warnmsg("Invalid entry for chpasswd.users: missing 'password'") 723 else 724 exec_change_password(u.name, u.password, u.type, expire) 725 end 726 end 727 end 728 end 729 if obj.list ~= nil then 730 warnmsg("chpasswd.list is deprecated consider using chpasswd.users") 731 if type(obj.list) == "string" then 732 for line in obj.list:gmatch("[^\n]+") do 733 change_password_from_line(line, expire) 734 end 735 elseif type(obj.list) == "table" then 736 for _, u in ipairs(obj.list) do 737 change_password_from_line(u, expire) 738 end 739 end 740 end 741end 742 743local function settimezone(timezone) 744 if timezone == nil then 745 return 746 end 747 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 748 if not root then 749 root = "/" 750 end 751 752 local f, _, rc = os.execute("tzsetup -s -C " .. shell_escape(root) .. " " .. shell_escape(timezone)) 753 754 if not f then 755 warnmsg("Impossible to configure time zone ( rc = " .. rc .. " )") 756 return 757 end 758end 759 760local function pkg_bootstrap() 761 if os.getenv("NUAGE_RUN_TESTS") then 762 return true 763 end 764 if os.execute("pkg -N 2>/dev/null") then 765 return true 766 end 767 print("Bootstrapping pkg") 768 return os.execute("env ASSUME_ALWAYS_YES=YES pkg bootstrap") 769end 770 771local function install_package(package) 772 if package == nil then 773 return true 774 end 775 local install_cmd = "pkg install -y " .. shell_escape(package) 776 local test_cmd = "pkg info -q " .. shell_escape(package) 777 if os.getenv("NUAGE_RUN_TESTS") then 778 print(install_cmd) 779 print(test_cmd) 780 return true 781 end 782 if os.execute(test_cmd) then 783 return true 784 end 785 return os.execute(install_cmd) 786end 787 788local function run_pkg_cmd(subcmd) 789 local cmd = "env ASSUME_ALWAYS_YES=yes pkg " .. subcmd 790 if os.getenv("NUAGE_RUN_TESTS") then 791 print(cmd) 792 return true 793 end 794 return os.execute(cmd) 795end 796local function update_packages() 797 return run_pkg_cmd("update") 798end 799 800local function upgrade_packages() 801 return run_pkg_cmd("upgrade") 802end 803 804local function addfile(file, defer) 805 if type(file) ~= "table" then 806 return false, "Invalid object" 807 end 808 if defer and not file.defer then 809 return true 810 end 811 if not defer and file.defer then 812 return true 813 end 814 if not file.path then 815 return false, "No path provided for the file to write" 816 end 817 local content = nil 818 if file.content then 819 if file.encoding then 820 if file.encoding == "b64" or file.encoding == "base64" then 821 content = decode_base64(file.content) 822 else 823 return false, "Unsupported encoding: " .. file.encoding 824 end 825 else 826 content = file.content 827 end 828 end 829 local mode = "w" 830 if file.append then 831 mode = "a" 832 end 833 834 local root = os.getenv("NUAGE_FAKE_ROOTDIR") 835 if not root then 836 root = "" 837 end 838 local filepath = root .. file.path 839 mkdir_p(dirname(filepath)) 840 local f = assert(io.open(filepath, mode)) 841 if content then 842 f:write(content) 843 end 844 f:close() 845 if file.permissions then 846 chmod(filepath, file.permissions) 847 end 848 if file.owner then 849 local owner, group = string.match(file.owner, "([^:]+):([^:]+)") 850 if not owner then 851 owner = file.owner 852 end 853 chown(filepath, owner, group) 854 end 855 return true 856end 857 858local function add_fstab_entry(root, device, mount_point, fstype, options, dump_freq, passno) 859 local fstab_path = root .. "/etc/fstab" 860 local f = io.open(fstab_path, "a") 861 if not f then 862 warnmsg("unable to open " .. fstab_path .. " for writing") 863 return false 864 end 865 options = options or "rw" 866 dump_freq = dump_freq or 0 867 passno = passno or 0 868 f:write(string.format("%s\t\t%s\t\t%s\t\t%s\t\t%d\t\t%d\n", 869 device, mount_point, fstype, options, dump_freq, passno)) 870 f:close() 871 return true 872end 873 874local function write_resolv_conf(root, config) 875 local path = root .. "/etc/resolv.conf" 876 local f = io.open(path, "w") 877 if not f then 878 warnmsg("unable to open " .. path .. " for writing") 879 return 880 end 881 if config.domain then 882 f:write("domain " .. config.domain .. "\n") 883 end 884 if config.searchdomains then 885 f:write("search " .. table.concat(config.searchdomains, " ") .. "\n") 886 end 887 if config.sortlist then 888 f:write("sortlist " .. table.concat(config.sortlist, " ") .. "\n") 889 end 890 if config.options then 891 local opts = {} 892 for k, v in pairs(config.options) do 893 table.insert(opts, k .. ":" .. v) 894 end 895 f:write("options " .. table.concat(opts, " ") .. "\n") 896 end 897 if config.nameservers then 898 for _, ns in ipairs(config.nameservers) do 899 f:write("nameserver " .. ns .. "\n") 900 end 901 end 902 f:close() 903end 904 905local function remove_fstab_entry(root, mount_point) 906 local fstab_path = root .. "/etc/fstab" 907 local f = io.open(fstab_path, "r") 908 if not f then 909 return 910 end 911 local lines = {} 912 for line in f:lines() do 913 local fields = {} 914 for field in line:gmatch("%S+") do 915 table.insert(fields, field) 916 end 917 if fields[2] ~= mount_point then 918 table.insert(lines, line) 919 end 920 end 921 f:close() 922 local nf = io.open(fstab_path, "w") 923 if not nf then 924 warnmsg("unable to open " .. fstab_path .. " for writing") 925 return 926 end 927 for _, line in ipairs(lines) do 928 nf:write(line .. "\n") 929 end 930 nf:close() 931end 932 933local function parse_mime_multipart(data) 934 local boundary = data:match("boundary=\"([^\"]+)\"") 935 if not boundary then 936 boundary = data:match("boundary=([^%s;]+)") 937 end 938 if not boundary then 939 return nil 940 end 941 local parts = {} 942 local pos = data:find("\n") or 1 943 local first = data:find("--" .. boundary, pos, true) 944 if not first then 945 return nil 946 end 947 pos = data:find("\n", first) 948 if not pos then return nil end 949 pos = pos + 1 950 while true do 951 local nextb = data:find("--" .. boundary, pos, true) 952 if not nextb then break end 953 local part = data:sub(pos, nextb - 1) 954 part = part:gsub("^\r?\n", ""):gsub("\r?\n$", "") 955 local header_end = part:find("\r?\n\r?\n") 956 local headers_str, body 957 if header_end then 958 headers_str = part:sub(1, header_end - 1) 959 body = part:sub(header_end + 2):gsub("^\r?\n", ""):gsub("\r?\n$", "") 960 else 961 body = part 962 end 963 local ct = "text/plain" 964 if headers_str then 965 local m = headers_str:match("[Cc]ontent%-[Tt]ype:%s*([^%s;]+)") 966 if m then ct = m:lower() end 967 end 968 table.insert(parts, {content_type = ct, body = body}) 969 local after = data:sub(nextb + 2 + #boundary, nextb + 3 + #boundary) 970 if after == "--" then break end 971 pos = data:find("\n", nextb) or nextb 972 if pos then pos = pos + 1 end 973 end 974 return parts 975end 976 977local n = { 978 shell_escape = shell_escape, 979 warn = warnmsg, 980 err = errmsg, 981 chmod = chmod, 982 chown = chown, 983 dirname = dirname, 984 mkdir_p = mkdir_p, 985 sethostname = sethostname, 986 settimezone = settimezone, 987 adduser = adduser, 988 addgroup = addgroup, 989 addsshkey = addsshkey, 990 update_sshd_config = update_sshd_config, 991 delete_ssh_host_keys = delete_ssh_host_keys, 992 update_etc_hosts = update_etc_hosts, 993 chpasswd = chpasswd, 994 pkg_bootstrap = pkg_bootstrap, 995 install_package = install_package, 996 update_packages = update_packages, 997 upgrade_packages = upgrade_packages, 998 addsudo = addsudo, 999 adddoas = adddoas, 1000 addfile = addfile, 1001 decode_base64 = decode_base64, 1002 encode_base64 = encode_base64, 1003 add_fstab_entry = add_fstab_entry, 1004 remove_fstab_entry = remove_fstab_entry, 1005 write_resolv_conf = write_resolv_conf, 1006 parse_mime_multipart = parse_mime_multipart, 1007} 1008 1009return n 1010