def _authenticate(self, timeout: int, retries: int, retry_wait: int): """ Authenticates with the device and retrieves a session token to be used in actual requests Args: timeout: The timeout to use when establishing the connection retries: How many times to retry to connect to the device if it fails retry_wait: Time in seconds to wait between retries """ # URL to authenticate and receive the token url = f"{self.base_url}/mgmt/shared/authn/login" payload = { 'username': self.username, 'password': self.password, 'loginProviderName': self._auth_provider } iCRS = iControlRESTSession(self.username, self.password, timeout=timeout, verify=self.verify) log.info("Connecting to '%s'", self.device.name) response = iCRS.post( url, json=payload, ) log.debug(response.json()) if response.status_code != 200: if b'Configuration Utility restarting...' in response.content: if retries > 0: time.sleep(retry_wait) return self._authenticate(timeout, retries - 1, retry_wait) else: raise iControlUnexpectedHTTPError( f"Failed to connect to {self.device.name}: " f"{response.content}") else: raise iControlUnexpectedHTTPError( f"Failed to authenticate with {self.device.name}") self.token = response.json()['token']['token'] log.debug("The following token is used to connect: '%s'", self.token)
def wrapper(self, RIC_base_uri, **kwargs): partition = kwargs.pop('partition', '') name = kwargs.pop('name', '') suffix = kwargs.pop('suffix', '') uri_as_parts = kwargs.pop('uri_as_parts', False) if uri_as_parts: REST_uri = generate_bigip_uri(RIC_base_uri, partition, name, suffix, **kwargs) else: REST_uri = RIC_base_uri pre_message = "%s WITH uri: %s AND suffix: %s AND kwargs: %s" %\ (method.__name__, REST_uri, suffix, kwargs) print("About to log pre_message.") logging.info(pre_message) response = method(self, REST_uri, **kwargs) post_message =\ "RESPONSE::STATUS: %s Content-Type: %s Content-Encoding:"\ " %s\nText: %r" % (response.status_code, response.headers.get('Content-Type', None), response.headers.get('Content-Encoding', None), response.text) logging.debug(post_message) if response.status_code not in range(200, 207): error_message = '%s Unexpected Error: %s for uri: %s\nText: %r' %\ (response.status_code, response.reason, response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) return response
def delete(self, api_url, timeout=30, verbose=False): """DELETE REST Command to delete information from the device""" if not self.connected: raise Exception(f"No active connection for '{self.device.name}'") full_url = "{b}{a}".format(b=self.base_url, a=api_url) log.info("Sending Post to '{d}': " "{u}" " with the header '{h}'".format(d=self.device.name, u=full_url, h=self.header)) response = self.icr_session.delete(full_url, timeout=timeout) output = response.text log.debug("Response: {c}, headers: {h}".format(c=response.status_code, h=self.header)) if verbose: log.info("Output received:\n{output}".format(output=output)) # Make sure it returned ok if not response.ok: raise iControlUnexpectedHTTPError( "Connection to '{d}' has returned the " "following code '{c}', instead of the " "expected status code 'ok'".format(d=self.device.name, c=response.status_code)) log.info( "Successfully fetched data from '{d}'".format(d=self.device.name)) return output
def get(self, api_url, timeout=30, verbose=False): """GET REST Command to retrieve information from the device""" full_url = "{b}{a}".format(b=self.base_url, a=api_url) log.info("Sending GET to '{d}': " "{u}".format(d=self.device.name, u=full_url)) response = self.icr_session.get(full_url, timeout=timeout) output = response log.debug("Response: {c}, headers: {h}".format(c=response.status_code, h=self.header)) if verbose: log.info("Output received:\n{output}".format(output=output)) # Make sure it returned ok if not response.ok: raise iControlUnexpectedHTTPError( "Connection to '{d}' has returned the " "following code '{c}', instead of the " "expected status code 'ok'".format(d=self.device.name, c=response.status_code)) log.info( "Successfully fetched data from '{d}'".format(d=self.device.name)) log.info("Successfully fetched data using token: '{t}'".format( t=self.token)) return output
def delete(self, **kwargs): """Custom deletion logic to handle edge cases This shouldn't be needed, but ASM has a tendency to raise various errors that are painful to handle from a customer point-of-view. These errors are especially pronounced when doing things concurrently with asm. The error itself are described in their exception handler To address these failure, we try a number of exception handling cases to catch and reliably deal with the error. :param kwargs: :return: """ ex = iControlUnexpectedHTTPError("Failed to delete the signature") for _ in range(0, 30): try: return self._delete(**kwargs) except iControlUnexpectedHTTPError as ex: if self._check_exception(ex): continue else: raise raise ex
def _get_last_update_micros(self, token): try: last_updated = token['lastUpdateMicros'] created_bigip = int(last_updated) / 1000000.0 except (KeyError, ValueError): raise iControlUnexpectedHTTPError( "lastUpdateMicros field was not found in the response") return created_bigip
def _get_token_from_response(self, respJson): try: token = respJson['token'] self.token = token['token'] except KeyError: raise iControlUnexpectedHTTPError( "Token field not found in the response") return token
def update(self, **kwargs): ex = iControlUnexpectedHTTPError("Failed to delete the signature") for _ in range(0, 30): try: return self._update(**kwargs) except iControlUnexpectedHTTPError as ex: if self._check_exception(ex): continue else: raise raise ex
def store_create(self, **items): try: key = self.derive_key(items) self.__store_create[key] = items except Exception: response = Mock() response.status_code = 409 error = iControlUnexpectedHTTPError( "Key Word 'name' or 'partition' is missing!", response=response) raise error
def test_create_subresource_icontrol_500_exception(bigip, response): """Test create can handle HTTP server exception.""" data = resource_data() subres = SubResource(name=data['name'], partition=data['partition']) response.status_code = 500 bigip.tm.ltm.subresources.subresource.create.side_effect = ([ iControlUnexpectedHTTPError(response=response), None ]) with pytest.raises(cccl_exc.F5CcclError): obj = subres.create(bigip) assert not obj
def test_create_subresource_icontrol_500_exception(bigip, response): u"""Test create can handle HTTP server exception.""" data = resource_data() subres = SubResource(name=data['name'], partition=data['partition']) response.status_code = 500 bigip.tm.ltm.subresources.subresource.create.side_effect = ( [iControlUnexpectedHTTPError(response=response), None] ) with pytest.raises(cccl_exc.F5CcclError): obj = subres.create(bigip) assert not obj
def read_from_create(self, *args, **items): key = self.derive_key(items) retval = MagicMock(spec=f5.bigip.resource.Resource) retval.delete = self.nuke_from_create retval.modify = self.update_create retval.update = self.update_create self.last_key = key try: retval.raw = self.__store_create[key].copy() except KeyError: response = Mock() response.status_code = 404 raise iControlUnexpectedHTTPError("NOT FOUND HERE!", response=response) return retval
def patch(self, api_url, payload, timeout=30, verbose=False): """PATCH REST Command to update information on the device""" if not self.connected: raise Exception( "'{d}' is not connected for " "alias '{a}'".format(d=self.device.name, a=self.alias) ) full_url = "{b}{a}".format(b=self.base_url, a=api_url) log.info( "Sending Post to '{d}': " "{u}" " with the header '{h}'" " and payload '{p}'".format( d=self.device.name, u=full_url, h=self.header, p=payload ) ) response = self.icr_session.patch(full_url, json=payload, timeout=timeout) output = response log.debug( "Response: {c}, headers: {h}".format( c=response.status_code, h=self.header ) ) if verbose: log.info("Output received:\n{output}".format(output=output)) # Make sure it returned ok if not response.ok: raise iControlUnexpectedHTTPError( "Connection to '{d}' has returned the " "following code '{c}', instead of the " "expected status code 'ok'".format( d=self.device.name, c=response.status_code ) ) log.info( "Successfully fetched data from '{d}'".format(d=self.device.name) ) return output
def test_delete_subresource_icontrol_4XX_exception(bigip, response): """Test delete can handle gener HTTP client request exception.""" data = resource_data() subres = SubResource(name=data['name'], partition=data['partition']) response.status_code = 400 bigip.tm.ltm.subresources.subresource.load.return_value = ( bigip.tm.ltm.subresources.subresource) bigip.tm.ltm.subresources.subresource.delete.side_effect = ([ iControlUnexpectedHTTPError(response=response), None ]) with pytest.raises(cccl_exc.F5CcclResourceRequestError): obj = subres.delete(bigip) assert not obj
def test_delete_subresource_icontrol_4XX_exception(bigip, response): u"""Test delete can handle gener HTTP client request exception.""" data = resource_data() subres = SubResource(name=data['name'], partition=data['partition']) response.status_code = 400 bigip.tm.ltm.subresources.subresource.load.return_value = ( bigip.tm.ltm.subresources.subresource ) bigip.tm.ltm.subresources.subresource.delete.side_effect = ( [iControlUnexpectedHTTPError(response=response), None] ) with pytest.raises(cccl_exc.F5CcclResourceRequestError): obj = subres.delete(bigip) assert not obj
def test_delete_subresource_icontrol_404_exception(bigip, response): u"""Test delete can handle HTTP 404 not found exception. A NotFound error should occur when the resource load is performed. """ data = resource_data() subres = SubResource(name=data['name'], partition=data['partition']) response.status_code = 404 bigip.tm.ltm.subresources.subresource.load.side_effect = ( [iControlUnexpectedHTTPError(response=response), None] ) with pytest.raises(cccl_exc.F5CcclResourceNotFoundError): subres.delete(bigip) bigip.tm.ltm.subresources.subresource.delete.assert_not_called()
def test_delete_subresource_icontrol_404_exception(bigip, response): """Test delete can handle HTTP 404 not found exception. A NotFound error should occur when the resource load is performed. """ data = resource_data() subres = SubResource(name=data['name'], partition=data['partition']) response.status_code = 404 bigip.tm.ltm.subresources.subresource.load.side_effect = ([ iControlUnexpectedHTTPError(response=response), None ]) with pytest.raises(cccl_exc.F5CcclResourceNotFoundError): subres.delete(bigip) bigip.tm.ltm.subresources.subresource.delete.assert_not_called()
def _extend_session_ttl(self, ttl: int) -> None: """ Sets the TTL for the active session Args: ttl: The TTL to be set for the session """ # Self-link of the token timeout_url = f"{self.base_url}/mgmt/shared/authz/tokens/{self.token}" timeout_payload = {"timeout": ttl} token_icr_session = iControlRESTSession(self.username, self.password, verify=self.verify, token_to_use=self.token) # Extending the timeout for the token received response = token_icr_session.patch(timeout_url, json=timeout_payload) if response.status_code != 200 or not response.ok: raise iControlUnexpectedHTTPError( "Failed to refresh session: " f"{response.reason} ({response.status_code})") log.debug("Token TTL extended to '%d' seconds", ttl)
def wrapper(self, RIC_base_uri, **kwargs): partition = kwargs.pop('partition', '') sub_path = kwargs.pop('subPath', '') suffix = kwargs.pop('suffix', '') identifier, kwargs = _unique_resource_identifier_from_kwargs(**kwargs) uri_as_parts = kwargs.pop('uri_as_parts', False) transform_name = kwargs.pop('transform_name', False) transform_subpath = kwargs.pop('transform_subpath', False) if uri_as_parts: REST_uri = generate_bigip_uri(RIC_base_uri, partition, identifier, sub_path, suffix, transform_name=transform_name, transform_subpath=transform_subpath, **kwargs) else: REST_uri = RIC_base_uri pre_message = "%s WITH uri: %s AND suffix: %s AND kwargs: %s" %\ (method.__name__, REST_uri, suffix, kwargs) logger = logging.getLogger(__name__) logger.debug(pre_message) response = method(self, REST_uri, **kwargs) post_message =\ "RESPONSE::STATUS: %s Content-Type: %s Content-Encoding:"\ " %s\nText: %r" % (response.status_code, response.headers.get('Content-Type', None), response.headers.get('Content-Encoding', None), response.text) logger.debug(post_message) if response.status_code not in range(200, 207): error_message = '%s Unexpected Error: %s for uri: %s\nText: %r' %\ (response.status_code, response.reason, response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) return response
def load(self, **kwargs): # If delete() was called, then there is no reason to check if something # exists in the loop below. deleted = False if 'deleted' in self.__dict__ and self.__dict__['deleted'] is True: deleted = True if deleted: return self._load(**kwargs) # It is possible that even after creation, ASM will not correctly be # reporting creation of a Session Tracking Status. Therefore, try several # times before we finally fail. ex = iControlUnexpectedHTTPError( "Failed to load the session-tracking-status" ) for _ in range(0, 30): try: return self._load(**kwargs) except iControlUnexpectedHTTPError as ex: if self._check_exception(ex): continue else: raise raise ex
def get_auth_providers(self, netloc): """BIG-IQ specific query for auth providers BIG-IP doesn't really need this because BIG-IP's multiple auth providers seem to handle fallthrough just fine. BIG-IQ on the other hand, needs to have its auth provider specified if you're using one of the non-default ones. :param netloc: :return: """ url = "https://%s/info/system?null" % (netloc) response = requests.get(url, verify=self.verify) if not response.ok or not hasattr(response, "json"): error_message = '%s Unexpected Error: %s for uri: %s\nText: %r' %\ (response.status_code, response.reason, response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) respJson = response.json() result = respJson['providers'] return result
def get_new_token(self, netloc): """Get a new token from BIG-IP and store it internally. Throws relevant exception if it fails to get a new token. This method will be called automatically if a request is attempted but there is no authentication token, or the authentication token is expired. It is usually not necessary for users to call it, but it can be called if it is known that the authentication token has been invalidated by other means. """ login_body = { 'username': self.username, 'password': self.password, 'loginProviderName': self.login_provider_name, } login_url = "https://%s/mgmt/shared/authn/login" % (netloc) req_start_time = time.time() response = requests.post(login_url, json=login_body, verify=False, auth=HTTPBasicAuth(self.username, self.password)) self.attempts += 1 if not response.ok or not hasattr(response, "json"): error_message = '%s Unexpected Error: %s for uri: %s\nText: %r' %\ (response.status_code, response.reason, response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) respJson = response.json() expiration_bigip, created_bigip = None, None try: self.token = respJson['token']['token'] expiration_bigip = int(respJson['token']['expirationMicros']) / \ 1000000.0 created_bigip = int(respJson['token']['lastUpdateMicros']) / \ 1000000.0 except (KeyError, ValueError): error_message = \ '%s Unparseable Response: %s for uri: %s\nText: %r' %\ (response.status_code, response.reason, response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) # Set our token expiration time. # The expirationMicros field is when BIG-IP will expire the token # relative to its local clock. To avoid issues caused by incorrect # clocks or network latency, we'll compute an expiration time that is # referenced to our local clock, and expires slightly before the token # should actually expire on BIG-IP # Reference to our clock: compute for how long this token is valid as # the difference between when it expires and when it was created, # according to BIG-IP. if expiration_bigip < created_bigip: error_message = \ '%s Token already expired: %s for uri: %s\nText: %r' % \ (response.status_code, time.ctime(expiration_bigip), response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) valid_duration = expiration_bigip - created_bigip # Assign new expiration time that is 1 minute earlier than BIG-IP's # expiration time, as long as that would still be at least a minute in # the future. This should account for clock skew between us and # BIG-IP. By default tokens last for 60 minutes so getting one every # 59 minutes instead of 60 is harmless. if valid_duration > 120.0: valid_duration -= 60.0 self.expiration = req_start_time + valid_duration
def get_new_token(self, netloc): """Get a new token from BIG-IP and store it internally. Throws relevant exception if it fails to get a new token. This method will be called automatically if a request is attempted but there is no authentication token, or the authentication token is expired. It is usually not necessary for users to call it, but it can be called if it is known that the authentication token has been invalidated by other means. """ login_body = { 'username': self.username, 'password': self.password, } if self.auth_provider: if self.auth_provider == 'local': login_body['loginProviderName'] = 'local' elif self.auth_provider == 'tmos': login_body['loginProviderName'] = 'tmos' elif self.auth_provider not in ['none', 'default']: providers = self.get_auth_providers(netloc) for provider in providers: if self.auth_provider in provider['link']: login_body['loginProviderName'] = provider['name'] break elif self.auth_provider == provider['name']: login_body['loginProviderName'] = provider['name'] break else: if self.login_provider_name == 'tmos': login_body['loginProviderName'] = self.login_provider_name login_url = "https://%s/mgmt/shared/authn/login" % (netloc) response = requests.post(login_url, json=login_body, verify=self.verify, auth=HTTPBasicAuth(self.username, self.password)) self.attempts += 1 if not response.ok or not hasattr(response, "json"): error_message = '%s Unexpected Error: %s for uri: %s\nText: %r' %\ (response.status_code, response.reason, response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) respJson = response.json() token = self._get_token_from_response(respJson) created_bigip = self._get_last_update_micros(token) try: expiration_bigip = self._get_expiration_micros( token, created_bigip) except (KeyError, ValueError): error_message = \ '%s Unparseable Response: %s for uri: %s\nText: %r' %\ (response.status_code, response.reason, response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response) try: self.expiration = self._get_token_expiration_time( created_bigip, expiration_bigip) except iControlUnexpectedHTTPError: error_message = \ '%s Token already expired: %s for uri: %s\nText: %r' % \ (response.status_code, time.ctime(expiration_bigip), response.url, response.text) raise iControlUnexpectedHTTPError(error_message, response=response)