/* * disk.cc - Implementation of the Key classes for disk manipulation * 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 /* strerror */ #include #include #ifdef HAS_INSTALL_ENV # include /* assert */ # include /* blkid_get_tag_value */ # include /* udev_* */ # include /* mount */ # include /* mkdir, stat */ # include /* S_* */ # include /* access */ #endif /* HAS_INSTALL_ENV */ #include "disk.hh" #include "util/output.hh" using namespace Horizon::Keys; #ifdef HAS_INSTALL_ENV /*! Determine if _block is a valid block device. * @param key The key associated with this test. * @param line The line number where the key exists. * @param _block The path to test. * @returns true if _block is valid, false otherwise. * @note Will output_error if an error occurs. */ bool is_block_device(const std::string &key, int line, const std::string &_block) { struct stat blk_stat; const char *block_c = _block.c_str(); if(access(block_c, F_OK) != 0 || stat(block_c, &blk_stat) != 0) { output_error("installfile:" + std::to_string(line), key + ": error opening device " + _block, strerror(errno)); return false; } if(!S_ISBLK(blk_stat.st_mode)) { output_error("installfile:" + std::to_string(line), key + ": " + _block + " is not a valid block device"); return false; } return true; } #endif /* HAS_INSTALL_ENV */ Key *DiskId::parseFromData(const std::string &data, int lineno, int *errors, int *warnings) { std::string block, ident; std::string::size_type block_end = data.find_first_of(' '); if(block_end == std::string::npos) { if(errors) *errors += 1; output_error("installfile:" + std::to_string(lineno), "diskid: expected an identification string", "valid format for diskid is: [block] [id-string]"); return nullptr; } block = data.substr(0, block_end); ident = data.substr(block_end + 1); return new DiskId(lineno, block, ident); } bool DiskId::validate(ScriptOptions options) const { #ifdef HAS_INSTALL_ENV /* We only validate if running in an Installation Environment. */ if(options.test(InstallEnvironment)) { /* Unlike 'mount', 'diskid' *does* require that the block device exist * before installation begins. This test is always valid. */ return is_block_device("diskid", this->lineno(), _block); } #endif /* HAS_INSTALL_ENV */ return true; } bool DiskId::execute(ScriptOptions options) const { bool match = false; if(!options.test(InstallEnvironment)) return true; #ifdef HAS_INSTALL_ENV struct udev *udev; struct udev_device *device; const char *serial; struct stat blk_stat; const char *block_c = _block.c_str(); if(stat(block_c, &blk_stat) != 0) { output_error("installfile:" + std::to_string(line), "diskid: error opening device " + _block, strerror(errno)); return false; } assert(S_ISBLK(blk_stat.st_mode)); udev = udev_new(); if(!udev) { output_error("installfile:" + std::to_string(line), "diskid: failed to communicate with udevd", "cannot read disk information"); return false; } device = udev_device_new_from_devnum(udev, 'b', blk_stat.st_rdev); if(!device) { udev_unref(udev); output_error("installfile:" + std::to_string(line), "diskid: failed to retrieve disk from udevd", "cannot read disk information"); return false; } serial = udev_device_get_property_value(device, "ID_SERIAL"); /* If we can't get the serial for this device, it's not a disk */ if(serial) { std::string full_str(serial); match = (full_str.find(_ident) != std::string::npos); } udev_device_unref(device); udev_unref(udev); #endif /* HAS_INSTALL_ENV */ return match; } Key *DiskLabel::parseFromData(const std::string &data, int lineno, int *errors, int *warnings) { std::string block, label; std::string::size_type sep = data.find_first_of(' '); LabelType type; /* REQ: Runner.Validate.disklabel.Validity */ if(sep == std::string::npos || data.length() == sep + 1) { if(errors) *errors += 1; output_error("installfile:" + std::to_string(lineno), "disklabel: expected a label type", "valid format for disklabel is: [disk] [type]"); return nullptr; } block = data.substr(0, sep); label = data.substr(sep + 1); std::transform(label.begin(), label.end(), label.begin(), ::tolower); /* REQ: Runner.Validate.disklabel.LabelType */ if(label == "apm") { type = APM; } else if(label == "mbr") { type = MBR; } else if(label == "gpt") { type = GPT; } else { if(errors) *errors += 1; output_error("installfile:" + std::to_string(lineno), "disklabel: '" + label + "' is not a valid label type", "valid label types are: apm, mbr, gpt"); return nullptr; } return new DiskLabel(lineno, block, type); } bool DiskLabel::validate(ScriptOptions options) const { #ifdef HAS_INSTALL_ENV /* REQ: Runner.Validate.disklabel.Block */ if(options.test(InstallEnvironment)) { /* disklabels are created before any others, so we can check now */ return is_block_device("disklabel", this->lineno(), _block); } #endif /* HAS_INSTALL_ENV */ return true; } bool DiskLabel::execute(ScriptOptions) const { /* TODO XXX NOTIMPLEMENTED */ return false; } Key *Mount::parseFromData(const std::string &data, int lineno, int *errors, int *warnings) { std::string dev, where, opt; std::string::size_type where_pos, opt_pos; bool any_failure = false; long spaces = std::count(data.cbegin(), data.cend(), ' '); if(spaces < 1 || spaces > 2) { if(errors) *errors += 1; /* Don't bother with any_failure, because this is immediately fatal. */ output_error("installfile:" + std::to_string(lineno), "mount: expected either 2 or 3 elements, got: " + std::to_string(spaces), ""); return nullptr; } where_pos = data.find_first_of(' '); opt_pos = data.find_first_of(' ', where_pos + 1); dev = data.substr(0, where_pos); where = data.substr(where_pos + 1, (opt_pos - where_pos - 1)); if(opt_pos != std::string::npos && data.length() > opt_pos + 1) { opt = data.substr(opt_pos + 1); } if(dev.compare(0, 4, "/dev")) { if(errors) *errors += 1; any_failure = true; output_error("installfile:" + std::to_string(lineno), "mount: element 1: expected device node", "'" + dev + "' is not a valid device node"); } if(where[0] != '/') { if(errors) *errors += 1; any_failure = true; output_error("installfile:" + std::to_string(lineno), "mount: element 2: expected absolute path", "'" + where + "' is not a valid absolute path"); } if(any_failure) return nullptr; return new Mount(lineno, dev, where, opt); } bool Mount::validate(ScriptOptions options) const { /* We only validate if running in an Installation Environment. */ if(!options.test(InstallEnvironment)) return true; #ifdef HAS_INSTALL_ENV /* XXX TODO: This will fail validation if the block device does not * already exist. However, we must take in to account that block devices * may not yet exist during the script validation phase. This check may * need to happen in Script::validate like the Uniqueness tests. */ return(access(this->device().c_str(), F_OK) == 0); #endif /* HAS_INSTALL_ENV */ } bool Mount::execute(ScriptOptions options) const { const std::string actual_mount = "/target" + this->mountpoint(); const char *fstype = nullptr; /* We have to get the filesystem for the node. */ if(options.test(Simulate)) { fstype = "auto"; } #ifdef HAS_INSTALL_ENV else { fstype = blkid_get_tag_value(nullptr, "TYPE", this->device().c_str()); if(fstype == nullptr) { output_error("installfile:" + std::to_string(this->lineno()), "mount: cannot determine filesystem type for device", this->device()); return false; } } #endif /* HAS_INSTALL_ENV */ output_info("installfile:" + std::to_string(this->lineno()), "mount: mounting " + this->device() + " on " + this->mountpoint()); if(options.test(Simulate)) { std::cout << "mount "; if(!this->options().empty()) { std::cout << "-o " << this->options() << " "; } std::cout << this->device() << " " << actual_mount << std::endl; } #ifdef HAS_INSTALL_ENV else { /* mount */ if(mount(this->device().c_str(), actual_mount.c_str(), fstype, 0, this->options().c_str()) != 0) { output_warning("installfile:" + std::to_string(this->lineno()), "mount: error mounting " + this->mountpoint() + "with options; retrying without", strerror(errno)); if(mount(this->device().c_str(), actual_mount.c_str(), fstype, 0, nullptr) != 0) { output_error("installfile:" + std::to_string(this->lineno()), "mount: error mounting " + this->mountpoint() + "without options", strerror(errno)); return false; } } } #endif /* HAS_INSTALL_ENV */ /* Handle fstab. We're guaranteed to have a /target since mount has * already ran and /target is the first mount done. */ output_info("installfile:" + std::to_string(this->lineno()), "mount: adding " + this->mountpoint() + " to /etc/fstab"); char pass = (this->mountpoint() == "/" ? '1' : '0'); const std::string fstab_opts = (this->options().empty() ? "defaults" : this->options()); if(options.test(Simulate)) { if(this->mountpoint() == "/") { std::cout << "mkdir -p /target/etc" << std::endl; } std::cout << "printf '%s\\t%s\\t%s\\t%s\\t0\\t" << pass << "\\" << "n' " << this->device() << " " << this->mountpoint() << " " << fstype << " " << fstab_opts << " >> /target/etc/fstab" << std::endl; } #ifdef HAS_INSTALL_ENV else { if(this->mountpoint() == "/") { /* failure of mkdir will be handled in the !fstab_f case */ mkdir("/target/etc", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); } std::ofstream fstab_f("/target/etc/fstab"); if(!fstab_f) { output_error("installfile:" + std::to_string(this->lineno()), "mount: failure opening /etc/fstab for writing"); return false; } fstab_f << this->device() << "\t" << this->mountpoint() << "\t" << fstype << "\t" << fstab_opts << "\t0\t" << pass << std::endl; } #endif /* HAS_INSTALL_ENV */ return true; }