From 8632495b9a9f77f35154069e69ac320051320514 Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 11:44:50 +0100 Subject: [PATCH 01/12] restructured project, added setup.py and pipenv support --- __init__.py => CHANGELOG.md | 0 Pipfile | 23 ++++++++ README.md | 27 ++++----- fireREST.py => fireREST/__init__.py | 92 ++++++++++++++--------------- pyproject.toml | 17 ++++++ requirements.txt | 3 - schema.py | 10 ---- setup.cfg | 2 + setup.py | 36 +++++++++++ 9 files changed, 137 insertions(+), 73 deletions(-) rename __init__.py => CHANGELOG.md (100%) create mode 100644 Pipfile rename fireREST.py => fireREST/__init__.py (98%) create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 schema.py create mode 100644 setup.cfg create mode 100755 setup.py diff --git a/__init__.py b/CHANGELOG.md similarity index 100% rename from __init__.py rename to CHANGELOG.md diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..079cfe5 --- /dev/null +++ b/Pipfile @@ -0,0 +1,23 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = "*" +PyYAML = "*" +urllib3 = "*" + +[dev-packages] +black = "*" +pytest = "*" +pytest-runner = "*" +mypy = "*" +pep8 = "*" +pycodestyle = "*" +flake8 = "*" +autopep8 = "*" +pre-commit = "*" + +[requires] +python_version = "3.6" diff --git a/README.md b/README.md index 2053e18..235adf8 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,24 @@ FireREST is a simple wrapper for Cisco Firepower Management Center REST API. It exposes various api calls as functions and takes care of authentication, token refresh and paging for large datasets. -## Compatibility +## Requirements -FireREST is compatible with Python3. +* Python >= 3.6 ## Features -* API Authentication (incl. api token refresh & re-authentication after max refresh is reached) -* POST, PUT, GET, DELETE operations for... -** TBD - -* GET operations for... -** TBD +* API Authentication +* Automatic Re-authentication if token expires +* Paging ## Examples -## TODO +n/a + +## Authors + +Oliver Kaiser (oliver.kaiser@outlook.com) + +## License -* Support for api rate limiting -* Provide json validation for implemented api functions -* Test coverage for core functions -* Test coverage for api calls exposed as functions -* Pypi integration +GNU General Public License v3.0 diff --git a/fireREST.py b/fireREST/__init__.py similarity index 98% rename from fireREST.py rename to fireREST/__init__.py index 6bec958..e5cff00 100644 --- a/fireREST.py +++ b/fireREST/__init__.py @@ -57,7 +57,7 @@ class FireREST(object): def __init__(self, hostname=str(), username=str(), password=str(), session=dict(), protocol='https', verify_cert=False, logger=None, domain='Global', timeout=120): - """ + ''' Initialize FireREST object :param hostname: ip address or dns name of fmc :param username: fmc username @@ -70,7 +70,7 @@ def __init__(self, hostname=str(), username=str(), password=str(), session=dict( :param logger: optional logger instance, in case debug logging is needed :param domain: name of the fmc domain. default = Global :param timeout: timeout value for http requests. default = 120 - """ + ''' self.headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -95,11 +95,11 @@ def __init__(self, hostname=str(), username=str(), password=str(), session=dict( @staticmethod def _get_logger(logger: object): - """ + ''' Generate dummy logger in case FireREST has been initialized without a logger :param logger: logger instance :return: dummy logger instance if logger is None, otherwise return logger variable again - """ + ''' if not logger: dummy_logger = logging.getLogger('FireREST') dummy_logger.addHandler(logging.NullHandler()) @@ -107,12 +107,12 @@ def _get_logger(logger: object): return logger def _url(self, namespace='base', path=str()): - """ + ''' Generate URLs on the fly for requests to firepower api :param namespace: name of the url namespace that should be used. options: base, config, auth. default = base :param path: the url path for which a full url should be created :return: url in string format - """ + ''' if namespace == 'config': return '{}://{}{}/domain/{}{}'.format(self.protocol, self.hostname, self.API_CONFIG_URL, self.domain, path) if namespace == 'platform': @@ -124,9 +124,9 @@ def _url(self, namespace='base', path=str()): return '{}://{}{}'.format(self.protocol, self.hostname, path) def _login(self): - """ + ''' Login to fmc api and save X-auth-access-token, X-auth-refresh-token and DOMAINS to variables - """ + ''' request = self._url('auth') try: response = requests.post(request, headers=self.headers, auth=self.cred, verify=self.verify_cert) @@ -163,10 +163,10 @@ def _login(self): self._login() def _refresh(self): - """ + ''' Refresh X-auth-access-token using X-auth-refresh-token. This operation is performed for up to three times, afterwards a re-authentication using _login will be performed - """ + ''' if self.refresh_counter > 2: self.logger.info( 'Authentication token has already been used 3 times, api re-authentication will be performed') @@ -204,12 +204,12 @@ def _refresh(self): @RequestDebugDecorator('DELETE') def _delete(self, request: str, params=None): - """ + ''' DELETE Operation for FMC REST API. In case of authentication issues session will be refreshed :param request: URL of request that should be performed :param params: dict of parameters for http request :return: requests.Response object - """ + ''' if params is None: params = dict() try: @@ -229,13 +229,13 @@ def _delete(self, request: str, params=None): @RequestDebugDecorator('GET') def _get_request(self, request: str, params=None, limit=25): - """ + ''' GET Operation for FMC REST API. In case of authentication issues session will be refreshed :param request: URL of request that should be performed :param params: dict of parameters for http request :param limit: set custom limit for paging. If not set, api will default to 25 :return: requests.Response object - """ + ''' if params is None: params = dict() params['limit'] = limit @@ -255,13 +255,13 @@ def _get_request(self, request: str, params=None, limit=25): return response def _get(self, request: str, params=None, limit=25): - """ + ''' GET Operation that supports paging for FMC REST API. In case of authentication issues session will be refreshed :param request: URL of request that should be performed :param params: dict of parameters for http request :param limit: set custom limit for paging. If not set, api will default to 25 :return: list of requests.Response objects - """ + ''' if params is None: params = dict() responses = list() @@ -279,14 +279,14 @@ def _get(self, request: str, params=None, limit=25): @RequestDebugDecorator('PATCH') def _patch(self, request: str, data: Dict, params=None): - """ + ''' PATCH Operation for FMC REST API. In case of authentication issues session will be refreshed As of FPR 6.2.3 this function is not in use because FMC API does not support PATCH operations :param request: URL of request that should be performed :param data: dictionary of data that will be sent to the api :param params: dict of parameters for http request :return: requests.Response object - """ + ''' if params is None: params = dict() try: @@ -307,13 +307,13 @@ def _patch(self, request: str, data: Dict, params=None): @RequestDebugDecorator('POST') def _post(self, request: str, data: Dict, params=None): - """ + ''' POST Operation for FMC REST API. In case of authentication issues session will be refreshed :param request: URL of request that should be performed :param data: dictionary of data that will be sent to the api :param params: dict of parameters for http request :return: requests.Response object - """ + ''' if params is None: params = dict() try: @@ -334,13 +334,13 @@ def _post(self, request: str, data: Dict, params=None): @RequestDebugDecorator('PUT') def _put(self, request: str, data: Dict, params=None): - """ + ''' PUT Operation for FMC REST API. In case of authentication issues session will be refreshed :param request: URL of request that should be performed :param data: dictionary of data that will be sent to the api :param params: dict of parameters for http request :return: requests.Response object - """ + ''' if params is None: params = dict() try: @@ -360,32 +360,32 @@ def _put(self, request: str, data: Dict, params=None): return response def prepare_json(self, operation: str, obj_type: str, data: Dict): - """ + ''' Prepare json object for api operation :param operation: PUT, POST :param obj_type: see supported types in schema.py :param data: json representing api object :return: sanatized api object - """ + ''' return def valid_json(self, operation: str, obj_type: str, data: Dict): - """ + ''' Validate json object to verify :param operation: PUT, POST :param obj_type: see supported types in schema.py :param data: json representing api object :return: dictionary containing results of json evaluation - """ + ''' return def get_object_id_by_name(self, obj_type: str, obj_name: str): - """ + ''' helper function to retrieve object id by name :param obj_type: object types that will be queried :param obj_name: name of the object :return: object id if object is found, None otherwise - """ + ''' request = '/object/{}'.format(obj_type) url = self._url('config', request) response = self._get(url) @@ -396,11 +396,11 @@ def get_object_id_by_name(self, obj_type: str, obj_name: str): return None def get_device_id_by_name(self, device_name: str): - """ + ''' helper function to retrieve device id by name :param device_name: name of the device :return: device id if device is found, None otherwise - """ + ''' request = '/devices/devicerecords' url = self._url('config', request) response = self._get(url) @@ -411,11 +411,11 @@ def get_device_id_by_name(self, device_name: str): return None def get_device_hapair_id_by_name(self, device_hapair_name: str): - """ + ''' heloer function to retrieve device ha-pair id by name :param device_hapair_name: name of the ha-pair :return: id if ha-pair is found, None otherwise - """ + ''' request = 'devicehapairs/ftddevicehapairs' url = self._url(request) response = self._get(url) @@ -426,11 +426,11 @@ def get_device_hapair_id_by_name(self, device_hapair_name: str): return None def get_nat_policy_id_by_name(self, nat_policy_name: str): - """ + ''' helper function to retrieve nat policy id by name :param nat_policy_name: name of nat policy :return: policy id if nat policy is found, None otherwise - """ + ''' request = '/policy/ftdnatpolicies' url = self._url(request) response = self._get(url) @@ -441,11 +441,11 @@ def get_nat_policy_id_by_name(self, nat_policy_name: str): return None def get_acp_id_by_name(self, policy_name: str): - """ + ''' helper function to retrieve access control policy id by name :param policy_name: name of the access control policy :return: acp id if access control policy is found, None otherwise - """ + ''' request = '/policy/accesspolicies' url = self._url('config', request) response = self._get(url) @@ -456,12 +456,12 @@ def get_acp_id_by_name(self, policy_name: str): return None def get_acp_rule_id_by_name(self, policy_name: str, rule_name: str): - """ + ''' helper function to retrieve access control policy rule id by name :param policy_name: name of the access control policy that will be queried :param rule_name: name of the access control policy rule :return: acp rule id if access control policy rule is found, None otherwise - """ + ''' policy_id = self.get_acp_id_by_name(policy_name) request = '/policy/accesspolicies/{}/accessrules'.format(policy_id) url = self._url('config', request) @@ -473,11 +473,11 @@ def get_acp_rule_id_by_name(self, policy_name: str, rule_name: str): return None def get_syslog_alert_id_by_name(self, syslog_alert_name: str): - """ + ''' helper function to retrieve syslog alert object id by name :param syslog_alert_name: name of syslog alert object :return: syslogalert id if syslog alert is found, None otherwise - """ + ''' response = self.get_syslogalerts() for item in response: for syslog_alert in item.json()['items']: @@ -486,11 +486,11 @@ def get_syslog_alert_id_by_name(self, syslog_alert_name: str): return None def get_snmp_alert_id_by_name(self, snmp_alert_name: str): - """ + ''' helper function to retrieve snmp alert object id by name :param snmp_alert_name: name of snmp alert object :return: snmpalert id if snmp alert is found, None otherwise - """ + ''' response = self.get_snmpalerts() for item in response: for snmp_alert in item.json()['items']: @@ -499,11 +499,11 @@ def get_snmp_alert_id_by_name(self, snmp_alert_name: str): return None def get_domain_id_by_name(self, domain_name: str): - """ + ''' helper function to retrieve domain id from list of domains :param domain_name: name of the domain :return: did if domain is found, None otherwise - """ + ''' for domain in self.domains: if domain['name'] == domain_name: return domain['uuid'] @@ -512,11 +512,11 @@ def get_domain_id_by_name(self, domain_name: str): return None def get_domain_name_by_id(self, domain_id: str): - """ + ''' helper function to retrieve domain name by id :param domain_id: id of the domain :return: name if domain is found, None otherwise - """ + ''' for domain in self.domains: if domain['uuid'] == domain_id: return domain['name'] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7b36491 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.black] +line-length = 120 +skip-string-normalization = true +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3bf4b04..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jsonschema -requests -urllib3 diff --git a/schema.py b/schema.py deleted file mode 100644 index 72c44a2..0000000 --- a/schema.py +++ /dev/null @@ -1,10 +0,0 @@ -network_object = { - "title": "network_object", - "type": "object", - "properties": { - "id": { - "description": "auto-generated unique identifier of api object", - "type": "string" - } - } -} \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b7e4789 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..4c171a6 --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +import setuptools + +from pathlib import Path + +ROOT = Path(__file__).parent + +with open(f'{ROOT}/README.md', 'r') as readme: + long_description = readme.read() + +setuptools.setup( + name='fireREST', + version='0.0.1', + author='Oliver Kaiser', + author_email='oliver.kaiser@outlook.com', + description='Wrapper for Cisco Firepower Management Center REST API', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/kaisero/fireREST.git', + packages=setuptools.find_packages(), + install_requires=['PyYAML', 'requests', 'urllib3'], + setup_requires=['pytest-runner'], + tests_require=['pytest'], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Natural Language :: English', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Operating System :: POSIX', + ], +) From 593d5bf2a7bbf74b0e4086d40fb32601553aeef3 Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 12:35:36 +0100 Subject: [PATCH 02/12] added devel config for flake8 and pre-commit --- .flake8 | 5 +++++ .pre-commit-config.yaml | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7bf302c --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E266, E501, W503, F403, F401 +max-line-length = 120 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8540530 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-ast + - id: check-case-conflict + - id: check-json + - id: check-toml + - id: check-yaml + - id: check-added-large-files + - id: flake8 + - id: double-quote-string-fixer + - repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black From d7a3a5379b44b7a2ed45744a13380386c30c92ae Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 14:02:42 +0100 Subject: [PATCH 03/12] replaced format with f strings --- fireREST/__init__.py | 233 +++++++++++++++++++------------------------ 1 file changed, 101 insertions(+), 132 deletions(-) diff --git a/fireREST/__init__.py b/fireREST/__init__.py index e5cff00..eda6d6c 100644 --- a/fireREST/__init__.py +++ b/fireREST/__init__.py @@ -38,12 +38,12 @@ def wrapped_f(*args): action = self.action logger = args[0].logger request = args[1] - logger.debug('{}: {}'.format(action, request)) + logger.debug(f'{action}: {request}') result = f(*args) status_code = result.status_code - logger.debug('Response Code: {}'.format(status_code)) + logger.debug(f'Response Code: {status_code}') if status_code >= 400: - logger.debug('Error: {}'.format(result.content)) + logger.debug(f'Error: {result.content}') return result return wrapped_f @@ -55,7 +55,7 @@ class FireREST(object): API_PLATFORM_URL = '/api/fmc_platform/v1' API_CONFIG_URL = '/api/fmc_config/v1' - def __init__(self, hostname=str(), username=str(), password=str(), session=dict(), protocol='https', + def __init__(self, hostname: str, username: str, password: str, session=dict(), protocol='https', verify_cert=False, logger=None, domain='Global', timeout=120): ''' Initialize FireREST object @@ -74,7 +74,7 @@ def __init__(self, hostname=str(), username=str(), password=str(), session=dict( self.headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', - 'User-Agent': 'fireREST' + 'User-Agent': 'FireREST/0.0.1' } self.refresh_counter = 0 self.logger = self._get_logger(logger) @@ -114,14 +114,14 @@ def _url(self, namespace='base', path=str()): :return: url in string format ''' if namespace == 'config': - return '{}://{}{}/domain/{}{}'.format(self.protocol, self.hostname, self.API_CONFIG_URL, self.domain, path) + return f'{self.protocol}://{self.hostname}{self.API_CONFIG_URL}/domain/{self.domain}{path}' if namespace == 'platform': - return '{}://{}{}{}'.format(self.protocol, self.hostname, self.API_PLATFORM_URL, path) + return f'{self.protocol}://{self.hostname}{self.API_PLATFORM_URL}{path}' if namespace == 'auth': - return '{}://{}{}'.format(self.protocol, self.hostname, self.API_AUTH_URL) + return f'{self.protocol}://{self.hostname}{self.API_AUTH_URL}' if namespace == 'refresh': - return '{}://{}{}'.format(self.protocol, self.hostname, self.API_REFRESH_URL) - return '{}://{}{}'.format(self.protocol, self.hostname, path) + return f'{self.protocol}://{self.hostname}{self.API_REFRESH_URL}' + return f'{self.protocol}://{self.hostname}{path}' def _login(self): ''' @@ -132,33 +132,32 @@ def _login(self): response = requests.post(request, headers=self.headers, auth=self.cred, verify=self.verify_cert) if response.status_code in (401, 403): - self.logger.error('API Authentication to {} failed.'.format(self.hostname)) - raise FireRESTAuthException('API Authentication to {} failed.'.format(self.hostname)) + self.logger.error(f'API Authentication to {self.hostname} failed.') + raise FireRESTAuthException(f'API Authentication to {self.hostname} failed.') if response.status_code == 429: - msg = 'API Authentication to {} failed due to FMC rate limiting. Backing off for 10 seconds.'\ - .format(self.hostname) + msg = f'API Authentication to {self.hostname} failed due to FMC rate limiting. Backing off for 10 seconds.' raise FireRESTRateLimitException(msg) access_token = response.headers.get('X-auth-access-token', default=None) refresh_token = response.headers.get('X-auth-refresh-token', default=None) if not access_token or not refresh_token: - self.logger.error('Could not retrieve tokens from {}.'.format(request)) - raise FireRESTApiException('Could not retrieve tokens from {}.'.format(request)) + self.logger.error(f'Could not retrieve tokens from {request}.') + raise FireRESTApiException(f'Could not retrieve tokens from {request}.') self.headers['X-auth-access-token'] = access_token self.headers['X-auth-refresh-token'] = refresh_token self.domains = json.loads(response.headers.get('DOMAINS', default=None)) self.refresh_counter = 0 - self.logger.debug('Successfully authenticated to {}'.format(self.hostname)) + self.logger.debug(f'Successfully authenticated to {self.hostname}') except ConnectionRefusedError: - self.logger.error('Could not connect to {}. Connection refused'.format(self.hostname)) + self.logger.error(f'Could not login to {self.hostname}. Connection refused') raise except ConnectionError: - self.logger.error('Could not connect to {}. Max retries exceeded with url: {}' - .format(self.hostname, request)) + self.logger.error(f'Could not login to {self.hostname}. Max retries exceeded with url: {request}') raise except FireRESTRateLimitException: + self.logger.debug(f'Could not login to {self.hostname}. Rate limit exceeded. Backing of for 10 seconds.') sleep(10) self._login() @@ -168,8 +167,7 @@ def _refresh(self): times, afterwards a re-authentication using _login will be performed ''' if self.refresh_counter > 2: - self.logger.info( - 'Authentication token has already been used 3 times, api re-authentication will be performed') + self.logger.info(f'Authentication token expired. Re-authenticating to {self.hostname}') self._login() return @@ -179,28 +177,28 @@ def _refresh(self): response = requests.post(request, headers=self.headers, verify=self.verify_cert) if response.status_code == 429: - msg = 'API Refresh to {} failed due to FMC rate limiting. Backing off for 10 seconds.'\ - .format(self.hostname) + msg = f'API token refresh to {self.hostname} failed due to FMC rate limiting. Backing off for 10 seconds.' raise FireRESTRateLimitException(msg) access_token = response.headers.get('X-auth-access-token', default=None) refresh_token = response.headers.get('X-auth-refresh-token', default=None) if not access_token or not refresh_token: - raise FireRESTAuthRefreshException('Could not refresh tokens from {}. Response Code: {}' - .format(request, response.status_code)) + msg = f'API token refresh to {self.hostname} failed. Response Code: {response.status_code}' + raise FireRESTAuthRefreshException(msg) self.headers['X-auth-access-token'] = access_token self.headers['X-auth-refresh-token'] = refresh_token except ConnectionError: - self.logger.error('Could not connect to {}. Max retries exceeded with url: {}' - .format(self.hostname, request)) + msg = f'Could not connect to {self.hostname}. Max retries exceeded with url: {request}' + self.logger.error(msg) except FireRESTRateLimitException: + self.logger.debug(f'API token refresh to {self.hostname} failed. Rate limit exceeded. Backing of for 10 seconds.') sleep(10) self._login() except FireRESTApiException as exc: self.logger.error(str(exc)) - self.logger.debug('Successfully refreshed authorization token for {}'.format(self.hostname)) + self.logger.debug(f'Successfully refreshed authorization token for {self.hostname}') @RequestDebugDecorator('DELETE') def _delete(self, request: str, params=None): @@ -220,7 +218,7 @@ def _delete(self, request: str, params=None): self._refresh() return self._delete(request, params) if response.status_code == 429: - msg = 'DELETE operation {} failed due to FMC rate limiting. Backing off for 10 seconds.'.format(request) + msg = f'DELETE operation {request} failed due to FMC rate limiting. Backing off for 10 seconds.' raise FireRESTRateLimitException(msg) except FireRESTRateLimitException: sleep(10) @@ -247,7 +245,7 @@ def _get_request(self, request: str, params=None, limit=25): self._refresh() return self._get_request(request, params, limit) if response.status_code == 429: - msg = 'GET operation {} failed due to FMC rate limiting. Backing off for 10 seconds.'.format(request) + msg = f'GET operation {request} failed due to FMC rate limiting. Backing off for 10 seconds.' raise FireRESTRateLimitException(msg) except FireRESTRateLimitException: sleep(10) @@ -277,34 +275,6 @@ def _get(self, request: str, params=None, limit=25): responses.append(response_page) return responses - @RequestDebugDecorator('PATCH') - def _patch(self, request: str, data: Dict, params=None): - ''' - PATCH Operation for FMC REST API. In case of authentication issues session will be refreshed - As of FPR 6.2.3 this function is not in use because FMC API does not support PATCH operations - :param request: URL of request that should be performed - :param data: dictionary of data that will be sent to the api - :param params: dict of parameters for http request - :return: requests.Response object - ''' - if params is None: - params = dict() - try: - response = requests.patch(request, data=json.dumps(data), headers=self.headers, params=params, - verify=self.verify_cert, timeout=self.timeout) - if response.status_code == 401: - if 'Access token invalid' in str(response.json()): - self._refresh() - return self._patch(request, data, params) - if response.status_code == 429: - msg = 'PATCH operation {} failed due to FMC rate limiting. Backing off for 10 seconds.'\ - .format(request) - raise FireRESTRateLimitException(msg) - except FireRESTRateLimitException: - sleep(10) - return self._patch(request, data, params) - return response - @RequestDebugDecorator('POST') def _post(self, request: str, data: Dict, params=None): ''' @@ -324,8 +294,7 @@ def _post(self, request: str, data: Dict, params=None): self._refresh() return self._post(request, data, params) if response.status_code == 429: - msg = 'POST operation {} failed due to FMC rate limiting. Backing off for 10 seconds.'\ - .format(request) + msg = f'POST operation {request} failed due to FMC rate limiting. Backing off for 10 seconds.' raise FireRESTRateLimitException(msg) except FireRESTRateLimitException: sleep(10) @@ -351,8 +320,7 @@ def _put(self, request: str, data: Dict, params=None): self._refresh() return self._put(request, data, params) if response.status_code == 429: - msg = 'PUT operation {} failed due to FMC rate limiting. Backing off for 10 seconds.' \ - .format(request) + msg = f'PUT operation {request} failed due to FMC rate limiting. Backing off for 10 seconds.' raise FireRESTRateLimitException(msg) except FireRESTRateLimitException: sleep(10) @@ -386,7 +354,7 @@ def get_object_id_by_name(self, obj_type: str, obj_name: str): :param obj_name: name of the object :return: object id if object is found, None otherwise ''' - request = '/object/{}'.format(obj_type) + request = f'/object/{obj_type}' url = self._url('config', request) response = self._get(url) for item in response: @@ -463,7 +431,7 @@ def get_acp_rule_id_by_name(self, policy_name: str, rule_name: str): :return: acp rule id if access control policy rule is found, None otherwise ''' policy_id = self.get_acp_id_by_name(policy_name) - request = '/policy/accesspolicies/{}/accessrules'.format(policy_id) + request = f'/policy/accesspolicies/{policy_id}/accessrules' url = self._url('config', request) response = self._get(url) for item in response: @@ -507,8 +475,9 @@ def get_domain_id_by_name(self, domain_name: str): for domain in self.domains: if domain['name'] == domain_name: return domain['uuid'] - self.logger.error('Could not find domain with name {}. Make sure full path is provided'.format(domain_name)) - self.logger.debug('Available Domains: {}'.format(', '.join((domain['name'] for domain in self.domains)))) + self.logger.error(f'Could not find domain with name {domain_name}. Make sure full path is provided') + available_domains = ', '.join((domain['name'] for domain in self.domains)) + self.logger.debug(f'Available Domains: {available_domains}') return None def get_domain_name_by_id(self, domain_id: str): @@ -520,8 +489,9 @@ def get_domain_name_by_id(self, domain_id: str): for domain in self.domains: if domain['uuid'] == domain_id: return domain['name'] - self.logger.error('Could not find domain with id {}. Make sure full path is provided'.format(domain_id)) - self.logger.debug('Available Domains: {}'.format(', '.join((domain['uuid'] for domain in self.domains)))) + self.logger.error(f'Could not find domain with id {domain_id}. Make sure full path is provided') + available_domains = ', '.join((domain['uuid'] for domain in self.domains)) + self.logger.debug(f'Available Domains: {available_domains}') return None def get_system_version(self): @@ -545,12 +515,12 @@ def get_snmpalerts(self): return self._get(url) def create_object(self, object_type: str, data: Dict): - request = '/object/{}'.format(object_type) + request = f'/object/{object_type}' url = self._url('config', request) return self._post(url, data) def get_objects(self, object_type: str, expanded=False): - request = '/object/{}'.format(object_type) + request = f'/object/{object_type}' url = self._url('config', request) params = { 'expanded': expanded @@ -558,17 +528,17 @@ def get_objects(self, object_type: str, expanded=False): return self._get(url, params) def get_object(self, object_type: str, object_id: str): - request = '/object/{}/{}'.format(object_type, object_id) + request = f'/object/{object_type}/{object_id}' url = self._url('config', request) return self._get(url) def update_object(self, object_type: str, object_id: str, data: Dict): - request = '/object/{}/{}'.format(object_type, object_id) + request = f'/object/{object_type}/{object_id}' url = self._url('config', request) return self._put(url, data) def delete_object(self, object_type: str, object_id: str): - request = '/object/{}/{}'.format(object_type, object_id) + request = f'/object/{object_type}/{object_id}' url = self._url('config', request) return self._delete(url) @@ -583,17 +553,17 @@ def get_devices(self): return self._get(url) def get_device(self, device_id: str): - request = '/devices/devicerecords/{}'.format(device_id) + request = f'/devices/devicerecords/{device_id}' url = self._url('config', request) return self._get(url) def update_device(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}'.format(device_id) + request = f'/devices/devicerecords/{device_id}' url = self._url('config', request) return self._put(url, data) def delete_device(self, device_id: str): - request = '/devices/devicerecords/{}'.format(device_id) + request = f'/devices/devicerecords/{device_id}' url = self._url('config', request) return self._delete(url) @@ -608,158 +578,157 @@ def create_device_hapair(self, data: Dict): return self._get(url, data) def get_device_hapair(self, device_hapair_id: str): - request = '/devicehapairs/ftddevicehapairs/{}'.format(device_hapair_id) + request = f'/devicehapairs/ftddevicehapairs/{device_hapair_id}' url = self._url('config', request) return self._get(url) def update_device_hapair(self, data: Dict, device_hapair_id: str): - request = '/devicehapairs/ftddevicehapairs/{}'.format(device_hapair_id) + request = f'/devicehapairs/ftddevicehapairs/{device_hapair_id}' url = self._url('config', request) return self._put(url, data) def delete_device_hapair(self, device_hapair_id: str): - request = '/devicehapairs/ftddevicehapairs/{}'.format(device_hapair_id) + request = f'/devicehapairs/ftddevicehapairs/{device_hapair_id}' url = self._url('config', request) return self._delete(url) def get_ftd_physical_interfaces(self, device_id: str): - request = '/devices/devicerecords/{}/physicalinterfaces'.format( - device_id) + request = f'/devices/devicerecords/{device_id}/physicalinterfaces' url = self._url('config', request) return self._get(url) def get_ftd_physical_interface(self, device_id: str, interface_id: str): - request = '/devices/devicerecords/{}/physicalinterfaces/{}'.format(device_id, interface_id) + request = f'/devices/devicerecords/{device_id}/physicalinterfaces/{interface_id}' url = self._url('config', request) return self._get(url) def update_ftd_physical_interface(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/physicalinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/physicalinterfaces' url = self._url('config', request) return self._put(url, data) def create_ftd_redundant_interface(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/redundantinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/redundantinterfaces' url = self._url('config', request) return self._post(url, data) def get_ftd_redundant_interfaces(self, device_id: str): - request = '/devices/devicerecords/{}/redundantinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/redundantinterfaces' url = self._url('config', request) return self._get(url) def get_ftd_redundant_interface(self, device_id: str, interface_id: str): - request = '/devices/devicerecords/{}/redundantinterfaces/{}'.format(device_id, interface_id) + request = f'/devices/devicerecords/{device_id}/redundantinterfaces/{interface_id}' url = self._url('config', request) return self._get(url) def update_ftd_redundant_interface(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/redundantinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/redundantinterfaces' url = self._url('config', request) return self._put(url, data) def delete_ftd_redundant_interface(self, device_id: str, interface_id: str): - request = '/devices/devicerecords/{}/redundantinterfaces/{}'.format(device_id, interface_id) + request = f'/devices/devicerecords/{device_id}/redundantinterfaces/{interface_id}' url = self._url('config', request) return self._delete(url) def create_ftd_portchannel_interface(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/etherchannelinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/etherchannelinterfaces' url = self._url('config', request) return self._post(url, data) def get_ftd_portchannel_interfaces(self, device_id: str): - request = '/devices/devicerecords/{}/etherchannelinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/etherchannelinterfaces' url = self._url('config', request) return self._get(url) def get_ftd_portchannel_interface(self, device_id: str, interface_id: str): - request = '/devices/devicerecords/{}/etherchannelinterfaces/{}'.format(device_id, interface_id) + request = f'/devices/devicerecords/{device_id}/etherchannelinterfaces/{interface_id}' url = self._url('config', request) return self._get(url) def update_ftd_portchannel_interface(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/etherchannelinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/etherchannelinterfaces' url = self._url('config', request) return self._put(url, data) def delete_ftd_portchannel_interface(self, device_id: str, interface_id: str): - request = '/devices/devicerecords/{}/etherchannelinterfaces/{}'.format(device_id, interface_id) + request = f'/devices/devicerecords/{device_id}/etherchannelinterfaces/{interface_id}' url = self._url('config', request) return self._delete(url) def create_ftd_sub_interface(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/subinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/subinterfaces' url = self._url('config', request) return self._post(url, data) def get_ftd_sub_interfaces(self, device_id: str): - request = '/devices/devicerecords/{}/subinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/subinterfaces' url = self._url('config', request) return self._get(url) def get_ftd_sub_interface(self, device_id: str, interface_id: str): - request = '/devices/devicerecords/{}/subinterfaces/{}'.format(device_id, interface_id) + request = f'/devices/devicerecords/{device_id}/subinterfaces/{interface_id}' url = self._url('config', request) return self._get(url) def update_ftd_sub_interface(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/subinterfaces'.format(device_id) + request = f'/devices/devicerecords/{device_id}/subinterfaces' url = self._url('config', request) return self._put(url, data) def delete_ftd_sub_interface(self, device_id: str, interface_id: str): - request = '/devices/devicerecords/{}/subinterfaces/{}'.format(device_id, interface_id) + request = f'/devices/devicerecords/{device_id}/subinterfaces/{interface_id}' url = self._url('config', request) return self._delete(url) def create_ftd_ipv4_route(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/ipv4staticroutes'.format(device_id) + request = f'/devices/devicerecords/{device_id}/ipv4staticroutes' url = self._url('config', request) return self._post(url, data) def get_ftd_ipv4_routes(self, device_id: str): - request = '/devices/devicerecords/{}/ipv4staticroutes'.format(device_id) + request = f'/devices/devicerecords/{device_id}/ipv4staticroutes' url = self._url('config', request) return self._get(url) def get_ftd_ipv4_route(self, device_id: str, route_id: str): - request = '/devices/devicerecords/{}/ipv4staticroutes/{}'.format(device_id, route_id) + request = f'/devices/devicerecords/{device_id}/ipv4staticroutes/{route_id}' url = self._url('config', request) return self._get(url) def update_ftd_ipv4_route(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/ipv4staticroutes'.format(device_id) + request = f'/devices/devicerecords/{device_id}/ipv4staticroutes' url = self._url('config', request) return self._put(url, data) def delete_ftd_ipv4_route(self, device_id: str, route_id: str): - request = '/devices/devicerecords/{}/ipv4staticroutes/{}'.format(device_id, route_id) + request = f'/devices/devicerecords/{device_id}/ipv4staticroutes/{route_id}' url = self._url('config', request) return self._delete(url) def create_ftd_ipv6_route(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/ipv6staticroutes'.format(device_id) + request = f'/devices/devicerecords/{device_id}/ipv6staticroutes' url = self._url('config', request) return self._post(url, data) def get_ftd_ipv6_routes(self, device_id: str): - request = '/devices/devicerecords/{}/ipv6staticroutes'.format(device_id) + request = f'/devices/devicerecords/{device_id}/ipv6staticroutes' url = self._url('config', request) return self._get(url) def get_ftd_ipv6_route(self, device_id: str, route_id: str): - request = '/devices/devicerecords/{}/ipv6staticroutes/{}'.format(device_id, route_id) + request = f'/devices/devicerecords/{device_id}/ipv6staticroutes/{route_id}' url = self._url('config', request) return self._get(url) def update_ftd_ipv6_route(self, device_id: str, data: Dict): - request = '/devices/devicerecords/{}/ipv6staticroutes'.format(device_id) + request = f'/devices/devicerecords/{device_id}/ipv6staticroutes' url = self._url('config', request) return self._put(url, data) def delete_ftd_ipv6_route(self, device_id: str, route_id: str): - request = '/devices/devicerecords/{}/ipv6staticroutes/{}'.format(device_id, route_id) + request = f'/devices/devicerecords/{device_id}/ipv6staticroutes/{route_id}' url = self._url('config', request) return self._delete(url) @@ -774,17 +743,17 @@ def get_deployment(self): return self._get(url) def create_policy(self, policy_type: str, data: Dict): - request = '/policy/{}'.format(policy_type) + request = f'/policy/{policy_type}' url = self._url('config', request) return self._post(url, data) def get_policies(self, policy_type: str): - request = '/policy/{}'.format(policy_type) + request = f'/policy/{policy_type}' url = self._url('config', request) return self._get(url) def get_policy(self, policy_id: str, policy_type: str, expanded=False): - request = '/policy/{}/{}'.format(policy_type, policy_id) + request = f'/policy/{policy_type}/{policy_id}' params = { 'expanded': expanded } @@ -792,18 +761,18 @@ def get_policy(self, policy_id: str, policy_type: str, expanded=False): return self._get(url, params) def update_policy(self, policy_id: str, policy_type: str, data: Dict): - request = '/policy/{}/{}'.format(policy_type, policy_id) + request = f'/policy/{policy_type}/{policy_id}' url = self._url('config', request) return self._put(url, data) def delete_policy(self, policy_id: str, policy_type: str): - request = '/policy/{}/{}'.format(policy_type, policy_id) + request = f'/policy/{policy_type}/{policy_id}' url = self._url('config', request) return self._delete(url) def create_acp_rule(self, policy_id: str, data: Dict, section=str(), category=str(), insert_before=int(), insert_after=int()): - request = '/policy/accesspolicies/{}/accessrules'.format(policy_id) + request = f'/policy/accesspolicies/{policy_id}/accessrules' url = self._url('config', request) params = { 'category': category, @@ -815,7 +784,7 @@ def create_acp_rule(self, policy_id: str, data: Dict, section=str(), category=st def create_acp_rules(self, policy_id: str, data: Dict, section=str(), category=str(), insert_before=int(), insert_after=int()): - request = '/policy/accesspolicies/{}/accessrules'.format(policy_id) + request = f'/policy/accesspolicies/{policy_id}/accessrules' url = self._url('config', request) params = { 'category': category, @@ -826,12 +795,12 @@ def create_acp_rules(self, policy_id: str, data: Dict, section=str(), category=s return self._post(url, data, params) def get_acp_rule(self, policy_id: str, rule_id: str): - request = '/policy/accesspolicies/{}/accessrules/{}'.format(policy_id, rule_id) + request = f'/policy/accesspolicies/{policy_id}/accessrules/{rule_id}' url = self._url('config', request) return self._get(url) def get_acp_rules(self, policy_id: str, expanded=False): - request = '/policy/accesspolicies/{}/accessrules'.format(policy_id) + request = f'/policy/accesspolicies/{policy_id}/accessrules' params = { 'expanded': expanded } @@ -839,62 +808,62 @@ def get_acp_rules(self, policy_id: str, expanded=False): return self._get(url, params) def update_acp_rule(self, policy_id: str, rule_id: str, data: Dict): - request = '/policy/accesspolicies/{}/accessrules/{}'.format(policy_id, rule_id) + request = '/policy/accesspolicies/{policy_id}/accessrules/{rule_id}' url = self._url('config', request) return self._put(url, data) def delete_acp_rule(self, policy_id: str, rule_id: str): - request = '/policy/accesspolicies/{}/accessrules/{}'.format(policy_id, rule_id) + request = f'/policy/accesspolicies/{policy_id}/accessrules/{rule_id}' url = self._url('config', request) return self._delete(url) def create_autonat_rule(self, policy_id: str, data: Dict): - request = '/policy/ftdnatpolicies/{}/autonatrules'.format(policy_id) + request = f'/policy/ftdnatpolicies/{policy_id}/autonatrules' url = self._url('config', request) return self._post(url, data) def get_autonat_rule(self, policy_id: str, rule_id: str): - request = '/policy/ftdnatpolicies/{}/autonatrules/{}'.format(policy_id, rule_id) + request = f'/policy/ftdnatpolicies/{policy_id}/autonatrules/{rule_id}' url = self._url('config', request) return self._get(url) def get_autonat_rules(self, policy_id: str): - request = '/policy/ftdnatpolicies/{}/autonatrules'.format(policy_id) + request = f'/policy/ftdnatpolicies/{policy_id}/autonatrules' url = self._url('config', request) return self._get(url) def update_autonat_rule(self, policy_id: str, data: Dict): - request = '/policy/ftdnatpolicies/{}/autonatrules'.format(policy_id) + request = f'/policy/ftdnatpolicies/{policy_id}/autonatrules' url = self._url('config', request) return self._put(url, data) def delete_autonat_rule(self, policy_id: str, rule_id: str): - request = '/policy/ftdnatpolicies/{}/autonatrules/{}'.format(policy_id, rule_id) + request = f'/policy/ftdnatpolicies/{policy_id}/autonatrules/{rule_id}' url = self._url('config', request) return self._delete(url) def create_manualnat_rule(self, policy_id: str, data: Dict): - request = '/policy/ftdnatpolicies/{}/manualnatrules'.format(policy_id) + request = f'/policy/ftdnatpolicies/{policy_id}/manualnatrules' url = self._url('config', request) return self._post(url, data) def get_manualnat_rule(self, policy_id: str, rule_id: str): - request = '/policy/ftdnatpolicies/{}/manualnatrules/{}'.format(policy_id, rule_id) + request = f'/policy/ftdnatpolicies/{policy_id}/manualnatrules/{rule_id}' url = self._url('config', request) return self._get(url) def get_manualnat_rules(self, policy_id: str): - request = '/policy/ftdnatpolicies/manualnatrules/{}'.format(policy_id) + request = f'/policy/ftdnatpolicies/manualnatrules/{policy_id}' url = self._url('config', request) return self._get(url) def update_manualnat_rule(self, policy_id: str, data: Dict): - request = '/policy/ftdnatpolicies/{}/manualnatrules'.format(policy_id) + request = f'/policy/ftdnatpolicies/{policy_id}/manualnatrules' url = self._url('config', request) return self._put(url, data) def delete_manualnat_rule(self, policy_id: str, rule_id: str): - request = '/policy/ftdnatpolicies/{}/manualnatrules/{}'.format(policy_id, rule_id) + request = f'/policy/ftdnatpolicies/{policy_id}/manualnatrules/{rule_id}' url = self._url('config', request) return self._delete(url) @@ -909,11 +878,11 @@ def get_policy_assignments(self): return self._get(url) def get_policy_assignment(self, policy_id: str): - request = '/assignment/policyassignments/{}'.format(policy_id) + request = f'/assignment/policyassignments/{policy_id}' url = self._url('config', request) return self._get(url) def update_policy_assignment(self, policy_id: str, data: Dict): - request = '/assignment/policyassignments/{}'.format(policy_id) + request = f'/assignment/policyassignments/{policy_id}' url = self._url('config', request) return self._put(url, data) From c28e814426507f0aa41dbbc2d6277abef7b77f47 Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 14:04:34 +0100 Subject: [PATCH 04/12] added install instructions --- README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 235adf8..c4d8f69 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FireREST +# What is FireREST FireREST is a simple wrapper for Cisco Firepower Management Center REST API. It exposes various api calls as functions and takes care of authentication, token refresh and paging for large datasets. @@ -7,15 +7,11 @@ as functions and takes care of authentication, token refresh and paging for larg * Python >= 3.6 -## Features +## Installation -* API Authentication -* Automatic Re-authentication if token expires -* Paging - -## Examples - -n/a +```bash +$ pip install fireREST +``` ## Authors From f68406d099005fedfe965e5f198d95927b2dccc3 Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 14:09:30 +0100 Subject: [PATCH 05/12] updated gitignore --- .gitignore | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a0757c0..d8e5463 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,121 @@ -.idea -.idea/* -*.pyc -*~ -__pycache__ -prototype.py +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ From f1355571f61bfe8f59db6f4a6e5fe27318a5bf89 Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 14:16:52 +0100 Subject: [PATCH 06/12] fixed incorrect request paths as indicated in #8 --- fireREST/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fireREST/__init__.py b/fireREST/__init__.py index eda6d6c..6020dbf 100644 --- a/fireREST/__init__.py +++ b/fireREST/__init__.py @@ -505,12 +505,12 @@ def get_audit_records(self): return self._get(url) def get_syslogalerts(self): - request = 'policy/syslogalerts' + request = '/policy/syslogalerts' url = self._url('config', request) return self._get(url) def get_snmpalerts(self): - request = 'policy/snmpalerts' + request = '/policy/snmpalerts' url = self._url('config', request) return self._get(url) From d0c5f13b24e13819957d6f57d856c73b7d714cdc Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 14:18:30 +0100 Subject: [PATCH 07/12] removed unused valid_json function --- fireREST/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/fireREST/__init__.py b/fireREST/__init__.py index 6020dbf..fd1bbb7 100644 --- a/fireREST/__init__.py +++ b/fireREST/__init__.py @@ -337,16 +337,6 @@ def prepare_json(self, operation: str, obj_type: str, data: Dict): ''' return - def valid_json(self, operation: str, obj_type: str, data: Dict): - ''' - Validate json object to verify - :param operation: PUT, POST - :param obj_type: see supported types in schema.py - :param data: json representing api object - :return: dictionary containing results of json evaluation - ''' - return - def get_object_id_by_name(self, obj_type: str, obj_name: str): ''' helper function to retrieve object id by name From 7ec511736193a86c1884c1ebbc313bfb86f3effe Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 14:44:46 +0100 Subject: [PATCH 08/12] implemented #6 --- fireREST/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/fireREST/__init__.py b/fireREST/__init__.py index fd1bbb7..19d29f0 100644 --- a/fireREST/__init__.py +++ b/fireREST/__init__.py @@ -537,10 +537,13 @@ def create_device(self, data: Dict): url = self._url('config', request) return self._post(url, data) - def get_devices(self): + def get_devices(self, expanded=False): request = '/devices/devicerecords' url = self._url('config', request) - return self._get(url) + params = { + 'expanded': expanded + } + return self._get(url, params) def get_device(self, device_id: str): request = f'/devices/devicerecords/{device_id}' @@ -557,10 +560,13 @@ def delete_device(self, device_id: str): url = self._url('config', request) return self._delete(url) - def get_device_hapairs(self): + def get_device_hapairs(self, expanded=False): request = '/devicehapairs/ftddevicehapairs' + params = { + 'expanded': expanded + } url = self._url('config', request) - return self._get(url) + return self._get(url, params) def create_device_hapair(self, data: Dict): request = '/devicehapairs/ftddevicehapairs/{}' From 5bfed6118cc29e0841a4c2f2faddb99f327381e1 Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 15:28:52 +0100 Subject: [PATCH 09/12] moved constants out of api client, renamed fireREST class, cleaned up tests --- fireREST/__init__.py | 29 +++++++++++++++-------------- setup.py | 5 +++-- test/test_fireREST.py | 32 +++++++++----------------------- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/fireREST/__init__.py b/fireREST/__init__.py index 19d29f0..cb81be4 100644 --- a/fireREST/__init__.py +++ b/fireREST/__init__.py @@ -9,6 +9,12 @@ from urllib3.exceptions import ConnectionError +API_AUTH_URL = '/api/fmc_platform/v1/auth/generatetoken' +API_REFRESH_URL = '/api/fmc_platform/v1/auth/refreshtoken' +API_PLATFORM_URL = '/api/fmc_platform/v1' +API_CONFIG_URL = '/api/fmc_config/v1' + + class FireRESTApiException(Exception): def __init__(self, message): super().__init__(message) @@ -49,20 +55,15 @@ def wrapped_f(*args): return wrapped_f -class FireREST(object): - API_AUTH_URL = '/api/fmc_platform/v1/auth/generatetoken' - API_REFRESH_URL = '/api/fmc_platform/v1/auth/refreshtoken' - API_PLATFORM_URL = '/api/fmc_platform/v1' - API_CONFIG_URL = '/api/fmc_config/v1' - +class Client(object): def __init__(self, hostname: str, username: str, password: str, session=dict(), protocol='https', verify_cert=False, logger=None, domain='Global', timeout=120): ''' - Initialize FireREST object + Initialize api client object :param hostname: ip address or dns name of fmc :param username: fmc username :param password: fmc password - :param session: authentication session (can be provided in case FireREST should not generate one at init). + :param session: authentication session (can be provided in case api client should not generate one at init). Make sure to pass the headers of a successful authentication to the session variable, otherwise this will fail :param protocol: protocol used to access fmc api. default = https @@ -96,12 +97,12 @@ def __init__(self, hostname: str, username: str, password: str, session=dict(), @staticmethod def _get_logger(logger: object): ''' - Generate dummy logger in case FireREST has been initialized without a logger + Generate dummy logger in case api client has been initialized without a logger :param logger: logger instance :return: dummy logger instance if logger is None, otherwise return logger variable again ''' if not logger: - dummy_logger = logging.getLogger('FireREST') + dummy_logger = logging.getLogger('FireREST.Client') dummy_logger.addHandler(logging.NullHandler()) return dummy_logger return logger @@ -114,13 +115,13 @@ def _url(self, namespace='base', path=str()): :return: url in string format ''' if namespace == 'config': - return f'{self.protocol}://{self.hostname}{self.API_CONFIG_URL}/domain/{self.domain}{path}' + return f'{self.protocol}://{self.hostname}{API_CONFIG_URL}/domain/{self.domain}{path}' if namespace == 'platform': - return f'{self.protocol}://{self.hostname}{self.API_PLATFORM_URL}{path}' + return f'{self.protocol}://{self.hostname}{API_PLATFORM_URL}{path}' if namespace == 'auth': - return f'{self.protocol}://{self.hostname}{self.API_AUTH_URL}' + return f'{self.protocol}://{self.hostname}{API_AUTH_URL}' if namespace == 'refresh': - return f'{self.protocol}://{self.hostname}{self.API_REFRESH_URL}' + return f'{self.protocol}://{self.hostname}{API_REFRESH_URL}' return f'{self.protocol}://{self.hostname}{path}' def _login(self): diff --git a/setup.py b/setup.py index 4c171a6..ec15aa9 100755 --- a/setup.py +++ b/setup.py @@ -10,11 +10,12 @@ long_description = readme.read() setuptools.setup( - name='fireREST', + name='FireREST', version='0.0.1', author='Oliver Kaiser', author_email='oliver.kaiser@outlook.com', - description='Wrapper for Cisco Firepower Management Center REST API', + description='Python api client for Cisco Firepower Management Center REST API', + keywords="cisco firepower fmc ftd fpr api rest python api", long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/kaisero/fireREST.git', diff --git a/test/test_fireREST.py b/test/test_fireREST.py index e6c6d30..bdf58fc 100644 --- a/test/test_fireREST.py +++ b/test/test_fireREST.py @@ -1,7 +1,8 @@ import unittest -from fireREST import FireREST +from fireREST import Client from fireREST import FireRESTApiException, FireRESTAuthException, FireRESTAuthRefreshException +from fireREST import API_AUTH_URL, API_REFRESH_URL, API_PLATFORM_URL, API_CONFIG_URL from requests.auth import HTTPBasicAuth @@ -11,10 +12,7 @@ def setUp(self): hostname = 'fmc.example.com' username = 'admin' password = 'Cisco123' - - self.api = FireREST(hostname=hostname, - username=username, - password=password) + self.api = Client(hostname=hostname, username=username, password=password) def tearDown(self): return @@ -69,9 +67,7 @@ def setUpClass(cls): cls.username = 'admin' cls.password = 'Cisco123' - cls.api = FireREST(hostname=cls.hostname, - username=cls.username, - password=cls.password) + cls.api = Client(hostname=cls.hostname, username=cls.username, password=cls.password) def setUp(self): return @@ -97,38 +93,28 @@ def test_initialization(self): self.assertEqual(self.api.domain, expected_domain) def test_default_url(self): - expected_url = '{}://{}{}'.format(self.api.protocol, - self.api.hostname, '/test') + expected_url = f'{self.api.protocol}://{self.api.hostname}/test' actual_url = self.api._url(path='/test') - self.assertEqual(actual_url, expected_url) def test_config_url(self): - expected_url = '{}://{}{}/domain/{}{}'.format(self.api.protocol, self.api.hostname, FireREST.API_CONFIG_URL, - self.api.domain, '/test') + expected_url = f'{self.api.protocol}://{self.api.hostname}{API_CONFIG_URL}/domain/{self.api.domain}/test' actual_url = self.api._url(namespace='config', path='/test') - self.assertEqual(actual_url, expected_url) def test_platform_url(self): - expected_url = '{}://{}{}{}'.format(self.api.protocol, - self.api.hostname, FireREST.API_PLATFORM_URL, '/test') + expected_url = f'{self.api.protocol}://{self.api.hostname}{API_PLATFORM_URL}/test' actual_url = self.api._url(namespace='platform', path='/test') - self.assertEqual(actual_url, expected_url) def test_auth_url(self): - expected_url = '{}://{}{}'.format(self.api.protocol, - self.api.hostname, FireREST.API_AUTH_URL) + expected_url = f'{self.api.protocol}://{self.api.hostname}{API_AUTH_URL}' actual_url = self.api._url(namespace='auth') - self.assertEqual(actual_url, expected_url) def test_refresh_url(self): - expected_url = '{}://{}{}'.format(self.api.protocol, - self.api.hostname, FireREST.API_REFRESH_URL) + expected_url = f'{self.api.protocol}://{self.api.hostname}{API_REFRESH_URL}' actual_url = self.api._url(namespace='refresh', path='/test') - self.assertEqual(actual_url, expected_url) From 80c224c9280d265a2f24b589d5d5bc40fb04301d Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 15:30:36 +0100 Subject: [PATCH 10/12] added twine to pipenv dev packages --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 079cfe5..8be5365 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,7 @@ pycodestyle = "*" flake8 = "*" autopep8 = "*" pre-commit = "*" +twine = "*" [requires] python_version = "3.6" From adeb1e78ef9308de35516b3b854451c2c34a5c16 Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Tue, 29 Oct 2019 16:23:33 +0100 Subject: [PATCH 11/12] added better version referencing --- fireREST/__init__.py | 4 +++- fireREST/version.py | 1 + setup.py | 6 ++++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 fireREST/version.py diff --git a/fireREST/__init__.py b/fireREST/__init__.py index cb81be4..a89b711 100644 --- a/fireREST/__init__.py +++ b/fireREST/__init__.py @@ -3,6 +3,8 @@ import logging import urllib3 +from .version import __version__ + from time import sleep from typing import Dict from requests.auth import HTTPBasicAuth @@ -75,7 +77,7 @@ def __init__(self, hostname: str, username: str, password: str, session=dict(), self.headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', - 'User-Agent': 'FireREST/0.0.1' + 'User-Agent': f'FireREST/{__version__}' } self.refresh_counter = 0 self.logger = self._get_logger(logger) diff --git a/fireREST/version.py b/fireREST/version.py new file mode 100644 index 0000000..d18f409 --- /dev/null +++ b/fireREST/version.py @@ -0,0 +1 @@ +__version__ = '0.0.2' diff --git a/setup.py b/setup.py index ec15aa9..65b3668 100755 --- a/setup.py +++ b/setup.py @@ -9,9 +9,11 @@ with open(f'{ROOT}/README.md', 'r') as readme: long_description = readme.read() +exec(open(f'{ROOT}/fireREST/version.py', 'r').read()) + setuptools.setup( - name='FireREST', - version='0.0.1', + name='fireREST', + version=__version__, # noqa: F821 author='Oliver Kaiser', author_email='oliver.kaiser@outlook.com', description='Python api client for Cisco Firepower Management Center REST API', From dbe5026e89cafa0b821f2abcefdc6e1702bb46db Mon Sep 17 00:00:00 2001 From: Oliver Kaiser Date: Wed, 30 Oct 2019 11:46:46 +0100 Subject: [PATCH 12/12] updated version to 0.0.3 --- fireREST/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fireREST/version.py b/fireREST/version.py index d18f409..ffcc925 100644 --- a/fireREST/version.py +++ b/fireREST/version.py @@ -1 +1 @@ -__version__ = '0.0.2' +__version__ = '0.0.3'