Merge "CI: re-enable ARM builds"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39043a0..448b094 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -49,7 +49,7 @@
 option(WITH_DOCS "Create and install internal documentation (needs Doxygen)" ${DOXYGEN_FOUND})
 
 find_package(docopt REQUIRED)
-find_package(Boost REQUIRED)
+find_package(Boost REQUIRED COMPONENTS filesystem)
 find_library(REPLXX_LIBRARY NAMES replxx replxx-d REQUIRED)
 find_path(REPLXX_PATH replxx.hxx)
 if("${REPLXX_PATH}" STREQUAL REPLXX_PATH-NOTFOUND)
@@ -61,9 +61,6 @@
 pkg_check_modules(SYSREPO REQUIRED sysrepo-cpp>=1.4.79 IMPORTED_TARGET sysrepo)
 pkg_check_modules(LIBNETCONF2 REQUIRED libnetconf2>=1.1.32 IMPORTED_TARGET libnetconf2)
 
-# we don't need filename tracking, and we prefer to use header-only Boost
-add_definitions(-DBOOST_SPIRIT_X3_NO_FILESYSTEM)
-
 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/)
 
 add_library(ast_values STATIC
@@ -165,6 +162,19 @@
     target_link_libraries(yang-cli stdc++fs)
 endif()
 
+add_executable(netconf-cli
+    src/cli.cpp
+    src/cli-netconf.cpp
+    )
+target_compile_definitions(netconf-cli PRIVATE NETCONF_CLI)
+
+# Boost.Process needs linking with threads, but doesn't have special CMake target which would do that for us. So, we
+# need to link manually. This will hopefully change in the future.
+# https://discourse.cmake.org/t/boost-process-target-doesnt-exist-for-thread-linking/2113
+set(THREADS_PREFER_PTHREAD_FLAG ON)
+find_package(Threads)
+target_link_libraries(netconf-cli netconfaccess Threads::Threads Boost::filesystem)
+cli_link_required(netconf-cli)
 
 
 include(CTest)
@@ -372,6 +382,7 @@
 endif()
 
 install(TARGETS
+    netconf-cli
     sysrepo-cli
     yang-cli
     RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/)
diff --git a/src/cli-netconf.cpp b/src/cli-netconf.cpp
new file mode 100644
index 0000000..4f9275a
--- /dev/null
+++ b/src/cli-netconf.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+
+#include <boost/fusion/adapted.hpp>
+#include <boost/spirit/home/x3.hpp>
+#include <optional>
+#include <stdexcept>
+#include <unistd.h>
+#include "cli-netconf.hpp"
+
+SshProcess sshProcess(const std::string& target, const std::string& port)
+{
+    namespace bp = boost::process;
+    bp::pipe in;
+    bp::pipe out;
+    auto sshPath = bp::search_path("ssh");
+    if (sshPath.empty()) {
+        throw std::runtime_error("ssh not found in PATH.");
+    }
+    if (target.front() == '@') {
+        throw std::runtime_error("Invalid username.");
+    }
+    bp::child ssh(sshPath,
+            target,
+            "-p",
+            port,
+            "-s",
+            "netconf",
+            bp::std_out > out, bp::std_in < in);
+
+    return {std::move(ssh), std::move(in), std::move(out)};
+}
diff --git a/src/cli-netconf.hpp b/src/cli-netconf.hpp
new file mode 100644
index 0000000..4849531
--- /dev/null
+++ b/src/cli-netconf.hpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+#include <boost/process.hpp>
+#include <string>
+
+struct SshProcess {
+    boost::process::child process;
+    boost::process::pipe std_in;
+    boost::process::pipe std_out;
+};
+SshProcess sshProcess(const std::string& target, const std::string& port);
diff --git a/src/cli.cpp b/src/cli.cpp
index 68d4a27..f89c8d6 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -13,9 +13,9 @@
 #include "NETCONF_CLI_VERSION.h"
 #include "interpreter.hpp"
 #include "proxy_datastore.hpp"
+#include "yang_schema.hpp"
 #if defined(SYSREPO_CLI)
 #include "sysrepo_access.hpp"
-#include "yang_schema.hpp"
 #define PROGRAM_NAME "sysrepo-cli"
 static const auto usage = R"(CLI interface to sysrepo
 
@@ -47,6 +47,22 @@
   -e <enable_features>  Feature to enable after modules are loaded. This option can be supplied more than once. Format: <module_name>:<feature>
   -i <data_file>        File to import data from
   --configonly          Disable editing of operational data)";
+#elif defined(NETCONF_CLI)
+// FIXME: improve usage
+static const auto usage = R"(CLI interface for NETCONF
+
+Usage:
+  netconf-cli [-v] [-p <port>] <host>
+  netconf-cli (-h | --help)
+  netconf-cli --version
+
+Options:
+  -v         enable verbose mode
+  -p <port>  port number [default: 830]
+)";
+#include "netconf_access.hpp"
+#include "cli-netconf.hpp"
+#define PROGRAM_NAME "netconf-access"
 #else
 #error "Unknown CLI backend"
 #endif
@@ -121,11 +137,27 @@
             datastore->addDataFile(dataFile);
         }
     }
+#elif defined(NETCONF_CLI)
+    auto verbose = args.at("-v").asBool();
+    if (verbose) {
+        NetconfAccess::setNcLogLevel(NC_VERB_DEBUG);
+    }
+
+    SshProcess process;
+    std::shared_ptr<NetconfAccess> datastore;
+
+    try {
+        process = sshProcess(args.at("<host>").asString(), args.at("-p").asString());
+        datastore = std::make_shared<NetconfAccess>(process.std_out.native_source(), process.std_in.native_sink());
+    } catch (std::runtime_error& ex) {
+        std::cerr << "SSH connection failed: " << ex.what() << "\n";
+        return 1;
+    }
 #else
 #error "Unknown CLI backend"
 #endif
 
-#if defined(SYSREPO_CLI)
+#if defined(SYSREPO_CLI) || defined(NETCONF_CLI)
     auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
         return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
     };
diff --git a/src/netconf-client.cpp b/src/netconf-client.cpp
index 1ff83de..42e9edc 100644
--- a/src/netconf-client.cpp
+++ b/src/netconf-client.cpp
@@ -20,6 +20,14 @@
 
 namespace impl {
 
+static client::LogCb logCallback;
+
+static void logViaCallback(NC_VERB_LEVEL level, const char* message)
+{
+    logCallback(level, message);
+}
+
+
 /** @short Initialization of the libnetconf2 library client
 
 Just a safe wrapper over nc_client_init and nc_client_destroy, really.
@@ -28,7 +36,6 @@
     ClientInit()
     {
         nc_client_init();
-        nc_verbosity(NC_VERB_DEBUG);
     }
 
     ~ClientInit()
@@ -178,6 +185,17 @@
 
 namespace client {
 
+void setLogLevel(NC_VERB_LEVEL level)
+{
+    nc_verbosity(level);
+}
+
+void setLogCallback(const client::LogCb& callback)
+{
+    impl::logCallback = callback;
+    nc_set_print_clb(impl::logViaCallback);
+}
+
 struct nc_session* Session::session_internal()
 {
     return m_session;
@@ -238,6 +256,17 @@
     return session;
 }
 
+std::unique_ptr<Session> Session::connectFd(const int source, const int sink)
+{
+    impl::ClientInit::instance();
+
+    auto session = std::make_unique<Session>(nc_connect_inout(source, sink, nullptr));
+    if (!session->m_session) {
+        throw std::runtime_error{"nc_connect_inout failed"};
+    }
+    return session;
+}
+
 std::unique_ptr<Session> Session::connectSocket(const std::string& path)
 {
     impl::ClientInit::instance();
diff --git a/src/netconf-client.hpp b/src/netconf-client.hpp
index db004e3..6bf4169 100644
--- a/src/netconf-client.hpp
+++ b/src/netconf-client.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <functional>
+#include <libnetconf2/log.h>
 #include <libnetconf2/messages_client.h>
 #include <memory>
 #include <optional>
@@ -25,6 +26,10 @@
 };
 
 using KbdInteractiveCb = std::function<std::string(const std::string&, const std::string&, const std::string&, bool)>;
+using LogCb = std::function<void(NC_VERB_LEVEL, const char*)>;
+
+void setLogLevel(NC_VERB_LEVEL level);
+void setLogCallback(const LogCb& callback);
 
 class Session {
 public:
@@ -33,6 +38,7 @@
     static std::unique_ptr<Session> connectPubkey(const std::string& host, const uint16_t port, const std::string& user, const std::string& pubPath, const std::string& privPath);
     static std::unique_ptr<Session> connectKbdInteractive(const std::string& host, const uint16_t port, const std::string& user, const KbdInteractiveCb& callback);
     static std::unique_ptr<Session> connectSocket(const std::string& path);
+    static std::unique_ptr<Session> connectFd(const int source, const int sink);
     [[nodiscard]] std::vector<std::string_view> capabilities() const;
     std::shared_ptr<libyang::Data_Node> getConfig(const NC_DATASTORE datastore,
                                                   const std::optional<const std::string> filter = std::nullopt); // TODO: arguments...
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index 4b151d3..307ccf1 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -33,6 +33,12 @@
 {
 }
 
+NetconfAccess::NetconfAccess(const int source, const int sink)
+    : m_session(libnetconf::client::Session::connectFd(source, sink))
+    , m_schema(std::make_shared<YangSchema>(m_session->libyangContext()))
+{
+}
+
 NetconfAccess::NetconfAccess(std::unique_ptr<libnetconf::client::Session>&& session)
     : m_session(std::move(session))
     , m_schema(std::make_shared<YangSchema>(m_session->libyangContext()))
@@ -45,6 +51,16 @@
 {
 }
 
+void NetconfAccess::setNcLogLevel(NC_VERB_LEVEL level)
+{
+    libnetconf::client::setLogLevel(level);
+}
+
+void NetconfAccess::setNcLogCallback(const LogCb& callback)
+{
+    libnetconf::client::setLogCallback(callback);
+}
+
 void NetconfAccess::setLeaf(const std::string& path, leaf_data_ value)
 {
     auto lyValue = value.type() == typeid(empty_) ? std::nullopt : std::optional(leafDataToString(value));
diff --git a/src/netconf_access.hpp b/src/netconf_access.hpp
index d6100a7..45973a6 100644
--- a/src/netconf_access.hpp
+++ b/src/netconf_access.hpp
@@ -7,6 +7,7 @@
 
 #pragma once
 
+#include <libnetconf2/log.h>
 #include <string>
 #include "datastore_access.hpp"
 
@@ -27,13 +28,19 @@
 class Schema;
 class YangSchema;
 
+using LogCb = std::function<void(NC_VERB_LEVEL, const char*)>;
+
 class NetconfAccess : public DatastoreAccess {
 public:
     NetconfAccess(const std::string& hostname, uint16_t port, const std::string& user, const std::string& pubKey, const std::string& privKey);
     NetconfAccess(const std::string& socketPath);
+    NetconfAccess(const int source, const int sink);
     NetconfAccess(std::unique_ptr<libnetconf::client::Session>&& session);
     ~NetconfAccess() override;
     [[nodiscard]] Tree getItems(const std::string& path) const override;
+
+    static void setNcLogLevel(NC_VERB_LEVEL level);
+    static void setNcLogCallback(const LogCb& callback);
     void setLeaf(const std::string& path, leaf_data_ value) override;
     void createItem(const std::string& path) override;
     void deleteItem(const std::string& path) override;