From e45f6d79862f3eaff3f8bf92c7d3096be104eb49 Mon Sep 17 00:00:00 2001 From: "A. Wilcox" Date: Wed, 13 May 2020 01:33:42 -0500 Subject: tools: Add new hscript-fromjson tool --- tools/CMakeLists.txt | 1 + tools/hscript-fromjson/CMakeLists.txt | 24 ++++ tools/hscript-fromjson/jsonconv.cc | 245 ++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 tools/hscript-fromjson/CMakeLists.txt create mode 100644 tools/hscript-fromjson/jsonconv.cc (limited to 'tools') diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 575403a..1b3803e 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -3,3 +3,4 @@ include_directories(${Boost_INCLUDE_DIR}) add_subdirectory(hscript-validate) add_subdirectory(hscript-simulate) +add_subdirectory(hscript-fromjson) diff --git a/tools/hscript-fromjson/CMakeLists.txt b/tools/hscript-fromjson/CMakeLists.txt new file mode 100644 index 0000000..54e1ef0 --- /dev/null +++ b/tools/hscript-fromjson/CMakeLists.txt @@ -0,0 +1,24 @@ +set(JSONCONV_SRCS + jsonconv.cc +) +add_executable(hscript-fromjson ${JSONCONV_SRCS}) +target_link_libraries(hscript-fromjson hscript ${FS_LIBRARY} ${Boost_LIBRARIES}) + +install(TARGETS hscript-fromjson DESTINATION bin) + +if("cxx_std_17" IN_LIST CMAKE_CXX_COMPILE_FEATURES) + set_property(TARGET hscript-fromjson PROPERTY CXX_STANDARD 17) +endif() + +#IF(RSPEC_EXECUTABLE) +#add_test(NAME "RSpecJSONConv" +# COMMAND ${RSPEC_EXECUTABLE} spec/jsonconv_spec.rb +# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests) +#set_property(TEST "RSpecJSONConv" +# PROPERTY ENVIRONMENT "PATH=$ENV{PATH}:${CMAKE_CURRENT_BINARY_DIR}") +#ENDIF(RSPEC_EXECUTABLE) +# +#IF(VALGRIND) +#add_test(NAME "ValgrindJSONConv" +# COMMAND ${VALGRIND_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/hscript-fromjson ${CMAKE_SOURCE_DIR}/tests/fixtures/json/0001-basic.json) +#ENDIF(VALGRIND) diff --git a/tools/hscript-fromjson/jsonconv.cc b/tools/hscript-fromjson/jsonconv.cc new file mode 100644 index 0000000..9ee1a86 --- /dev/null +++ b/tools/hscript-fromjson/jsonconv.cc @@ -0,0 +1,245 @@ +/* + * 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 +#include /* EXIT_* */ +#include +#include +#include +#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::endl; + out << "rootpw " << desc["rootpw"].get() << std::endl; + out << "mount " << desc["root"].get() << " /" << std::endl; + + if(desc.find("netaddresses") != desc.end()) { + out << "network false" << std::endl; + for(const auto &addr : desc["netaddresses"]) { + ENSURE_KEY(addr, "interface"); + ENSURE_KEY(addr, "addr-type"); + std::string addrtype = addr["addr-type"].get(); + out << "netaddress " << addr["interface"].get() + << " " << 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() + << " " << ipaddr["net-prefix"].get(); + if(ipaddr.find("gateway") != ipaddr.end()) { + out << " " << ipaddr["gateway"].get(); + } + } + out << std::endl; + } + } else { + out << "network false" << std::endl; + } + + std::ostringstream pkgs; + for(const auto &pkg : desc.at("packages")) { + pkgs << " " << pkg.get(); + } + + 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::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::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::endl;\ + }\ + } + + SIMPLE_PLURAL_KEY("nameservers", "nameserver"); + SIMPLE_PLURAL_KEY("repositories", "repository"); + SIMPLE_PLURAL_KEY("signingkeys", "signingkey"); + +#undef SIMPLE_PLURAL_KEY + + 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{}; + 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.") + ("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()->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(); + } + + if(!vm["jsonfile"].empty()) { + json_path = vm["jsonfile"].as(); + } else { + json_path = "-"; + } + + if(!force && fs::exists(if_path)) { + output_error("