summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorA. Wilcox <AWilcox@Wilcox-Tech.com>2020-05-13 01:33:42 -0500
committerA. Wilcox <AWilcox@Wilcox-Tech.com>2020-05-13 01:33:42 -0500
commite45f6d79862f3eaff3f8bf92c7d3096be104eb49 (patch)
tree6f8d1b466c3c8e341ab2235efe1c2f8ee4caf7be /tools
parentb4861811d406318b26773523a0bf34119bc309e1 (diff)
downloadhorizon-e45f6d79862f3eaff3f8bf92c7d3096be104eb49.tar.gz
horizon-e45f6d79862f3eaff3f8bf92c7d3096be104eb49.tar.bz2
horizon-e45f6d79862f3eaff3f8bf92c7d3096be104eb49.tar.xz
horizon-e45f6d79862f3eaff3f8bf92c7d3096be104eb49.zip
tools: Add new hscript-fromjson tool
Diffstat (limited to 'tools')
-rw-r--r--tools/CMakeLists.txt1
-rw-r--r--tools/hscript-fromjson/CMakeLists.txt24
-rw-r--r--tools/hscript-fromjson/jsonconv.cc245
3 files changed, 270 insertions, 0 deletions
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 <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 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<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
+
+ 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<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(!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;
+}