summaryrefslogtreecommitdiffstats
path: root/cvprac/cvp_client.py
diff options
context:
space:
mode:
Diffstat (limited to 'cvprac/cvp_client.py')
-rw-r--r--cvprac/cvp_client.py1018
1 files changed, 1018 insertions, 0 deletions
diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py
new file mode 100644
index 0000000..0d901b7
--- /dev/null
+++ b/cvprac/cvp_client.py
@@ -0,0 +1,1018 @@
+#
+# Copyright (c) 2017, Arista Networks, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# Neither the name of Arista Networks nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+''' RESTful API Client class for Cloudvision(R) Portal
+
+This module provides a RESTful API client for Cloudvision(R) Portal (CVP)
+which can be used for building applications that work with Arista CVP.
+
+When the class is instantiated the logging is configured. Either syslog,
+file logging, both, or none can be enabled. If neither syslog nor filename is
+specified then no logging will be performed.
+
+This class supports creating a connection to a CVP node and then issuing
+subsequent GET and POST requests to CVP. A GET or POST request will be
+automatically retried on the same node if the request receives a
+requests.exceptions.Timeout or ReadTimeout error. A GET or POST request will
+be automatically retried on the same node if the request receives a
+CvpSessionLogOutError. For this case a login will be performed before the
+request is retried. For either case, the maximum number of times a request
+will be retried on the same node is specified by the class attribute
+NUM_RETRY_REQUESTS.
+
+If more than one CVP node is specified when creating a connection, and a GET
+or POST request that receives a requests.exceptions.ConnectionError,
+requests.exceptions.HTTPError, or a requests.exceptions.TooManyRedirects will
+be retried on the next CVP node in the list. If a GET or POST request that
+receives a requests.exceptions.Timeout or CvpSessionLogOutError and the retries
+on the same node exceed NUM_RETRY_REQUESTS, then the request will be retried
+on the next node on the list.
+
+If any of the errors persists across all nodes then the GET or POST request
+will fail and the last error that occurred will be raised.
+
+The class provides connect, get, and post methods that allow the user to make
+direct RESTful API calls to CVP.
+
+Example:
+
+ >>> from cvprac.cvp_client import CvpClient
+ >>> clnt = CvpClient()
+ >>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word')
+ >>> result = clnt.get('/cvpInfo/getCvpInfo.do')
+ >>> print result
+ {u'version': u'2016.1.0'}
+ >>>
+
+The class provides a wrapper function around the CVP RESTful API operations.
+Each API method takes the RESTful API parameters as method parameters to the
+operation method. The API class was added to the client class because the
+API functions are required when using the CVP RESTful API and placing them
+in this library avoids duplicating the calls in every application that uses
+this class.
+
+Example:
+
+ >>> from cvprac.cvp_client import CvpClient
+ >>> clnt = CvpClient()
+ >>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word')
+ >>> result = clnt.api.get_cvp_info()
+ >>> print result
+ {u'version': u'2016.1.0'}
+ >>>
+'''
+
+import os
+import re
+import json
+import logging
+from logging.handlers import SysLogHandler
+from itertools import cycle
+from pkg_resources import parse_version
+
+import requests
+from requests.exceptions import ConnectionError, HTTPError, Timeout, \
+ ReadTimeout, TooManyRedirects, JSONDecodeError
+
+from cvprac.cvp_api import CvpApi
+from cvprac.cvp_client_errors import CvpApiError, CvpLoginError, \
+ CvpRequestError, CvpSessionLogOutError
+
+
+class CvpClient(object):
+ ''' Use this class to create a persistent connection to CVP.
+ '''
+ # pylint: disable=too-many-instance-attributes
+ # Maximum number of times to retry a get or post to the same
+ # CVP node.
+ NUM_RETRY_REQUESTS = 3
+ LATEST_API_VERSION = 8.0
+
+ def __init__(self, logger='cvprac', syslog=False, filename=None,
+ log_level='INFO'):
+ ''' Initialize the client and configure logging. Either syslog, file
+ logging, both, or none can be enabled. If neither syslog
+ nor filename is specified then no logging will be performed.
+
+ Args:
+ logger (str): The name assigned to the logger.
+ syslog (bool): If True enable logging to syslog. Default is
+ False.
+ filename (str): Log to the file specified by filename. Default
+ is None.
+ log_level (str): Log level to use for logger. Default is INFO.
+ '''
+ self.apiversion = None
+ self.authdata = None
+ self.cert = False
+ self.connect_timeout = None
+ self.cookies = None
+ self.error_msg = ''
+ self.node_cnt = None
+ self.node_pool = None
+ self.nodes = None
+ self.port = None
+ self.protocol = None
+ self.session = None
+ self.url_prefix = None
+ self.url_prefix_short = None
+ self.is_cvaas = False
+ self.tenant = None
+ self.cvaas_token = None
+ self.api_token = None
+ self.version = None
+ self._last_used_node = None
+ self.proxies = None
+
+ # Save proper headers
+ self.headers = {'Accept': 'application/json',
+ 'Content-Type': 'application/json'}
+
+ self.log = logging.getLogger(logger)
+ self.set_log_level(log_level)
+ if syslog:
+ # Enables sending logging messages to the local syslog server.
+ self.log.addHandler(SysLogHandler())
+ if filename:
+ # Enables sending logging messages to a file.
+ self.log.addHandler(logging.FileHandler(filename))
+ if syslog is False and filename is None:
+ # Not logging so use the null handler
+ self.log.addHandler(logging.NullHandler())
+
+ # Instantiate the CvpApi class
+ self.api = CvpApi(self)
+
+ @property
+ def last_used_node(self):
+ ''' Returns the node that the last request was sent to regardless of
+ whether the request was successful or not.
+
+ Returns:
+ String identifying the node that the last request was sent to.
+ '''
+ return self._last_used_node
+
+ def set_log_level(self, log_level='INFO'):
+ ''' Set log level for logger. Defaults to INFO if no level passed in or
+ if an invalid level is passed in.
+
+ Args:
+ log_level (str): Log level to use for logger. Default is INFO.
+ '''
+ log_level = log_level.upper()
+ if log_level not in ['NOTSET', 'DEBUG', 'INFO',
+ 'WARNING', 'ERROR', 'CRITICAL']:
+ log_level = 'INFO'
+ self.log.setLevel(getattr(logging, log_level))
+
+ def set_version(self, version):
+ ''' Set the CVP API version to be used when making api calls.
+
+ For CVP versions 2018.1.X and prior, use api version 1.0
+ For CVP versions 2018.2.X, use api version 2.0
+ For CVP versions 2019.0.0 through 2020.1.0, use api version 3.0
+ For CVP versions 2020.1.1 through 2020.2.3, use api version 4.0
+ For CVP versions 2020.2.4 through 2021.1.x, use api version 5.0
+ For CVP versions 2021.2.x, use api version 6.0
+ For CVP versions 2021.3.x, use api version 7.0
+ For CVP versions 2022.1.0 and beyond, use api version 8.0
+
+ Args:
+ version (str): The CVP version in use.
+ '''
+ self.version = version
+ self.log.info('Version %s', version)
+ # Set apiversion to latest available API version for CVaaS
+ # Set apiversion to 8.0 for 2022.1.x
+ # Set apiversion to 7.0 for 2021.3.x
+ # Set apiversion to 6.0 for 2021.2.x
+ # Set apiversion to 5.0 for 2020.2.4 through 2021.1.x
+ # Set apiversion to 4.0 for 2020.1.1 through 2020.2.3
+ # Set apiversion to 3.0 for 2019.0.0 through 2020.1.0
+ # Set apiversion to 2.0 for 2018.2.X
+ # Set apiversion to 1.0 for 2018.1.X and prior
+ if self.is_cvaas:
+ self.log.info('Setting API version to %d for CVaaS',
+ self.LATEST_API_VERSION)
+ self.apiversion = self.LATEST_API_VERSION
+ else:
+ version_components = version.split(".")
+ if len(version_components) < 3:
+ version_components.append("0")
+ self.log.info('Version found with less than 3 components.'
+ ' Appending 0. Updated Version String - %s',
+ ".".join(version_components))
+ full_version = ".".join(version_components)
+ if parse_version(full_version) >= parse_version('2022.1.0'):
+ self.log.info('Setting API version to v8')
+ self.apiversion = 8.0
+ elif parse_version(full_version) >= parse_version('2021.3.0'):
+ self.log.info('Setting API version to v7')
+ self.apiversion = 7.0
+ elif parse_version(full_version) >= parse_version('2021.2.0'):
+ self.log.info('Setting API version to v6')
+ self.apiversion = 6.0
+ elif parse_version(full_version) >= parse_version('2020.2.4'):
+ self.log.info('Setting API version to v5')
+ self.apiversion = 5.0
+ elif parse_version(full_version) >= parse_version('2020.1.1'):
+ self.log.info('Setting API version to v4')
+ self.apiversion = 4.0
+ elif parse_version(full_version) >= parse_version('2019.0.0'):
+ self.log.info('Setting API version to v3')
+ self.apiversion = 3.0
+ elif parse_version(full_version) >= parse_version('2018.2.0'):
+ self.log.info('Setting API version to v2')
+ self.apiversion = 2.0
+ else:
+ self.log.info('Setting API version to v1')
+ self.apiversion = 1.0
+
+ def connect(self, nodes, username, password, connect_timeout=10,
+ request_timeout=30, protocol='https', port=None, cert=False,
+ is_cvaas=False, tenant=None, api_token=None, cvaas_token=None,
+ proxies=None):
+ ''' Login to CVP and get a session ID and cookie. Currently
+ certificates are not verified if the https protocol is specified. A
+ warning may be printed out from the requests module for this case.
+
+ Args:
+ nodes (list): A list of hostname/IP addresses for CVP nodes
+ username (str): The CVP username
+ password (str): The CVP password
+ connect_timeout (int): The number of seconds to wait for a
+ connection.
+ request_timeout (int): The default number of seconds to allow
+ api requests to complete before timing out.
+ protocol (str): The protocol to use to connect to CVP.
+ THIS PARAMETER IS NOT USED AND WILL BE DEPRECATED.
+ ONLY INCLUDED TO NOT BREAK EXISTING CODE THAT HAS PROTOCOL
+ SPECIFIED IN CONNECTION.
+ port (int): The TCP port of the endpoint for the connection.
+ If this keyword is not specified, the default value is
+ automatically determined by the transport type.
+ (http=80, https=443)
+ cert (str or boolean): Path to a cert file used for a https
+ connection or boolean with default False. If a cert is
+ provided then the connection will not attempt to fallback
+ to http. The False default sets the request to not verify
+ the servers TLS certificate.
+ is_cvaas (boolean): Flag for enabling connection to CVaaS.
+ tenant: (string): Tenant/Org within CVaaS to connect to.
+ Required if is_cvaas is enabled.
+ cvaas_token (string): API Token to use in place of UN/PW login
+ for CVaaS.
+ api_token (string): API Token to use in place of UN/PW login
+ for CVP 2020.3.0 and beyond.
+ proxies (dict): A dictionary of proxy protocol to URL. Example:
+
+ {'http': 'hostname.domain.com:8080',
+ 'https': 'hostname.domain.com:8080'}
+
+ Proxies can also be set via environment variables.
+ Please reference the below link for details of precedence.
+ https://requests.readthedocs.io/en/latest/user/advanced/#proxies
+
+ Raises:
+ CvpLoginError: A CvpLoginError is raised if a connection
+ could not be established to any of the nodes.
+ TypeError: A TypeError is raised if the nodes argument is not
+ a list.
+ ValueError: A ValueError is raised if a port is not specified
+ and the protocol is not http or https.
+ '''
+ # pylint: disable=too-many-arguments
+ if not isinstance(nodes, list):
+ raise TypeError('nodes argument must be a list')
+
+ for idx, _ in enumerate(nodes):
+ if (os.environ.get('CURRENT_NODE_IP') and
+ nodes[idx] in ['127.0.0.1', 'localhost']):
+ # We set this env in script-executor container.
+ # Mask localhost or 127.0.0.1 with node IP if this
+ # is called from configlet builder scripts.
+ nodes[idx] = os.environ.get('CURRENT_NODE_IP')
+
+ self.cert = cert
+ self.nodes = nodes
+ self.node_cnt = len(nodes)
+ self.node_pool = cycle(nodes)
+ self.authdata = {'userId': username, 'password': password}
+ self.connect_timeout = connect_timeout
+ self.api.request_timeout = request_timeout
+ # protocol is deprecated and not used.
+ self.protocol = protocol
+ self.port = port
+ self.is_cvaas = is_cvaas
+ self.tenant = tenant
+ if cvaas_token is not None:
+ self.log.warning('The cvaas_token parameter will be deprecated'
+ ' soon. Please start using the api_token'
+ ' parameter instead. It provides the same'
+ ' functionality that was previously provided'
+ ' by cvaas_token. The api_token parameter is'
+ ' a more general API token parameter because'
+ ' using the CVP REST API via token is also'
+ ' available for on premises CVP as of'
+ ' CVP version 2020.3.0')
+ self.cvaas_token = cvaas_token
+ self.api_token = cvaas_token
+ if api_token is not None:
+ self.log.warning('Using the new api_token parameter.'
+ ' This will override usage of the cvaas_token'
+ ' parameter if both are provided. This is because'
+ ' api_token and cvaas_token parameters are for'
+ ' the same use case and api_token is more'
+ ' generic')
+ self.api_token = api_token
+ self.cvaas_token = api_token
+ self.proxies = proxies
+ self._create_session(all_nodes=True)
+ # Verify that we can connect to at least one node
+ if not self.session:
+ raise CvpLoginError(self.error_msg)
+
+ def _create_session(self, all_nodes=False):
+ ''' Login to CVP and get a session ID and user information.
+ If the all_nodes parameter is True then try creating a session
+ with each CVP node. If False, then try creating a session with
+ each node except the one currently connected to.
+ '''
+ num_nodes = self.node_cnt
+ if not all_nodes and num_nodes > 1:
+ num_nodes -= 1
+
+ self.error_msg = '\n'
+ for _ in range(0, num_nodes):
+ host = next(self.node_pool)
+ self.url_prefix = ('https://%s:%d/web' % (host, self.port or 443))
+ self.url_prefix_short = ('https://%s:%d'
+ % (host, self.port or 443))
+ error = self._reset_session()
+ if error is None:
+ break
+ self.error_msg += '%s: %s\n' % (host, error)
+
+ def _reset_session(self):
+ ''' Get a new request session and try logging into the current
+ CVP node. If the login succeeded None will be returned and
+ self.session will be valid. If the login failed then an
+ exception error will be returned and self.session will
+ be set to None.
+ '''
+ self.session = requests.Session()
+ if self.proxies:
+ self.session.proxies.update(self.proxies)
+ return_error = None
+ try:
+ self._login()
+ except (ConnectionError, CvpApiError, CvpRequestError,
+ CvpSessionLogOutError, HTTPError, ReadTimeout, Timeout,
+ TooManyRedirects) as error:
+ self.log.error(error)
+ # Use outer scope var for return to handle
+ # Python 3 UnboundLocalError
+ return_error = error
+ # Any error that occurs during login is a good reason not to use
+ # this CVP node.
+ self.session = None
+ return return_error
+
+ def _is_good_response(self, response, prefix):
+ ''' Check for errors in a response from a GET or POST request.
+ The response argument contains a response object from a GET or POST
+ request. The prefix argument contains the prefix to put into the
+ error message.
+
+ Raises:
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ '''
+ if not response.ok:
+ if 'Unauthorized' in response.reason:
+ # Check for 'Unauthorized' User error because this is how
+ # CVP responds to a logged out users requests in 2018.x.
+ msg = '%s: Request Error: %s' % (prefix, response.reason)
+ self.log.error(msg)
+ raise CvpApiError(msg)
+ if 'User is unauthorized' in response.text:
+ # Check for 'User is unauthorized' response text because this
+ # is how CVP responds to a logged out users requests in 2019.x.
+ msg = '%s: Request Error: User is unauthorized' % prefix
+ self.log.error(msg)
+ raise CvpApiError(msg)
+ else:
+ msg = '%s: Request Error: %s - %s' % (prefix, response.reason,
+ response.text)
+ self.log.error(msg)
+ raise CvpRequestError(msg)
+
+ if 'LOG OUT MESSAGE' in response.text:
+ msg = ('%s: Request Error: session logged out' % prefix)
+ raise CvpSessionLogOutError(msg)
+
+ joutput = json_decoder(response.text)
+ err_code_val = self._finditem(joutput, 'errorCode')
+ if err_code_val:
+ if 'errorMessage' in joutput:
+ err_msg = joutput['errorMessage']
+ else:
+ if 'errors' in joutput:
+ error_list = joutput['errors']
+ else:
+ error_list = [joutput['errorCode']]
+ # Build the error message from all the errors.
+ err_msg = error_list[0]
+ for idx in range(1, len(error_list)):
+ err_msg = '%s\n%s' % (err_msg, error_list[idx])
+
+ msg = ('%s: Request Error: %s' % (prefix, err_msg))
+ self.log.error(msg)
+ raise CvpApiError(msg)
+
+ def _check_response_status(self, response, prefix):
+ ''' Check for status OK in a response from a GET or POST request.
+ The response argument contains a response object from a GET or POST
+ request. The prefix argument contains the prefix to put into the
+ error message.
+
+ Raises:
+ CvpRequestError: A CvpRequestError is raised if request
+ response status is not OK.
+ '''
+ if not response.ok:
+ msg = '%s: Request Error: %s - %s' % (prefix, response.reason,
+ response.text)
+ self.log.error(msg)
+ raise CvpRequestError(msg)
+
+ def _login(self):
+ ''' Make a POST request to CVP login authentication.
+ An error can be raised from the post method call or the
+ _is_good_response method call. Any errors raised would be a good
+ reason not to use this host.
+
+ Raises:
+ ConnectionError: A ConnectionError is raised if there was a
+ network problem (e.g. DNS failure, refused connection, etc)
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ HTTPError: A HTTPError is raised if there was an invalid HTTP
+ response.
+ ReadTimeout: A ReadTimeout is raised if there was a request
+ timeout when reading from the connection.
+ Timeout: A Timeout is raised if there was a request timeout.
+ TooManyRedirects: A TooManyRedirects is raised if the request
+ exceeds the configured number of maximum redirections
+ ValueError: A ValueError is raised when there is no valid
+ CVP session. This occurs because the previous get or post
+ request failed and no session could be established to a
+ CVP node. Destroy the class and re-instantiate.
+ '''
+ # Remove any previous session id from the headers
+ self.headers.pop('APP_SESSION_ID', None)
+ if self.api_token is not None:
+ return self._set_headers_api_token()
+ elif self.is_cvaas:
+ raise CvpLoginError('CVaaS only supports API token authentication.'
+ ' Please create an API token and provide it'
+ ' via the api_token parameter in combination'
+ ' with the is_cvaas parameter')
+ return self._login_on_prem()
+
+ def _login_on_prem(self):
+ ''' Make a POST request to CVP login authentication.
+ An error can be raised from the post method call or the
+ _is_good_response method call. Any errors raised would be a good
+ reason not to use this host.
+
+ Raises:
+ ConnectionError: A ConnectionError is raised if there was a
+ network problem (e.g. DNS failure, refused connection, etc)
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ HTTPError: A HTTPError is raised if there was an invalid HTTP
+ response.
+ ReadTimeout: A ReadTimeout is raised if there was a request
+ timeout when reading from the connection.
+ Timeout: A Timeout is raised if there was a request timeout.
+ TooManyRedirects: A TooManyRedirects is raised if the request
+ exceeds the configured number of maximum redirections
+ ValueError: A ValueError is raised when there is no valid
+ CVP session. This occurs because the previous get or post
+ request failed and no session could be established to a
+ CVP node. Destroy the class and re-instantiate.
+ '''
+ url = self.url_prefix + '/login/authenticate.do'
+ response = self.session.post(url,
+ data=json.dumps(self.authdata),
+ headers=self.headers,
+ timeout=self.connect_timeout,
+ verify=self.cert)
+ self._is_good_response(response, 'Authenticate: %s' % url)
+
+ self.cookies = response.cookies
+ self.headers['APP_SESSION_ID'] = response.json()['sessionId']
+
+ def _set_headers_api_token(self):
+ ''' Sets headers with API token instead of making a call to login API.
+ '''
+ # If using an API token there is no need to run a Login API.
+ # Simply add the token into the headers or cookies
+ self.headers['Authorization'] = 'Bearer %s' % self.api_token
+ # Alternative to adding token to headers it can be added to
+ # cookies as shown below.
+ # self.cookies = {'access_token': self.api_token}
+
+ def logout(self):
+ '''
+
+ :return:
+ '''
+ response = self.post('/login/logout.do')
+ if response['data'] == 'success':
+ self.log.info('User logged out.')
+ self.session = None
+ else:
+ err = 'Error trying to logout %s' % response
+ self.log.error(err)
+
+ def _make_request(self, req_type, url, timeout, data=None,
+ files=None):
+ ''' Make a GET, POST or DELETE request to CVP. If the request call raises a
+ timeout or CvpSessionLogOutError then the request will be retried
+ on the same CVP node. Otherwise the request will be tried on the
+ next CVP node.
+
+ Args:
+ req_type (str): Either 'GET', 'POST' or 'DELETE'.
+ url (str): Portion of request URL that comes after the host.
+ timeout (int): Number of seconds the client will wait between
+ bytes sent from the server.
+ data (dict): Dict of key/value pairs to pass as parameters into
+ the request. Default is None.
+ files (dict): Dict of file name to files for upload. Currently
+ only used for adding images to CVP. Default is None.
+
+ Returns:
+ The JSON response.
+
+ Raises:
+ ConnectionError: A ConnectionError is raised if there was a
+ network problem (e.g. DNS failure, refused connection, etc)
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ HTTPError: A HTTPError is raised if there was an invalid HTTP
+ response.
+ ReadTimeout: A ReadTimeout is raised if there was a request
+ timeout when reading from the connection.
+ Timeout: A Timeout is raised if there was a request timeout.
+ TooManyRedirects: A TooManyRedirects is raised if the request
+ exceeds the configured number of maximum re-directions
+ ValueError: A ValueError is raised when there is no valid
+ CVP session. This occurs because the previous get, post
+ or delete request failed and no session could be
+ established to a CVP node. Destroy the class and
+ re-instantiate.
+ JSONDecodeError: A JSONDecodeError is raised when the response
+ content contains invalid JSON. Potentially in the case of
+ Resource APIs that will return Stream JSON format with
+ multiple object or in the case where the response contains
+ incomplete JSON.
+ '''
+ # pylint: disable=too-many-branches
+ # pylint: disable=too-many-statements
+ # pylint: disable=too-many-arguments
+ # pylint: disable=raising-bad-type
+ if not self.session:
+ raise ValueError('No valid session to CVP node')
+ # Keep note of which node is handling this request.
+ self._last_used_node = re.match('http[s]?://(.*):',
+ self.url_prefix).group(1)
+ # Retry the request for the number of nodes.
+ response = None
+ for node_num in range(self.node_cnt):
+ # Set full URL based on current node
+ if '/api/' in url or '/cvpservice/' in url:
+ full_url = self.url_prefix_short + url
+ elif self.is_cvaas:
+ # For CVaaS use cvpservice instead of web or api
+ full_url = self.url_prefix_short + '/cvpservice' + url
+ else:
+ full_url = self.url_prefix + url
+ try:
+ response = self._send_request(req_type, full_url, timeout,
+ data, files)
+ except CvpApiError as error:
+ # If this is not an Unauthorized CvpApiError raise the error
+ # 'Unauthorized' is for 2018.x
+ # 'User is unauthorized' is for 2019.x
+ if ('Unauthorized' not in error.msg and
+ 'User is unauthorized' not in error.msg):
+ raise error
+ # If this is the final CVP node raise error
+ if node_num + 1 == self.node_cnt:
+ raise error
+ # Create a new session to retry on another CVP node.
+ self._create_session()
+ # Verify that we can connect to at least one node
+ # otherwise raise the last error
+ if not self.session:
+ raise error
+ continue
+ except (ConnectionError, HTTPError, TooManyRedirects, ReadTimeout,
+ Timeout, CvpSessionLogOutError) as error:
+ # If this is the final CVP node raise error
+ if node_num + 1 == self.node_cnt:
+ raise error
+ # Create a new session to retry on another CVP node.
+ self._create_session()
+ # Verify that we can connect to at least one node
+ # otherwise raise the last error
+ if not self.session:
+ raise error
+ continue
+ break
+
+ if not response:
+ self.log.debug('Received no response for request %s %s',
+ req_type, url)
+ return None
+
+ # Added check for response.content being 'null' because of the
+ # service account APIs being a special case /services/ API that
+ # returns a null string for no objects instead of an empty string.
+ if not response.content or response.content == b'null':
+ return {'data': []}
+
+ try:
+ resp_data = response.json()
+ if (resp_data is not None and 'result' in resp_data
+ and '/resources/' in full_url):
+ # Resource APIs use JSON streaming and will return
+ # multiple JSON objects during GetAll type API
+ # calls. We are wrapping the multiple objects into
+ # a key "data" and we also return a dictionary with
+ # key "data" as an empty dict for no data. This
+ # checks and keeps consistent the "data" key wrapper
+ # for a Resource API GetAll that returns a single
+ # object.
+ return {'data': [resp_data]}
+ return resp_data
+ except JSONDecodeError as error:
+ # Truncate long error messages
+ err_str = str(error)
+ if len(err_str) > 700:
+ err_str = f"{err_str[:300]}[... truncated ...]" \
+ f" {err_str[-300:]}"
+ self.log.debug('Error trying to decode request response - %s',
+ err_str)
+ if 'Extra data' in str(error):
+ self.log.debug('Found multiple objects or NO objects in'
+ 'response data. Attempt to decode')
+ decoded_data = json_decoder(response.text)
+ return {'data': decoded_data}
+ else:
+ self.log.error('Unknown format for JSONDecodeError - %s',
+ err_str)
+ raise error
+
+ def _send_request(self, req_type, full_url, timeout, data=None,
+ files=None):
+ ''' Make a GET, POST or DELETE request to CVP. If the request call
+ raises a timeout or CvpSessionLogOutError then the request will be
+ retried on the same CVP node. Otherwise the request will be tried
+ on the next CVP node.
+
+ Args:
+ req_type (str): Either 'GET', 'POST' or 'DELETE'.
+ full_url (str): Portion of request URL that comes after the
+ host.
+ timeout (int): Number of seconds the client will wait between
+ bytes sent from the server.
+ data (dict): Dict of key/value pairs to pass as parameters into
+ the request. Default is None.
+ files (dict): Dict of file name to files for upload. Currently
+ only used for adding images to CVP. Default is None.
+
+ Returns:
+ The JSON response.
+
+ Raises:
+ ConnectionError: A ConnectionError is raised if there was a
+ network problem (e.g. DNS failure, refused connection, etc)
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ HTTPError: A HTTPError is raised if there was an invalid HTTP
+ response.
+ ReadTimeout: A ReadTimeout is raised if there was a request
+ timeout when reading from the connection.
+ Timeout: A Timeout is raised if there was a request timeout.
+ TooManyRedirects: A TooManyRedirects is raised if the request
+ exceeds the configured number of maximum re-directions
+ ValueError: A ValueError is raised when there is no valid
+ CVP session. This occurs because the previous get, post
+ or delete request failed and no session could be
+ established to a CVP node. Destroy the class and
+ re-instantiate.
+ '''
+ # pylint: disable=too-many-branches
+ # pylint: disable=too-many-statements
+ # pylint: disable=too-many-arguments
+ # pylint: disable=raising-bad-type
+ # For get or post requests apply both the connect and read timeout.
+ timeout = (self.connect_timeout, timeout)
+ for req_try in range(self.NUM_RETRY_REQUESTS):
+ try:
+ if req_type == 'GET':
+ response = self.session.get(full_url,
+ cookies=self.cookies,
+ headers=self.headers,
+ timeout=timeout,
+ verify=self.cert)
+ elif req_type == 'POST':
+ if files is None:
+ response = self.session.post(full_url,
+ cookies=self.cookies,
+ data=json.dumps(data),
+ headers=self.headers,
+ timeout=timeout,
+ verify=self.cert)
+ else:
+ fhs = dict()
+ fhs['Accept'] = self.headers['Accept']
+ if 'APP_SESSION_ID' in self.headers:
+ fhs['APP_SESSION_ID'] = self.headers[
+ 'APP_SESSION_ID']
+ if 'Authorization' in self.headers:
+ fhs['Authorization'] = self.headers[
+ 'Authorization']
+ response = self.session.post(full_url,
+ cookies=self.cookies,
+ headers=fhs,
+ timeout=timeout,
+ verify=self.cert,
+ files=files)
+ elif req_type == 'DELETE':
+ response = self.session.delete(full_url,
+ cookies=self.cookies,
+ data=json.dumps(data),
+ headers=self.headers,
+ timeout=timeout,
+ verify=self.cert)
+ except (ConnectionError, HTTPError, TooManyRedirects) as error:
+ # Any of these errors is a good reason to try another CVP node
+ self.log.error(error)
+ raise error
+ except (ReadTimeout, Timeout) as error:
+ self.log.debug(error)
+ # If there was a timeout and this is not the final try,
+ # retry this request to the same node. If this is the final
+ # try raise the error so another CVP node can be tried
+ if req_try + 1 == self.NUM_RETRY_REQUESTS:
+ raise error
+ continue
+
+ try:
+ self._is_good_response(response, '%s: %s ' %
+ (req_type, full_url))
+ except CvpSessionLogOutError as error:
+ self.log.debug(error)
+ # Retry the request to the same node if there was a CVP session
+ # logout. Reset the session which will login. If a valid
+ # session comes back then clear the error so this request will
+ # be retried on the same node.
+ if req_try + 1 == self.NUM_RETRY_REQUESTS:
+ raise error
+ else:
+ self._reset_session()
+ if not self.session:
+ raise error
+ continue
+ except CvpApiError as error:
+ self.log.debug(error)
+ if ('Unauthorized' in error.msg or
+ 'User is unauthorized' in error.msg):
+ # Retry the request to the same node if there was an
+ # Unauthorized User error because this is how CVP responds
+ # to a logged out users requests in 2017.1.
+ # Check for 'User is unauthorized' in error because this is
+ # how CVP responds to a logged out user requests in 2019.x.
+ # Reset the session which will login. If a valid
+ # session comes back then clear the error so this request
+ # will be retried on the same node.
+ if req_try + 1 == self.NUM_RETRY_REQUESTS:
+ raise error
+ else:
+ self._reset_session()
+ if not self.session:
+ raise error
+ continue
+ else:
+ # pylint: disable=raising-bad-type
+ raise error
+ return response
+
+ def get(self, url, timeout=30):
+ ''' Make a GET request to CVP. If the request call raises an error
+ or if the JSON response contains a CVP session related error then
+ retry the request on another CVP node.
+
+ Args:
+ url (str): Portion of request URL that comes after the host.
+ timeout (int): Number of seconds the client will wait between
+ bytes sent from the server. Default value is 30 seconds.
+
+ Returns:
+ The JSON response.
+
+ Raises:
+ ConnectionError: A ConnectionError is raised if there was a
+ network problem (e.g. DNS failure, refused connection, etc)
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ HTTPError: A HTTPError is raised if there was an invalid HTTP
+ response.
+ ReadTimeout: A ReadTimeout is raised if there was a request
+ timeout when reading from the connection.
+ Timeout: A Timeout is raised if there was a request timeout.
+ TooManyRedirects: A TooManyRedirects is raised if the request
+ exceeds the configured number of maximum re-directions
+ ValueError: A ValueError is raised when there is no valid
+ CVP session. This occurs because the previous get, post
+ or delete request failed and no session could be
+ established to a CVP node. Destroy the class and
+ re-instantiate.
+ '''
+ return self._make_request('GET', url, timeout)
+
+ def post(self, url, data=None, files=None, timeout=30):
+ ''' Make a POST request to CVP. If the request call raises an error
+ or if the JSON response contains a CVP session related error then
+ retry the request on another CVP node.
+
+ Args:
+ url (str): Portion of request URL that comes after the host.
+ data (dict): Dict of key/value pairs to pass as parameters into
+ the request. Default is None.
+ files (dict): Dict of file name to files for upload. Currently
+ only used for adding images to CVP. Default is None.
+ timeout (int): Number of seconds the client will wait between
+ bytes sent from the server. Default value is 30 seconds.
+
+ Returns:
+ The JSON response.
+
+ Raises:
+ ConnectionError: A ConnectionError is raised if there was a
+ network problem (e.g. DNS failure, refused connection, etc)
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ HTTPError: A HTTPError is raised if there was an invalid HTTP
+ response.
+ ReadTimeout: A ReadTimeout is raised if there was a request
+ timeout when reading from the connection.
+ Timeout: A Timeout is raised if there was a request timeout.
+ TooManyRedirects: A TooManyRedirects is raised if the request
+ exceeds the configured number of maximum re-directions
+ ValueError: A ValueError is raised when there is no valid
+ CVP session. This occurs because the previous get, post
+ or delete request failed and no session could be
+ established to a CVP node. Destroy the class and
+ re-instantiate.
+ '''
+ return self._make_request('POST', url, timeout, data=data, files=files)
+
+ def delete(self, url, data=None, timeout=30):
+ ''' Make a DELETE request to CVP. If the request call raises an error
+ or if the JSON response contains a CVP session related error then
+ retry the request on another CVP node.
+
+ Args:
+ url (str): Portion of request URL that comes after the host.
+ data (dict): Dict of key/value pairs to pass as parameters into
+ the request. Default is None.
+ timeout (int): Number of seconds the client will wait between
+ bytes sent from the server. Default value is 30 seconds.
+
+ Returns:
+ The JSON response.
+
+ Raises:
+ ConnectionError: A ConnectionError is raised if there was a
+ network problem (e.g. DNS failure, refused connection, etc)
+ CvpApiError: A CvpApiError is raised if there was a JSON error.
+ CvpRequestError: A CvpRequestError is raised if the request
+ is not properly constructed.
+ CvpSessionLogOutError: A CvpSessionLogOutError is raised if
+ response from server indicates session was logged out.
+ HTTPError: A HTTPError is raised if there was an invalid HTTP
+ response.
+ ReadTimeout: A ReadTimeout is raised if there was a request
+ timeout when reading from the connection.
+ Timeout: A Timeout is raised if there was a request timeout.
+ TooManyRedirects: A TooManyRedirects is raised if the request
+ exceeds the configured number of maximum re-directions
+ ValueError: A ValueError is raised when there is no valid
+ CVP session. This occurs because the previous get, post
+ or delete request failed and no session could be
+ established to a CVP node. Destroy the class and
+ re-instantiate.
+ '''
+ return self._make_request('DELETE', url, timeout, data=data)
+
+ def _finditem(self, obj, key):
+ """ Find a key in a a nested list/dict.
+
+ Args:
+ obj (dict): Object to iterate to return value for provided key
+ key (str): The key to locate in dict and return the value for
+
+ Returns:
+ Value of found key or None if not found.
+ """
+ item = None
+ if isinstance(obj, dict):
+ if key in obj:
+ item = obj[key]
+ else:
+ for _, value in obj.items():
+ if isinstance(value, (dict, list)):
+ item = self._finditem(value, key)
+ if item is not None:
+ break
+ elif isinstance(obj, list):
+ for i in obj:
+ if isinstance(i, (dict, list)):
+ item = self._finditem(i, key)
+ if item is not None:
+ break
+ return item
+
+
+def json_decoder(data):
+ ''' Check for ...
+ '''
+ decoder = json.JSONDecoder()
+ position = 0
+ decoded_data = []
+ while True:
+ try:
+ obj, position = decoder.raw_decode(data, position)
+ decoded_data.append(obj)
+ position += 1
+ except ValueError:
+ break
+ if len(decoded_data) == 1:
+ return decoded_data[0]
+ return decoded_data