Add get_current_time_with_offset to netconf/restconf

This allows the timezone tests to run on restconf as well
This commit is contained in:
Mattias Walström
2024-06-27 15:43:06 +02:00
parent 29f56eb384
commit dd0f1610ab
6 changed files with 82 additions and 73 deletions
+1 -2
View File
@@ -19,8 +19,7 @@ with infamy.Test() as test:
})
with test.step("Verify current time."):
root = target.get_dict("/ietf-system:system-state/clock",as_xml=True)
current_datetime = root.find('.//{urn:ietf:params:xml:ns:yang:ietf-system}current-datetime').text
current_datetime=target.get_current_time_with_offset()
offset=current_datetime[-6:]
assert(offset == "+08:00")
+1 -2
View File
@@ -18,8 +18,7 @@ with infamy.Test() as test:
})
with test.step("Verify current time."):
root = target.get_dict("/ietf-system:system-state/clock",as_xml=True)
current_datetime = root.find('.//{urn:ietf:params:xml:ns:yang:ietf-system}current-datetime').text
current_datetime=target.get_current_time_with_offset()
offset=current_datetime[-6:]
assert(offset == "+12:00")
+1 -1
View File
@@ -11,6 +11,6 @@ with infamy.Test() as test:
target = env.attach("target", "mgmt")
with test.step("Get all Operational data"):
target.get_data(as_xml=True)
target.get_data(parse=False)
test.succeed()
+16 -12
View File
@@ -233,7 +233,7 @@ class Device(Transport):
"system-restart": {}
})
def _get(self, xpath, getter, as_xml=False):
def _get(self, xpath, getter, parse=True):
# Figure out which modules we are referencing
mods = self._modules_in_xpath(xpath)
@@ -243,17 +243,17 @@ class Device(Transport):
filt = f"<filter type=\"xpath\" select=\"{xpath}\" {xmlns} />"
# pylint: disable=c-extension-no-member
data = getter(filter=filt).data_ele[0]
if as_xml:
if parse==False:
return data
cfg = lxml.etree.tostring(data)
return self.ly.parse_data_mem(cfg, "xml", parse_only=True)
def _get_data(self, xpath, as_xml=False):
def _get_data(self, xpath, parse=True):
"""Local member wrapper for netconf-client <get-data> RPC"""
# pylint: disable=protected-access
(raw, ele) = self.ncc._send_rpc(self._ncc_get_data_rpc(filter=xpath))
data = NccGetDataReply(raw, ele)
if(as_xml):
if(parse==False):
return data
if len(data.data_ele) == 0:
@@ -263,14 +263,12 @@ class Device(Transport):
cfg = lxml.etree.tostring(data.data_ele[0])
return self.ly.parse_data_mem(cfg, "xml", parse_only=True)
def get(self, xpath, as_xml=False):
def get(self, xpath, parse=True):
"""RPC <get> (legacy NETCONF) fetches config:false data"""
return self._get(xpath, self.ncc.get, as_xml)
return self._get(xpath, self.ncc.get, parse)
def get_dict(self, xpath, as_xml=False):
def get_dict(self, xpath):
"""Return Python dictionary of <get> RPC data"""
if as_xml:
return self.get(xpath, as_xml=True)
data = self.get(xpath)
if not data:
@@ -278,10 +276,11 @@ class Device(Transport):
return data.print_dict()
def get_data(self, xpath=None, as_xml=False):
def get_data(self, xpath=None, parse=True):
"""RPC <get-data> to fetch operational data"""
if as_xml:
return self._get_data(xpath, as_xml)
if parse==False:
return self._get_data(xpath,parse)
data = self._get_data(xpath)
if not data:
@@ -419,3 +418,8 @@ class Device(Transport):
lyd = oldd.diff(newd)
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
+46 -48
View File
@@ -29,19 +29,19 @@ class Device(Transport):
mapping: dict,
yangdir: None | str = None,
factory_default = True):
self.location = location
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.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)
@@ -68,7 +68,7 @@ class Device(Transport):
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):
self.get_schema(schema["name"], schema["revision"], yangdir)
@@ -84,14 +84,14 @@ 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)
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
@@ -101,31 +101,31 @@ class Device(Transport):
def _get_raw(self, url, parse=True):
"""Actually send a GET to RESTCONF server"""
response = requests.get(url, headers=self.headers, auth=self.auth, verify=False)
response=requests.get(url, headers=self.headers, auth=self.auth, verify=False)
response.raise_for_status() # Raise an exception for HTTP errors
if parse:
data = response.json()
data=response.json()
data=self.lyctx.parse_data_mem(json.dumps(data), "json", parse_only=True)
return data
else:
return response.content
def get_datastore(self, datastore="operational" , xpath=""):
def get_datastore(self, datastore="operational" , xpath="", parse=True):
"""Get a datastore"""
path = f"/ds/ietf-datastores:{datastore}"
path=f"/ds/ietf-datastores:{datastore}"
if not xpath is None:
path=f"{path}/{xpath}"
path = quote(path, safe="/:")
url = f"{self.restconf_url}{path}"
return self._get_raw(url)
path=quote(path, safe="/:")
url=f"{self.restconf_url}{path}"
return self._get_raw(url, parse)
def get_running(self, xpath=None):
"""Wrapper function to get running datastore"""
return self.get_datastore("running", xpath)
def get_operational(self, xpath=None):
def get_operational(self, xpath=None, parse=True):
"""Wrapper function to get operational datastore"""
return self.get_datastore("operational", xpath)
return self.get_datastore("operational", xpath, parse)
def get_factory(self, xpath=None):
"""Wrapper function to get factory defaults"""
@@ -133,7 +133,7 @@ class Device(Transport):
def post_datastore(self, datastore, data):
"""Actually send a POST to RESTCONF server"""
response = requests.post(
response=requests.post(
f"{self.restconf_url}/ds/ietf-datastores:{datastore}/",
json=data, # Directly pass the dictionary without using json.dumps
headers=self.headers,
@@ -144,7 +144,7 @@ class Device(Transport):
def put_datastore(self, datastore, data):
"""Actually send a PUT to RESTCONF server"""
response = requests.put(
response=requests.put(
f"{self.restconf_url}/ds/ietf-datastores:{datastore}/",
json=data, # Directly pass the dictionary without using json.dumps
headers=self.headers,
@@ -157,15 +157,15 @@ class Device(Transport):
"""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(":")
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"""
running = self.get_running()
mod = self.lyctx.get_module(modname)
change = mod.parse_data_dict(edit, no_state=True, validate=False)
running=self.get_running()
mod=self.lyctx.get_module(modname)
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)))
@@ -173,7 +173,7 @@ class Device(Transport):
def call_rpc(self, rpc):
url=f"{self.rpc_url}/{rpc}"
"""Actually send a POST to RESTCONF server"""
response = requests.post(
response=requests.post(
url,
headers=self.headers,
auth=self.auth,
@@ -181,30 +181,27 @@ class Device(Transport):
)
response.raise_for_status() # Raise an exception for HTTP errors
def get_dict(self, xpath=None, as_xml=False):
def get_dict(self, xpath=None, parse=True):
"""NETCONF compat function, just wraps get_data"""
return self.get_data(xpath, as_xml)
return self.get_data(xpath, parse)
def get_data(self, xpath=None, as_xml=False):
def get_data(self, xpath=None, parse=True):
"""Get operational data"""
data=self.get_operational(xpath)
if as_xml:
data=data.print_mem("xml", with_siblings=True, pretty=False)
return etree.fromstring(data)
if xpath is None:
data=self.get_operational(xpath, parse)
if parse==False:
return data
data=json.loads(data.print_mem("json", with_siblings=True, pretty=False))
for k,v in data.items():
model, container = k.split(":")
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)
data=factory.print_mem("json", with_siblings=True, pretty=False)
self.put_datastore(target, json.loads(data))
def reboot(self):
@@ -223,9 +220,15 @@ class Device(Transport):
return xpath
def get_current_time_with_offset(self):
data=self.get_data("/ietf-system:system-state/clock", parse=False)
data=json.loads(data)
print(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(self.get_iface_xpath(iface))
content=self.get_data(self.get_iface_xpath(iface))
interface=content.get("interfaces", {}).get("interface", None)
if interface is None:
return None
@@ -233,16 +236,11 @@ class Device(Transport):
# This is a bug in rousette, should be able to use the same code as NETCONF
return interface[0]
def get_iface_xpath(self, iface, path=None):
"""Compose complete XPath to a YANG node in /ietf-interfaces"""
xpath = f"/ietf-interfaces:interfaces/interface"
return self.get_xpath(xpath, "", iface, path)
def delete_xpath(self, xpath):
"""Delete XPath from running config"""
path = f"/ds/ietf-datastores:running/{xpath}"
path = quote(path, safe="/:")
url = f"{self.restconf_url}{path}"
response = requests.delete(url, headers=self.headers, auth=self.auth, verify=False)
path=f"/ds/ietf-datastores:running/{xpath}"
path=quote(path, safe="/:")
url=f"{self.restconf_url}{path}"
response=requests.delete(url, headers=self.headers, auth=self.auth, verify=False)
response.raise_for_status() # Raise an exception for HTTP errors
return True
+17 -8
View File
@@ -5,7 +5,7 @@ class Transport(ABC):
"""Common functions for NETCONF/RESTCONF"""
@abstractmethod
def get_data(self, xpath=None, as_xml=False):
def get_data(self, xpath=None, parse=True):
pass
@abstractmethod
def get_config_dict(self, modname):
@@ -14,18 +14,12 @@ class Transport(ABC):
def put_config_dict(self, modname, edit):
pass
@abstractmethod
def get_dict(self, xpath=None, as_xml=False):
def get_dict(self, xpath=None):
pass
@abstractmethod
def get_xpath(self, xpath, key, value, path=None):
pass
@abstractmethod
def get_iface(self, iface):
pass
@abstractmethod
def get_iface_xpath(self, iface, path=None):
pass
@abstractmethod
def delete_xpath(self, xpath):
pass
@abstractmethod
@@ -34,6 +28,16 @@ class Transport(ABC):
@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
pass
@abstractmethod
def get_iface_xpath(self, iface, path=None):
pass
@abstractmethod
def get_iface(self, iface): # Should be common, but is not due to bug in rousette
pass
def get_mgmt_ip(self):
"""Return managment IP address used for RESTCONF"""
@@ -51,3 +55,8 @@ class Transport(ABC):
"""Check if the device reachable on ll6"""
neigh = ll6ping(self.location.interface, flags=["-w1", "-c1", "-L", "-n"])
return bool(neigh)
def get_iface_xpath(self, iface, path=None):
"""Compose complete XPath to a YANG node in /ietf-interfaces"""
xpath = f"/ietf-interfaces:interfaces/interface"
return self.get_xpath(xpath, "name", iface, path)