1# Copyright (c) 2018 Cisco and/or its affiliates. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Util functions for the NXOS modules. 17""" 18 19import collections 20import http.client 21import json 22import logging 23import os 24import re 25import socket 26from collections.abc import Iterable 27 28import salt.utils.http 29from salt.exceptions import ( 30 CommandExecutionError, 31 NxosClientError, 32 NxosError, 33 NxosRequestNotSupported, 34) 35from salt.utils.args import clean_kwargs 36 37log = logging.getLogger(__name__) 38 39 40class UHTTPConnection(http.client.HTTPConnection): 41 """ 42 Subclass of Python library HTTPConnection that uses a unix-domain socket. 43 """ 44 45 def __init__(self, path): 46 http.client.HTTPConnection.__init__(self, "localhost") 47 self.path = path 48 49 def connect(self): 50 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 51 sock.connect(self.path) 52 self.sock = sock 53 54 55class NxapiClient: 56 """ 57 Class representing an NX-API client that connects over http(s) or 58 unix domain socket (UDS). 59 """ 60 61 # Location of unix domain socket for NX-API localhost 62 NXAPI_UDS = "/tmp/nginx_local/nginx_1_be_nxapi.sock" 63 # NXAPI listens for remote connections to "http(s)://<switch IP>/ins" 64 # NXAPI listens for local connections to "http(s)://<UDS>/ins_local" 65 NXAPI_REMOTE_URI_PATH = "/ins" 66 NXAPI_UDS_URI_PATH = "/ins_local" 67 NXAPI_VERSION = "1.0" 68 69 def __init__(self, **nxos_kwargs): 70 """ 71 Initialize NxapiClient() connection object. By default this connects 72 to the local unix domain socket (UDS). If http(s) is required to 73 connect to a remote device then 74 nxos_kwargs['host'], 75 nxos_kwargs['username'], 76 nxos_kwargs['password'], 77 nxos_kwargs['transport'], 78 nxos_kwargs['port'], 79 parameters must be provided. 80 """ 81 self.nxargs = self._prepare_conn_args(clean_kwargs(**nxos_kwargs)) 82 # Default: Connect to unix domain socket on localhost. 83 if self.nxargs["connect_over_uds"]: 84 if not os.path.exists(self.NXAPI_UDS): 85 raise NxosClientError( 86 "No host specified and no UDS found at {}\n".format(self.NXAPI_UDS) 87 ) 88 89 # Create UHTTPConnection object for NX-API communication over UDS. 90 log.info("Nxapi connection arguments: %s", self.nxargs) 91 log.info("Connecting over unix domain socket") 92 self.connection = UHTTPConnection(self.NXAPI_UDS) 93 else: 94 # Remote connection - Proxy Minion, connect over http(s) 95 log.info("Nxapi connection arguments: %s", self.nxargs) 96 log.info("Connecting over %s", self.nxargs["transport"]) 97 self.connection = salt.utils.http.query 98 99 def _use_remote_connection(self, kwargs): 100 """ 101 Determine if connection is local or remote 102 """ 103 kwargs["host"] = kwargs.get("host") 104 kwargs["username"] = kwargs.get("username") 105 kwargs["password"] = kwargs.get("password") 106 if ( 107 kwargs["host"] is None 108 or kwargs["username"] is None 109 or kwargs["password"] is None 110 ): 111 return False 112 else: 113 return True 114 115 def _prepare_conn_args(self, kwargs): 116 """ 117 Set connection arguments for remote or local connection. 118 """ 119 kwargs["connect_over_uds"] = True 120 kwargs["timeout"] = kwargs.get("timeout", 60) 121 kwargs["cookie"] = kwargs.get("cookie", "admin") 122 if self._use_remote_connection(kwargs): 123 kwargs["transport"] = kwargs.get("transport", "https") 124 if kwargs["transport"] == "https": 125 kwargs["port"] = kwargs.get("port", 443) 126 else: 127 kwargs["port"] = kwargs.get("port", 80) 128 kwargs["verify"] = kwargs.get("verify", True) 129 if isinstance(kwargs["verify"], bool): 130 kwargs["verify_ssl"] = kwargs["verify"] 131 else: 132 kwargs["ca_bundle"] = kwargs["verify"] 133 kwargs["connect_over_uds"] = False 134 return kwargs 135 136 def _build_request(self, type, commands): 137 """ 138 Build NX-API JSON request. 139 """ 140 request = {} 141 headers = { 142 "content-type": "application/json", 143 } 144 if self.nxargs["connect_over_uds"]: 145 user = self.nxargs["cookie"] 146 headers["cookie"] = "nxapi_auth=" + user + ":local" 147 request["url"] = self.NXAPI_UDS_URI_PATH 148 else: 149 request["url"] = "{transport}://{host}:{port}{uri}".format( 150 transport=self.nxargs["transport"], 151 host=self.nxargs["host"], 152 port=self.nxargs["port"], 153 uri=self.NXAPI_REMOTE_URI_PATH, 154 ) 155 156 if isinstance(commands, (list, set, tuple)): 157 commands = " ; ".join(commands) 158 payload = {} 159 # Some versions of NX-OS fail to process the payload properly if 160 # 'input' gets serialized before 'type' and the payload of 'input' 161 # contains the string 'type'. Use an ordered dict to enforce ordering. 162 payload["ins_api"] = collections.OrderedDict() 163 payload["ins_api"]["version"] = self.NXAPI_VERSION 164 payload["ins_api"]["type"] = type 165 payload["ins_api"]["chunk"] = "0" 166 payload["ins_api"]["sid"] = "1" 167 payload["ins_api"]["input"] = commands 168 payload["ins_api"]["output_format"] = "json" 169 170 request["headers"] = headers 171 request["payload"] = json.dumps(payload) 172 request["opts"] = {"http_request_timeout": self.nxargs["timeout"]} 173 log.info("request: %s", request) 174 return request 175 176 def request(self, type, command_list): 177 """ 178 Send NX-API JSON request to the NX-OS device. 179 """ 180 req = self._build_request(type, command_list) 181 if self.nxargs["connect_over_uds"]: 182 self.connection.request("POST", req["url"], req["payload"], req["headers"]) 183 response = self.connection.getresponse() 184 else: 185 response = self.connection( 186 req["url"], 187 method="POST", 188 opts=req["opts"], 189 data=req["payload"], 190 header_dict=req["headers"], 191 decode=True, 192 decode_type="json", 193 **self.nxargs 194 ) 195 196 return self.parse_response(response, command_list) 197 198 def parse_response(self, response, command_list): 199 """ 200 Parse NX-API JSON response from the NX-OS device. 201 """ 202 # Check for 500 level NX-API Server Errors 203 if isinstance(response, Iterable) and "status" in response: 204 if int(response["status"]) >= 500: 205 raise NxosError("{}".format(response)) 206 else: 207 raise NxosError("NX-API Request Not Supported: {}".format(response)) 208 209 if isinstance(response, Iterable): 210 body = response["dict"] 211 else: 212 body = response 213 214 if self.nxargs["connect_over_uds"]: 215 body = json.loads(response.read().decode("utf-8")) 216 217 # Proceed with caution. The JSON may not be complete. 218 # Don't just return body['ins_api']['outputs']['output'] directly. 219 output = body.get("ins_api") 220 if output is None: 221 raise NxosClientError("Unexpected JSON output\n{}".format(body)) 222 if output.get("outputs"): 223 output = output["outputs"] 224 if output.get("output"): 225 output = output["output"] 226 227 # The result list stores results for each command that was sent to 228 # nxapi. 229 result = [] 230 # Keep track of successful commands using previous_commands list so 231 # they can be displayed if a specific command fails in a chain of 232 # commands. 233 previous_commands = [] 234 235 # Make sure output and command_list lists to be processed in the 236 # subesequent loop. 237 if not isinstance(output, list): 238 output = [output] 239 if not isinstance(command_list, list): 240 command_list = [command_list] 241 if len(command_list) == 1 and ";" in command_list[0]: 242 command_list = [cmd.strip() for cmd in command_list[0].split(";")] 243 244 for cmd_result, cmd in zip(output, command_list): 245 code = cmd_result.get("code") 246 msg = cmd_result.get("msg") 247 log.info("command %s:", cmd) 248 log.info("PARSE_RESPONSE: %s %s", code, msg) 249 if code == "400": 250 raise CommandExecutionError( 251 { 252 "rejected_input": cmd, 253 "code": code, 254 "message": msg, 255 "cli_error": cmd_result.get("clierror"), 256 "previous_commands": previous_commands, 257 } 258 ) 259 elif code == "413": 260 raise NxosRequestNotSupported("Error 413: {}".format(msg)) 261 elif code != "200": 262 raise NxosError("Unknown Error: {}, Code: {}".format(msg, code)) 263 else: 264 previous_commands.append(cmd) 265 result.append(cmd_result["body"]) 266 267 return result 268 269 270def nxapi_request(commands, method="cli_show", **kwargs): 271 """ 272 Send exec and config commands to the NX-OS device over NX-API. 273 274 commands 275 The exec or config commands to be sent. 276 277 method: 278 ``cli_show_ascii``: Return raw test or unstructured output. 279 ``cli_show``: Return structured output. 280 ``cli_conf``: Send configuration commands to the device. 281 Defaults to ``cli_show``. 282 283 transport: ``https`` 284 Specifies the type of connection transport to use. Valid values for the 285 connection are ``http``, and ``https``. 286 287 host: ``localhost`` 288 The IP address or DNS host name of the device. 289 290 username: ``admin`` 291 The username to pass to the device to authenticate the NX-API connection. 292 293 password 294 The password to pass to the device to authenticate the NX-API connection. 295 296 port 297 The TCP port of the endpoint for the NX-API connection. If this keyword is 298 not specified, the default value is automatically determined by the 299 transport type (``80`` for ``http``, or ``443`` for ``https``). 300 301 timeout: ``60`` 302 Time in seconds to wait for the device to respond. Default: 60 seconds. 303 304 verify: ``True`` 305 Either a boolean, in which case it controls whether we verify the NX-API 306 TLS certificate, or a string, in which case it must be a path to a CA bundle 307 to use. Defaults to ``True``. 308 """ 309 client = NxapiClient(**kwargs) 310 return client.request(method, commands) 311 312 313def ping(**kwargs): 314 """ 315 Verify connection to the NX-OS device over UDS. 316 """ 317 return NxapiClient(**kwargs).nxargs["connect_over_uds"] 318 319 320# Grains Functions 321 322 323def _parser(block): 324 return re.compile("^{block}\n(?:^[ \n].*$\n?)+".format(block=block), re.MULTILINE) 325 326 327def _parse_software(data): 328 """ 329 Internal helper function to parse sotware grain information. 330 """ 331 ret = {"software": {}} 332 software = _parser("Software").search(data).group(0) 333 matcher = re.compile("^ ([^:]+): *([^\n]+)", re.MULTILINE) 334 for line in matcher.finditer(software): 335 key, val = line.groups() 336 ret["software"][key] = val 337 return ret["software"] 338 339 340def _parse_hardware(data): 341 """ 342 Internal helper function to parse hardware grain information. 343 """ 344 ret = {"hardware": {}} 345 hardware = _parser("Hardware").search(data).group(0) 346 matcher = re.compile("^ ([^:\n]+): *([^\n]+)", re.MULTILINE) 347 for line in matcher.finditer(hardware): 348 key, val = line.groups() 349 ret["hardware"][key] = val 350 return ret["hardware"] 351 352 353def _parse_plugins(data): 354 """ 355 Internal helper function to parse plugin grain information. 356 """ 357 ret = {"plugins": []} 358 plugins = _parser("plugin").search(data).group(0) 359 matcher = re.compile("^ (?:([^,]+), )+([^\n]+)", re.MULTILINE) 360 for line in matcher.finditer(plugins): 361 ret["plugins"].extend(line.groups()) 362 return ret["plugins"] 363 364 365def version_info(): 366 client = NxapiClient() 367 return client.request("cli_show_ascii", "show version")[0] 368 369 370def system_info(data): 371 """ 372 Helper method to return parsed system_info 373 from the 'show version' command. 374 """ 375 if not data: 376 return {} 377 info = { 378 "software": _parse_software(data), 379 "hardware": _parse_hardware(data), 380 "plugins": _parse_plugins(data), 381 } 382 return {"nxos": info} 383