summaryrefslogblamecommitdiff
path: root/hscript/script.cc
blob: 8eb7c18ff0863ba49732411f4b7aa1d03804497b (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
  
                                                 









                                                                           
                    

                   
              
              
 
                    
                  


                     
 



                         
 

































                                                                          


  


                              
                                                          
                                                    
                                        
                                                      
                                                        
                                   
                                
                                                          

                                                                
 


                                                                        




                                                                               
                                                   

                                                                              
                                                                    





                                                                   
                                                                   
 

                                   

                                                                         

                             


                                                              


                                           

                                                                        
                                                        






                                                               
                                             


                                                                                   
                                                

                                                                                      
                                       
                             


                                     
                           
                        
                                         









                                                                     
                                        




                                                            





                                                                 


                         
                      
     

  
 
                  
                                 

 
                                                                              
                             
               
                                                          



                                    

 
                               
              
                                                          
                               



                                                   


                                                            
                                 
 
                                                                             
                                  




                                    
                                 








                                                        
                        
                                                           






                                                   
                                                             
                                          
                                                                              
                                   
                                                          
                     

         


                                                                       
                                                      



                                                                
                                                                  
             
                     
         




                                                                           



                                                                          
                                                               

                                                        
         




                                                                 
                                                                           
                 



                                                             
                                                                
                 

     
                                               

                                                          
                                                                      
                                          

             















                                                        


                    

                                                           
                                                                  




                          
                                

                          

 
                               
                     
                                      
                                                




                                                                   
                                               
                                          


                       






                                                                            
                                            

                                                
                                                 
                                                                             

         
 


                                                    











                                                                          
 
















                                                                                

     

                                                                  
                           


 
/*
 * 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 <fstream>
#include <iostream>
#include <map>
#include <set>

#include "script.hh"
#include "disk.hh"
#include "meta.hh"
#include "network.hh"
#include "user.hh"

#include "util/output.hh"

#define LINE_MAX 512


typedef Horizon::Keys::Key *(*key_parse_fn)(std::string, int, int*, int*);

const std::map<std::string, key_parse_fn> valid_keys = {
    {"network", &Horizon::Keys::Network::parseFromData},
    {"hostname", &Horizon::Keys::Hostname::parseFromData},
    {"pkginstall", &Horizon::Keys::PkgInstall::parseFromData},
    {"rootpw", &Horizon::Keys::RootPassphrase::parseFromData},

    {"language", &Horizon::Keys::Language::parseFromData},
    {"keymap", &Horizon::Keys::Keymap::parseFromData},
    {"firmware", &Horizon::Keys::Firmware::parseFromData},
    {"timezone", &Horizon::Keys::Timezone::parseFromData},
    {"repository", &Horizon::Keys::Repository::parseFromData},
    {"signingkey", &Horizon::Keys::SigningKey::parseFromData},

    {"netaddress", &Horizon::Keys::NetAddress::parseFromData},
    {"nameserver", &Horizon::Keys::Nameserver::parseFromData},
    {"netssid", &Horizon::Keys::NetSSID::parseFromData},

    {"username", &Horizon::Keys::Username::parseFromData},
    {"useralias", &Horizon::Keys::UserAlias::parseFromData},
    {"userpw", &Horizon::Keys::UserPassphrase::parseFromData},
    {"usericon", &Horizon::Keys::UserIcon::parseFromData},
    {"usergroups", &Horizon::Keys::UserGroups::parseFromData},

    {"diskid", &Horizon::Keys::DiskId::parseFromData},
    {"disklabel", &Horizon::Keys::DiskLabel::parseFromData},
    {"partition", &Horizon::Keys::Partition::parseFromData},
    {"lvm_pv", &Horizon::Keys::LVMPhysical::parseFromData},
    {"lvm_vg", &Horizon::Keys::LVMGroup::parseFromData},
    {"lvm_lv", &Horizon::Keys::LVMVolume::parseFromData},
    {"encrypt", &Horizon::Keys::Encrypt::parseFromData},
    {"fs", &Horizon::Keys::Filesystem::parseFromData},
    {"mount", &Horizon::Keys::Mount::parseFromData}
};


namespace Horizon {

struct Script::ScriptPrivate {
    /*! Determines whether or not to enable networking. */
    std::unique_ptr<Horizon::Keys::Network> network;
    /*! The target system's hostname. */
    std::unique_ptr<Horizon::Keys::Hostname> hostname;
    /*! The packages to install to the target system. */
    std::set<std::string> packages;
    /*! The root shadow line. */
    std::unique_ptr<Horizon::Keys::RootPassphrase> rootpw;
    /*! Target system's mountpoints. */
    std::vector< std::unique_ptr<Horizon::Keys::Mount> > mounts;

    /*! Network addressing configuration */
    std::vector< std::unique_ptr<Horizon::Keys::NetAddress> > addresses;

    /*! Store +key_obj+ representing the key +key_name+.
     * @param key_name      The name of the key that is being stored.
     * @param key_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 *key_obj, int lineno,
                   int *errors, int *warnings, ScriptOptions opts) {
#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 '" + KEY + "'", err_str);

        if(key_name == "network") {
            if(this->network) {
                DUPLICATE_ERROR(this->network, std::string("network"),
                                this->network->test() ? "true" : "false")
                return false;
            }
            std::unique_ptr<Keys::Network> net(
                        dynamic_cast<Keys::Network *>(key_obj)
            );
            this->network = std::move(net);
            return true;
        } else if(key_name == "hostname") {
            if(this->hostname) {
                DUPLICATE_ERROR(this->hostname, std::string("hostname"),
                                this->hostname->value())
                return false;
            }
            std::unique_ptr<Keys::Hostname> name(
                        dynamic_cast<Keys::Hostname *>(key_obj)
            );
            this->hostname = std::move(name);
            return true;
        } else if(key_name == "pkginstall") {
            Keys::PkgInstall *install = dynamic_cast<Keys::PkgInstall *>(key_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),
                                   "package '" + pkg + "' has already been specified",
                                   "");
                    continue;
                }
                packages.insert(pkg);
            }
            delete install;
            return true;
        } else if(key_name == "rootpw") {
            if(this->rootpw) {
                DUPLICATE_ERROR(this->rootpw, std::string("rootpw"),
                                "an encrypted passphrase")
                return false;
            }
            std::unique_ptr<Keys::RootPassphrase> name(
                        dynamic_cast<Keys::RootPassphrase *>(key_obj)
            );
            this->rootpw = std::move(name);
            return true;
        } else if(key_name == "mount") {
            std::unique_ptr<Keys::Mount> mount(
                        dynamic_cast<Keys::Mount *>(key_obj)
            );
            this->mounts.push_back(std::move(mount));
            return true;
        } else if(key_name == "netaddress") {
            std::unique_ptr<Keys::NetAddress> addr(
                        dynamic_cast<Keys::NetAddress *>(key_obj)
            );
            this->addresses.push_back(std::move(addr));
            return true;
        } else {
            return false;
        }
#undef DUPLICATE_ERROR
    }
};


Script::Script() {
    internal = new ScriptPrivate;
}

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);
}

#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, "");

const Script *Script::load(std::istream &sstream, const ScriptOptions opts) {
    using namespace Horizon::Keys;
    Script *the_script = new Script;

    int lineno = 0;
    char nextline[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);
        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(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_message("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;
    }
}

bool Script::validate() const {
    int failures = 0;
    std::set<std::string> seen_mounts;
    std::map<const std::string, int> seen_iface;

    if(!this->internal->network->validate(this->opts)) failures++;
    if(!this->internal->hostname->validate(this->opts)) failures++;
    if(!this->internal->rootpw->validate(this->opts)) failures++;

    for(auto &mount : this->internal->mounts) {
        if(!mount->validate(this->opts)) {
            failures++;
            continue;
        }

        /* 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());
        if(this->opts.test(InstallEnvironment)) {
            /* TODO: Runner.Validate.mount.Block for not-yet-created devs. */
        }
    }

    /* Runner.Validate.mount.Root */
    if(seen_mounts.find("/") == seen_mounts.end()) {
        failures++;
        output_error("installfile:0", "mount: no root mount specified");
    }

    /* 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++;
        }

        /* 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");
            }
        }
    }

    output_message("validator", "0", "installfile",
                   std::to_string(failures) + " failure(s).", "");
    return (failures == 0);
}

}