From a50dc31eaef7033444311f0917e8ffde373bea1b Mon Sep 17 00:00:00 2001
From: Erik Reid <erik.reid@geant.org>
Date: Sun, 23 Dec 2018 19:00:12 +0100
Subject: [PATCH] added validation to netconf input

---
 inventory_provider/netconf.py | 112 +++++++++++++++++++++++++++++++++-
 test/test_netconf_data.py     |   1 -
 2 files changed, 109 insertions(+), 4 deletions(-)

diff --git a/inventory_provider/netconf.py b/inventory_provider/netconf.py
index e20edbea..a93c693e 100644
--- a/inventory_provider/netconf.py
+++ b/inventory_provider/netconf.py
@@ -1,8 +1,94 @@
-# import os
-from jnpr.junos import Device
+import logging
 
+from jnpr.junos import Device
 from lxml import etree
 
+CONFIG_SCHEMA = """<?xml version="1.1" encoding="UTF-8" ?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+  <xs:complexType name="generic-sequence">
+    <xs:sequence>
+        <xs:any processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+    </xs:sequence>
+    <xs:anyAttribute processContents="skip" />
+  </xs:complexType>
+
+  <!-- NOTE: 'unit' content isn't validated -->  
+  <xs:complexType name="juniper-interface">
+    <xs:sequence>
+        <xs:choice minOccurs="1" maxOccurs="unbounded">
+          <xs:element name="name" minOccurs="1" maxOccurs="1" type="xs:string" />
+          <xs:element name="description" minOccurs="0" maxOccurs="1" type="xs:string" />
+          <xs:any processContents="lax" minOccurs="0" maxOccurs="unbounded" />
+        </xs:choice>
+    </xs:sequence>
+    <xs:attribute name="inactive" type="xs:string" />
+  </xs:complexType>
+
+  <xs:element name="configuration">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:choice minOccurs="1" maxOccurs="unbounded">
+          <xs:element name="version" minOccurs="0" type="xs:string" />
+          <xs:element name="groups" minOccurs="0" type="generic-sequence" />
+          <xs:element name="apply-groups" minOccurs="0" type="xs:string" />
+          <xs:element name="system" minOccurs="0" type="generic-sequence" />
+          <xs:element name="logical-systems" minOccurs="0" type="generic-sequence" />
+          <xs:element name="chassis" minOccurs="0" type="generic-sequence" />
+          <xs:element name="services" minOccurs="0" type="generic-sequence" />
+          <xs:element name="interfaces" minOccurs="0">
+            <xs:complexType>
+              <xs:sequence>
+                <xs:choice minOccurs="1" maxOccurs="unbounded">
+                    <xs:element name="apply-groups" minOccurs="0" type="xs:string" />
+                    <xs:element name="interface" minOccurs="1" maxOccurs="unbounded" type="juniper-interface" />
+                </xs:choice>
+              </xs:sequence>
+            </xs:complexType>
+          </xs:element>
+          <xs:element name="snmp" minOccurs="0" type="generic-sequence" />
+          <xs:element name="forwarding-options" minOccurs="0" type="generic-sequence" />
+          <xs:element name="routing-options" minOccurs="0" type="generic-sequence" />
+          <xs:element name="protocols" minOccurs="0" type="generic-sequence" />
+          <xs:element name="policy-options" minOccurs="0" type="generic-sequence" />
+          <xs:element name="class-of-service" minOccurs="0" type="generic-sequence" />
+          <xs:element name="firewall" minOccurs="0" type="generic-sequence" />
+          <xs:element name="routing-instances" minOccurs="0" type="generic-sequence" />
+          <xs:element name="bridge-domains" minOccurs="0" type="generic-sequence" />
+        </xs:choice>
+      </xs:sequence>
+      <xs:attribute name="changed-seconds" type="xs:string" />
+      <xs:attribute name="changed-localtime" type="xs:string" />
+    </xs:complexType>
+  </xs:element>
+
+</xs:schema>
+"""
+
+UNIT_SCHEMA = """<?xml version="1.1" encoding="UTF-8" ?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+  <xs:complexType name="generic-sequence">
+    <xs:sequence>
+        <xs:any processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+    </xs:sequence>
+    <xs:anyAttribute processContents="skip" />
+  </xs:complexType>
+
+  <xs:element name="unit">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element name="name" minOccurs="1" maxOccurs="1" type="xs:int" />
+        <xs:element name="description" minOccurs="0" maxOccurs="1" type="xs:string" />
+        <xs:any processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+      </xs:sequence>
+      <xs:attribute name="inactive" type="xs:string" />
+    </xs:complexType>
+  </xs:element>
+
+</xs:schema>
+"""
+
 
 def _rpc(hostname, ssh):
     dev = Device(
@@ -15,7 +101,27 @@ def _rpc(hostname, ssh):
 
 def load_config(hostname, ssh_params):
     # data = dev.rpc.get_config(options={'format': 'json'})
-    return _rpc(hostname, ssh_params).get_config()
+    config = _rpc(hostname, ssh_params).get_config()
+
+    def _validate(schema, doc):
+        if schema.validate(doc):
+            return
+        for e in schema.error_log:
+            logging.debug("%d.%d: %s" % (e.line, e.column, e.message))
+        assert False
+
+    schema_doc = etree.XML(CONFIG_SCHEMA.encode('utf-8'))
+    config_schema = etree.XMLSchema(schema_doc)
+    _validate(config_schema, config)
+
+    # validate interfaces/interface/unit elements ...
+    schema_doc = etree.XML(UNIT_SCHEMA.encode('utf-8'))
+    unit_schema = etree.XMLSchema(schema_doc)
+    for i in config.xpath('//configuration/interfaces/interface'):
+        for u in i.xpath('./unit'):
+            _validate(unit_schema, u)
+
+    return config
 
 
 def load_interfaces(hostname, ssh_params):
diff --git a/test/test_netconf_data.py b/test/test_netconf_data.py
index 6a20a272..5e628ce1 100644
--- a/test/test_netconf_data.py
+++ b/test/test_netconf_data.py
@@ -28,7 +28,6 @@ def test_query_doc_and_validate(mocker, data_config):
         'inventory_provider.netconf.Device',
         MockedJunosDevice)
 
-    etree.parse(SCHEMA_FILENAME)
     for r in data_config['routers']:
         doc = netconf.load_config(r['hostname'], data_config['ssh'])
 
-- 
GitLab