|
23 | 23 |
|
24 | 24 | """Wrapper for NX-OS to simplify usage of gNMI implementation."""
|
25 | 25 |
|
26 |
| - |
| 26 | +import json |
27 | 27 | import logging
|
28 | 28 |
|
29 | 29 | from six import string_types
|
@@ -56,12 +56,149 @@ class NXClient(Client):
|
56 | 56 | >>> capabilities = client.capabilities()
|
57 | 57 | >>> print(capabilities)
|
58 | 58 | """
|
| 59 | + def delete_xpaths(self, xpaths, prefix=None): |
| 60 | + """A convenience wrapper for set() which constructs Paths from supplied xpaths |
| 61 | + to be passed to set() as the delete parameter. |
| 62 | +
|
| 63 | + Parameters |
| 64 | + ---------- |
| 65 | + xpaths : iterable of str |
| 66 | + XPaths to specify to be deleted. |
| 67 | + If prefix is specified these strings are assumed to be the suffixes. |
| 68 | + prefix : str |
| 69 | + The XPath prefix to apply to all XPaths for deletion. |
| 70 | +
|
| 71 | + Returns |
| 72 | + ------- |
| 73 | + set() |
| 74 | + """ |
| 75 | + if isinstance(xpaths, string_types): |
| 76 | + xpaths = [xpaths] |
| 77 | + paths = [] |
| 78 | + # prefix is not supported on NX yet |
| 79 | + prefix = None |
| 80 | + for xpath in xpaths: |
| 81 | + if prefix: |
| 82 | + if prefix.endswith("/") and xpath.startswith("/"): |
| 83 | + xpath = "{prefix}{xpath}".format( |
| 84 | + prefix=prefix[:-1], xpath=xpath[1:] |
| 85 | + ) |
| 86 | + elif prefix.endswith("/") or xpath.startswith("/"): |
| 87 | + xpath = "{prefix}{xpath}".format(prefix=prefix, xpath=xpath) |
| 88 | + else: |
| 89 | + xpath = "{prefix}/{xpath}".format(prefix=prefix, xpath=xpath) |
| 90 | + paths.append(self.parse_xpath_to_gnmi_path(xpath)) |
| 91 | + return self.set(deletes=paths) |
| 92 | + |
| 93 | + def set_json(self, update_json_configs=None, replace_json_configs=None, ietf=False, prefix=None): |
| 94 | + """A convenience wrapper for set() which assumes JSON payloads and constructs desired messages. |
| 95 | + All parameters are optional, but at least one must be present. |
| 96 | +
|
| 97 | + This method expects JSON in the same format as what you might send via the native gRPC interface |
| 98 | + with a fully modeled configuration which is then parsed to meet the gNMI implementation. |
| 99 | +
|
| 100 | + Parameters |
| 101 | + ---------- |
| 102 | + update_json_configs : iterable of JSON configurations, optional |
| 103 | + JSON configs to apply as updates. |
| 104 | + replace_json_configs : iterable of JSON configurations, optional |
| 105 | + JSON configs to apply as replacements. |
| 106 | + ietf : bool, optional |
| 107 | + Use JSON_IETF vs JSON. |
| 108 | + prefix : proto.gnmi_pb2.Path, optional |
| 109 | + A common path prepended to all path elements in the message. This reduces message size by |
| 110 | + removing redundent path elements. Smaller message == improved thoughput. |
| 111 | +
|
| 112 | + Returns |
| 113 | + ------- |
| 114 | + set() |
| 115 | + """ |
| 116 | + # JSON_IETF and prefix are not supported on NX yet |
| 117 | + ietf = False |
| 118 | + prefix = None |
| 119 | + |
| 120 | + if not any([update_json_configs, replace_json_configs]): |
| 121 | + raise Exception("Must supply at least one set of configurations to method!") |
59 | 122 |
|
60 |
| - def get(self, *args, **kwargs): |
61 |
| - raise NotImplementedError("Get not yet supported on NX-OS!") |
| 123 | + def check_configs(name, configs): |
| 124 | + if isinstance(configs, string_types): |
| 125 | + logger.debug("Handling %s as JSON string.", name) |
| 126 | + try: |
| 127 | + configs = json.loads(configs) |
| 128 | + except: |
| 129 | + raise Exception("{name} is invalid JSON!".format(name=name)) |
| 130 | + configs = [configs] |
| 131 | + elif isinstance(configs, dict): |
| 132 | + logger.debug("Handling %s as already serialized JSON object.", name) |
| 133 | + configs = [configs] |
| 134 | + elif not isinstance(configs, (list, set)): |
| 135 | + raise Exception( |
| 136 | + "{name} must be an iterable of configs!".format(name=name) |
| 137 | + ) |
| 138 | + return configs |
62 | 139 |
|
63 |
| - def set(self, *args, **kwargs): |
64 |
| - raise NotImplementedError("Set not yet supported on NX-OS!") |
| 140 | + def create_updates(name, configs): |
| 141 | + if not configs: |
| 142 | + return None |
| 143 | + configs = check_configs(name, configs) |
| 144 | + updates = [] |
| 145 | + for config in configs: |
| 146 | + if not isinstance(config, dict): |
| 147 | + raise Exception("config must be a JSON object!") |
| 148 | + if len(config.keys()) > 1: |
| 149 | + raise Exception("config should only target one YANG module!") |
| 150 | + top_element = next(iter(config.keys())) |
| 151 | + update = proto.gnmi_pb2.Update() |
| 152 | + update.path.CopyFrom(self.parse_xpath_to_gnmi_path(top_element)) |
| 153 | + config = config.pop(top_element) |
| 154 | + if ietf: |
| 155 | + update.val.json_ietf_val = json.dumps(config).encode("utf-8") |
| 156 | + else: |
| 157 | + update.val.json_val = json.dumps(config).encode("utf-8") |
| 158 | + updates.append(update) |
| 159 | + return updates |
| 160 | + |
| 161 | + updates = create_updates("update_json_configs", update_json_configs) |
| 162 | + replaces = create_updates("replace_json_configs", replace_json_configs) |
| 163 | + return self.set(prefix=prefix, updates=updates, replaces=replaces) |
| 164 | + |
| 165 | + def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON"): |
| 166 | + """A convenience wrapper for get() which forms proto.gnmi_pb2.Path from supplied xpaths. |
| 167 | +
|
| 168 | + Parameters |
| 169 | + ---------- |
| 170 | + xpaths : iterable of str or str |
| 171 | + An iterable of XPath strings to request data of |
| 172 | + If simply a str, wraps as a list for convenience |
| 173 | + data_type : proto.gnmi_pb2.GetRequest.DataType, optional |
| 174 | + A direct value or key from the GetRequest.DataType enum |
| 175 | + [ALL, CONFIG, STATE, OPERATIONAL] |
| 176 | + encoding : proto.gnmi_pb2.GetRequest.Encoding, optional |
| 177 | + A direct value or key from the Encoding enum |
| 178 | + [JSON] is only setting supported at this time |
| 179 | +
|
| 180 | + Returns |
| 181 | + ------- |
| 182 | + get() |
| 183 | + """ |
| 184 | + supported_encodings = ["JSON"] |
| 185 | + encoding = util.validate_proto_enum( |
| 186 | + "encoding", |
| 187 | + encoding, |
| 188 | + "Encoding", |
| 189 | + proto.gnmi_pb2.Encoding, |
| 190 | + supported_encodings, |
| 191 | + ) |
| 192 | + gnmi_path = None |
| 193 | + if isinstance(xpaths, (list, set)): |
| 194 | + gnmi_path = map(self.parse_xpath_to_gnmi_path, set(xpaths)) |
| 195 | + elif isinstance(xpaths, string_types): |
| 196 | + gnmi_path = [self.parse_xpath_to_gnmi_path(xpaths)] |
| 197 | + else: |
| 198 | + raise Exception( |
| 199 | + "xpaths must be a single xpath string or iterable of xpath strings!" |
| 200 | + ) |
| 201 | + return self.get(gnmi_path, data_type=data_type, encoding=encoding) |
65 | 202 |
|
66 | 203 | def subscribe_xpaths(
|
67 | 204 | self,
|
@@ -154,19 +291,18 @@ def subscribe_xpaths(
|
154 | 291 |
|
155 | 292 | def parse_xpath_to_gnmi_path(self, xpath, origin=None):
|
156 | 293 | """Attempts to determine whether origin should be YANG (device) or DME.
|
157 |
| - Errors on OpenConfig until support is present. |
158 | 294 | """
|
159 |
| - if xpath.startswith("openconfig"): |
160 |
| - raise NotImplementedError( |
161 |
| - "OpenConfig data models not yet supported on NX-OS!" |
162 |
| - ) |
163 | 295 | if origin is None:
|
164 | 296 | if any(
|
165 |
| - map(xpath.startswith, ["Cisco-NX-OS-device", "/Cisco-NX-OS-device"]) |
| 297 | + map(xpath.startswith, [ |
| 298 | + "Cisco-NX-OS-device", |
| 299 | + "/Cisco-NX-OS-device", |
| 300 | + "cisco-nx-os-device", |
| 301 | + "/cisco-nx-os-device"]) |
166 | 302 | ):
|
167 | 303 | origin = "device"
|
168 | 304 | # Remove the module
|
169 | 305 | xpath = xpath.split(":", 1)[1]
|
170 | 306 | else:
|
171 |
| - origin = "DME" |
| 307 | + origin = "openconfig" |
172 | 308 | return super(NXClient, self).parse_xpath_to_gnmi_path(xpath, origin)
|
0 commit comments