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