/*
* 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 <algorithm>
#include <assert.h>
#include "util/filesystem.hh"
#include <fstream>
#include <iostream>
#include <map>
#ifdef HAS_INSTALL_ENV
# include <parted/parted.h>
#endif /* HAS_INSTALL_ENV */
#include <set>
#include <sstream>
#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<std::string, key_parse_fn> 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<Username> name;
std::unique_ptr<UserAlias> alias;
std::unique_ptr<UserPassphrase> passphrase;
std::unique_ptr<UserIcon> icon;
std::vector< std::unique_ptr<UserGroups> > groups;
};
struct Script::ScriptPrivate {
/*! Determines whether or not to enable networking. */
std::unique_ptr<Network> network;
/*! The target system's hostname. */
std::unique_ptr<Hostname> hostname;
/*! The packages to install to the target system. */
std::set<std::string> packages;
/*! The root shadow line. */
std::unique_ptr<RootPassphrase> rootpw;
/*! The system language. */
std::unique_ptr<Language> lang;
/*! The system keymap. */
std::unique_ptr<Keymap> keymap;
/*! The system timezone. */
std::unique_ptr<Timezone> tzone;
/*! Network addressing configuration */
std::vector< std::unique_ptr<NetAddress> > addresses;
/*! Network nameserver resolver addresses */
std::vector< std::unique_ptr<Nameserver> > nses;
/*! Wireless networking configuration */
std::vector< std::unique_ptr<NetSSID> > ssids;
/*! APK repositories */
std::vector< std::unique_ptr<Repository> > repos;
/*! APK repository keys */
std::vector< std::unique_ptr<SigningKey> > repo_keys;
/*! User account information */
std::map< std::string, std::unique_ptr<UserDetail> > accounts;
/*! Disk identification keys */
std::vector< std::unique_ptr<DiskId> > diskids;
/*! Disklabel configuration keys */
std::vector< std::unique_ptr<DiskLabel> > disklabels;
/*! Partition creation keys */
std::vector< std::unique_ptr<Partition> > partitions;
/*! LVM physical volume keys */
std::vector< std::unique_ptr<LVMPhysical> > lvm_pvs;
/*! LVM volume group keys */
std::vector< std::unique_ptr<LVMGroup> > lvm_vgs;
/*! LVM logical volume keys */
std::vector< std::unique_ptr<LVMVolume> > lvm_lvs;
/*! LUKS creation keys */
std::vector< std::unique_ptr<Encrypt> > luks;
/*! Filesystem creation keys */
std::vector< std::unique_ptr<Filesystem> > fses;
/*! Target system's mountpoints. */
std::vector< std::unique_ptr<Mount> > mounts;
#ifdef NON_LIBRE_FIRMWARE
std::unique_ptr<Firmware> 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<NetAddress> addr(dynamic_cast<NetAddress *>(obj));
this->addresses.push_back(std::move(addr));
return true;
} else if(key_name == "nameserver") {
std::unique_ptr<Nameserver> ns(dynamic_cast<Nameserver *>(obj));
this->nses.push_back(std::move(ns));
return true;
} else if(key_name == "netssid") {
std::unique_ptr<NetSSID> ssid(dynamic_cast<NetSSID *>(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<Repository> repo(dynamic_cast<Repository *>(obj));
this->repos.push_back(std::move(repo));
return true;
} else if(key_name == "signingkey") {
std::unique_ptr<SigningKey> key(dynamic_cast<SigningKey *>(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> diskid(dynamic_cast<DiskId *>(obj));
this->diskids.push_back(std::move(diskid));
return true;
} else if(key_name == "disklabel") {
std::unique_ptr<DiskLabel> l(dynamic_cast<DiskLabel *>(obj));
this->disklabels.push_back(std::move(l));
return true;
} else if(key_name == "partition") {
std::unique_ptr<Partition> p(dynamic_cast<Partition *>(obj));
this->partitions.push_back(std::move(p));
return true;
} else if(key_name == "lvm_pv") {
std::unique_ptr<LVMPhysical> pv(dynamic_cast<LVMPhysical *>(obj));
this->lvm_pvs.push_back(std::move(pv));
return true;
} else if(key_name == "lvm_vg") {
std::unique_ptr<LVMGroup> vg(dynamic_cast<LVMGroup *>(obj));
this->lvm_vgs.push_back(std::move(vg));
return true;
} else if(key_name == "lvm_lv") {
std::unique_ptr<LVMVolume> lv(dynamic_cast<LVMVolume *>(obj));
this->lvm_lvs.push_back(std::move(lv));
return true;
} else if(key_name == "encrypt") {
std::unique_ptr<Encrypt> e(dynamic_cast<Encrypt *>(obj));
this->luks.push_back(std::move(e));
return true;
} else if(key_name == "fs") {
std::unique_ptr<Filesystem> fs(dynamic_cast<Filesystem *>(obj));
this->fses.push_back(std::move(fs));
return true;
} else if(key_name == "mount") {
std::unique_ptr<Mount> mount(dynamic_cast<Mount *>(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<Network> net(dynamic_cast<Network *>(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<Hostname> name(dynamic_cast<Hostname *>(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<PkgInstall *>(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<RootPassphrase> r(dynamic_cast<RootPassphrase *>(obj));
this->rootpw = std::move(r);
return true;
}
bool store_firmware(Keys::Key *obj, int lineno, int *errors, int *,
ScriptOptions) {
std::unique_ptr<Firmware> f(dynamic_cast<Firmware *>(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<Language> l(dynamic_cast<Language *>(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<Keymap> k(dynamic_cast<Keymap *>(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<Timezone> t(dynamic_cast<Timezone *>(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<Username> name(dynamic_cast<Username *>(obj));
if(accounts.find(name->value()) != accounts.end()) {
DUPLICATE_ERROR((*accounts.find(name->value())).second->name,
"username", "assigned")
return false;
}
std::unique_ptr<UserDetail> 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<UserAlias> alias(dynamic_cast<UserAlias *>(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<UserPassphrase> pw(dynamic_cast<UserPassphrase *>(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<UserIcon> icon(dynamic_cast<UserIcon *>(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<UserGroups> grp(dynamic_cast<UserGroups *>(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<std::string> seen_groups;
for(auto &group : detail->groups) {
/* REQ: Runner.Validate.usergroups */
if(!group->validate(opts)) {
failures++;
}
/* REQ: Runner.Validate.usergroups.Unique */
const std::set<std::string> 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<std::unique_ptr<Keys::Repository>> &repos) {
Keys::Repository *sys_key = dynamic_cast<Keys::Repository *>(
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<Keys::Repository> sys_repo(sys_key);
repos.push_back(std::move(sys_repo));
Keys::Repository *user_key = dynamic_cast<Keys::Repository *>(
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<Keys::Repository> 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<Keys::Repository *>(
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<Keys::Repository> 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<std::unique_ptr<Keys::SigningKey>> &keys) {
Keys::SigningKey *key = dynamic_cast<Keys::SigningKey *>(
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<Keys::SigningKey> repo_key(key);
keys.push_back(std::move(repo_key));
return true;
}
bool Script::validate() const {
int failures = 0;
std::set<std::string> seen_diskids, seen_labels, seen_parts, seen_pvs,
seen_vg_names, seen_vg_pvs, seen_lvs, seen_fses, seen_mounts,
seen_luks;
std::map<const std::string, int> 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<Keys::Timezone *>(
Horizon::Keys::Timezone::parseFromData("UTC", 0,
&failures, nullptr)
);
if(!utc) {
output_error("internal", "failed to create default timezone");
return false;
}
std::unique_ptr<Keys::Timezone> 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<Keys::Partition> const &e1,
std::unique_ptr<Keys::Partition> 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<Keys::Mount> const &e1,
std::unique_ptr<Keys::Mount> 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;
}
}