/* * script.cc - Implementation of the Script class * libhscript, the HorizonScript library for * Project Horizon * * Copyright (c) 2019 Adélie Linux and contributors. All rights reserved. * This code is licensed under the AGPL 3.0 license, as noted in the * LICENSE-code file in the root directory of this repository. * * SPDX-License-Identifier: AGPL-3.0-only */ #include #include #include "util/filesystem.hh" #include #include #include #ifdef HAS_INSTALL_ENV # include #endif /* HAS_INSTALL_ENV */ #include #include #include "script.hh" #include "disk.hh" #include "meta.hh" #include "network.hh" #include "user.hh" #include "util/output.hh" #define SCRIPT_LINE_MAX 512 typedef Horizon::Keys::Key *(*key_parse_fn)(const std::string &, int, int*, int*); using namespace Horizon::Keys; const std::map valid_keys = { {"network", &Network::parseFromData}, {"hostname", &Hostname::parseFromData}, {"pkginstall", &PkgInstall::parseFromData}, {"rootpw", &RootPassphrase::parseFromData}, {"language", &Language::parseFromData}, {"keymap", &Keymap::parseFromData}, {"firmware", &Firmware::parseFromData}, {"timezone", &Timezone::parseFromData}, {"repository", &Repository::parseFromData}, {"signingkey", &SigningKey::parseFromData}, {"netaddress", &NetAddress::parseFromData}, {"nameserver", &Nameserver::parseFromData}, {"netssid", &NetSSID::parseFromData}, {"username", &Username::parseFromData}, {"useralias", &UserAlias::parseFromData}, {"userpw", &UserPassphrase::parseFromData}, {"usericon", &UserIcon::parseFromData}, {"usergroups", &UserGroups::parseFromData}, {"diskid", &DiskId::parseFromData}, {"disklabel", &DiskLabel::parseFromData}, {"partition", &Partition::parseFromData}, {"lvm_pv", &LVMPhysical::parseFromData}, {"lvm_vg", &LVMGroup::parseFromData}, {"lvm_lv", &LVMVolume::parseFromData}, {"encrypt", &Encrypt::parseFromData}, {"fs", &Filesystem::parseFromData}, {"mount", &Mount::parseFromData} }; namespace Horizon { /*! Describes a user account. */ struct UserDetail { std::unique_ptr name; std::unique_ptr alias; std::unique_ptr passphrase; std::unique_ptr icon; std::vector< std::unique_ptr > groups; }; struct Script::ScriptPrivate { /*! Determines whether or not to enable networking. */ std::unique_ptr network; /*! The target system's hostname. */ std::unique_ptr hostname; /*! The packages to install to the target system. */ std::set packages; /*! The root shadow line. */ std::unique_ptr rootpw; /*! The system language. */ std::unique_ptr lang; /*! The system keymap. */ std::unique_ptr keymap; /*! The system timezone. */ std::unique_ptr tzone; /*! Network addressing configuration */ std::vector< std::unique_ptr > addresses; /*! Network nameserver resolver addresses */ std::vector< std::unique_ptr > nses; /*! Wireless networking configuration */ std::vector< std::unique_ptr > ssids; /*! APK repositories */ std::vector< std::unique_ptr > repos; /*! APK repository keys */ std::vector< std::unique_ptr > repo_keys; /*! User account information */ std::map< std::string, std::unique_ptr > accounts; /*! Disk identification keys */ std::vector< std::unique_ptr > diskids; /*! Disklabel configuration keys */ std::vector< std::unique_ptr > disklabels; /*! Partition creation keys */ std::vector< std::unique_ptr > partitions; /*! LVM physical volume keys */ std::vector< std::unique_ptr > lvm_pvs; /*! LVM volume group keys */ std::vector< std::unique_ptr > lvm_vgs; /*! LVM logical volume keys */ std::vector< std::unique_ptr > lvm_lvs; /*! LUKS creation keys */ std::vector< std::unique_ptr > luks; /*! Filesystem creation keys */ std::vector< std::unique_ptr > fses; /*! Target system's mountpoints. */ std::vector< std::unique_ptr > mounts; #ifdef NON_LIBRE_FIRMWARE std::unique_ptr firmware; #endif /*! Store +key_obj+ representing the key +key_name+. * @param key_name The name of the key that is being stored. * @param obj The Key object associated with the key. * @param errors Output parameter: if given, incremented on error. * @param warnings Output parameter: if given, incremented on warning. * @param opts Script parsing options. */ bool store_key(const std::string &key_name, Keys::Key *obj, int lineno, int *errors, int *warnings, const ScriptOptions &opts) { if(key_name == "network") { return store_network(obj, lineno, errors, warnings, opts); } else if(key_name == "netaddress") { std::unique_ptr addr(dynamic_cast(obj)); this->addresses.push_back(std::move(addr)); return true; } else if(key_name == "nameserver") { std::unique_ptr ns(dynamic_cast(obj)); this->nses.push_back(std::move(ns)); return true; } else if(key_name == "netssid") { std::unique_ptr ssid(dynamic_cast(obj)); this->ssids.push_back(std::move(ssid)); return true; } else if(key_name == "hostname") { return store_hostname(obj, lineno, errors, warnings, opts); } else if(key_name == "pkginstall") { return store_pkginstall(obj, lineno, errors, warnings, opts); } else if(key_name == "rootpw") { return store_rootpw(obj, lineno, errors, warnings, opts); } else if(key_name == "language") { return store_lang(obj, lineno, errors, warnings, opts); } else if(key_name == "keymap") { return store_keymap(obj, lineno, errors, warnings, opts); } else if(key_name == "firmware") { return store_firmware(obj, lineno, errors, warnings, opts); } else if(key_name == "timezone") { return store_timezone(obj, lineno, errors, warnings, opts); } else if(key_name == "repository") { std::unique_ptr repo(dynamic_cast(obj)); this->repos.push_back(std::move(repo)); return true; } else if(key_name == "signingkey") { std::unique_ptr key(dynamic_cast(obj)); this->repo_keys.push_back(std::move(key)); return true; } else if(key_name == "username") { return store_username(obj, lineno, errors, warnings, opts); } else if(key_name == "useralias") { return store_useralias(obj, lineno, errors, warnings, opts); } else if(key_name == "userpw") { return store_userpw(obj, lineno, errors, warnings, opts); } else if(key_name == "usericon") { return store_usericon(obj, lineno, errors, warnings, opts); } else if(key_name == "usergroups") { return store_usergroups(obj, lineno, errors, warnings, opts); } else if(key_name == "diskid") { std::unique_ptr diskid(dynamic_cast(obj)); this->diskids.push_back(std::move(diskid)); return true; } else if(key_name == "disklabel") { std::unique_ptr l(dynamic_cast(obj)); this->disklabels.push_back(std::move(l)); return true; } else if(key_name == "partition") { std::unique_ptr p(dynamic_cast(obj)); this->partitions.push_back(std::move(p)); return true; } else if(key_name == "lvm_pv") { std::unique_ptr pv(dynamic_cast(obj)); this->lvm_pvs.push_back(std::move(pv)); return true; } else if(key_name == "lvm_vg") { std::unique_ptr vg(dynamic_cast(obj)); this->lvm_vgs.push_back(std::move(vg)); return true; } else if(key_name == "lvm_lv") { std::unique_ptr lv(dynamic_cast(obj)); this->lvm_lvs.push_back(std::move(lv)); return true; } else if(key_name == "encrypt") { std::unique_ptr e(dynamic_cast(obj)); this->luks.push_back(std::move(e)); return true; } else if(key_name == "fs") { std::unique_ptr fs(dynamic_cast(obj)); this->fses.push_back(std::move(fs)); return true; } else if(key_name == "mount") { std::unique_ptr mount(dynamic_cast(obj)); this->mounts.push_back(std::move(mount)); return true; } else { return false; } } #define DUPLICATE_ERROR(OBJ, KEY, OLD_VAL) \ std::string err_str("previous value was ");\ err_str += OLD_VAL;\ err_str += " at installfile:" + std::to_string(OBJ->lineno());\ if(errors) *errors += 1;\ output_error("installfile:" + std::to_string(lineno),\ "duplicate value for key '" + std::string(KEY) + "'",\ err_str); bool store_network(Keys::Key* obj, int lineno, int *errors, int *, ScriptOptions) { if(this->network) { DUPLICATE_ERROR(this->network, "network", this->network->test() ? "true" : "false") return false; } std::unique_ptr net(dynamic_cast(obj)); this->network = std::move(net); return true; } bool store_hostname(Keys::Key* obj, int lineno, int *errors, int *, ScriptOptions) { if(this->hostname) { DUPLICATE_ERROR(this->hostname, "hostname", this->hostname->value()) return false; } std::unique_ptr name(dynamic_cast(obj)); this->hostname = std::move(name); return true; } bool store_pkginstall(Keys::Key* obj, int lineno, int *, int *warnings, ScriptOptions opts) { PkgInstall *install = dynamic_cast(obj); for(auto &pkg : install->packages()) { if(opts.test(StrictMode) && packages.find(pkg) != packages.end()) { if(warnings) *warnings += 1; output_warning("installfile:" + std::to_string(lineno), "pkginstall: package '" + pkg + "' has already been specified"); continue; } packages.insert(pkg); } delete install; return true; } bool store_rootpw(Keys::Key* obj, int lineno, int *errors, int *, ScriptOptions) { if(this->rootpw) { DUPLICATE_ERROR(this->rootpw, std::string("rootpw"), "an encrypted passphrase") return false; } std::unique_ptr r(dynamic_cast(obj)); this->rootpw = std::move(r); return true; } bool store_firmware(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { std::unique_ptr f(dynamic_cast(obj)); #ifdef NON_LIBRE_FIRMWARE if(this->firmware) { DUPLICATE_ERROR(this->firmware, std::string("firmware"), (this->firmware->test()) ? "true" : "false") return false; } this->firmware = std::move(f); return true; #else assert(!f->test()); return true; #endif } bool store_lang(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { if(this->lang) { DUPLICATE_ERROR(this->lang, std::string("language"), this->lang->value()) return false; } std::unique_ptr l(dynamic_cast(obj)); this->lang = std::move(l); return true; } bool store_keymap(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { if(this->keymap) { DUPLICATE_ERROR(this->keymap, std::string("keymap"), this->keymap->value()) return false; } std::unique_ptr k(dynamic_cast(obj)); this->keymap = std::move(k); return true; } bool store_timezone(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { if(this->tzone) { DUPLICATE_ERROR(this->tzone, std::string("timezone"), this->tzone->value()) return false; } std::unique_ptr t(dynamic_cast(obj)); this->tzone = std::move(t); return true; } bool store_username(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { if(accounts.size() >= 255) { if(errors) *errors += 1; output_error("installfile:" + std::to_string(lineno), "username: too many users", "you may only specify 255 users"); return false; } std::unique_ptr name(dynamic_cast(obj)); if(accounts.find(name->value()) != accounts.end()) { DUPLICATE_ERROR((*accounts.find(name->value())).second->name, "username", "assigned") return false; } std::unique_ptr detail(new UserDetail); detail->name = std::move(name); accounts.insert(std::make_pair(detail->name->value(), std::move(detail))); return true; } #define GET_USER_DETAIL(OBJ, KEY) \ if(accounts.find(OBJ->username()) == accounts.end()) {\ if(errors) *errors += 1;\ output_error("installfile:" + std::to_string(lineno),\ std::string(KEY) + ": account name " + OBJ->username() +\ " is unknown");\ return false;\ }\ UserDetail *detail = (*accounts.find(OBJ->username())).second.get(); bool store_useralias(Keys::Key* obj, int lineno, int *errors, int *, ScriptOptions) { std::unique_ptr alias(dynamic_cast(obj)); GET_USER_DETAIL(alias, "useralias") /* REQ: Runner.Validate.useralias.Unique */ if(detail->alias) { DUPLICATE_ERROR(detail->alias, "useralias", detail->alias->alias()) return false; } detail->alias = std::move(alias); return true; } bool store_userpw(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { std::unique_ptr pw(dynamic_cast(obj)); GET_USER_DETAIL(pw, "userpw") /* REQ: Runner.Validate.userpw.Unique */ if(detail->passphrase) { DUPLICATE_ERROR(detail->passphrase, "userpw", "an encrypted passphrase") return false; } detail->passphrase = std::move(pw); return true; } bool store_usericon(Keys::Key *obj, int lineno, int *errors, int *, ScriptOptions) { std::unique_ptr icon(dynamic_cast(obj)); GET_USER_DETAIL(icon, "usericon") /* REQ: Runner.Validate.usericon.Unique */ if(detail->icon) { DUPLICATE_ERROR(detail->icon, "usericon", detail->icon->icon()) return false; } detail->icon = std::move(icon); return true; } bool store_usergroups(Keys::Key* obj, int lineno, int *errors, int *, ScriptOptions) { std::unique_ptr grp(dynamic_cast(obj)); GET_USER_DETAIL(grp, "usergroups") detail->groups.push_back(std::move(grp)); return true; } #undef GET_USER_DETAIL #undef DUPLICATE_ERROR }; Script::Script() { internal = new ScriptPrivate; } Script::~Script() { delete internal; } const Script *Script::load(const std::string &path, const ScriptOptions &opts) { std::ifstream file(path); if(!file) { output_error(path, "Cannot open installfile", ""); return nullptr; } return Script::load(file, opts); } const Script *Script::load(std::istream &sstream, const ScriptOptions &opts) { #define PARSER_ERROR(err_str) \ errors++;\ output_error("installfile:" + std::to_string(lineno),\ err_str, "");\ if(!opts.test(ScriptOptionFlags::KeepGoing)) {\ break;\ } #define PARSER_WARNING(warn_str) \ warnings++;\ output_warning("installfile:" + std::to_string(lineno),\ warn_str, ""); using namespace Horizon::Keys; Script *the_script = new Script; int lineno = 0; char nextline[SCRIPT_LINE_MAX]; const std::string delim(" \t"); int errors = 0, warnings = 0; while(sstream.getline(nextline, sizeof(nextline))) { lineno++; if(nextline[0] == '#') { /* This is a comment line; ignore it. */ continue; } const std::string line(nextline); std::string key; std::string::size_type start, key_end, value_begin; start = line.find_first_not_of(delim); if(start == std::string::npos) { /* This is a blank line; ignore it. */ continue; } key_end = line.find_first_of(delim, start); value_begin = line.find_first_not_of(delim, key_end); key = line.substr(start, (key_end - start)); if(key_end == std::string::npos || value_begin == std::string::npos) { /* Key without value */ PARSER_ERROR("key '" + key + "' has no value") continue; } /* Normalise key to lower-case */ std::transform(key.begin(), key.end(), key.begin(), ::tolower); if(valid_keys.find(key) == valid_keys.end()) { /* Invalid key */ if(opts.test(StrictMode)) { PARSER_ERROR("key '" + key + "' is not defined") } else { PARSER_WARNING("key '" + key + "' is not defined") } continue; } Key *key_obj = valid_keys.at(key)(line.substr(value_begin), lineno, &errors, &warnings); if(!key_obj) { PARSER_ERROR("value for key '" + key + "' was invalid") continue; } if(!the_script->internal->store_key(key, key_obj, lineno, &errors, &warnings, opts)) { PARSER_ERROR("stopping due to prior errors") continue; } } if(sstream.fail() && !sstream.eof()) { output_error("installfile:" + std::to_string(lineno + 1), "line exceeds maximum length", "Maximum line length is " + std::to_string(SCRIPT_LINE_MAX)); errors++; } if(sstream.bad() && !sstream.eof()) { output_error("installfile:" + std::to_string(lineno), "I/O error while reading installfile", ""); errors++; } /* Ensure all required keys are present. */ #define MISSING_ERROR(key) \ output_error("installfile:" + std::to_string(lineno),\ "expected value for key '" + std::string(key) + "'",\ "this key is required");\ errors++; if(errors == 0) { if(!the_script->internal->network) { MISSING_ERROR("network") } if(!the_script->internal->hostname) { MISSING_ERROR("hostname") } if(the_script->internal->packages.size() == 0) { MISSING_ERROR("pkginstall") } if(!the_script->internal->rootpw) { MISSING_ERROR("rootpw") } if(the_script->internal->mounts.size() == 0) { MISSING_ERROR("mount") } } #undef MISSING_ERROR output_log("parser", "0", "installfile", std::to_string(errors) + " error(s), " + std::to_string(warnings) + " warning(s).", ""); if(errors > 0) { delete the_script; return nullptr; } else { the_script->opts = opts; return the_script; } #undef PARSER_WARNING #undef PARSER_ERROR } /*! Perform all necessary validations on a single user account. * @param name The username of the account. * @param detail The UserDetail record of the account. * @param opts The ScriptOptions in use. * @returns A count of errors encountered, or 0 if the account is valid. */ int validate_one_account(const std::string &name, UserDetail *detail, ScriptOptions opts) { int failures = 0; /* REQ: Runner.Validate.username */ if(!detail->name->validate(opts)) { failures++; } /* REQ: Runner.Validate.useralias */ if(detail->alias && !detail->alias->validate(opts)) { failures++; } /* REQ: Runner.Validate.userpw */ if(detail->passphrase && !detail->passphrase->validate(opts)) { failures++; } /* REQ: Runner.Validate.userpw.None */ if(!detail->passphrase) { long line = detail->name->lineno(); output_warning("installfile:" + std::to_string(line), "username: " + name + " has no set passphrase", "This account will not be able to log in."); } /* REQ: Runner.Validate.usericon */ if(detail->icon && !detail->icon->validate(opts)) { failures++; } if(detail->groups.size() > 0) { std::set seen_groups; for(auto &group : detail->groups) { /* REQ: Runner.Validate.usergroups */ if(!group->validate(opts)) { failures++; } /* REQ: Runner.Validate.usergroups.Unique */ const std::set these = group->groups(); if(!std::all_of(these.begin(), these.end(), [&seen_groups](std::string elem) { return seen_groups.find(elem) == seen_groups.end(); }) ) { output_error("installfile:" + std::to_string(group->lineno()), "usergroups: duplicate group name specified"); failures++; } seen_groups.insert(these.begin(), these.end()); } /* REQ: Runner.Validate.usergroups.Count */ if(seen_groups.size() > 16) { output_error("installfile:0", "usergroups: " + name + " is a member of " + "more than 16 groups"); failures++; } } return failures; } /*! Add the default repositories to the repo list. * @param repos The list of repositories * The list +repos+ will be modified with the default repositories for * Adélie Linux. Both system/ and user/ will be added. */ bool add_default_repos(std::vector> &repos) { Keys::Repository *sys_key = dynamic_cast( Horizon::Keys::Repository::parseFromData( "https://distfiles.adelielinux.org/adelie/stable/system", 0, nullptr, nullptr ) ); if(!sys_key) { output_error("internal", "failed to create default system repository"); return false; } std::unique_ptr sys_repo(sys_key); repos.push_back(std::move(sys_repo)); Keys::Repository *user_key = dynamic_cast( Horizon::Keys::Repository::parseFromData( "https://distfiles.adelielinux.org/adelie/stable/user", 0, nullptr, nullptr ) ); if(!user_key) { output_error("internal", "failed to create default user repository"); return false; } std::unique_ptr user_repo(user_key); repos.push_back(std::move(user_repo)); #ifdef NON_LIBRE_FIRMWARE /* REQ: Runner.Execute.firmware.Repository */ if(this->internal->firmware && this->internal->firmware->test()) { Keys::Repository *fw_key = dynamic_cast( Horizon::Keys::Repository::parseFromData( "https://distfiles.apkfission.net/adelie-stable/nonfree", 0, nullptr, nullptr ) ); if(!fw_key) { output_error("internal", "failed to create firmware repository"); return false; } std::unique_ptr fw_repo(fw_key); repos.push_back(std::move(fw_repo)); } #endif return true; } /*! Add the default repository keys to the signing key list. * @param keys The list of repository keys. * The list +keys+ will be modified with the default repository signing keys * for Adélie Linux. */ bool add_default_repo_keys(std::vector> &keys) { Keys::SigningKey *key = dynamic_cast( Horizon::Keys::SigningKey::parseFromData( "/etc/apk/keys/packages@adelielinux.org.pub", 0, nullptr, nullptr) ); if(!key) { output_error("internal", "failed to create default repository signing key"); return false; } std::unique_ptr repo_key(key); keys.push_back(std::move(repo_key)); return true; } bool Script::validate() const { int failures = 0; std::set seen_diskids, seen_labels, seen_parts, seen_pvs, seen_vg_names, seen_vg_pvs, seen_lvs, seen_fses, seen_mounts, seen_luks; std::map seen_iface; #ifdef HAS_INSTALL_ENV error_code ec; #endif /* HAS_INSTALL_ENV */ /* REQ: Runner.Validate.network */ if(!this->internal->network->validate(this->opts)) failures++; /* REQ: Runner.Validate.network.netaddress */ if(this->internal->network->test() && this->internal->addresses.size() == 0) { failures++; output_error("installfile:0", "networking requested but no 'netaddress' defined", "You need to specify at least one address to enable " "networking."); } for(auto &address : this->internal->addresses) { if(!address->validate(this->opts)) { failures++; } /* REQ: Runner.Validate.network.netaddress.Count */ if(seen_iface.find(address->iface()) == seen_iface.end()) { seen_iface.insert(std::make_pair(address->iface(), 1)); } else { seen_iface[address->iface()] += 1; if(seen_iface[address->iface()] > 255) { failures++; output_error("installfile:" + std::to_string(address->lineno()), "netaddress: interface '" + address->iface() + "' has too many addresses assigned"); } } } /* REQ: Runner.Validate.nameserver */ for(auto &ns : this->internal->nses) { if(!ns->validate(this->opts)) { failures++; } } /* REQ: Runner.Validate.network.netssid */ for(auto &ssid : this->internal->ssids) { if(!ssid->validate(this->opts)) { failures++; } } /* REQ: Runner.Validate.hostname */ if(!this->internal->hostname->validate(this->opts)) failures++; /* REQ: Runner.Validate.rootpw */ if(!this->internal->rootpw->validate(this->opts)) failures++; /* REQ: Runner.Validate.language */ if(internal->lang && !internal->lang->validate(this->opts)) failures++; /* REQ: Runner.Validate.keymap */ if(internal->keymap && !internal->keymap->validate(this->opts)) failures++; #ifdef NON_LIBRE_FIRMWARE /* REQ: Runner.Validate.firmware */ if(!this->internal->firmware->validate(this->opts)) failures++; #endif /* REQ: Runner.Execute.timezone */ if(!internal->tzone) { Keys::Timezone *utc = dynamic_cast( Horizon::Keys::Timezone::parseFromData("UTC", 0, &failures, nullptr) ); if(!utc) { output_error("internal", "failed to create default timezone"); return false; } std::unique_ptr zone(utc); this->internal->tzone = std::move(zone); } /* REQ: Runner.Validate.timezone */ if(!this->internal->tzone->validate(this->opts)) failures++; /* REQ: Script.repository */ if(this->internal->repos.size() == 0) { if(!add_default_repos(this->internal->repos)) { return false; } } /* REQ: Runner.Validate.repository */ for(auto &repo : this->internal->repos) { if(!repo->validate(this->opts)) { failures++; } } if(this->internal->repos.size() > 10) { failures++; output_error("installfile:" + std::to_string(this->internal->repos[11]->lineno()), "repository: too many repositories specified", "You may only specify up to 10 repositories."); } /* REQ: Script.signingkey */ if(this->internal->repo_keys.size() == 0) { if(!add_default_repo_keys(this->internal->repo_keys)) { return false; } } /* REQ: Runner.Validate.signingkey */ for(auto &key : this->internal->repo_keys) { if(!key->validate(this->opts)) { failures++; } } if(this->internal->repo_keys.size() > 10) { failures++; output_error("installfile:" + std::to_string(this->internal->repo_keys[11]->lineno()), "signingkey: too many keys specified", "You may only specify up to 10 repository keys."); } for(auto &acct : this->internal->accounts) { UserDetail *detail = acct.second.get(); failures += validate_one_account(acct.first, detail, this->opts); } /* REQ: Runner.Validate.diskid */ for(auto &diskid : this->internal->diskids) { if(!diskid->validate(this->opts)) { failures++; continue; } /* REQ: Runner.Validate.diskid.Unique */ if(seen_diskids.find(diskid->device()) != seen_diskids.end()) { failures++; output_error("installfile:" + std::to_string(diskid->lineno()), "diskid: device " + diskid->device() + " has already been identified"); } seen_diskids.insert(diskid->device()); } /* REQ: Runner.Validate.disklabel */ for(auto &label : this->internal->disklabels) { if(!label->validate(this->opts)) { failures++; continue; } /* REQ: Runner.Validate.disklabel.Unique */ if(seen_labels.find(label->device()) != seen_labels.end()) { failures++; output_error("installfile:" + std::to_string(label->lineno()), "disklabel: device " + label->device() + " already has a label queued"); } seen_labels.insert(label->device()); } /* REQ: Runner.Validate.partition */ for(auto &part : this->internal->partitions) { if(!part->validate(this->opts)) { failures++; continue; } /* REQ: Runner.Validate.partition.Unique */ const std::string &dev = part->device(); const std::string maybe_p(::isdigit(dev[dev.size() - 1]) ? "p" : ""); std::string name = dev + maybe_p + std::to_string(part->partno()); if(seen_parts.find(name) != seen_parts.end()) { failures++; output_error("installfile:" + std::to_string(part->lineno()), "partition: partition #" + std::to_string(part->partno()) + " already exists on device " + part->device()); } seen_parts.insert(name); } /* REQ: Runner.Validate.lvm_pv */ for(auto &pv : this->internal->lvm_pvs) { if(!pv->validate(this->opts)) { failures++; continue; } /* We don't actually have a requirement, but... */ if(seen_pvs.find(pv->value()) != seen_pvs.end()) { failures++; output_error("installfile:" + std::to_string(pv->lineno()), "lvm_pv: a physical volume already exists on device " + pv->value()); } seen_pvs.insert(pv->value()); /* REQ: Runner.Validate.lvm_pv.Block */ if(opts.test(InstallEnvironment)) { #ifdef HAS_INSTALL_ENV if(!fs::exists(pv->value(), ec) && seen_parts.find(pv->value()) == seen_parts.end()) { failures++; output_error("installfile:" + std::to_string(pv->lineno()), "lvm_pv: device " + pv->value() + " does not exist"); } #endif /* HAS_INSTALL_ENV */ } } /* REQ: Runner.Validate.lvm_vg */ for(auto &vg : this->internal->lvm_vgs) { if(!vg->validate(this->opts)) { failures++; continue; } if(seen_vg_names.find(vg->name()) != seen_vg_names.end()) { failures++; output_error("installfile:" + std::to_string(vg->lineno()), "lvm_vg: duplicate volume group name specified", vg->name() + " already given"); } seen_vg_names.insert(vg->name()); if(seen_vg_pvs.find(vg->pv()) != seen_vg_pvs.end()) { failures++; output_error("installfile:" + std::to_string(vg->lineno()), "lvm_vg: a volume group already exists on " + vg->pv()); } seen_vg_pvs.insert(vg->pv()); /* REQ: Runner.Validate.lvm_vg.PhysicalVolume */ /* If we already know a PV is being created there, we know it's fine */ if(seen_pvs.find(vg->pv()) == seen_pvs.end()) { /* Okay, let's see if a PV already exists there... */ if(opts.test(InstallEnvironment)) { #ifdef HAS_INSTALL_ENV if(!vg->test_pv(opts)) { failures++; output_error("installfile:" + std::to_string(vg->lineno()), "lvm_vg: a physical volume does not exist on " + vg->pv()); } #endif /* HAS_INSTALL_ENV */ } else { /* We can't tell if we aren't running on the target. */ output_warning("installfile:" + std::to_string(vg->lineno()), "lvm_vg: please ensure an LVM physical volume " "already exists at " + vg->pv()); } } } /* REQ: Runner.Validate.lvm_lv */ for(auto &lv : this->internal->lvm_lvs) { const std::string lvpath(lv->vg() + "/" + lv->name()); if(!lv->validate(this->opts)) { failures++; continue; } /* REQ: Runner.Validate.lvm_lv.Name */ if(seen_lvs.find(lvpath) != seen_lvs.end()) { failures++; output_error("installfile:" + std::to_string(lv->lineno()), "lvm_lv: a volume with the name " + lv->name() + " already exists on the volume group " + lv->vg()); } seen_lvs.insert(lvpath); /* REQ: Runner.Validate.lvm_lv.VolumeGroup */ if(seen_vg_names.find(lv->vg()) == seen_vg_names.end()) { /* Let's make sure it still exists, if we are running in the IE */ if(opts.test(InstallEnvironment)) { #ifdef HAS_INSTALL_ENV if(!fs::exists("/dev/" + lv->vg())) { failures++; output_error("installfile:" + std::to_string(lv->lineno()), "lvm_lv: volume group " + lv->vg() + " does not exist"); } #endif /* HAS_INSTALL_ENV */ } } } #define CHECK_EXIST_PART_LV(device, key, line) \ if(!fs::exists(device, ec) &&\ seen_parts.find(device) == seen_parts.end() &&\ seen_lvs.find(device.substr(5)) == seen_lvs.end()) {\ failures++;\ output_error("installfile:" + std::to_string(line),\ std::string(key) + ": device " + device +\ " does not exist");\ } /* REQ: Runner.Validate.encrypt */ for(auto &crypt : this->internal->luks) { if(!crypt->validate(this->opts)) { failures++; continue; } /* REQ: Runner.Validate.encrypt.Unique */ if(seen_luks.find(crypt->device()) != seen_luks.end()) { failures++; output_error("installfile:" + std::to_string(crypt->lineno()), "encrypt: encryption is already scheduled for " + crypt->device()); } seen_luks.insert(crypt->device()); /* REQ: Runner.Validate.encrypt.Block */ if(opts.test(InstallEnvironment)) { #ifdef HAS_INSTALL_ENV CHECK_EXIST_PART_LV(crypt->device(), "encrypt", crypt->lineno()) #endif /* HAS_INSTALL_ENV */ } } /* REQ: Runner.Validate.fs */ for(auto &fs : this->internal->fses) { if(!fs->validate(this->opts)) { failures++; continue; } /* REQ: Runner.Validate.fs.Unique */ if(seen_fses.find(fs->device()) != seen_fses.end()) { failures++; output_error("installfile:" + std::to_string(fs->lineno()), "fs: a filesystem is already scheduled to be " "created on " + fs->device()); } seen_fses.insert(fs->device()); /* REQ: Runner.Validate.fs.Block */ if(opts.test(InstallEnvironment)) { #ifdef HAS_INSTALL_ENV CHECK_EXIST_PART_LV(fs->device(), "fs", fs->lineno()) #endif /* HAS_INSTALL_ENV */ } } /* REQ: Runner.Validate.mount */ for(auto &mount : this->internal->mounts) { if(!mount->validate(this->opts)) { failures++; continue; } /* REQ: Runner.Validate.mount.Unique */ if(seen_mounts.find(mount->mountpoint()) != seen_mounts.end()) { failures++; output_error("installfile:" + std::to_string(mount->lineno()), "mount: mountpoint " + mount->mountpoint() + " has already been specified; " + mount->device() + " is a duplicate"); } seen_mounts.insert(mount->mountpoint()); /* REQ: Runner.Validate.mount.Block */ if(opts.test(InstallEnvironment)) { #ifdef HAS_INSTALL_ENV CHECK_EXIST_PART_LV(mount->device(), "mount", mount->lineno()) #endif /* HAS_INSTALL_ENV */ } } #undef CHECK_EXIST_PART_LV /* REQ: Runner.Validate.mount.Root */ if(seen_mounts.find("/") == seen_mounts.end()) { failures++; output_error("installfile:0", "mount: no root mount specified"); } output_log("validator", "0", "installfile", std::to_string(failures) + " failure(s).", ""); return (failures == 0); } bool Script::execute() const { bool success; error_code ec; /* assume create_directory will give us the error if removal fails */ if(fs::exists("/tmp/horizon", ec)) { fs::remove_all("/tmp/horizon", ec); } if(!fs::create_directory("/tmp/horizon", ec)) { output_error("internal", "could not create temporary directory", ec.message()); return false; } /* REQ: Runner.Execute.Verify */ output_step_start("validate"); success = this->validate(); output_step_end("validate"); if(!success) { /* REQ: Runner.Execute.Verify.Failure */ output_error("validator", "The HorizonScript failed validation", "Check the output from the validator."); return false; } #define EXECUTE_FAILURE(phase) \ output_error(phase, "The HorizonScript failed to execute",\ "Check the log file for more details.") /**************** DISK SETUP ****************/ output_step_start("disk"); #ifdef HAS_INSTALL_ENV if(opts.test(InstallEnvironment)) ped_device_probe_all(); #endif /* HAS_INSTALL_ENV */ /* REQ: Runner.Execute.diskid */ for(auto &diskid : this->internal->diskids) { if(!diskid->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } /* REQ: Runner.Execute.disklabel */ for(auto &label : this->internal->disklabels) { if(!label->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } /* REQ: Runner.Execute.partition */ /* Ensure partitions are created in on-disk order. */ std::sort(this->internal->partitions.begin(), this->internal->partitions.end(), [](std::unique_ptr const &e1, std::unique_ptr const &e2) { return (e1->device() + "p" + std::to_string(e1->partno())) < (e2->device() + "p" + std::to_string(e2->partno())); }); for(auto &part : this->internal->partitions) { if(!part->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } /* encrypt PVs */ /* REQ: Runner.Execute.lvm_pv */ for(auto &pv : this->internal->lvm_pvs) { if(!pv->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } /* REQ: Runner.Execute.lvm_vg */ for(auto &vg : this->internal->lvm_vgs) { if(!vg->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } /* REQ: Runner.Execute.lvm_lv */ for(auto &lv : this->internal->lvm_lvs) { if(!lv->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } /* encrypt */ /* REQ: Runner.Execute.fs */ for(auto &fs : this->internal->fses) { if(!fs->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } /* REQ: Runner.Execute.mount */ /* Sort by mountpoint. * This ensures that any subdirectory mounts come after their parent. */ std::sort(this->internal->mounts.begin(), this->internal->mounts.end(), [](std::unique_ptr const &e1, std::unique_ptr const &e2) { return e1->mountpoint() < e2->mountpoint(); }); for(auto &mount : this->internal->mounts) { if(!mount->execute(opts)) { EXECUTE_FAILURE("disk"); return false; } } #ifdef HAS_INSTALL_ENV if(opts.test(InstallEnvironment)) ped_device_free_all(); #endif /* HAS_INSTALL_ENV */ output_step_end("disk"); /**************** PRE PACKAGE METADATA ****************/ output_step_start("pre-metadata"); /* REQ: Runner.Execute.hostname */ if(!this->internal->hostname->execute(opts)) { EXECUTE_FAILURE("pre-metadata"); return false; } /* REQ: Runner.Execute.repository */ if(opts.test(Simulate)) { std::cout << "mkdir -p /target/etc/apk" << std::endl; } #ifdef HAS_INSTALL_ENV else { if(!fs::exists("/target/etc/apk", ec)) { fs::create_directory("/target/etc/apk", ec); if(ec) { output_error("internal", "failed to initialise APK"); EXECUTE_FAILURE("pre-metadata"); return false; } } } #endif /* HAS_INSTALL_ENV */ for(auto &repo : this->internal->repos) { if(!repo->execute(opts)) { EXECUTE_FAILURE("pre-metadata"); return false; } } #ifdef NON_LIBRE_FIRMWARE /* REQ: Runner.Execute.firmware */ if(this->internal->firmware && this->internal->firmware->test()) { this->internal->packages.insert("linux-firmware"); } #endif output_step_end("pre-metadata"); /**************** NETWORK ****************/ output_step_start("net"); if(!this->internal->ssids.empty()) { std::ofstream wpao("/tmp/horizon/wpa_supplicant.conf", std::ios_base::trunc); if(wpao) { wpao << "# Enable the control interface for wpa_cli and wpa_gui" << std::endl << "ctrl_interface=/var/run/wpa_supplicant" << std::endl << "ctrl_interface_group=wheel" << std::endl << "update_config=1" << std::endl; wpao.close(); } else { output_error("internal", "cannot write wireless networking configuration"); } for(auto &ssid : this->internal->ssids) { if(!ssid->execute(opts)) { EXECUTE_FAILURE("net"); /* "Soft" error. Not fatal. */ } } if(opts.test(Simulate)) { std::ifstream wpai("/tmp/horizon/wpa_supplicant.conf"); if(wpai) { std::cout << "cat >/target/etc/wpa_supplicant/wpa_supplicant.conf " << "<<- WPA_EOF" << std::endl << wpai.rdbuf() << std::endl << "WPA_EOF" << std::endl; } else { output_error("internal", "cannot read wireless networking configuration"); } } else { if(!fs::exists("/target/etc/wpa_supplicant", ec)) { fs::create_directory("/target/etc/wpa_supplicant", ec); } fs::copy_file("/tmp/horizon/wpa_supplicant.conf", "/target/etc/wpa_supplicant/wpa_supplicant.conf", fs_overwrite, ec); if(ec) { output_error("internal", "cannot save wireless networking " "configuration to target", ec.message()); } } } if(!this->internal->addresses.empty()) { fs::path netifrc_dir("/tmp/horizon/netifrc"); if(!fs::exists(netifrc_dir) && !fs::create_directory(netifrc_dir, ec)) { output_error("internal", "cannot create temporary directory", ec.message()); } for(auto &addr : this->internal->addresses) { if(!addr->execute(opts)) { EXECUTE_FAILURE("net"); /* "Soft" error. Not fatal. */ } } std::ostringstream conf; for(auto &&var_dent : fs::directory_iterator(netifrc_dir)) { const std::string variable(var_dent.path().filename().string()); std::ifstream contents(var_dent.path().string()); if(!contents) { output_error("internal", "cannot read network configuration"); EXECUTE_FAILURE("net"); continue; } conf << variable << "=\""; if(contents.rdbuf()->in_avail()) conf << contents.rdbuf(); conf << "\"" << std::endl; } if(opts.test(Simulate)) { std::cout << "mkdir -p /target/etc/conf.d" << std::endl; std::cout << "cat >>/target/etc/conf.d/net <<- NETCONF_EOF" << std::endl << conf.str() << std::endl << "NETCONF_EOF" << std::endl; } else { if(!fs::exists("/target/etc/conf.d")) { fs::create_directory("/target/etc/conf.d", ec); if(ec) { output_error("internal", "could not create /etc/conf.d " "directory", ec.message()); } } std::ofstream conf_file("/target/etc/conf.d/net", std::ios_base::app); if(!conf_file) { output_error("internal", "cannot save network configuration " "to target"); EXECUTE_FAILURE("net"); } else { conf_file << conf.str(); } } } if(!this->internal->network->execute(opts)) { EXECUTE_FAILURE("net"); return false; } if(this->internal->network->test()) { bool do_wpa = !this->internal->ssids.empty(); if(opts.test(Simulate)) { if(do_wpa) { std::cout << "cp /target/etc/wpa_supplicant/wpa_supplicant.conf " << "/etc/wpa_supplicant/wpa_supplicant.conf" << std::endl; } std::cout << "cp /target/etc/conf.d/net /etc/conf.d/net" << std::endl; } else { if(do_wpa) { fs::copy_file("/target/etc/wpa_supplicant/wpa_supplicant.conf", "/etc/wpa_supplicant/wpa_supplicant.conf", fs_overwrite, ec); if(ec) { output_error("internal", "cannot use wireless configuration " "during installation", ec.message()); EXECUTE_FAILURE("net"); } } fs::copy_file("/target/etc/conf.d/net", "/etc/conf.d/net", fs_overwrite, ec); if(ec) { output_error("internal", "cannot use networking configuration " "during installation", ec.message()); EXECUTE_FAILURE("net"); return false; } } } output_step_end("net"); /**************** PKGDB ****************/ output_step_start("pkgdb"); /* REQ: Runner.Execute.signingkey */ for(auto &key : this->internal->repo_keys) { if(!key->execute(opts)) { EXECUTE_FAILURE("pkgdb"); return false; } } /* REQ: Runner.Execute.pkginstall.APKDB */ output_info("internal", "initialising APK"); if(opts.test(Simulate)) { std::cout << "apk --root /target --initdb add" << std::endl; } else { if(system("apk --root /target --initdb add") != 0) { EXECUTE_FAILURE("pkgdb"); return false; } } /* REQ: Runner.Execute.pkginstall */ output_info("internal", "installing packages to target"); std::ostringstream pkg_list; for(auto &pkg : this->internal->packages) { pkg_list << pkg << " "; } if(opts.test(Simulate)) { std::cout << "apk --root /target update" << std::endl; std::cout << "apk --root /target add " << pkg_list.str() << std::endl; } else { if(system("apk --root /target update") != 0) { EXECUTE_FAILURE("pkgdb"); return false; } std::string apk_invoc = "apk --root /target add " + pkg_list.str(); if(system(apk_invoc.c_str()) != 0) { EXECUTE_FAILURE("pkgdb"); return false; } } output_step_end("pkgdb"); /**************** POST PACKAGE METADATA ****************/ output_step_start("post-metadata"); if(!this->internal->rootpw->execute(opts)) { EXECUTE_FAILURE("post-metadata"); return false; } if(this->internal->lang && !this->internal->lang->execute(opts)) { EXECUTE_FAILURE("post-metadata"); return false; } /* keymap */ /* UserAccounts */ if(!this->internal->tzone->execute(opts)) { EXECUTE_FAILURE("post-metadata"); return false; } output_step_end("post-metadata"); return true; } }