From 1d9c703a8eebcf8f328e610879c7a1799258bba3 Mon Sep 17 00:00:00 2001
From: "A. Wilcox" <AWilcox@Wilcox-Tech.com>
Date: Sat, 4 Jul 2020 08:56:03 -0500
Subject: hscipt: Implement parse and validation of 'pppoe' key

---
 hscript/network.cc                              | 114 +++++++++++++++++++++++-
 hscript/network.hh                              |  25 ++++++
 hscript/script.cc                               |   3 +
 hscript/script_e.cc                             |   9 +-
 hscript/script_i.hh                             |  17 ++++
 hscript/script_v.cc                             |   4 +
 tests/fixtures/0241-pppoe-basic.installfile     |   6 ++
 tests/fixtures/0242-pppoe-auth.installfile      |   6 ++
 tests/fixtures/0243-pppoe-allkeys.installfile   |   6 ++
 tests/fixtures/0244-pppoe-invalid.installfile   |   6 ++
 tests/fixtures/0245-pppoe-valueless.installfile |   6 ++
 tests/spec/validator_spec.rb                    |  35 +++++++-
 12 files changed, 230 insertions(+), 7 deletions(-)
 create mode 100644 tests/fixtures/0241-pppoe-basic.installfile
 create mode 100644 tests/fixtures/0242-pppoe-auth.installfile
 create mode 100644 tests/fixtures/0243-pppoe-allkeys.installfile
 create mode 100644 tests/fixtures/0244-pppoe-invalid.installfile
 create mode 100644 tests/fixtures/0245-pppoe-valueless.installfile

diff --git a/hscript/network.cc b/hscript/network.cc
index 1e915b3..3d847a2 100644
--- a/hscript/network.cc
+++ b/hscript/network.cc
@@ -14,6 +14,7 @@
 #include <arpa/inet.h>          /* inet_pton */
 #include <cstring>              /* memcpy */
 #include <fstream>              /* ofstream for Net write */
+#include <set>                  /* for PPPoE valid param keys */
 #ifdef HAS_INSTALL_ENV
 #   include <linux/wireless.h>     /* struct iwreq */
 #   include <string.h>             /* strerror */
@@ -352,9 +353,7 @@ bool execute_address_eni(const NetAddress *addr) {
     return true;
 }
 
-bool NetAddress::execute() const {
-    output_info(pos, "netaddress: adding configuration for " + _iface);
-
+NetConfigType::ConfigSystem current_system(const Horizon::Script *script) {
     NetConfigType::ConfigSystem system = NetConfigType::Netifrc;
 
     const Key *key = script->getOneValue("netconfigtype");
@@ -363,7 +362,13 @@ bool NetAddress::execute() const {
         system = nct->type();
     }
 
-    switch(system) {
+    return system;
+}
+
+bool NetAddress::execute() const {
+    output_info(pos, "netaddress: adding configuration for " + _iface);
+
+    switch(current_system(script)) {
     case NetConfigType::Netifrc:
     default:
         return execute_address_netifrc(this);
@@ -373,6 +378,107 @@ bool NetAddress::execute() const {
 }
 
 
+Key *PPPoE::parseFromData(const std::string &data, const ScriptLocation &pos,
+                          int *errors, int *, const Script *script) {
+    std::string::size_type spos, next;
+    std::map<std::string, std::string> params;
+    std::string iface;
+
+    spos = data.find_first_of(' ');
+    iface = data.substr(0, spos);
+    if(iface.length() > IFNAMSIZ) {
+        if(errors) *errors += 1;
+        output_error(pos, "pppoe: invalid interface name '" + iface + "'",
+                     "interface names must be 16 characters or less");
+        return nullptr;
+    }
+
+    while(spos != std::string::npos) {
+        std::string key, val;
+        std::string::size_type equals;
+
+        spos++;
+
+        next = data.find_first_of(' ', spos);
+        equals = data.find_first_of('=', spos);
+        if(equals != std::string::npos && equals < next) {
+            key = data.substr(spos, equals - spos);
+            if(next == std::string::npos) {
+                val = data.substr(equals + 1);
+            } else {
+                val = data.substr(equals + 1, next - equals);
+            }
+        } else {
+            if(next == std::string::npos) {
+                key = data.substr(spos);
+            } else {
+                key = data.substr(spos, next - spos);
+            }
+        }
+
+        params[key] = val;
+
+        spos = next;
+    }
+
+    return new PPPoE(script, pos, iface, params);
+}
+
+bool PPPoE::validate() const {
+    bool valid = true;
+    const std::set<std::string> valid_keys = {"mtu", "username", "password",
+                                              "lcp-echo-interval",
+                                              "lcp-echo-failure"};
+
+    for(const auto &elem : this->params()) {
+        if(valid_keys.find(elem.first) == valid_keys.end()) {
+            output_error(this->pos, "pppoe: invalid parameter", elem.first);
+            valid = false;
+        }
+    }
+
+    return valid;
+}
+
+static int ppp_link_count = 0;
+
+bool execute_pppoe_netifrc(const PPPoE *link) {
+    std::ofstream ethconfig("/tmp/horizon/netifrc/config_" + link->iface(),
+                            std::ios_base::trunc);
+    if(!ethconfig) {
+        output_error(link->where(), "pppoe: couldn't write network "
+                     "configuration for " + link->iface());
+        return false;
+    }
+
+    ethconfig << "null";
+
+    std::string linkiface{"ppp" + std::to_string(ppp_link_count)};
+
+    std::ofstream config("/tmp/horizon/netifrc/config_" + linkiface);
+    if(!config) {
+        output_error(link->where(), "pppoe: couldn't write network "
+                     "configuration for " + linkiface);
+        return false;
+    }
+    config << "ppp";
+    return true;
+}
+
+bool PPPoE::execute() const {
+    output_info(pos, "pppoe: adding configuration for " + _iface);
+
+    switch(current_system(script)) {
+    case NetConfigType::Netifrc:
+    default:
+        return execute_pppoe_netifrc(this);
+    case NetConfigType::ENI:
+        /* eni */
+        return false;
+    }
+}
+
+
 Key *Nameserver::parseFromData(const std::string &data,
                                const ScriptLocation &pos, int *errors, int *,
                                const Script *script) {
diff --git a/hscript/network.hh b/hscript/network.hh
index 18ffc34..2ed40ca 100644
--- a/hscript/network.hh
+++ b/hscript/network.hh
@@ -13,6 +13,8 @@
 #ifndef __HSCRIPT_NETWORK_HH_
 #define __HSCRIPT_NETWORK_HH_
 
+#include <map>
+
 #include "key.hh"
 #include "util/output.hh"
 
@@ -93,6 +95,29 @@ public:
     bool execute() const override;
 };
 
+class PPPoE : public Key {
+private:
+    const std::string _iface;
+    const std::map<std::string, std::string> _params;
+
+    PPPoE(const Script *_sc, const ScriptLocation &_pos, const std::string &_i,
+          const std::map<std::string, std::string> &_p) : Key(_sc, _pos),
+        _iface(_i), _params(_p) {}
+public:
+    static Key *parseFromData(const std::string &, const ScriptLocation &,
+                              int*, int*, const Script *);
+
+    /*! Retrieve the interface to which this PPPoE link is associated. */
+    const std::string iface() const { return this->_iface; }
+    /*! Retrieve the parameters for this PPPoE link. */
+    const std::map<std::string, std::string> params() const {
+        return this->_params;
+    }
+
+    bool validate() const override;
+    bool execute() const override;
+};
+
 class Nameserver : public StringKey {
 private:
     Nameserver(const Script *_s, const ScriptLocation &_pos,
diff --git a/hscript/script.cc b/hscript/script.cc
index 897d5d4..20ccbe5 100644
--- a/hscript/script.cc
+++ b/hscript/script.cc
@@ -58,6 +58,7 @@ const std::map<std::string, key_parse_fn> valid_keys = {
     {"netaddress", &NetAddress::parseFromData},
     {"nameserver", &Nameserver::parseFromData},
     {"netssid", &NetSSID::parseFromData},
+    {"pppoe", &PPPoE::parseFromData},
 
     {"username", &Username::parseFromData},
     {"useralias", &UserAlias::parseFromData},
@@ -98,6 +99,8 @@ bool Script::ScriptPrivate::store_key(const std::string &key_name, Key *obj,
         std::unique_ptr<NetSSID> ssid(dynamic_cast<NetSSID *>(obj));
         this->ssids.push_back(std::move(ssid));
         return true;
+    } else if(key_name == "pppoe") {
+        return store_pppoe(obj, pos, errors, warnings, opts);
     } else if(key_name == "hostname") {
         return store_hostname(obj, pos, errors, warnings, opts);
     } else if(key_name == "pkginstall") {
diff --git a/hscript/script_e.cc b/hscript/script_e.cc
index 866a836..87ade0f 100644
--- a/hscript/script_e.cc
+++ b/hscript/script_e.cc
@@ -287,7 +287,7 @@ bool Script::execute() const {
     }
 
     bool dhcp = false;
-    if(!internal->addresses.empty()) {
+    if(!internal->addresses.empty() || !internal->pppoes.empty()) {
         fs::path conf_dir, targ_netconf_dir, targ_netconf_file;
         switch(netconfsys) {
         case NetConfigType::Netifrc:
@@ -319,6 +319,13 @@ bool Script::execute() const {
             if(addr->type() == NetAddress::DHCP) dhcp = true;
         }
 
+        int pppcnt = 0;
+        for(auto &ppp_link : internal->pppoes) {
+            EXECUTE_OR_FAIL("pppoe", ppp_link);
+            ifaces.insert(ppp_link->iface());
+            ifaces.insert("ppp" + std::to_string(pppcnt++));
+        }
+
         std::ostringstream conf;
 
         if(netconfsys == NetConfigType::ENI) {
diff --git a/hscript/script_i.hh b/hscript/script_i.hh
index abe56ec..b82b807 100644
--- a/hscript/script_i.hh
+++ b/hscript/script_i.hh
@@ -72,6 +72,8 @@ struct Script::ScriptPrivate {
     std::vector< std::unique_ptr<Nameserver> > nses;
     /*! Wireless networking configuration */
     std::vector< std::unique_ptr<NetSSID> > ssids;
+    /*! PPPoE configuration */
+    std::vector< std::unique_ptr<PPPoE> > pppoes;
 
     /*! APK repositories */
     std::vector< std::unique_ptr<Repository> > repos;
@@ -153,6 +155,21 @@ struct Script::ScriptPrivate {
         return true;
     }
 
+    bool store_pppoe(Key *obj, const ScriptLocation &pos, int *errors,
+                     int *, const ScriptOptions &) {
+        PPPoE *ppp = dynamic_cast<PPPoE *>(obj);
+        for(const auto &ppplink : pppoes) {
+            if(ppplink->iface() == ppp->iface()) {
+                DUPLICATE_ERROR(ppplink, "pppoe", ppplink->iface());
+                return false;
+            }
+        }
+
+        std::unique_ptr<PPPoE> uppp(ppp);
+        pppoes.push_back(std::move(uppp));
+        return true;
+    }
+
     bool store_hostname(Key* obj, const ScriptLocation &pos, int *errors,
                         int *, const ScriptOptions &) {
         if(hostname) {
diff --git a/hscript/script_v.cc b/hscript/script_v.cc
index 1fc3726..41f1756 100644
--- a/hscript/script_v.cc
+++ b/hscript/script_v.cc
@@ -252,6 +252,10 @@ bool Horizon::Script::validate() const {
         if(!ssid->validate()) failures++;
     }
 
+    for(auto &pppoe : internal->pppoes) {
+        if(!pppoe->validate()) failures++;
+    }
+
     /* REQ: Runner.Validate.hostname */
     if(!internal->hostname->validate()) failures++;
 
diff --git a/tests/fixtures/0241-pppoe-basic.installfile b/tests/fixtures/0241-pppoe-basic.installfile
new file mode 100644
index 0000000..9e7d4e5
--- /dev/null
+++ b/tests/fixtures/0241-pppoe-basic.installfile
@@ -0,0 +1,6 @@
+network false
+hostname test.machine
+pkginstall adelie-base
+rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/
+mount /dev/sda1 /
+pppoe eth0
diff --git a/tests/fixtures/0242-pppoe-auth.installfile b/tests/fixtures/0242-pppoe-auth.installfile
new file mode 100644
index 0000000..250e820
--- /dev/null
+++ b/tests/fixtures/0242-pppoe-auth.installfile
@@ -0,0 +1,6 @@
+network false
+hostname test.machine
+pkginstall adelie-base
+rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/
+mount /dev/sda1 /
+pppoe eth0 username=awilfox password=fuzzball
diff --git a/tests/fixtures/0243-pppoe-allkeys.installfile b/tests/fixtures/0243-pppoe-allkeys.installfile
new file mode 100644
index 0000000..6b70daa
--- /dev/null
+++ b/tests/fixtures/0243-pppoe-allkeys.installfile
@@ -0,0 +1,6 @@
+network false
+hostname test.machine
+pkginstall adelie-base
+rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/
+mount /dev/sda1 /
+pppoe eth0 username=awilfox password=fuzzball mtu=9001 lcp-echo-interval=10 lcp-echo-failure=5
diff --git a/tests/fixtures/0244-pppoe-invalid.installfile b/tests/fixtures/0244-pppoe-invalid.installfile
new file mode 100644
index 0000000..8fc2768
--- /dev/null
+++ b/tests/fixtures/0244-pppoe-invalid.installfile
@@ -0,0 +1,6 @@
+network false
+hostname test.machine
+pkginstall adelie-base
+rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/
+mount /dev/sda1 /
+pppoe eth0 cat=meow
diff --git a/tests/fixtures/0245-pppoe-valueless.installfile b/tests/fixtures/0245-pppoe-valueless.installfile
new file mode 100644
index 0000000..ee75450
--- /dev/null
+++ b/tests/fixtures/0245-pppoe-valueless.installfile
@@ -0,0 +1,6 @@
+network false
+hostname test.machine
+pkginstall adelie-base
+rootpw $6$gumtLGmHwOVIRpQR$2M9PUO24hy5mofzWWf9a.YLbzOgOlUby1g0hDj.wG67E2wrrvys59fq02PPdxBdbgkLZFtjfEx6MHZwMBamwu/
+mount /dev/sda1 /
+pppoe eth0 username password=fuzzball
diff --git a/tests/spec/validator_spec.rb b/tests/spec/validator_spec.rb
index 8fb6c95..d2d121a 100644
--- a/tests/spec/validator_spec.rb
+++ b/tests/spec/validator_spec.rb
@@ -278,6 +278,37 @@ RSpec.describe 'HorizonScript validation', :type => :aruba do
                     expect(last_command_started).to have_output(/error: .*nameserver.*valid IPv6/)
                 end
             end
+            context "for 'pppoe' key" do
+                it "succeeds with only an interface" do
+                    use_fixture '0241-pppoe-basic.installfile'
+                    run_validate
+                    expect(last_command_started).to have_output(PARSER_SUCCESS)
+                    expect(last_command_started).to have_output(VALIDATOR_SUCCESS)
+                end
+                it "succeeds with autnentication credentials" do
+                    use_fixture '0242-pppoe-auth.installfile'
+                    run_validate
+                    expect(last_command_started).to have_output(PARSER_SUCCESS)
+                    expect(last_command_started).to have_output(VALIDATOR_SUCCESS)
+                end
+                it "succeeds with all valid keys" do
+                    use_fixture '0243-pppoe-allkeys.installfile'
+                    run_validate
+                    expect(last_command_started).to have_output(PARSER_SUCCESS)
+                    expect(last_command_started).to have_output(VALIDATOR_SUCCESS)
+                end
+                it "fails with an invalid key" do
+                    use_fixture '0244-pppoe-invalid.installfile'
+                    run_validate
+                    expect(last_command_started).to have_output(/error: .*pppoe.*invalid/)
+                end
+                it "succeeds with a value-less key" do
+                    use_fixture '0245-pppoe-valueless.installfile'
+                    run_validate
+                    expect(last_command_started).to have_output(PARSER_SUCCESS)
+                    expect(last_command_started).to have_output(VALIDATOR_SUCCESS)
+                end
+            end
             context "for 'firmware' key" do
                 it "always supports 'false' value" do
                     use_fixture '0112-firmware-false.installfile'
@@ -687,12 +718,12 @@ RSpec.describe 'HorizonScript validation', :type => :aruba do
                     run_validate
                     expect(last_command_started).to have_output(/error: .*svcenable.*invalid/)
                 end
-		it "succeeds with a runlevel specified" do
+                it "succeeds with a runlevel specified" do
                     use_fixture '0239-svcenable-runlevel.installfile'
                     run_validate
                     expect(last_command_started).to have_output(PARSER_SUCCESS)
                     expect(last_command_started).to have_output(VALIDATOR_SUCCESS)
-		end
+                end
             end
             context "for 'diskid' key" do
                 it "succeeds with basic disk identification" do
-- 
cgit v1.2.3-70-g09d2