def _request(self, token, url): """ Perform a request and place the token header inside the request header :param token: A valid token :param url: The URL to perform a request :return: Request Object """ headers = {'Accept': 'application/json', 'X-SDS-AUTH-TOKEN': token} try: return self.session.get(url, verify=self.verify_ssl, headers=headers, timeout=self.request_timeout) except requests.ConnectionError as conn_err: msg = 'Connection error: {0}'.format(conn_err.args) log.error(msg) raise ECSClientException(msg) except requests.HTTPError as http_err: msg = 'HTTP error: {0}'.format(http_err.args) log.error(msg) raise ECSClientException(msg) except requests.RequestException as req_err: msg = 'Request error: {0}'.format(req_err.args) log.error(msg) raise ECSClientException(msg)
def _request(self, url, json_payload='{}', http_verb='GET', params=None): json_payload = json.dumps(json_payload) try: if http_verb == "PUT": req = self._session.put( self._construct_url(url), verify=self.verify_ssl, headers=self._fetch_headers(), timeout=self.request_timeout, data=json_payload) elif http_verb == 'POST': req = self._session.post( self._construct_url(url), verify=self.verify_ssl, headers=self._fetch_headers(), timeout=self.request_timeout, data=json_payload) elif http_verb == 'DELETE': # Need to follow up - if 'accept' is in the headers # delete calls are not working because ECS 2.0 is returning # XML even if JSON is specified headers = self._fetch_headers() del headers['Accept'] req = self._session.delete( self._construct_url(url), verify=self.verify_ssl, headers=headers, timeout=self.request_timeout, params=params) else: # Default to GET req = self._session.get( self._construct_url(url), verify=self.verify_ssl, headers=self._fetch_headers(), timeout=self.request_timeout, params=params) # Because some delete actions in the API return HTTP/1.1 204 No Content if not (200 <= req.status_code < 300): log.error("Status code NOT OK") raise ECSClientException.from_response(req) try: return req.json() except ValueError: return req.text except requests.ConnectionError as conn_err: msg = 'Connection error: {0}'.format(conn_err.args) log.error(msg) raise ECSClientException(message=msg) except requests.HTTPError as http_err: msg = 'HTTP error: {0}'.format(http_err.args) log.error(msg) raise ECSClientException(message=msg) except requests.RequestException as req_err: msg = 'Request error: {0}'.format(req_err.args) log.error(msg) raise ECSClientException(message=msg)
def __init__(self, username=None, password=None, token=None, ecs_endpoint=None, token_endpoint=None, verify_ssl=False, token_path='/tmp/ecsclient.tkn', request_timeout=15.0, cache_token=True, override_header=None): """ Creates the ECSClient class that the client will directly work with :param username: The username to fetch a token :param password: The password to fetch a token :param token: Supply a valid token to use instead of username/password :param ecs_endpoint: The URL where ECS is located :param token_endpoint: The URL where the ECS login is located :param verify_ssl: Verify SSL certificates :param token_path: Path to the cached token file :param request_timeout: How long to wait for ECS to respond :param cache_token: Whether to cache the token, by default this is true you should only switch this to false when you want to directly fetch a token for a user :param override_header: X-EMC-Override header value into API calls """ if not ecs_endpoint: raise ECSClientException("Missing 'ecs_endpoint'") self.token_endpoint = token_endpoint if token_endpoint: if not (username and password): raise ECSClientException("'token_endpoint' provided but missing ('username','password')") self.token_endpoint = self.token_endpoint.rstrip('/') else: if not (token or os.path.isfile(token_path)): raise ECSClientException("'token_endpoint' not provided and missing 'token'|'token_path'") self.override_header = override_header self.username = username self.password = password self.token = token self.ecs_endpoint = ecs_endpoint.rstrip('/') self.verify_ssl = verify_ssl self.token_path = token_path self.request_timeout = request_timeout self.cache_token = cache_token self._session = requests.Session() self._token_request = TokenRequest( username=self.username, password=self.password, ecs_endpoint=self.ecs_endpoint, token_endpoint=self.token_endpoint, verify_ssl=self.verify_ssl, token_path=self.token_path, request_timeout=self.request_timeout, cache_token=self.cache_token) # Authentication self.authentication = Authentication(self)
def get_token(self): """ Attempt to get an existing token, if successful then ensure it hasn't expired yet. If its expired, fetch a new token :return: A token """ token = self._get_existing_token() if not token: log.debug("No Token found getting new one") return self.get_new_token() # FIXME: Avoid validation at every call log.debug("Validating token") req = self._request(token, self.token_verification_endpoint) if req.status_code == 200: log.debug("Token validated successfully") return token elif req.status_code in (401, 403, 415): msg = "Invalid token. Trying to get a new one (Code: {})".format( req.status_code) log.warning(msg) return self.get_new_token() else: # i.e. 500 or unknown raise an exception msg = "Token validation error (Code: {})".format(req.status_code) log.error(msg) raise ECSClientException.from_response(req, message=msg)
def do_ping(): if c: msg = " (CTRL-C to break)" else: msg = "" o("Pinging endpoint {}...{}".format(conf.api_endpoint, msg)) pinging = True while pinging is True: try: resp_dict = conf.api_client.user_info.whoami() if resp_dict is not None: if resp_dict['common_name'] is not None: o('PONG: api_endpoint={} username={} {}'.format( conf.api_endpoint, resp_dict['common_name'], conf.diag_dt_status_text())) if x: pinging = False else: raise ECSClientException( "Unexpected response from API") except requests.ConnectionError or httplib.HTTPException: o("FAIL: API service unavailable {}".format( conf.diag_dt_status_text())) try: del conf.api_client if not c: sys.exit(1) except AssertionError: if not c: sys.exit(1) except ECSClientException as e: if 'Connection refused' in e.message: o('WAIT: API service is not alive. This is likely temporary.' ) elif 'connection failed' in e.message: o('WAIT: API service is alive but ECS is not. This is likely temporary.' ) elif 'Invalid username or password' in e.message: o('WAIT: Invalid username or password. If ECS authsvc is bootstrapping, this is likely temporary.' ) elif 'Non-200' in e.message: o('WAIT: ECS API internal error. If ECS services are still bootstrapping, this is likely temporary.' ) elif 'Read timed out' in e.message: o('WAIT: ECS API timed out. If ECS services are still bootstrapping, this is likely temporary.' ) else: o('FAIL: Unexpected response from API client: {0}'.format( e)) if not c: raise if not c: pinging = False if c and pinging is True: time.sleep(w)
def get_new_token(self): """ Request a new authentication token from ECS and persist it to a file for future usage if cache_token is true :return: Returns a valid token, or None if failed """ log.info("Getting new token") self.session.auth = (self.username, self.password) req = self.session.get(self.token_endpoint, verify=self.verify_ssl, headers={'Accept': 'application/json'}, timeout=self.request_timeout) if req.status_code == 401: msg = 'Invalid username or password' log.fatal(msg) raise ECSClientException.from_response(req, message=msg) if req.status_code != 200: msg = 'Non-200 status returned ({0})'.format(req.status_code) log.fatal(msg) raise ECSClientException.from_response(req, message=msg) self.token = req.headers['x-sds-auth-token'] if self.cache_token: log.debug("Caching token to '{0}'".format(self.token_path)) token_dir = os.path.dirname(os.path.abspath(self.token_path)) if not os.path.isdir(token_dir): raise ECSClientException('Token directory not found') with open(self.token_path, 'w') as token_file: token_file.write(self.token) return self.token
def test_format(self): test_kwargs = { 'ecs_description': 'Error description', 'ecs_details': 'Error details', 'http_scheme': 'https', 'http_host': 'localhost', 'http_port': 4443, 'http_path': '/path', 'http_query': 'param1=value1', 'http_status': 500, 'http_reason': 'Reason', 'http_response_content': 'Response content' } for key, value in test_kwargs.items(): kwargs = {key: value} exc = ECSClientException('test', **kwargs) self.assertIn(str(value), str(exc))
def test_attrs(self): test_kwargs = { 'ecs_code': '21000', 'ecs_retryable': True, 'ecs_description': 'Error description', 'ecs_details': 'Error details', 'http_scheme': 'https', 'http_host': 'localhost', 'http_port': 4443, 'http_path': '/path', 'http_query': 'param1=value1', 'http_status': 500, 'http_reason': 'Reason', 'http_response_content': 'Response content', 'http_response_headers': [{'x-header-1': 'value1'}] } exc = ECSClientException('test', **test_kwargs) for key, value in test_kwargs.items(): self.assertIs(True, hasattr(exc, key)) self.assertEqual(getattr(exc, key), value)
def test_from_response(self): response_content = '{"code": "1000", "retryable": false, ' \ '"description": "Error description", ' \ '"details": "Error details"}' self.requests_mock.register_uri('POST', self.TEST_URL, status_code=500, text=response_content) resp = requests.post(self.TEST_URL, data='body') exc = ECSClientException.from_response(resp) self.assertEqual(exc.ecs_code, '1000') self.assertEqual(exc.ecs_retryable, False) self.assertEqual(exc.ecs_description, 'Error description') self.assertEqual(exc.ecs_details, 'Error details') self.assertEqual(exc.http_scheme, 'https') self.assertEqual(exc.http_host, '127.0.0.1') self.assertEqual(exc.http_port, 4443) self.assertEqual(exc.http_path, '/foor/bar') self.assertEqual(exc.http_query, 'param1=value1¶m2=value2') self.assertEqual(exc.http_status, 500) self.assertEqual(exc.http_reason, None) self.assertEqual(exc.http_response_content, response_content)