summaryrefslogblamecommitdiff
path: root/tools/hscript-fromjson/jsonconv.cc
blob: b5c9504800e2eea2d52a3ed637f3a43f8244827f (plain) (tree)



















































                                                                                    
                                           





























































                                                                              
























                                                                        







                                                   
                                                                 







                                                                                                 
                                                                                         

















































                                                                                                   












                                                                               






























































                                                                                                        
/*
 * jsonconv.cc - Implementation of the JSON to HorizonScript conversion utility
 * Project Horizon
 *
 * Copyright (c) 2020 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 <boost/program_options.hpp>
#include <cstdlib>              /* EXIT_* */
#include <fstream>
#include <sstream>
#include <string>
#include "3rdparty/json.hpp"
#include "hscript/script.hh"
#include "util/filesystem.hh"
#include "util/output.hh"

bool pretty = true;     /*! Controls ASCII colour output */

using json = nlohmann::json;


#define DESCR_TEXT "Usage: hscript-fromjson [OPTION]... [INSTALLFILE] [JSONFILE]\n"\
                   "Write an INSTALLFILE based on the description found in JSONFILE"
/*! Text used at the top of usage output */

/*! Parse a single description of a HorizonScript.
 * In JSON files that describe multiple images (i.e. YANG), this will be
 * called multiple times. */
bool parse_one_desc(json desc, std::ostream &out) {
    /* Ensure mandatory keys are present. */
#define ENSURE_KEY(node, key_name) \
    if(node.find(key_name) == node.end()) {\
        output_error("parse_one_desc", "JSON does not contain required key '"\
                                       + std::string(key_name) + "'");\
        return false;\
    }

    ENSURE_KEY(desc, "hostname");
    ENSURE_KEY(desc, "rootpw");
    ENSURE_KEY(desc, "packages");
    ENSURE_KEY(desc, "root");

    out << "hostname " << desc["hostname"].get<std::string>() << std::endl;
    out << "rootpw " << desc["rootpw"].get<std::string>() << std::endl;
    out << "mount " << desc["root"].get<std::string>() << " /" << std::endl;

    if(desc.find("netaddresses") != desc.end()) {
        out << "network true" << std::endl;
        for(const auto &addr : desc["netaddresses"]) {
            ENSURE_KEY(addr, "interface");
            ENSURE_KEY(addr, "addr-type");
            std::string addrtype = addr["addr-type"].get<std::string>();
            out << "netaddress " << addr["interface"].get<std::string>()
                << " " << addrtype;
            if(addrtype == "static") {
                ENSURE_KEY(addr, "address");
                const auto &ipaddr = addr["address"];
                ENSURE_KEY(ipaddr, "ip-address");
                ENSURE_KEY(ipaddr, "net-prefix");
                out << " " << ipaddr["ip-address"].get<std::string>()
                    << " " << ipaddr["net-prefix"].get<std::string>();
                if(ipaddr.find("gateway") != ipaddr.end()) {
                    out << " " << ipaddr["gateway"].get<std::string>();
                }
            }
            out << std::endl;
        }
    } else {
        out << "network false" << std::endl;
    }

    std::ostringstream pkgs;
    for(const auto &pkg : desc.at("packages")) {
        pkgs << " " << pkg.get<std::string>();
    }

    out << "pkginstall" << pkgs.str() << std::endl;

#define SIMPLE_KEY(key_name) \
    if(desc.find(key_name) != desc.end()) {\
        out << key_name << " " << desc[key_name].get<std::string>()\
            << std::endl;\
    }

    SIMPLE_KEY("arch");
    SIMPLE_KEY("language");
    SIMPLE_KEY("keymap");
    SIMPLE_KEY("firmware");
    SIMPLE_KEY("timezone");

#undef SIMPLE_KEY

    if(desc.find("netconfig") != desc.end()) {
        out << "netconfigtype " << desc["netconfig"].get<std::string>()
            << std::endl;
    }

#define SIMPLE_PLURAL_KEY(key_name, hs_name) \
    if(desc.find(key_name) != desc.end()) {\
        for(const auto &element : desc[key_name]) {\
            out << hs_name << " " << element.get<std::string>() << std::endl;\
        }\
    }

    SIMPLE_PLURAL_KEY("nameservers", "nameserver");
    SIMPLE_PLURAL_KEY("repositories", "repository");
    SIMPLE_PLURAL_KEY("signingkeys", "signingkey");

#undef SIMPLE_PLURAL_KEY

    if(desc.find("users") != desc.end()) {
        for(const auto &user : desc["users"]) {
            if(user.find("username") == user.end()) {
                output_error("input json",
                             "user account specified without username");
                continue;
            }

            const auto &name = user["username"].get<std::string>();

            out << "username " << name << std::endl;

#define USER_KEY(key_name, hscript_name) \
            if(user.find(key_name) != user.end()) {\
                out << hscript_name << " " << name << " " \
                    << user[key_name].get<std::string>() << std::endl;\
            }

            USER_KEY("alias", "useralias");
            USER_KEY("passphrase", "userpw");
            USER_KEY("groups", "usergroups");
            /* Future expansion: user icons */
        }
    }

    return true;
}


/*! Entry-point for the JSON conversion utility. */
int main(int argc, char *argv[]) {
    using namespace boost::program_options;

    bool needs_help{}, disable_pretty{}, force{}, version_only{};
    std::string if_path{"/etc/horizon/installfile"}, json_path;
    int exit_code = EXIT_SUCCESS;

    options_description ui{DESCR_TEXT "\n\nDefault HorizonScript output path: " + if_path +
                           "\nJSON will be read from standard input unless otherwise specified"};
    options_description general{"General options"};
    general.add_options()
            ("help,h", bool_switch(&needs_help), "Display this message.")
            ("version,v", bool_switch(&version_only), "Show program version information")
            ("no-colour,n", bool_switch(&disable_pretty), "Do not 'prettify' output")
            ("force,f", bool_switch(&force), "Force writing, even if HorizonScript already exists")
            ;
    ui.add(general);

    options_description all;
    all.add(ui).add_options()("installfile", value<std::string>()->default_value(if_path),
                              "The file name where the HorizonScript will be written.")
                             ("jsonfile", "The path to the JSON file.");

    positional_options_description positional;
    positional.add("installfile", 1);
    positional.add("jsonfile", 2);

    command_line_parser parser{argc, argv};
    parser.options(all);
    parser.positional(positional);

    variables_map vm;
    try {
        auto result = parser.run();
        store(result, vm);
        notify(vm);
    } catch(const std::exception &ex) {
        std::cerr << ex.what() << std::endl;
        exit_code = EXIT_FAILURE;
        needs_help = true;
    }

    /* --help, or usage failure */
    if(needs_help) {
        std::cout << ui << std::endl;
        return exit_code;
    }

    /* -n/--no-colour, or logging to file */
    if(disable_pretty || !isatty(1)) {
        pretty = false;
    }

    if(!vm["installfile"].empty()) {
        if_path = vm["installfile"].as<std::string>();
    }

    if(!vm["jsonfile"].empty()) {
        json_path = vm["jsonfile"].as<std::string>();
    } else {
        json_path = "-";
    }

    if(version_only) {
        bold_if_pretty(std::cout);
        std::cout << "HorizonScript JSON Conversion Utility version " << VERSTR
                  << std::endl;
        reset_if_pretty(std::cout);
        std::cout << "Copyright (c) 2020 Adélie Linux and contributors."
                  << std::endl
                  << "This software is licensed to you under the terms of the "
                  << std::endl << "AGPL 3.0 license, unless otherwise noted."
                  << std::endl << std::endl;
        return EXIT_SUCCESS;
    }

    if(!force && fs::exists(if_path)) {
        output_error("<option>", "installfile already exists", if_path);
        return EXIT_FAILURE;
    }

    json j;
    std::string raw_json;
    char buff[8192];

    if(json_path == "-") {
        while(std::cin.getline(buff, sizeof(buff))) {
            raw_json.append(buff);
        }
    } else {
        if(!fs::exists(json_path)) {
            output_error("<option>", "JSON file does not exist", json_path);
            return EXIT_FAILURE;
        }

        auto stream = std::ifstream(json_path);
        while(stream.getline(buff, sizeof(buff))) {
            raw_json.append(buff);
        }
        if(stream.fail() && !stream.eof()) {
            output_error(json_path, "line exceeds maximum length",
                         "Maximum line length is " +
                         std::to_string(sizeof(buff)));
            return EXIT_FAILURE;
        }

        if(stream.bad() && !stream.eof()) {
            output_error(json_path, "I/O error while reading JSON");
            return EXIT_FAILURE;
        }
    }

    try {
        j = json::parse(raw_json);
    } catch(nlohmann::json::parse_error &error) {
        output_error(json_path, error.what());
        return EXIT_FAILURE;
    }

    bool res;
    if(j.find("images") != j.end()) {
        for(auto &image : j.at("images")) {
            std::string out_path = if_path + "_" + image.at("name").get<std::string>() + ".installfile";
            std::ofstream output(out_path);
            res = parse_one_desc(image, output);
            if(!res) exit_code = EXIT_FAILURE;
        }
    } else {
        if(if_path == "-") {
            res = parse_one_desc(j, std::cout);
        } else {
            std::ofstream output(if_path);
            res = parse_one_desc(j, output);
        }
        if(!res) exit_code = EXIT_FAILURE;
    }

    return exit_code;
}