Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New modules and updated HTTP API plugin for FTD devices #44578

Merged
merged 10 commits into from
Aug 29, 2018
Empty file.
177 changes: 177 additions & 0 deletions lib/ansible/module_utils/network/ftd/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Copyright (c) 2018 Cisco and/or its affiliates.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#

import re
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add copyright in all the files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added.


INVALID_IDENTIFIER_SYMBOLS = r'[^a-zA-Z0-9_]'

IDENTITY_PROPERTIES = ['id', 'version', 'ruleId']
NON_COMPARABLE_PROPERTIES = IDENTITY_PROPERTIES + ['isSystemDefined', 'links']


class HTTPMethod:
GET = 'get'
POST = 'post'
PUT = 'put'
DELETE = 'delete'


class ResponseParams:
SUCCESS = 'success'
STATUS_CODE = 'status_code'
RESPONSE = 'response'


class FtdConfigurationError(Exception):
pass


class FtdServerError(Exception):
def __init__(self, response, code):
super(FtdServerError, self).__init__(response)
self.response = response
self.code = code


def construct_ansible_facts(response, params):
facts = dict()
if response:
response_body = response['items'] if 'items' in response else response
if params.get('register_as'):
facts[params['register_as']] = response_body
elif 'name' in response_body and 'type' in response_body:
object_name = re.sub(INVALID_IDENTIFIER_SYMBOLS, '_', response_body['name'].lower())
fact_name = '%s_%s' % (response_body['type'], object_name)
facts[fact_name] = response_body
return facts


def copy_identity_properties(source_obj, dest_obj):
for property_name in IDENTITY_PROPERTIES:
if property_name in source_obj:
dest_obj[property_name] = source_obj[property_name]
return dest_obj


def is_object_ref(d):
"""
Checks if a dictionary is a reference object. The dictionary is considered to be a
reference object when it contains non-empty 'id' and 'type' fields.

:type d: dict
:return: True if passed dictionary is a reference object, otherwise False
"""
has_id = 'id' in d.keys() and d['id']
has_type = 'type' in d.keys() and d['type']
return has_id and has_type


def equal_object_refs(d1, d2):
"""
Checks whether two references point to the same object.

:type d1: dict
:type d2: dict
:return: True if passed references point to the same object, otherwise False
"""
have_equal_ids = d1['id'] == d2['id']
have_equal_types = d1['type'] == d2['type']
return have_equal_ids and have_equal_types


def equal_lists(l1, l2):
"""
Checks whether two lists are equal. The order of elements in the arrays is important.

:type l1: list
:type l2: list
:return: True if passed lists, their elements and order of elements are equal. Otherwise, returns False.
"""
if len(l1) != len(l2):
return False

for v1, v2 in zip(l1, l2):
if not equal_values(v1, v2):
return False

return True


def equal_dicts(d1, d2, compare_by_reference=True):
"""
Checks whether two dictionaries are equal. If `compare_by_reference` is set to True, dictionaries referencing
objects are compared using `equal_object_refs` method. Otherwise, every key and value is checked.

:type d1: dict
:type d2: dict
:param compare_by_reference: if True, dictionaries referencing objects are compared using `equal_object_refs` method
:return: True if passed dicts are equal. Otherwise, returns False.
"""
if compare_by_reference and is_object_ref(d1) and is_object_ref(d2):
return equal_object_refs(d1, d2)

if len(d1) != len(d2):
return False

for key, v1 in d1.items():
if key not in d2:
return False

v2 = d2[key]
if not equal_values(v1, v2):
return False

return True


def equal_values(v1, v2):
"""
Checks whether types and content of two values are the same. In case of complex objects, the method might be
called recursively.

:param v1: first value
:param v2: second value
:return: True if types and content of passed values are equal. Otherwise, returns False.
:rtype: bool
"""
if type(v1) != type(v2):
return False
value_type = type(v1)

if value_type == list:
return equal_lists(v1, v2)
elif value_type == dict:
return equal_dicts(v1, v2)
else:
return v1 == v2


def equal_objects(d1, d2):
"""
Checks whether two objects are equal. Ignores special object properties (e.g. 'id', 'version') and
properties with None and empty values. In case properties contains a reference to the other object,
only object identities (ids and types) are checked.

:type d1: dict
:type d2: dict
:return: True if passed objects and their properties are equal. Otherwise, returns False.
"""
d1 = dict((k, d1[k]) for k in d1.keys() if k not in NON_COMPARABLE_PROPERTIES and d1[k])
d2 = dict((k, d2[k]) for k in d2.keys() if k not in NON_COMPARABLE_PROPERTIES and d2[k])

return equal_dicts(d1, d2, compare_by_reference=False)
146 changes: 146 additions & 0 deletions lib/ansible/module_utils/network/ftd/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Copyright (c) 2018 Cisco and/or its affiliates.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#

from functools import partial

from ansible.module_utils.network.ftd.common import HTTPMethod, equal_objects, copy_identity_properties, \
FtdConfigurationError, FtdServerError, ResponseParams

DEFAULT_PAGE_SIZE = 10
DEFAULT_OFFSET = 0

UNPROCESSABLE_ENTITY_STATUS = 422
INVALID_UUID_ERROR_MESSAGE = "Validation failed due to an invalid UUID"
DUPLICATE_NAME_ERROR_MESSAGE = "Validation failed due to a duplicate name"


class BaseConfigurationResource(object):
def __init__(self, conn):
self._conn = conn
self.config_changed = False

def get_object_by_name(self, url_path, name, path_params=None):
item_generator = iterate_over_pageable_resource(
partial(self.send_request, url_path=url_path, http_method=HTTPMethod.GET, path_params=path_params),
{'filter': 'name:%s' % name}
)
# not all endpoints support filtering so checking name explicitly
return next((item for item in item_generator if item['name'] == name), None)

def get_objects_by_filter(self, url_path, filters, path_params=None, query_params=None):
def match_filters(obj):
for k, v in filters.items():
if k not in obj or obj[k] != v:
return False
return True

item_generator = iterate_over_pageable_resource(
partial(self.send_request, url_path=url_path, http_method=HTTPMethod.GET, path_params=path_params),
query_params
)
return [i for i in item_generator if match_filters(i)]

def add_object(self, url_path, body_params, path_params=None, query_params=None, update_if_exists=False):
def is_duplicate_name_error(err):
return err.code == UNPROCESSABLE_ENTITY_STATUS and DUPLICATE_NAME_ERROR_MESSAGE in str(err)

def update_existing_object(obj):
new_path_params = {} if path_params is None else path_params
new_path_params['objId'] = obj['id']
return self.send_request(url_path=url_path + '/{objId}',
http_method=HTTPMethod.PUT,
body_params=copy_identity_properties(obj, body_params),
path_params=new_path_params,
query_params=query_params)

try:
return self.send_request(url_path=url_path, http_method=HTTPMethod.POST, body_params=body_params,
path_params=path_params, query_params=query_params)
except FtdServerError as e:
if is_duplicate_name_error(e):
existing_obj = self.get_object_by_name(url_path, body_params['name'], path_params)
if equal_objects(existing_obj, body_params):
return existing_obj
elif update_if_exists:
return update_existing_object(existing_obj)
else:
raise FtdConfigurationError(
'Cannot add new object. An object with the same name but different parameters already exists.')
else:
raise e

def delete_object(self, url_path, path_params):
def is_invalid_uuid_error(err):
return err.code == UNPROCESSABLE_ENTITY_STATUS and INVALID_UUID_ERROR_MESSAGE in str(err)

try:
return self.send_request(url_path=url_path, http_method=HTTPMethod.DELETE, path_params=path_params)
except FtdServerError as e:
if is_invalid_uuid_error(e):
return {'status': 'Referenced object does not exist'}
else:
raise e

def edit_object(self, url_path, body_params, path_params=None, query_params=None):
existing_object = self.send_request(url_path=url_path, http_method=HTTPMethod.GET, path_params=path_params)

if not existing_object:
raise FtdConfigurationError('Referenced object does not exist')
elif equal_objects(existing_object, body_params):
return existing_object
else:
return self.send_request(url_path=url_path, http_method=HTTPMethod.PUT, body_params=body_params,
path_params=path_params, query_params=query_params)

def send_request(self, url_path, http_method, body_params=None, path_params=None, query_params=None):
def raise_for_failure(resp):
if not resp[ResponseParams.SUCCESS]:
raise FtdServerError(resp[ResponseParams.RESPONSE], resp[ResponseParams.STATUS_CODE])

response = self._conn.send_request(url_path=url_path, http_method=http_method, body_params=body_params,
path_params=path_params, query_params=query_params)
raise_for_failure(response)
if http_method != HTTPMethod.GET:
self.config_changed = True
return response[ResponseParams.RESPONSE]


def iterate_over_pageable_resource(resource_func, query_params=None):
"""
A generator function that iterates over a resource that supports pagination and lazily returns present items
one by one.

:param resource_func: function that receives `query_params` named argument and returns a page of objects
:type resource_func: callable
:param query_params: initial dictionary of query parameters that will be passed to the resource_func
:type query_params: dict
:return: an iterator containing returned items
:rtype: iterator of dict
"""
query_params = {} if query_params is None else dict(query_params)
query_params.setdefault('limit', DEFAULT_PAGE_SIZE)
query_params.setdefault('offset', DEFAULT_OFFSET)

result = resource_func(query_params=query_params)
while result['items']:
for item in result['items']:
yield item
# creating a copy not to mutate existing dict
query_params = dict(query_params)
query_params['offset'] = int(query_params['offset']) + int(query_params['limit'])
result = resource_func(query_params=query_params)