mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
test: minor, cleanup & pep-8 fixes
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -5,19 +5,19 @@ import copy
|
||||
import infamy.usb as usb
|
||||
import time
|
||||
import infamy.netconf as netconf
|
||||
from infamy.util import until,wait_boot
|
||||
from infamy.util import until, wait_boot
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Initialize"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
available=usb.get_usb_ports(target)
|
||||
available = usb.get_usb_ports(target)
|
||||
|
||||
if len(available) < 1:
|
||||
test.skip()
|
||||
|
||||
with test.step("Unlock USB ports"):
|
||||
components=[]
|
||||
components = []
|
||||
for port in available:
|
||||
component = {
|
||||
"name": port,
|
||||
@@ -40,7 +40,7 @@ with infamy.Test() as test:
|
||||
|
||||
if len(available) > 1:
|
||||
with test.step("Lock one port"):
|
||||
components=[]
|
||||
components = []
|
||||
component = {
|
||||
"name": available[1],
|
||||
"class": "infix-hardware:usb",
|
||||
@@ -60,7 +60,7 @@ with infamy.Test() as test:
|
||||
until(lambda: usb.get_usb_state(target, available[0]) == "unlocked")
|
||||
|
||||
with test.step("Lock USB ports"):
|
||||
components=[]
|
||||
components = []
|
||||
for port in available:
|
||||
component = {
|
||||
"class": "infix-hardware:usb",
|
||||
@@ -80,6 +80,7 @@ with infamy.Test() as test:
|
||||
with test.step("Verify USB ports locked"):
|
||||
for port in available:
|
||||
until(lambda: usb.get_usb_state(target, port) == "locked")
|
||||
|
||||
with test.step("Remove all hardware configuration"):
|
||||
for port in available:
|
||||
target.delete_xpath(f"/ietf-hardware:hardware/component[name='{port}']")
|
||||
@@ -88,9 +89,8 @@ with infamy.Test() as test:
|
||||
for port in available:
|
||||
until(lambda: usb.get_usb_state(target, port) == "locked")
|
||||
|
||||
|
||||
with test.step("Unlock USB ports"):
|
||||
components=[]
|
||||
components = []
|
||||
for port in available:
|
||||
component = {
|
||||
"name": port,
|
||||
@@ -107,7 +107,7 @@ with infamy.Test() as test:
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Verify USB ports unlocked"):
|
||||
with test.step("Verify USB ports are unlocked"):
|
||||
for port in available:
|
||||
until(lambda: usb.get_usb_state(target, port) == "unlocked")
|
||||
|
||||
@@ -115,11 +115,12 @@ with infamy.Test() as test:
|
||||
target.startup_override()
|
||||
target.copy("running", "startup")
|
||||
target.reboot()
|
||||
if wait_boot(target) == False:
|
||||
if not wait_boot(target, env):
|
||||
test.fail()
|
||||
target = env.attach("target", "mgmt", test_reset=False)
|
||||
|
||||
with test.step("Verify that all ports are unlocked"):
|
||||
with test.step("Verify USB port remain unlocked after reboot"):
|
||||
for port in available:
|
||||
until(lambda: usb.get_usb_state(target, port) == "unlocked")
|
||||
|
||||
test.succeed()
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
#
|
||||
|
||||
import infamy
|
||||
from infamy.util import until
|
||||
from infamy.util import until
|
||||
|
||||
|
||||
def _verify(server):
|
||||
url = infamy.Furl(f"http://[{server}]:91/index.html")
|
||||
return url.check("It works")
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
NAME = "web"
|
||||
NAME = "web"
|
||||
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
|
||||
+3
-15
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import socket
|
||||
import time
|
||||
import requests
|
||||
import infamy, infamy.neigh
|
||||
import infamy
|
||||
import infamy.neigh
|
||||
import infamy.restconf as restconf
|
||||
import infamy.netconf as netconf
|
||||
import infamy.netconf as netconf
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
def ll6ping(node):
|
||||
neigh = None
|
||||
@@ -20,16 +18,6 @@ def ll6ping(node):
|
||||
|
||||
return None
|
||||
|
||||
def netconf_syn(neigh):
|
||||
try:
|
||||
ai = socket.getaddrinfo(neigh, 830, 0, 0, socket.SOL_TCP)
|
||||
sock = socket.socket(ai[0][0], ai[0][1], 0)
|
||||
sock.connect(ai[0][4])
|
||||
sock.close()
|
||||
print(f"{neigh} answers to TCP connections on port 830 (NETCONF)")
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Initialize"):
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import infamy
|
||||
from infamy.util import wait_boot
|
||||
import copy
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Initialize"):
|
||||
@@ -17,16 +16,17 @@ with infamy.Test() as test:
|
||||
})
|
||||
target.delete_xpath("/ietf-hardware:hardware/component")
|
||||
target.copy("running", "startup")
|
||||
|
||||
with test.step("Reboot and wait for the unit to come back"):
|
||||
target.startup_override()
|
||||
target.copy("running", "startup")
|
||||
target.reboot()
|
||||
if wait_boot(target) == False:
|
||||
if not wait_boot(target, env):
|
||||
test.fail()
|
||||
target = env.attach("target", "mgmt", test_default = False)
|
||||
target = env.attach("target", "mgmt", test_default=False)
|
||||
|
||||
with test.step("Verify hostname"):
|
||||
data = target.get_dict("/ietf-system:system/hostname")
|
||||
assert(data["system"]["hostname"] == "test")
|
||||
assert data["system"]["hostname"] == "test"
|
||||
|
||||
test.succeed()
|
||||
|
||||
+15
-11
@@ -1,5 +1,4 @@
|
||||
import argparse
|
||||
import networkx
|
||||
import os
|
||||
import pydot
|
||||
import shlex
|
||||
@@ -9,18 +8,21 @@ import inspect
|
||||
|
||||
from . import neigh, netconf, restconf, ssh, tap, topology
|
||||
|
||||
|
||||
class NullEnv:
|
||||
def attr(self, _, default=None):
|
||||
return default
|
||||
|
||||
|
||||
ENV = NullEnv()
|
||||
|
||||
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
def __init__(self, ltop):
|
||||
def __init__(self, top):
|
||||
super().__init__()
|
||||
|
||||
self.add_argument("-d", "--debug", default=False, action="store_true")
|
||||
self.add_argument("-l", "--logical-topology", dest="ltop", default=ltop)
|
||||
self.add_argument("-l", "--logical-topology", dest="ltop", default=top)
|
||||
self.add_argument("-p", "--package", default=None)
|
||||
self.add_argument("-y", "--yangdir", default=None)
|
||||
self.add_argument("-t", "--transport", default=None)
|
||||
@@ -80,13 +82,14 @@ class Env(object):
|
||||
mapping = None
|
||||
|
||||
if protocol is None:
|
||||
if not self.args.transport is None:
|
||||
protocol=self.args.transport
|
||||
if self.args.transport is not None:
|
||||
protocol = self.args.transport
|
||||
else:
|
||||
random.seed(f"{sys.argv[0]}-{os.environ.get('PYTHONHASHSEED',0)}")
|
||||
hseed = os.environ.get('PYTHONHASHSEED', 0)
|
||||
random.seed(f"{sys.argv[0]}-{hseed}")
|
||||
protocol = random.choice(["netconf", "restconf"])
|
||||
|
||||
password=self.get_password(node)
|
||||
password = self.get_password(node)
|
||||
ctrl = self.ptop.get_ctrl()
|
||||
cport, _ = self.ptop.get_mgmt_link(ctrl, node)
|
||||
|
||||
@@ -107,12 +110,13 @@ class Env(object):
|
||||
|
||||
if protocol == "ssh":
|
||||
return ssh.Device(ssh.Location(mgmtip, password))
|
||||
|
||||
if protocol == "restconf":
|
||||
dev = restconf.Device(location=restconf.Location(cport,
|
||||
mgmtip,
|
||||
password),
|
||||
mapping=mapping,
|
||||
yangdir=self.args.yangdir)
|
||||
mgmtip,
|
||||
password),
|
||||
mapping=mapping,
|
||||
yangdir=self.args.yangdir)
|
||||
if test_reset:
|
||||
dev.test_reset()
|
||||
return dev
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
def ll6ping(ifname, flags=["-w60", "-c1", "-L", "-n"]):
|
||||
argv = ["ping"] + flags + ["ff02::1%{}".format(ifname)]
|
||||
|
||||
@@ -13,6 +13,6 @@ def ll6ping(ifname, flags=["-w60", "-c1", "-L", "-n"]):
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
m = re.search("^\d+ bytes from ([:0-9a-f]+%\S+):", ping.stdout, re.MULTILINE)
|
||||
m = re.search(r"^\d+ bytes from ([:0-9a-f]+%\S+):",
|
||||
ping.stdout, re.MULTILINE)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
+17
-4
@@ -71,6 +71,7 @@ class Manager(netconf_client.ncclient.Manager):
|
||||
self.set_logger_level(logging.DEBUG)
|
||||
self.logger().addHandler(logging.StreamHandler(sys.stderr))
|
||||
|
||||
|
||||
class NccGetDataReply:
|
||||
"""Fold in to DataReply class when upstreaming"""
|
||||
def __init__(self, raw, ele):
|
||||
@@ -78,12 +79,14 @@ class NccGetDataReply:
|
||||
self.data_xml = lxml.etree.tostring(self.data_ele)
|
||||
self.raw_reply = raw
|
||||
|
||||
|
||||
class NccGetSchemaReply:
|
||||
def __init__(self, raw):
|
||||
self.ele = lxml.etree.fromstring(raw.xml.decode())
|
||||
self.ele = self.ele.find("{urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring}data")
|
||||
self.schema = self.ele.text
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
interface: str
|
||||
@@ -92,6 +95,7 @@ class Location:
|
||||
username: str = "admin"
|
||||
port: int = 830
|
||||
|
||||
|
||||
class Device(Transport):
|
||||
def __init__(self,
|
||||
location: Location,
|
||||
@@ -401,9 +405,18 @@ class Device(Transport):
|
||||
newd = mod.parse_data_dict(new, no_state=True)
|
||||
lyd = oldd.diff(newd)
|
||||
|
||||
return self.put_config(lyd.print_mem("xml", with_siblings=True, pretty=False))
|
||||
return self.put_config(lyd.print_mem("xml", with_siblings=True,
|
||||
pretty=False))
|
||||
|
||||
def get_current_time_with_offset(self):
|
||||
root = self.get_data("/ietf-system:system-state/clock", parse=False).data_ele
|
||||
current_datetime = root.find('.//{urn:ietf:params:xml:ns:yang:ietf-system}current-datetime').text
|
||||
return current_datetime
|
||||
"""
|
||||
Return current datetime with offset.
|
||||
|
||||
This method retrieves the current datetime from the raw data
|
||||
before it is passed through libyang. This is necessary because
|
||||
libyang "adjusts" the time for the offset, and we need the
|
||||
unadjusted time.
|
||||
"""
|
||||
data = self.get_data("/ietf-system:system-state/clock", parse=False)
|
||||
xpath = './/{urn:ietf:params:xml:ns:yang:ietf-system}current-datetime'
|
||||
return data.data_ele.find(xpath).text
|
||||
|
||||
+143
-104
@@ -5,14 +5,11 @@ import os
|
||||
import sys
|
||||
import libyang
|
||||
import re
|
||||
import urllib.parse
|
||||
import time
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from urllib.parse import quote
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
from dataclasses import dataclass
|
||||
from lxml import etree
|
||||
from infamy.transport import Transport
|
||||
|
||||
# We know we have a self-signed certificate, silence warning about it
|
||||
@@ -45,108 +42,121 @@ def xpath_to_uri(xpath, extra=None):
|
||||
|
||||
return uri_path
|
||||
|
||||
|
||||
# Workaround for bug in requests 2.32.x: https://github.com/psf/requests/issues/6735
|
||||
def requests_workaround(method, url, json, headers, auth, verify=False, retry=0):
|
||||
# Create a session
|
||||
session=requests.Session()
|
||||
session = requests.Session()
|
||||
|
||||
# Prepare the request
|
||||
request=requests.Request(method, url, json=json, headers=headers, auth=auth)
|
||||
prepared_request=session.prepare_request(request)
|
||||
prepared_request.url=prepared_request.url.replace('%25', '%')
|
||||
response=session.send(prepared_request, verify=verify)
|
||||
request = requests.Request(method, url, json=json, headers=headers,
|
||||
auth=auth)
|
||||
prepared_request = session.prepare_request(request)
|
||||
prepared_request.url = prepared_request.url.replace('%25', '%')
|
||||
response = session.send(prepared_request, verify=verify)
|
||||
try:
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
# Raise exceptions for HTTP errors
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
if e.response.status_code == 502 and retry < 10: # most likely caused by nginx up, but not yet rousette
|
||||
retry=retry+1
|
||||
# most likely caused by nginx up, but not yet rousette
|
||||
if e.response.status_code == 502 and retry < 10:
|
||||
retry = retry + 1
|
||||
print(f"{method} {url}: HTTP error 502, retrying({retry})")
|
||||
time.sleep(1)
|
||||
response=requests_workaround(method, url, json, headers, auth, verify, retry)
|
||||
response = requests_workaround(method, url, json, headers, auth,
|
||||
verify, retry)
|
||||
else:
|
||||
raise e
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def requests_workaround_put(url, json, headers, auth, verify=False):
|
||||
return requests_workaround('PUT', url, json, headers, auth, verify=False)
|
||||
|
||||
|
||||
def requests_workaround_delete(url, headers, auth, verify=False):
|
||||
return requests_workaround('DELETE', url, None, headers, auth, verify=False)
|
||||
|
||||
|
||||
def requests_workaround_post(url, json, headers, auth, verify=False):
|
||||
return requests_workaround('POST', url, json, headers, auth, verify=False)
|
||||
|
||||
|
||||
def requests_workaround_get(url, headers, auth, verify=False):
|
||||
return requests_workaround('GET', url, None, headers, auth, verify=False)
|
||||
|
||||
|
||||
def restconf_reachable(neigh, password):
|
||||
try:
|
||||
headers={
|
||||
headers = {
|
||||
'Content-Type': 'application/yang-data+json',
|
||||
'Accept': 'application/yang-data+json'
|
||||
}
|
||||
url=f"https://[{neigh}]/restconf/data/ietf-system:system/hostname"
|
||||
auth=HTTPBasicAuth("admin", password)
|
||||
url = f"https://[{neigh}]/restconf/data/ietf-system:system/hostname"
|
||||
auth = HTTPBasicAuth("admin", password)
|
||||
|
||||
|
||||
response=requests_workaround_get(url, headers=headers, auth=auth, verify=False)
|
||||
if response.status_code==200:
|
||||
response = requests_workaround_get(url, headers=headers, auth=auth,
|
||||
verify=False)
|
||||
if response.status_code == 200:
|
||||
print(f"{neigh} answers to TCP connections on port 443 (RESTCONF)")
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Device(Transport):
|
||||
def __init__(self,
|
||||
location: Location,
|
||||
mapping: dict,
|
||||
yangdir: None | str = None):
|
||||
print("Testing using RESTCONF")
|
||||
self.location=location
|
||||
self.url_base=f"https://[{location.host}]:{location.port}"
|
||||
self.restconf_url=f"{self.url_base}/restconf"
|
||||
self.yang_url=f"{self.url_base}/yang"
|
||||
self.rpc_url=f"{self.url_base}/restconf/operations"
|
||||
self.headers={
|
||||
self.location = location
|
||||
self.url_base = f"https://[{location.host}]:{location.port}"
|
||||
self.restconf_url = f"{self.url_base}/restconf"
|
||||
self.yang_url = f"{self.url_base}/yang"
|
||||
self.rpc_url = f"{self.url_base}/restconf/operations"
|
||||
self.headers = {
|
||||
'Content-Type': 'application/yang-data+json',
|
||||
'Accept': 'application/yang-data+json'
|
||||
}
|
||||
self.auth=HTTPBasicAuth(location.username, location.password)
|
||||
self.modules={}
|
||||
self.auth = HTTPBasicAuth(location.username, location.password)
|
||||
self.modules = {}
|
||||
|
||||
self.lyctx=libyang.Context(yangdir)
|
||||
self.lyctx = libyang.Context(yangdir)
|
||||
self._ly_bootstrap(yangdir)
|
||||
self._ly_init(yangdir)
|
||||
|
||||
def get_schemas_list(self):
|
||||
data=self.get_operational("/ietf-yang-library:modules-state").print_dict()
|
||||
modules = self.get_operational("/ietf-yang-library:modules-state")
|
||||
data = modules.print_dict()
|
||||
return data["modules-state"]["module"]
|
||||
|
||||
def get_schema(self, name, revision, yangdir):
|
||||
schema_name=f"{name}@{revision}.yang"
|
||||
url=f"{self.yang_url}/{schema_name}"
|
||||
data=self._get_raw(url=url, parse=False)
|
||||
schema_name = f"{name}@{revision}.yang"
|
||||
url = f"{self.yang_url}/{schema_name}"
|
||||
data = self._get_raw(url=url, parse=False)
|
||||
|
||||
sys.stdout.write(f"Downloading YANG model {schema_name} ...\r\033[K")
|
||||
data=data.decode('utf-8')
|
||||
data = data.decode('utf-8')
|
||||
with open(f"{yangdir}/{schema_name}", 'w') as json_file:
|
||||
json_file.write(data)
|
||||
|
||||
def schema_exist(self, name, revision, yangdir):
|
||||
schema_name=f"{name}@{revision}.yang"
|
||||
schema_path=f"{yangdir}/{schema_name}"
|
||||
schema_name = f"{name}@{revision}.yang"
|
||||
schema_path = f"{yangdir}/{schema_name}"
|
||||
return os.path.exists(schema_path)
|
||||
|
||||
def _ly_bootstrap(self, yangdir):
|
||||
schemas=self.get_schemas_list()
|
||||
schemas = self.get_schemas_list()
|
||||
for schema in schemas:
|
||||
if not self.schema_exist(schema["name"], schema["revision"],yangdir):
|
||||
if not self.schema_exist(schema["name"], schema["revision"], yangdir):
|
||||
self.get_schema(schema["name"], schema["revision"], yangdir)
|
||||
if schema.get("submodule"):
|
||||
for submodule in schema["submodule"]:
|
||||
if not self.schema_exist(submodule["name"], submodule["revision"],yangdir):
|
||||
if not self.schema_exist(submodule["name"], submodule["revision"], yangdir):
|
||||
self.get_schema(submodule["name"], submodule["revision"], yangdir)
|
||||
|
||||
if not any("submodule" in x and schema["name"] in x["submodule"] for x in schemas):
|
||||
@@ -155,14 +165,11 @@ class Device(Transport):
|
||||
print("YANG models downloaded.")
|
||||
|
||||
def _ly_init(self, yangdir):
|
||||
lib=self.lyctx.load_module("ietf-yang-library")
|
||||
ns=libyang.util.c2str(lib.cdata.ns)
|
||||
|
||||
for ms in self.modules.values():
|
||||
if ms["conformance-type"] != "implement":
|
||||
continue
|
||||
|
||||
mod=self.lyctx.load_module(ms["name"])
|
||||
mod = self.lyctx.load_module(ms["name"])
|
||||
|
||||
# TODO: ms["feature"] contains the list of enabled
|
||||
# features, so ideally we should only enable the supported"
|
||||
@@ -172,21 +179,24 @@ class Device(Transport):
|
||||
|
||||
def _get_raw(self, url, parse=True):
|
||||
"""Actually send a GET to RESTCONF server"""
|
||||
response=requests_workaround_get(url, headers=self.headers, auth=self.auth, verify=False)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
response = requests_workaround_get(url, headers=self.headers,
|
||||
auth=self.auth, verify=False)
|
||||
# Raise exceptions for HTTP errors
|
||||
response.raise_for_status()
|
||||
if parse:
|
||||
data=response.json()
|
||||
data=self.lyctx.parse_data_mem(json.dumps(data), "json", parse_only=True)
|
||||
data = response.json()
|
||||
data = self.lyctx.parse_data_mem(json.dumps(data), "json",
|
||||
parse_only=True)
|
||||
return data
|
||||
else:
|
||||
return response.content
|
||||
return response.content
|
||||
|
||||
def get_datastore(self, datastore="operational" , path="", parse=True):
|
||||
def get_datastore(self, datastore="operational", path="", parse=True):
|
||||
"""Get a datastore"""
|
||||
dspath=f"/ds/ietf-datastores:{datastore}"
|
||||
if not path is None:
|
||||
dspath=f"{dspath}/{path}"
|
||||
url=f"{self.restconf_url}{dspath}"
|
||||
dspath = f"/ds/ietf-datastores:{datastore}"
|
||||
if path is not None:
|
||||
dspath = f"{dspath}/{path}"
|
||||
|
||||
url = f"{self.restconf_url}{dspath}"
|
||||
return self._get_raw(url, parse)
|
||||
|
||||
def get_running(self, path=None):
|
||||
@@ -203,58 +213,70 @@ class Device(Transport):
|
||||
|
||||
def post_datastore(self, datastore, data):
|
||||
"""Actually send a POST to RESTCONF server"""
|
||||
url=f"{self.restconf_url}/ds/ietf-datastores:{datastore}"
|
||||
response=requests_workaround_post(url,
|
||||
json=data, # Directly pass the dictionary without using json.dumps
|
||||
headers=self.headers,
|
||||
auth=self.auth,
|
||||
verify=False
|
||||
)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
url = f"{self.restconf_url}/ds/ietf-datastores:{datastore}"
|
||||
# Directly pass the dictionary without using json.dumps
|
||||
response = requests_workaround_post(url,
|
||||
json=data,
|
||||
headers=self.headers,
|
||||
auth=self.auth,
|
||||
verify=False)
|
||||
# Raise exceptions for HTTP errors
|
||||
response.raise_for_status()
|
||||
|
||||
def put_datastore(self, datastore, data):
|
||||
"""Actually send a PUT to RESTCONF server"""
|
||||
response=requests_workaround_put(f"{self.restconf_url}/ds/ietf-datastores:{datastore}/",
|
||||
json=data, # Directly pass the dictionary without using json.dumps
|
||||
|
||||
# Directly pass the dictionary without using json.dumps
|
||||
response = requests_workaround_put(
|
||||
f"{self.restconf_url}/ds/ietf-datastores:{datastore}/",
|
||||
json=data,
|
||||
headers=self.headers,
|
||||
auth=self.auth,
|
||||
verify=False
|
||||
)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
|
||||
# Raise exceptions for HTTP errors
|
||||
response.raise_for_status()
|
||||
|
||||
def get_config_dict(self, modname):
|
||||
"""Get all configuration for module @modname as dictionary"""
|
||||
ds=self.get_running(modname)
|
||||
ds=json.loads(ds.print_mem("json", with_siblings=True, pretty=False))
|
||||
model, container=modname.split(":")
|
||||
ds = self.get_running(modname)
|
||||
ds = json.loads(ds.print_mem("json", with_siblings=True, pretty=False))
|
||||
model, container = modname.split(":")
|
||||
for k, v in ds.items():
|
||||
return {container: v}
|
||||
|
||||
def put_config_dict(self, modname, edit):
|
||||
"""Add @edit to running config and put the whole configuration"""
|
||||
# This is hacky, refactor when rousette have PATCH support.
|
||||
running=self.get_running()
|
||||
mod=self.lyctx.get_module(modname)
|
||||
|
||||
for k, v in edit.items():
|
||||
module=modname+":"+k
|
||||
data=v
|
||||
# This is hacky, refactor when rousette have PATCH support.
|
||||
running = self.get_running()
|
||||
mod = self.lyctx.get_module(modname)
|
||||
|
||||
for k, _ in edit.items():
|
||||
module = modname + ":" + k
|
||||
break
|
||||
|
||||
# Ugly hack, but this function should be refactored when patch is avaible in rousette anyway.
|
||||
rundict = json.loads(running.print_mem("json", with_siblings=True, pretty=False))
|
||||
# Ugly hack, but this function should be refactored when patch
|
||||
# is available in rousette anyway.
|
||||
rundict = json.loads(running.print_mem("json", with_siblings=True,
|
||||
pretty=False))
|
||||
if rundict.get(module) is None:
|
||||
rundict[module] = {}
|
||||
running=self.lyctx.parse_data_mem(json.dumps(rundict), "json", parse_only=True)
|
||||
running = self.lyctx.parse_data_mem(json.dumps(rundict), "json",
|
||||
parse_only=True)
|
||||
|
||||
change=mod.parse_data_dict(edit, no_state=True, validate=False)
|
||||
change = mod.parse_data_dict(edit, no_state=True, validate=False)
|
||||
running.merge_module(change)
|
||||
return self.put_datastore("running", json.loads(running.print_mem("json", with_siblings=True, pretty=False)))
|
||||
data = json.loads(running.print_mem("json", with_siblings=True,
|
||||
pretty=False))
|
||||
|
||||
return self.put_datastore("running", data)
|
||||
|
||||
def call_rpc(self, rpc):
|
||||
url=f"{self.rpc_url}/{rpc}"
|
||||
"""Actually send a POST to RESTCONF server"""
|
||||
response=requests_workaround_post(
|
||||
url = f"{self.rpc_url}/{rpc}"
|
||||
response = requests_workaround_post(
|
||||
url,
|
||||
json=None,
|
||||
headers=self.headers,
|
||||
@@ -269,62 +291,79 @@ class Device(Transport):
|
||||
|
||||
def get_data(self, xpath=None, parse=True):
|
||||
"""Get operational data"""
|
||||
uri=xpath_to_uri(xpath) if xpath is not None else None
|
||||
data=self.get_operational(uri, parse)
|
||||
if parse==False:
|
||||
uri = xpath_to_uri(xpath) if xpath is not None else None
|
||||
data = self.get_operational(uri, parse)
|
||||
if parse is False:
|
||||
return data
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
data=json.loads(data.print_mem("json", with_siblings=True, pretty=False))
|
||||
|
||||
for k,v in data.items():
|
||||
model, container=k.split(":")
|
||||
data = json.loads(data.print_mem("json", with_siblings=True,
|
||||
pretty=False))
|
||||
for k, v in data.items():
|
||||
model, container = k.split(":")
|
||||
break
|
||||
|
||||
return {container: v}
|
||||
|
||||
|
||||
def copy(self, source, target):
|
||||
factory=self.get_datastore(source)
|
||||
data=factory.print_mem("json", with_siblings=True, pretty=False)
|
||||
factory = self.get_datastore(source)
|
||||
data = factory.print_mem("json", with_siblings=True, pretty=False)
|
||||
self.put_datastore(target, json.loads(data))
|
||||
|
||||
def reboot(self):
|
||||
self.call_rpc("ietf-system:system-restart")
|
||||
|
||||
def call_action(self, xpath):
|
||||
path=xpath_to_uri(xpath)
|
||||
url=f"{self.restconf_url}/data{path}"
|
||||
response=requests_workaround_post(
|
||||
path = xpath_to_uri(xpath)
|
||||
url = f"{self.restconf_url}/data{path}"
|
||||
response = requests_workaround_post(
|
||||
url,
|
||||
json=None,
|
||||
headers=self.headers,
|
||||
auth=self.auth,
|
||||
verify=False)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
verify=False
|
||||
)
|
||||
|
||||
# Raise exceptions for HTTP errors
|
||||
response.raise_for_status()
|
||||
|
||||
return response.content
|
||||
|
||||
def get_current_time_with_offset(self):
|
||||
"""Parse the time in the raw reply, before it has been passed through libyang, there all offset is lost"""
|
||||
data=self.get_data("/ietf-system:system-state/clock", parse=False)
|
||||
data=json.loads(data)
|
||||
"""
|
||||
Return current datetime with offset.
|
||||
|
||||
This method retrieves the current datetime from the raw data
|
||||
before it is passed through libyang. This is necessary because
|
||||
libyang "adjusts" the time for the offset, and we need the
|
||||
unadjusted time.
|
||||
"""
|
||||
data = self.get_data("/ietf-system:system-state/clock", parse=False)
|
||||
data = json.loads(data)
|
||||
return data["ietf-system:system-state"]["clock"]["current-datetime"]
|
||||
|
||||
def get_iface(self, iface):
|
||||
"""Fetch target dict for iface and extract param from JSON"""
|
||||
content=self.get_data(f"/ietf-interfaces:interfaces/interface={iface}")
|
||||
interface=content.get("interfaces", {}).get("interface", None)
|
||||
xpath = f"/ietf-interfaces:interfaces/interface={iface}"
|
||||
content = self.get_data(xpath)
|
||||
interface = content.get("interfaces", {}).get("interface", None)
|
||||
if interface is None:
|
||||
return None
|
||||
|
||||
# Restconf (rousette) address by id and netconf (netopper2) address by name
|
||||
# RESTCONF (at least rousette) addresses by id, but NETCONF (at
|
||||
# least netopper2) addresses by name
|
||||
return interface[0]
|
||||
|
||||
def delete_xpath(self, xpath):
|
||||
"""Delete XPath from running config"""
|
||||
path=f"/ds/ietf-datastores:running/{xpath_to_uri(xpath)}"
|
||||
url=f"{self.restconf_url}{path}"
|
||||
response=requests_workaround_delete(url, headers=self.headers, auth=self.auth, verify=False)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
path = f"/ds/ietf-datastores:running/{xpath_to_uri(xpath)}"
|
||||
url = f"{self.restconf_url}{path}"
|
||||
response = requests_workaround_delete(url, headers=self.headers,
|
||||
auth=self.auth, verify=False)
|
||||
|
||||
# Raise exceptions for HTTP errors
|
||||
response.raise_for_status()
|
||||
|
||||
return True
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ class Device(object):
|
||||
return None
|
||||
|
||||
args = list(args)
|
||||
if type(args[0]) == str:
|
||||
if type(args[0]) is str:
|
||||
if kwargs.get("shell"):
|
||||
args[0] = ["/bin/sh", "-c", args[0]]
|
||||
kwargs["shell"] = False
|
||||
|
||||
@@ -85,16 +85,23 @@ class Test:
|
||||
def fail(self):
|
||||
raise TestFail()
|
||||
|
||||
|
||||
class TestResult(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TestPass(TestResult):
|
||||
pass
|
||||
|
||||
|
||||
class TestFail(TestResult):
|
||||
pass
|
||||
|
||||
|
||||
class TestSkip(TestResult):
|
||||
pass
|
||||
|
||||
|
||||
class CommentWriter:
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
+12
-6
@@ -1,8 +1,9 @@
|
||||
import networkx as nx
|
||||
from networkx.algorithms import isomorphism
|
||||
|
||||
|
||||
def _qstrip(text):
|
||||
if text == None:
|
||||
if text is None:
|
||||
return None
|
||||
|
||||
if text.startswith("\"") and text.endswith("\""):
|
||||
@@ -10,6 +11,7 @@ def _qstrip(text):
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def map_edges(les, pes):
|
||||
acc = []
|
||||
les = sorted(list(les.values()), key=lambda x: x.get("kind", ""), reverse=True)
|
||||
@@ -23,11 +25,14 @@ def map_edges(les, pes):
|
||||
|
||||
return acc
|
||||
|
||||
|
||||
def match_node(pn, ln):
|
||||
return pn.get("kind") == ln.get("kind")
|
||||
|
||||
|
||||
def match_edge(pes, les):
|
||||
return map_edges(les, pes) != None
|
||||
return map_edges(les, pes) is not None
|
||||
|
||||
|
||||
class Topology:
|
||||
def __init__(self, dotg):
|
||||
@@ -47,7 +52,7 @@ class Topology:
|
||||
sn, sp = e.get_source().split(":")
|
||||
dn, dp = e.get_destination().split(":")
|
||||
|
||||
attrs = { _qstrip(k): _qstrip(v) for k, v in e.get_attributes().items() }
|
||||
attrs = {_qstrip(k): _qstrip(v) for k, v in e.get_attributes().items()}
|
||||
attrs[sn] = sp
|
||||
attrs[dn] = dp
|
||||
self.g.add_edge(sn, dn, **attrs)
|
||||
@@ -95,7 +100,7 @@ class Topology:
|
||||
return True
|
||||
|
||||
def xlate(self, lnode, lport=None):
|
||||
assert(self.mapping)
|
||||
assert self.mapping
|
||||
|
||||
if lnode not in self.mapping:
|
||||
return None
|
||||
@@ -121,7 +126,7 @@ class Topology:
|
||||
def get_password(self, node):
|
||||
n = self.dotg.get_node(node)
|
||||
b = n[0] if n else {}
|
||||
password=b.get("password")
|
||||
password = b.get("password")
|
||||
|
||||
return qstrip(password) if password is not None else "admin"
|
||||
|
||||
@@ -138,7 +143,7 @@ class Topology:
|
||||
|
||||
def get_ctrl(self):
|
||||
ns = self.get_nodes(lambda _, attrs: attrs.get("kind") == "controller")
|
||||
assert(len(ns) == 1)
|
||||
assert len(ns) == 1
|
||||
return ns[0]
|
||||
|
||||
def get_infixen(self):
|
||||
@@ -147,6 +152,7 @@ class Topology:
|
||||
def get_attr(self, name, default=None):
|
||||
return _qstrip(self.dotg.get_attributes().get(name, default))
|
||||
|
||||
|
||||
# Support calling this script like so...
|
||||
#
|
||||
# python3 topology.py <physical> <logical>
|
||||
|
||||
@@ -1,39 +1,50 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from infamy.neigh import ll6ping
|
||||
|
||||
|
||||
class Transport(ABC):
|
||||
"""Common functions for NETCONF/RESTCONF"""
|
||||
|
||||
@abstractmethod
|
||||
def get_data(self, xpath=None, parse=True):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_config_dict(self, modname):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def put_config_dict(self, modname, edit):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_dict(self, xpath=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_xpath(self, xpath):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def copy(self, source, target):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reboot(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_time_with_offset(self):
|
||||
# This is needed since libyang is too nice and removes the original offset
|
||||
"""Needed since libyang is too nice and removes the original offset"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def call_action(self, xpath):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_iface(self, iface): # Should be common, but is not due to bug in rousette
|
||||
def get_iface(self, iface):
|
||||
"""Should be common, but is not due to bug in rousette"""
|
||||
pass
|
||||
|
||||
def get_mgmt_ip(self):
|
||||
|
||||
Reference in New Issue
Block a user