diff --git a/docs/docsite/rst/dev_guide/developing_module_utilities.rst b/docs/docsite/rst/dev_guide/developing_module_utilities.rst index 84add50cff5..64d247d77e2 100644 --- a/docs/docsite/rst/dev_guide/developing_module_utilities.rst +++ b/docs/docsite/rst/dev_guide/developing_module_utilities.rst @@ -10,6 +10,7 @@ The following is a list of module_utils files and a general description. The mod - a10.py - Utilities used by the a10_server module to manage A10 Networks devices. - api.py - Adds shared support for generic API modules. +- aos.py - Module support utilities for managing Apstra AOS Server. - asa.py - Module support utilities for managing Cisco ASA network devices. - azure_rm_common.py - Definitions and utilities for Microsoft Azure Resource Manager template deployments. - basic.py - General definitions and helper utilities for Ansible modules. @@ -17,8 +18,8 @@ The following is a list of module_utils files and a general description. The mod - database.py - Miscellaneous helper functions for PostGRES and MySQL - docker_common.py - Definitions and helper utilities for modules working with Docker. - ec2.py - Definitions and utilities for modules working with Amazon EC2 -- eos.py - Helper functions for modules working with EOS networking devices. -- f5.py - Helper functions for modules working with F5 networking devices. +- eos.py - Helper functions for modules working with EOS networking devices. +- f5.py - Helper functions for modules working with F5 networking devices. - facts.py - Helper functions for modules that return facts. - gce.py - Definitions and helper functions for modules that work with Google Compute Engine resources. - ios.py - Definitions and helper functions for modules that manage Cisco IOS networking devices @@ -43,6 +44,6 @@ The following is a list of module_utils files and a general description. The mod - six.py - Module utils for working with the Six python 2 and 3 compatibility library - splitter.py - String splitting and manipulation utilities for working with Jinja2 templates - urls.py - Utilities for working with http and https requests -- vca.py - Contains utilities for modules that work with VMware vCloud Air +- vca.py - Contains utilities for modules that work with VMware vCloud Air - vmware.py - Contains utilities for modules that work with VMware vSphere VMs - vyos.py - Definitions and functions for working with VyOS networking diff --git a/lib/ansible/module_utils/aos.py b/lib/ansible/module_utils/aos.py new file mode 100644 index 00000000000..4f5b53af51d --- /dev/null +++ b/lib/ansible/module_utils/aos.py @@ -0,0 +1,181 @@ +# +# Copyright (c) 2017 Apstra Inc, +# +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +""" +This module adds shared support for Apstra AOS modules + +In order to use this module, include it as part of your module + +from ansible.module_utils.aos import * + +""" +import json + +from distutils.version import LooseVersion +from ansible.module_utils.pycompat24 import get_exception + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +try: + from apstra.aosom.session import Session + + HAS_AOS_PYEZ = True +except ImportError: + HAS_AOS_PYEZ = False + +def check_aos_version(module, min=False): + """ + Check if the library aos-pyez is present. + If provided, also check if the minimum version requirement is met + """ + if not HAS_AOS_PYEZ: + module.fail_json(msg='aos-pyez is not installed. Please see details ' + 'here: https://github.com/Apstra/aos-pyez') + + elif min: + import apstra.aosom + AOS_PYEZ_VERSION = apstra.aosom.__version__ + + if not LooseVersion(AOS_PYEZ_VERSION) >= LooseVersion(min): + module.fail_json(msg='aos-pyez >= %s is required for this module' % min) + + return True + +def get_aos_session(module, auth): + """ + Resume an existing session and return an AOS object. + + Args: + auth (dict): An AOS session as obtained by aos_login module blocks:: + + dict( token=, + server=, + port= + ) + + Return: + Aos object + """ + + check_aos_version(module) + + aos = Session() + aos.session = auth + + return aos + +def find_collection_item(collection, item_name=False, item_id=False): + """ + Find collection_item based on name or id from a collection object + Both Collection_item and Collection Objects are provided by aos-pyez library + + Return + collection_item: object corresponding to the collection type + """ + my_dict = None + + if item_name: + my_dict = collection.find(label=item_name) + elif item_id: + my_dict = collection.find(uid=item_id) + + if my_dict is None: + return collection[''] + else: + return my_dict + +def content_to_dict(module, content): + """ + Convert 'content' into a Python Dict based on 'content_format' + """ + + # if not HAS_YAML: + # module.fail_json(msg="Python Library Yaml is not present, mandatory to use 'content'") + + content_dict = None + + # try: + # content_dict = json.loads(content.replace("\'", '"')) + # except: + # module.fail_json(msg="Unable to convert 'content' from JSON, please check if valid") + # + # elif format in ['yaml', 'var']: + + try: + content_dict = yaml.load(content) + + if not isinstance(content_dict, dict): + raise + + # Check if dict is empty and return an error if it's + if not content_dict: + raise + + except: + module.fail_json(msg="Unable to convert 'content' to a dict, please check if valid") + + + # replace the string with the dict + module.params['content'] = content_dict + + return content_dict + +def do_load_resource(module, collection, name): + """ + Create a new object (collection.item) by loading a datastructure directly + """ + + try: + item = find_collection_item(collection, name, '') + except: + module.fail_json(msg="Ans error occured while running 'find_collection_item'") + + if item.exists: + module.exit_json( changed=False, + name=item.name, + id=item.id, + value=item.value ) + + # If not in check mode, apply the changes + if not module.check_mode: + try: + item.datum = module.params['content'] + item.write() + except: + e = get_exception() + module.fail_json(msg="Unable to write item content : %r" % e) + + module.exit_json( changed=True, + name=item.name, + id=item.id, + value=item.value ) diff --git a/lib/ansible/modules/network/aos/__init__.py b/lib/ansible/modules/network/aos/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/network/aos/aos_ip_pool.py b/lib/ansible/modules/network/aos/aos_ip_pool.py new file mode 100644 index 00000000000..6270be40767 --- /dev/null +++ b/lib/ansible/modules/network/aos/aos_ip_pool.py @@ -0,0 +1,344 @@ +#!/usr/bin/python +# +# (c) 2017 Apstra Inc, +# +# 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 . +# + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + +DOCUMENTATION = ''' +--- +module: aos_ip_pool +author: Damien Garros (@dgarros) +version_added: "2.3" +short_description: Manage AOS IP Pool +description: + - Apstra AOS Ip Pool module let you manage your IP Pool easily. You can create + create and delete IP Pool by Name, ID or by using a JSON File. This module + is idempotent and support the I(check) mode. It's using the AOS REST API +requirements: + - "aos-pyez >= 0.6.0" +options: + session: + description: + - An existing AOS session as obtained by aos_login module + required: true + name: + description: + - Name of the IP Pool to manage. + Only one of I(name), I(id) or I(content) can be set. + required: false + id: + description: + - AOS Id of the IP Pool to manage (can't be used to create a new IP Pool), + Only one of I(name), I(id) or I(content) can be set. + required: false + content: + description: + - Datastructure of the IP Pool to create. The data can be in YAML / JSON or + directly a variable. It's the same datastructure that is returned + on success in I(value). + required: false + state: + description: + - Indicate what is the expected state of the IP Pool (present or not) + default: present + choices: ['present', 'absent'] + required: false + subnets: + description: + - List of subnet that needs to be part of the IP Pool + required: false +''' + +EXAMPLES = ''' + +- name: "Create an IP Pool with one subnet" + aos_ip_pool: + session: "{{ session_ok }}" + name: "my-ip-pool" + subnets: [ 172.10.0.0/16 ] + state: present + +- name: "Create an IP Pool with multiple subnets" + aos_ip_pool: + session: "{{ session_ok }}" + name: "my-other-ip-pool" + subnets: [ 172.10.0.0/16, 192.168.0.0./24 ] + state: present + +- name: "Check if an IP Pool exist with same subnets by ID" + aos_ip_pool: + session: "{{ session_ok }}" + name: "45ab26fc-c2ed-4307-b330-0870488fa13e" + subnets: [ 172.10.0.0/16, 192.168.0.0./24 ] + state: present + +- name: "Delete an IP Pool by name" + aos_ip_pool: + session: "{{ session }}" + name: "my-ip-pool" + state: absent + +- name: "Delete an IP pool by id" + aos_ip_pool: + session: "{{ session }}" + id: "45ab26fc-c2ed-4307-b330-0870488fa13e" + state: absent + +# Save an IP Pool to a file + +- name: "Access IP Pool 1/3" + aos_ip_pool: + session: "{{ session_ok }}" + name: "my-ip-pool" + subnets: [ 172.10.0.0/16, 172.12.0.0/16 ] + state: present + register: ip_pool + +- name: "Save Ip Pool into a file in JSON 2/3" + copy: + content: "{{ ip_pool.value | to_nice_json }}" + dest: ip_pool_saved.json + +- name: "Save Ip Pool into a file in YAML 3/3" + copy: + content: "{{ ip_pool.value | to_nice_yaml }}" + dest: ip_pool_saved.yaml + +- name: "Load IP Pool from a JSON file" + aos_ip_pool: + session: "{{ session_ok }}" + content: "{{ lookup('file', 'resources/ip_pool_saved.json') }}" + state: present + +- name: "Load IP Pool from a YAML file" + aos_ip_pool: + session: "{{ session_ok }}" + content: "{{ lookup('file', 'resources/ip_pool_saved.yaml') }}" + state: present + +- name: "Load IP Pool from a Variable" + aos_ip_pool: + session: "{{ session_ok }}" + content: + display_name: my-ip-pool + id: 4276738d-6f86-4034-9656-4bff94a34ea7 + subnets: + - network: 172.10.0.0/16 + - network: 172.12.0.0/16 + state: present +''' + +RETURNS = ''' +name: + description: Name of the IP Pool + returned: always + type: str + sample: Server-IpAddrs + +id: + description: AOS unique ID assigned to the IP Pool + returned: always + type: str + sample: fcc4ac1c-e249-4fe7-b458-2138bfb44c06 + +value: + description: Value of the object as returned by the AOS Server + returned: always + type: dict + sample: {'...'} +''' + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.aos import get_aos_session, find_collection_item, do_load_resource, check_aos_version, content_to_dict + +def get_list_of_subnets(ip_pool): + subnets = [] + + for subnet in ip_pool.value['subnets']: + subnets.append(subnet['network']) + + return subnets + +def create_new_ip_pool(ip_pool, name, subnets): + + # Create value + datum = dict(display_name=name, subnets=[]) + for subnet in subnets: + datum['subnets'].append(dict(network=subnet)) + + ip_pool.datum = datum + + ## Write to AOS + return ip_pool.write() + +######################################################### +# State Processing +######################################################### +def ip_pool_absent(module, aos, my_pool): + + margs = module.params + + # If the module do not exist, return directly + if my_pool.exists is False: + module.exit_json(changed=False, name=margs['name'], id='', value={}) + + ## Check if object is currently in Use or Not + # If in Use, return an error + if my_pool.value: + if my_pool.value['status'] != 'not_in_use': + module.fail_json(msg="unable to delete this ip Pool, currently in use") + else: + module.fail_json(msg="Ip Pool object has an invalid format, value['status'] must be defined") + + # If not in check mode, delete Ip Pool + if not module.check_mode: + try: + my_pool.delete() + except: + module.fail_json(msg="An error occured, while trying to delete the IP Pool") + + module.exit_json( changed=True, + name=my_pool.name, + id=my_pool.id, + value={} ) + +def ip_pool_present(module, aos, my_pool): + + margs = module.params + + # if content is defined, create object from Content + try: + if margs['content'] is not None: + + if 'display_name' in module.params['content'].keys(): + do_load_resource(module, aos.IpPools, module.params['content']['display_name']) + else: + module.fail_json(msg="Unable to find display_name in 'content', Mandatory") + + except: + module.fail_json(msg="Unable to load resource from content, something went wrong") + + # if ip_pool doesn't exist already, create a new one + + if my_pool.exists is False and 'name' not in margs.keys(): + module.fail_json(msg="Name is mandatory for module that don't exist currently") + + elif my_pool.exists is False: + + if not module.check_mode: + try: + my_new_pool = create_new_ip_pool(my_pool, margs['name'], margs['subnets']) + my_pool = my_new_pool + except: + module.fail_json(msg="An error occured while trying to create a new IP Pool ") + + module.exit_json( changed=True, + name=my_pool.name, + id=my_pool.id, + value=my_pool.value ) + + # if pool already exist, check if list of network is the same + # if same just return the object and report change false + if set(get_list_of_subnets(my_pool)) == set(margs['subnets']): + module.exit_json( changed=False, + name=my_pool.name, + id=my_pool.id, + value=my_pool.value ) + else: + module.fail_json(msg="ip_pool already exist but value is different, currently not supported to update a module") + +######################################################### +# Main Function +######################################################### +def ip_pool(module): + + margs = module.params + + try: + aos = get_aos_session(module, margs['session']) + except: + module.fail_json(msg="Unable to login to the AOS server") + + item_name = False + item_id = False + + if margs['content'] is not None: + + content = content_to_dict(module, margs['content'] ) + + if 'display_name' in content.keys(): + item_name = content['display_name'] + else: + module.fail_json(msg="Unable to extract 'display_name' from 'content'") + + elif margs['name'] is not None: + item_name = margs['name'] + + elif margs['id'] is not None: + item_id = margs['id'] + + #---------------------------------------------------- + # Find Object if available based on ID or Name + #---------------------------------------------------- + try: + my_pool = find_collection_item(aos.IpPools, + item_name=item_name, + item_id=item_id) + except: + module.fail_json(msg="Unable to find the IP Pool based on name or ID, something went wrong") + + #---------------------------------------------------- + # Proceed based on State value + #---------------------------------------------------- + if margs['state'] == 'absent': + + ip_pool_absent(module, aos, my_pool) + + elif margs['state'] == 'present': + + ip_pool_present(module, aos, my_pool) + +def main(): + module = AnsibleModule( + argument_spec=dict( + session=dict(required=True, type="dict"), + name=dict(required=False ), + id=dict(required=False ), + content=dict(required=False, type="json"), + state=dict( required=False, + choices=['present', 'absent'], + default="present"), + subnets=dict(required=False, type="list") + ), + mutually_exclusive = [('name', 'id', 'content')], + required_one_of=[('name', 'id', 'content')], + supports_check_mode=True + ) + + # Check if aos-pyez is present and match the minimum version + check_aos_version(module, '0.6.0') + + ip_pool(module) + +if __name__ == "__main__": + main()