mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-06 13:22:48 +00:00
Initial commit
This commit is contained in:
0
plugins/module_utils/__init__.py
Normal file
0
plugins/module_utils/__init__.py
Normal file
158
plugins/module_utils/alicloud_ecs.py
Normal file
158
plugins/module_utils/alicloud_ecs.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c) 2017 Alibaba Group Holding Limited. He Guimin <heguimin36@163.com>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
|
||||
try:
|
||||
import footmark
|
||||
import footmark.ecs
|
||||
import footmark.slb
|
||||
import footmark.vpc
|
||||
import footmark.rds
|
||||
import footmark.ess
|
||||
HAS_FOOTMARK = True
|
||||
except ImportError:
|
||||
HAS_FOOTMARK = False
|
||||
|
||||
|
||||
class AnsibleACSError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def acs_common_argument_spec():
|
||||
return dict(
|
||||
alicloud_access_key=dict(required=True, aliases=['access_key_id', 'access_key'], no_log=True,
|
||||
fallback=(env_fallback, ['ALICLOUD_ACCESS_KEY', 'ALICLOUD_ACCESS_KEY_ID'])),
|
||||
alicloud_secret_key=dict(required=True, aliases=['secret_access_key', 'secret_key'], no_log=True,
|
||||
fallback=(env_fallback, ['ALICLOUD_SECRET_KEY', 'ALICLOUD_SECRET_ACCESS_KEY'])),
|
||||
alicloud_security_token=dict(aliases=['security_token'], no_log=True,
|
||||
fallback=(env_fallback, ['ALICLOUD_SECURITY_TOKEN'])),
|
||||
)
|
||||
|
||||
|
||||
def ecs_argument_spec():
|
||||
spec = acs_common_argument_spec()
|
||||
spec.update(
|
||||
dict(
|
||||
alicloud_region=dict(required=True, aliases=['region', 'region_id'],
|
||||
fallback=(env_fallback, ['ALICLOUD_REGION', 'ALICLOUD_REGION_ID'])),
|
||||
)
|
||||
)
|
||||
return spec
|
||||
|
||||
|
||||
def get_acs_connection_info(module):
|
||||
|
||||
ecs_params = dict(acs_access_key_id=module.params.get('alicloud_access_key'),
|
||||
acs_secret_access_key=module.params.get('alicloud_secret_key'),
|
||||
security_token=module.params.get('alicloud_security_token'),
|
||||
user_agent='Ansible-Provider-Alicloud')
|
||||
|
||||
return module.params.get('alicloud_region'), ecs_params
|
||||
|
||||
|
||||
def connect_to_acs(acs_module, region, **params):
|
||||
conn = acs_module.connect_to_region(region, **params)
|
||||
if not conn:
|
||||
if region not in [acs_module_region.id for acs_module_region in acs_module.regions()]:
|
||||
raise AnsibleACSError(
|
||||
"Region %s does not seem to be available for acs module %s." % (region, acs_module.__name__))
|
||||
else:
|
||||
raise AnsibleACSError(
|
||||
"Unknown problem connecting to region %s for acs module %s." % (region, acs_module.__name__))
|
||||
return conn
|
||||
|
||||
|
||||
def ecs_connect(module):
|
||||
""" Return an ecs connection"""
|
||||
|
||||
region, ecs_params = get_acs_connection_info(module)
|
||||
# If we have a region specified, connect to its endpoint.
|
||||
if region:
|
||||
try:
|
||||
ecs = connect_to_acs(footmark.ecs, region, **ecs_params)
|
||||
except AnsibleACSError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
# Otherwise, no region so we fallback to the old connection method
|
||||
return ecs
|
||||
|
||||
|
||||
def slb_connect(module):
|
||||
""" Return an slb connection"""
|
||||
|
||||
region, slb_params = get_acs_connection_info(module)
|
||||
# If we have a region specified, connect to its endpoint.
|
||||
if region:
|
||||
try:
|
||||
slb = connect_to_acs(footmark.slb, region, **slb_params)
|
||||
except AnsibleACSError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
# Otherwise, no region so we fallback to the old connection method
|
||||
return slb
|
||||
|
||||
|
||||
def vpc_connect(module):
|
||||
""" Return an vpc connection"""
|
||||
|
||||
region, vpc_params = get_acs_connection_info(module)
|
||||
# If we have a region specified, connect to its endpoint.
|
||||
if region:
|
||||
try:
|
||||
vpc = connect_to_acs(footmark.vpc, region, **vpc_params)
|
||||
except AnsibleACSError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
# Otherwise, no region so we fallback to the old connection method
|
||||
return vpc
|
||||
|
||||
|
||||
def rds_connect(module):
|
||||
""" Return an rds connection"""
|
||||
|
||||
region, rds_params = get_acs_connection_info(module)
|
||||
# If we have a region specified, connect to its endpoint.
|
||||
if region:
|
||||
try:
|
||||
rds = connect_to_acs(footmark.rds, region, **rds_params)
|
||||
except AnsibleACSError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
# Otherwise, no region so we fallback to the old connection method
|
||||
return rds
|
||||
|
||||
|
||||
def ess_connect(module):
|
||||
""" Return an ess connection"""
|
||||
|
||||
region, ess_params = get_acs_connection_info(module)
|
||||
# If we have a region specified, connect to its endpoint.
|
||||
if region:
|
||||
try:
|
||||
ess = connect_to_acs(footmark.ess, region, **ess_params)
|
||||
except AnsibleACSError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
# Otherwise, no region so we fallback to the old connection method
|
||||
return ess
|
||||
217
plugins/module_utils/cloud.py
Normal file
217
plugins/module_utils/cloud.py
Normal file
@@ -0,0 +1,217 @@
|
||||
#
|
||||
# (c) 2016 Allen Sanabria, <asanabria@linuxdynasty.org>
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
"""
|
||||
This module adds shared support for generic cloud modules
|
||||
|
||||
In order to use this module, include it as part of a custom
|
||||
module as shown below.
|
||||
|
||||
from ansible.module_utils.cloud import CloudRetry
|
||||
|
||||
The 'cloud' module provides the following common classes:
|
||||
|
||||
* CloudRetry
|
||||
- The base class to be used by other cloud providers, in order to
|
||||
provide a backoff/retry decorator based on status codes.
|
||||
|
||||
- Example using the AWSRetry class which inherits from CloudRetry.
|
||||
|
||||
@AWSRetry.exponential_backoff(retries=10, delay=3)
|
||||
get_ec2_security_group_ids_from_names()
|
||||
|
||||
@AWSRetry.jittered_backoff()
|
||||
get_ec2_security_group_ids_from_names()
|
||||
|
||||
"""
|
||||
import random
|
||||
from functools import wraps
|
||||
import syslog
|
||||
import time
|
||||
|
||||
|
||||
def _exponential_backoff(retries=10, delay=2, backoff=2, max_delay=60):
|
||||
""" Customizable exponential backoff strategy.
|
||||
Args:
|
||||
retries (int): Maximum number of times to retry a request.
|
||||
delay (float): Initial (base) delay.
|
||||
backoff (float): base of the exponent to use for exponential
|
||||
backoff.
|
||||
max_delay (int): Optional. If provided each delay generated is capped
|
||||
at this amount. Defaults to 60 seconds.
|
||||
Returns:
|
||||
Callable that returns a generator. This generator yields durations in
|
||||
seconds to be used as delays for an exponential backoff strategy.
|
||||
Usage:
|
||||
>>> backoff = _exponential_backoff()
|
||||
>>> backoff
|
||||
<function backoff_backoff at 0x7f0d939facf8>
|
||||
>>> list(backoff())
|
||||
[2, 4, 8, 16, 32, 60, 60, 60, 60, 60]
|
||||
"""
|
||||
def backoff_gen():
|
||||
for retry in range(0, retries):
|
||||
sleep = delay * backoff ** retry
|
||||
yield sleep if max_delay is None else min(sleep, max_delay)
|
||||
return backoff_gen
|
||||
|
||||
|
||||
def _full_jitter_backoff(retries=10, delay=3, max_delay=60, _random=random):
|
||||
""" Implements the "Full Jitter" backoff strategy described here
|
||||
https://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||
Args:
|
||||
retries (int): Maximum number of times to retry a request.
|
||||
delay (float): Approximate number of seconds to sleep for the first
|
||||
retry.
|
||||
max_delay (int): The maximum number of seconds to sleep for any retry.
|
||||
_random (random.Random or None): Makes this generator testable by
|
||||
allowing developers to explicitly pass in the a seeded Random.
|
||||
Returns:
|
||||
Callable that returns a generator. This generator yields durations in
|
||||
seconds to be used as delays for a full jitter backoff strategy.
|
||||
Usage:
|
||||
>>> backoff = _full_jitter_backoff(retries=5)
|
||||
>>> backoff
|
||||
<function backoff_backoff at 0x7f0d939facf8>
|
||||
>>> list(backoff())
|
||||
[3, 6, 5, 23, 38]
|
||||
>>> list(backoff())
|
||||
[2, 1, 6, 6, 31]
|
||||
"""
|
||||
def backoff_gen():
|
||||
for retry in range(0, retries):
|
||||
yield _random.randint(0, min(max_delay, delay * 2 ** retry))
|
||||
return backoff_gen
|
||||
|
||||
|
||||
class CloudRetry(object):
|
||||
""" CloudRetry can be used by any cloud provider, in order to implement a
|
||||
backoff algorithm/retry effect based on Status Code from Exceptions.
|
||||
"""
|
||||
# This is the base class of the exception.
|
||||
# AWS Example botocore.exceptions.ClientError
|
||||
base_class = None
|
||||
|
||||
@staticmethod
|
||||
def status_code_from_exception(error):
|
||||
""" Return the status code from the exception object
|
||||
Args:
|
||||
error (object): The exception itself.
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def found(response_code, catch_extra_error_codes=None):
|
||||
""" Return True if the Response Code to retry on was found.
|
||||
Args:
|
||||
response_code (str): This is the Response Code that is being matched against.
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _backoff(cls, backoff_strategy, catch_extra_error_codes=None):
|
||||
""" Retry calling the Cloud decorated function using the provided
|
||||
backoff strategy.
|
||||
Args:
|
||||
backoff_strategy (callable): Callable that returns a generator. The
|
||||
generator should yield sleep times for each retry of the decorated
|
||||
function.
|
||||
"""
|
||||
def deco(f):
|
||||
@wraps(f)
|
||||
def retry_func(*args, **kwargs):
|
||||
for delay in backoff_strategy():
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if isinstance(e, cls.base_class):
|
||||
response_code = cls.status_code_from_exception(e)
|
||||
if cls.found(response_code, catch_extra_error_codes):
|
||||
msg = "{0}: Retrying in {1} seconds...".format(str(e), delay)
|
||||
syslog.syslog(syslog.LOG_INFO, msg)
|
||||
time.sleep(delay)
|
||||
else:
|
||||
# Return original exception if exception is not a ClientError
|
||||
raise e
|
||||
else:
|
||||
# Return original exception if exception is not a ClientError
|
||||
raise e
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return retry_func # true decorator
|
||||
|
||||
return deco
|
||||
|
||||
@classmethod
|
||||
def exponential_backoff(cls, retries=10, delay=3, backoff=2, max_delay=60, catch_extra_error_codes=None):
|
||||
"""
|
||||
Retry calling the Cloud decorated function using an exponential backoff.
|
||||
|
||||
Kwargs:
|
||||
retries (int): Number of times to retry a failed request before giving up
|
||||
default=10
|
||||
delay (int or float): Initial delay between retries in seconds
|
||||
default=3
|
||||
backoff (int or float): backoff multiplier e.g. value of 2 will
|
||||
double the delay each retry
|
||||
default=1.1
|
||||
max_delay (int or None): maximum amount of time to wait between retries.
|
||||
default=60
|
||||
"""
|
||||
return cls._backoff(_exponential_backoff(
|
||||
retries=retries, delay=delay, backoff=backoff, max_delay=max_delay), catch_extra_error_codes)
|
||||
|
||||
@classmethod
|
||||
def jittered_backoff(cls, retries=10, delay=3, max_delay=60, catch_extra_error_codes=None):
|
||||
"""
|
||||
Retry calling the Cloud decorated function using a jittered backoff
|
||||
strategy. More on this strategy here:
|
||||
|
||||
https://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||
|
||||
Kwargs:
|
||||
retries (int): Number of times to retry a failed request before giving up
|
||||
default=10
|
||||
delay (int): Initial delay between retries in seconds
|
||||
default=3
|
||||
max_delay (int): maximum amount of time to wait between retries.
|
||||
default=60
|
||||
"""
|
||||
return cls._backoff(_full_jitter_backoff(
|
||||
retries=retries, delay=delay, max_delay=max_delay), catch_extra_error_codes)
|
||||
|
||||
@classmethod
|
||||
def backoff(cls, tries=10, delay=3, backoff=1.1, catch_extra_error_codes=None):
|
||||
"""
|
||||
Retry calling the Cloud decorated function using an exponential backoff.
|
||||
|
||||
Compatibility for the original implementation of CloudRetry.backoff that
|
||||
did not provide configurable backoff strategies. Developers should use
|
||||
CloudRetry.exponential_backoff instead.
|
||||
|
||||
Kwargs:
|
||||
tries (int): Number of times to try (not retry) before giving up
|
||||
default=10
|
||||
delay (int or float): Initial delay between retries in seconds
|
||||
default=3
|
||||
backoff (int or float): backoff multiplier e.g. value of 2 will
|
||||
double the delay each retry
|
||||
default=1.1
|
||||
"""
|
||||
return cls.exponential_backoff(
|
||||
retries=tries - 1, delay=delay, backoff=backoff, max_delay=None, catch_extra_error_codes=catch_extra_error_codes)
|
||||
132
plugins/module_utils/cloudscale.py
Normal file
132
plugins/module_utils/cloudscale.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2017, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from copy import deepcopy
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
API_URL = 'https://api.cloudscale.ch/v1/'
|
||||
|
||||
|
||||
def cloudscale_argument_spec():
|
||||
return dict(
|
||||
api_token=dict(fallback=(env_fallback, ['CLOUDSCALE_API_TOKEN']),
|
||||
no_log=True,
|
||||
required=True,
|
||||
type='str'),
|
||||
api_timeout=dict(default=30, type='int'),
|
||||
)
|
||||
|
||||
|
||||
class AnsibleCloudscaleBase(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._auth_header = {'Authorization': 'Bearer %s' % module.params['api_token']}
|
||||
self._result = {
|
||||
'changed': False,
|
||||
'diff': dict(before=dict(), after=dict()),
|
||||
}
|
||||
|
||||
def _get(self, api_call):
|
||||
resp, info = fetch_url(self._module, API_URL + api_call,
|
||||
headers=self._auth_header,
|
||||
timeout=self._module.params['api_timeout'])
|
||||
|
||||
if info['status'] == 200:
|
||||
return self._module.from_json(to_text(resp.read(), errors='surrogate_or_strict'))
|
||||
elif info['status'] == 404:
|
||||
return None
|
||||
else:
|
||||
self._module.fail_json(msg='Failure while calling the cloudscale.ch API with GET for '
|
||||
'"%s".' % api_call, fetch_url_info=info)
|
||||
|
||||
def _post_or_patch(self, api_call, method, data):
|
||||
# This helps with tags when we have the full API resource href to update.
|
||||
if API_URL not in api_call:
|
||||
api_endpoint = API_URL + api_call
|
||||
else:
|
||||
api_endpoint = api_call
|
||||
|
||||
headers = self._auth_header.copy()
|
||||
if data is not None:
|
||||
# Sanitize data dictionary
|
||||
# Deepcopy: Duplicate the data object for iteration, because
|
||||
# iterating an object and changing it at the same time is insecure
|
||||
for k, v in deepcopy(data).items():
|
||||
if v is None:
|
||||
del data[k]
|
||||
|
||||
data = self._module.jsonify(data)
|
||||
headers['Content-type'] = 'application/json'
|
||||
|
||||
resp, info = fetch_url(self._module,
|
||||
api_endpoint,
|
||||
headers=headers,
|
||||
method=method,
|
||||
data=data,
|
||||
timeout=self._module.params['api_timeout'])
|
||||
|
||||
if info['status'] in (200, 201):
|
||||
return self._module.from_json(to_text(resp.read(), errors='surrogate_or_strict'))
|
||||
elif info['status'] == 204:
|
||||
return None
|
||||
else:
|
||||
self._module.fail_json(msg='Failure while calling the cloudscale.ch API with %s for '
|
||||
'"%s".' % (method, api_call), fetch_url_info=info)
|
||||
|
||||
def _post(self, api_call, data=None):
|
||||
return self._post_or_patch(api_call, 'POST', data)
|
||||
|
||||
def _patch(self, api_call, data=None):
|
||||
return self._post_or_patch(api_call, 'PATCH', data)
|
||||
|
||||
def _delete(self, api_call):
|
||||
resp, info = fetch_url(self._module,
|
||||
API_URL + api_call,
|
||||
headers=self._auth_header,
|
||||
method='DELETE',
|
||||
timeout=self._module.params['api_timeout'])
|
||||
|
||||
if info['status'] == 204:
|
||||
return None
|
||||
else:
|
||||
self._module.fail_json(msg='Failure while calling the cloudscale.ch API with DELETE for '
|
||||
'"%s".' % api_call, fetch_url_info=info)
|
||||
|
||||
def _param_updated(self, key, resource):
|
||||
param = self._module.params.get(key)
|
||||
if param is None:
|
||||
return False
|
||||
|
||||
if resource and key in resource:
|
||||
if param != resource[key]:
|
||||
self._result['changed'] = True
|
||||
|
||||
patch_data = {
|
||||
key: param
|
||||
}
|
||||
|
||||
self._result['diff']['before'].update({key: resource[key]})
|
||||
self._result['diff']['after'].update(patch_data)
|
||||
|
||||
if not self._module.check_mode:
|
||||
href = resource.get('href')
|
||||
if not href:
|
||||
self._module.fail_json(msg='Unable to update %s, no href found.' % key)
|
||||
|
||||
self._patch(href, patch_data)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_result(self, resource):
|
||||
if resource:
|
||||
for k, v in resource.items():
|
||||
self._result[k] = v
|
||||
return self._result
|
||||
664
plugins/module_utils/cloudstack.py
Normal file
664
plugins/module_utils/cloudstack.py
Normal file
@@ -0,0 +1,664 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, René Moser <mail@renemoser.net>
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_text, to_native
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
CS_IMP_ERR = None
|
||||
try:
|
||||
from cs import CloudStack, CloudStackException, read_config
|
||||
HAS_LIB_CS = True
|
||||
except ImportError:
|
||||
CS_IMP_ERR = traceback.format_exc()
|
||||
HAS_LIB_CS = False
|
||||
|
||||
|
||||
if sys.version_info > (3,):
|
||||
long = int
|
||||
|
||||
|
||||
def cs_argument_spec():
|
||||
return dict(
|
||||
api_key=dict(default=os.environ.get('CLOUDSTACK_KEY')),
|
||||
api_secret=dict(default=os.environ.get('CLOUDSTACK_SECRET'), no_log=True),
|
||||
api_url=dict(default=os.environ.get('CLOUDSTACK_ENDPOINT')),
|
||||
api_http_method=dict(choices=['get', 'post'], default=os.environ.get('CLOUDSTACK_METHOD')),
|
||||
api_timeout=dict(type='int', default=os.environ.get('CLOUDSTACK_TIMEOUT')),
|
||||
api_region=dict(default=os.environ.get('CLOUDSTACK_REGION') or 'cloudstack'),
|
||||
)
|
||||
|
||||
|
||||
def cs_required_together():
|
||||
return [['api_key', 'api_secret']]
|
||||
|
||||
|
||||
class AnsibleCloudStack:
|
||||
|
||||
def __init__(self, module):
|
||||
if not HAS_LIB_CS:
|
||||
module.fail_json(msg=missing_required_lib('cs'), exception=CS_IMP_ERR)
|
||||
|
||||
self.result = {
|
||||
'changed': False,
|
||||
'diff': {
|
||||
'before': dict(),
|
||||
'after': dict()
|
||||
}
|
||||
}
|
||||
|
||||
# Common returns, will be merged with self.returns
|
||||
# search_for_key: replace_with_key
|
||||
self.common_returns = {
|
||||
'id': 'id',
|
||||
'name': 'name',
|
||||
'created': 'created',
|
||||
'zonename': 'zone',
|
||||
'state': 'state',
|
||||
'project': 'project',
|
||||
'account': 'account',
|
||||
'domain': 'domain',
|
||||
'displaytext': 'display_text',
|
||||
'displayname': 'display_name',
|
||||
'description': 'description',
|
||||
}
|
||||
|
||||
# Init returns dict for use in subclasses
|
||||
self.returns = {}
|
||||
# these values will be casted to int
|
||||
self.returns_to_int = {}
|
||||
# these keys will be compared case sensitive in self.has_changed()
|
||||
self.case_sensitive_keys = [
|
||||
'id',
|
||||
'displaytext',
|
||||
'displayname',
|
||||
'description',
|
||||
]
|
||||
|
||||
self.module = module
|
||||
self._cs = None
|
||||
|
||||
# Helper for VPCs
|
||||
self._vpc_networks_ids = None
|
||||
|
||||
self.domain = None
|
||||
self.account = None
|
||||
self.project = None
|
||||
self.ip_address = None
|
||||
self.network = None
|
||||
self.physical_network = None
|
||||
self.vpc = None
|
||||
self.zone = None
|
||||
self.vm = None
|
||||
self.vm_default_nic = None
|
||||
self.os_type = None
|
||||
self.hypervisor = None
|
||||
self.capabilities = None
|
||||
self.network_acl = None
|
||||
|
||||
@property
|
||||
def cs(self):
|
||||
if self._cs is None:
|
||||
api_config = self.get_api_config()
|
||||
self._cs = CloudStack(**api_config)
|
||||
return self._cs
|
||||
|
||||
def get_api_config(self):
|
||||
api_region = self.module.params.get('api_region') or os.environ.get('CLOUDSTACK_REGION')
|
||||
try:
|
||||
config = read_config(api_region)
|
||||
except KeyError:
|
||||
config = {}
|
||||
|
||||
api_config = {
|
||||
'endpoint': self.module.params.get('api_url') or config.get('endpoint'),
|
||||
'key': self.module.params.get('api_key') or config.get('key'),
|
||||
'secret': self.module.params.get('api_secret') or config.get('secret'),
|
||||
'timeout': self.module.params.get('api_timeout') or config.get('timeout') or 10,
|
||||
'method': self.module.params.get('api_http_method') or config.get('method') or 'get',
|
||||
}
|
||||
self.result.update({
|
||||
'api_region': api_region,
|
||||
'api_url': api_config['endpoint'],
|
||||
'api_key': api_config['key'],
|
||||
'api_timeout': int(api_config['timeout']),
|
||||
'api_http_method': api_config['method'],
|
||||
})
|
||||
if not all([api_config['endpoint'], api_config['key'], api_config['secret']]):
|
||||
self.fail_json(msg="Missing api credentials: can not authenticate")
|
||||
return api_config
|
||||
|
||||
def fail_json(self, **kwargs):
|
||||
self.result.update(kwargs)
|
||||
self.module.fail_json(**self.result)
|
||||
|
||||
def get_or_fallback(self, key=None, fallback_key=None):
|
||||
value = self.module.params.get(key)
|
||||
if not value:
|
||||
value = self.module.params.get(fallback_key)
|
||||
return value
|
||||
|
||||
def has_changed(self, want_dict, current_dict, only_keys=None, skip_diff_for_keys=None):
|
||||
result = False
|
||||
for key, value in want_dict.items():
|
||||
|
||||
# Optionally limit by a list of keys
|
||||
if only_keys and key not in only_keys:
|
||||
continue
|
||||
|
||||
# Skip None values
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if key in current_dict:
|
||||
if isinstance(value, (int, float, long, complex)):
|
||||
|
||||
# ensure we compare the same type
|
||||
if isinstance(value, int):
|
||||
current_dict[key] = int(current_dict[key])
|
||||
elif isinstance(value, float):
|
||||
current_dict[key] = float(current_dict[key])
|
||||
elif isinstance(value, long):
|
||||
current_dict[key] = long(current_dict[key])
|
||||
elif isinstance(value, complex):
|
||||
current_dict[key] = complex(current_dict[key])
|
||||
|
||||
if value != current_dict[key]:
|
||||
if skip_diff_for_keys and key not in skip_diff_for_keys:
|
||||
self.result['diff']['before'][key] = current_dict[key]
|
||||
self.result['diff']['after'][key] = value
|
||||
result = True
|
||||
else:
|
||||
before_value = to_text(current_dict[key])
|
||||
after_value = to_text(value)
|
||||
|
||||
if self.case_sensitive_keys and key in self.case_sensitive_keys:
|
||||
if before_value != after_value:
|
||||
if skip_diff_for_keys and key not in skip_diff_for_keys:
|
||||
self.result['diff']['before'][key] = before_value
|
||||
self.result['diff']['after'][key] = after_value
|
||||
result = True
|
||||
|
||||
# Test for diff in case insensitive way
|
||||
elif before_value.lower() != after_value.lower():
|
||||
if skip_diff_for_keys and key not in skip_diff_for_keys:
|
||||
self.result['diff']['before'][key] = before_value
|
||||
self.result['diff']['after'][key] = after_value
|
||||
result = True
|
||||
else:
|
||||
if skip_diff_for_keys and key not in skip_diff_for_keys:
|
||||
self.result['diff']['before'][key] = None
|
||||
self.result['diff']['after'][key] = to_text(value)
|
||||
result = True
|
||||
return result
|
||||
|
||||
def _get_by_key(self, key=None, my_dict=None):
|
||||
if my_dict is None:
|
||||
my_dict = {}
|
||||
if key:
|
||||
if key in my_dict:
|
||||
return my_dict[key]
|
||||
self.fail_json(msg="Something went wrong: %s not found" % key)
|
||||
return my_dict
|
||||
|
||||
def query_api(self, command, **args):
|
||||
try:
|
||||
res = getattr(self.cs, command)(**args)
|
||||
|
||||
if 'errortext' in res:
|
||||
self.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||
|
||||
except CloudStackException as e:
|
||||
self.fail_json(msg='CloudStackException: %s' % to_native(e))
|
||||
|
||||
except Exception as e:
|
||||
self.fail_json(msg=to_native(e))
|
||||
|
||||
return res
|
||||
|
||||
def get_network_acl(self, key=None):
|
||||
if self.network_acl is None:
|
||||
args = {
|
||||
'name': self.module.params.get('network_acl'),
|
||||
'vpcid': self.get_vpc(key='id'),
|
||||
}
|
||||
network_acls = self.query_api('listNetworkACLLists', **args)
|
||||
if network_acls:
|
||||
self.network_acl = network_acls['networkacllist'][0]
|
||||
self.result['network_acl'] = self.network_acl['name']
|
||||
if self.network_acl:
|
||||
return self._get_by_key(key, self.network_acl)
|
||||
else:
|
||||
self.fail_json(msg="Network ACL %s not found" % self.module.params.get('network_acl'))
|
||||
|
||||
def get_vpc(self, key=None):
|
||||
"""Return a VPC dictionary or the value of given key of."""
|
||||
if self.vpc:
|
||||
return self._get_by_key(key, self.vpc)
|
||||
|
||||
vpc = self.module.params.get('vpc')
|
||||
if not vpc:
|
||||
vpc = os.environ.get('CLOUDSTACK_VPC')
|
||||
if not vpc:
|
||||
return None
|
||||
|
||||
args = {
|
||||
'account': self.get_account(key='name'),
|
||||
'domainid': self.get_domain(key='id'),
|
||||
'projectid': self.get_project(key='id'),
|
||||
'zoneid': self.get_zone(key='id'),
|
||||
}
|
||||
vpcs = self.query_api('listVPCs', **args)
|
||||
if not vpcs:
|
||||
self.fail_json(msg="No VPCs available.")
|
||||
|
||||
for v in vpcs['vpc']:
|
||||
if vpc in [v['name'], v['displaytext'], v['id']]:
|
||||
# Fail if the identifyer matches more than one VPC
|
||||
if self.vpc:
|
||||
self.fail_json(msg="More than one VPC found with the provided identifyer '%s'" % vpc)
|
||||
else:
|
||||
self.vpc = v
|
||||
self.result['vpc'] = v['name']
|
||||
if self.vpc:
|
||||
return self._get_by_key(key, self.vpc)
|
||||
self.fail_json(msg="VPC '%s' not found" % vpc)
|
||||
|
||||
def is_vpc_network(self, network_id):
|
||||
"""Returns True if network is in VPC."""
|
||||
# This is an efficient way to query a lot of networks at a time
|
||||
if self._vpc_networks_ids is None:
|
||||
args = {
|
||||
'account': self.get_account(key='name'),
|
||||
'domainid': self.get_domain(key='id'),
|
||||
'projectid': self.get_project(key='id'),
|
||||
'zoneid': self.get_zone(key='id'),
|
||||
}
|
||||
vpcs = self.query_api('listVPCs', **args)
|
||||
self._vpc_networks_ids = []
|
||||
if vpcs:
|
||||
for vpc in vpcs['vpc']:
|
||||
for n in vpc.get('network', []):
|
||||
self._vpc_networks_ids.append(n['id'])
|
||||
return network_id in self._vpc_networks_ids
|
||||
|
||||
def get_physical_network(self, key=None):
|
||||
if self.physical_network:
|
||||
return self._get_by_key(key, self.physical_network)
|
||||
physical_network = self.module.params.get('physical_network')
|
||||
args = {
|
||||
'zoneid': self.get_zone(key='id')
|
||||
}
|
||||
physical_networks = self.query_api('listPhysicalNetworks', **args)
|
||||
if not physical_networks:
|
||||
self.fail_json(msg="No physical networks available.")
|
||||
|
||||
for net in physical_networks['physicalnetwork']:
|
||||
if physical_network in [net['name'], net['id']]:
|
||||
self.physical_network = net
|
||||
self.result['physical_network'] = net['name']
|
||||
return self._get_by_key(key, self.physical_network)
|
||||
self.fail_json(msg="Physical Network '%s' not found" % physical_network)
|
||||
|
||||
def get_network(self, key=None):
|
||||
"""Return a network dictionary or the value of given key of."""
|
||||
if self.network:
|
||||
return self._get_by_key(key, self.network)
|
||||
|
||||
network = self.module.params.get('network')
|
||||
if not network:
|
||||
vpc_name = self.get_vpc(key='name')
|
||||
if vpc_name:
|
||||
self.fail_json(msg="Could not find network for VPC '%s' due missing argument: network" % vpc_name)
|
||||
return None
|
||||
|
||||
args = {
|
||||
'account': self.get_account(key='name'),
|
||||
'domainid': self.get_domain(key='id'),
|
||||
'projectid': self.get_project(key='id'),
|
||||
'zoneid': self.get_zone(key='id'),
|
||||
'vpcid': self.get_vpc(key='id')
|
||||
}
|
||||
networks = self.query_api('listNetworks', **args)
|
||||
if not networks:
|
||||
self.fail_json(msg="No networks available.")
|
||||
|
||||
for n in networks['network']:
|
||||
# ignore any VPC network if vpc param is not given
|
||||
if 'vpcid' in n and not self.get_vpc(key='id'):
|
||||
continue
|
||||
if network in [n['displaytext'], n['name'], n['id']]:
|
||||
self.result['network'] = n['name']
|
||||
self.network = n
|
||||
return self._get_by_key(key, self.network)
|
||||
self.fail_json(msg="Network '%s' not found" % network)
|
||||
|
||||
def get_project(self, key=None):
|
||||
if self.project:
|
||||
return self._get_by_key(key, self.project)
|
||||
|
||||
project = self.module.params.get('project')
|
||||
if not project:
|
||||
project = os.environ.get('CLOUDSTACK_PROJECT')
|
||||
if not project:
|
||||
return None
|
||||
args = {
|
||||
'account': self.get_account(key='name'),
|
||||
'domainid': self.get_domain(key='id')
|
||||
}
|
||||
projects = self.query_api('listProjects', **args)
|
||||
if projects:
|
||||
for p in projects['project']:
|
||||
if project.lower() in [p['name'].lower(), p['id']]:
|
||||
self.result['project'] = p['name']
|
||||
self.project = p
|
||||
return self._get_by_key(key, self.project)
|
||||
self.fail_json(msg="project '%s' not found" % project)
|
||||
|
||||
def get_ip_address(self, key=None):
|
||||
if self.ip_address:
|
||||
return self._get_by_key(key, self.ip_address)
|
||||
|
||||
ip_address = self.module.params.get('ip_address')
|
||||
if not ip_address:
|
||||
self.fail_json(msg="IP address param 'ip_address' is required")
|
||||
|
||||
args = {
|
||||
'ipaddress': ip_address,
|
||||
'account': self.get_account(key='name'),
|
||||
'domainid': self.get_domain(key='id'),
|
||||
'projectid': self.get_project(key='id'),
|
||||
'vpcid': self.get_vpc(key='id'),
|
||||
}
|
||||
|
||||
ip_addresses = self.query_api('listPublicIpAddresses', **args)
|
||||
|
||||
if not ip_addresses:
|
||||
self.fail_json(msg="IP address '%s' not found" % args['ipaddress'])
|
||||
|
||||
self.ip_address = ip_addresses['publicipaddress'][0]
|
||||
return self._get_by_key(key, self.ip_address)
|
||||
|
||||
def get_vm_guest_ip(self):
|
||||
vm_guest_ip = self.module.params.get('vm_guest_ip')
|
||||
default_nic = self.get_vm_default_nic()
|
||||
|
||||
if not vm_guest_ip:
|
||||
return default_nic['ipaddress']
|
||||
|
||||
for secondary_ip in default_nic['secondaryip']:
|
||||
if vm_guest_ip == secondary_ip['ipaddress']:
|
||||
return vm_guest_ip
|
||||
self.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip)
|
||||
|
||||
def get_vm_default_nic(self):
|
||||
if self.vm_default_nic:
|
||||
return self.vm_default_nic
|
||||
|
||||
nics = self.query_api('listNics', virtualmachineid=self.get_vm(key='id'))
|
||||
if nics:
|
||||
for n in nics['nic']:
|
||||
if n['isdefault']:
|
||||
self.vm_default_nic = n
|
||||
return self.vm_default_nic
|
||||
self.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm'))
|
||||
|
||||
def get_vm(self, key=None, filter_zone=True):
|
||||
if self.vm:
|
||||
return self._get_by_key(key, self.vm)
|
||||
|
||||
vm = self.module.params.get('vm')
|
||||
if not vm:
|
||||
self.fail_json(msg="Virtual machine param 'vm' is required")
|
||||
|
||||
args = {
|
||||
'account': self.get_account(key='name'),
|
||||
'domainid': self.get_domain(key='id'),
|
||||
'projectid': self.get_project(key='id'),
|
||||
'zoneid': self.get_zone(key='id') if filter_zone else None,
|
||||
'fetch_list': True,
|
||||
}
|
||||
vms = self.query_api('listVirtualMachines', **args)
|
||||
if vms:
|
||||
for v in vms:
|
||||
if vm.lower() in [v['name'].lower(), v['displayname'].lower(), v['id']]:
|
||||
self.vm = v
|
||||
return self._get_by_key(key, self.vm)
|
||||
self.fail_json(msg="Virtual machine '%s' not found" % vm)
|
||||
|
||||
def get_disk_offering(self, key=None):
|
||||
disk_offering = self.module.params.get('disk_offering')
|
||||
if not disk_offering:
|
||||
return None
|
||||
|
||||
# Do not add domain filter for disk offering listing.
|
||||
disk_offerings = self.query_api('listDiskOfferings')
|
||||
if disk_offerings:
|
||||
for d in disk_offerings['diskoffering']:
|
||||
if disk_offering in [d['displaytext'], d['name'], d['id']]:
|
||||
return self._get_by_key(key, d)
|
||||
self.fail_json(msg="Disk offering '%s' not found" % disk_offering)
|
||||
|
||||
def get_zone(self, key=None):
|
||||
if self.zone:
|
||||
return self._get_by_key(key, self.zone)
|
||||
|
||||
zone = self.module.params.get('zone')
|
||||
if not zone:
|
||||
zone = os.environ.get('CLOUDSTACK_ZONE')
|
||||
zones = self.query_api('listZones')
|
||||
|
||||
if not zones:
|
||||
self.fail_json(msg="No zones available. Please create a zone first")
|
||||
|
||||
# use the first zone if no zone param given
|
||||
if not zone:
|
||||
self.zone = zones['zone'][0]
|
||||
self.result['zone'] = self.zone['name']
|
||||
return self._get_by_key(key, self.zone)
|
||||
|
||||
if zones:
|
||||
for z in zones['zone']:
|
||||
if zone.lower() in [z['name'].lower(), z['id']]:
|
||||
self.result['zone'] = z['name']
|
||||
self.zone = z
|
||||
return self._get_by_key(key, self.zone)
|
||||
self.fail_json(msg="zone '%s' not found" % zone)
|
||||
|
||||
def get_os_type(self, key=None):
|
||||
if self.os_type:
|
||||
return self._get_by_key(key, self.zone)
|
||||
|
||||
os_type = self.module.params.get('os_type')
|
||||
if not os_type:
|
||||
return None
|
||||
|
||||
os_types = self.query_api('listOsTypes')
|
||||
if os_types:
|
||||
for o in os_types['ostype']:
|
||||
if os_type in [o['description'], o['id']]:
|
||||
self.os_type = o
|
||||
return self._get_by_key(key, self.os_type)
|
||||
self.fail_json(msg="OS type '%s' not found" % os_type)
|
||||
|
||||
def get_hypervisor(self):
|
||||
if self.hypervisor:
|
||||
return self.hypervisor
|
||||
|
||||
hypervisor = self.module.params.get('hypervisor')
|
||||
hypervisors = self.query_api('listHypervisors')
|
||||
|
||||
# use the first hypervisor if no hypervisor param given
|
||||
if not hypervisor:
|
||||
self.hypervisor = hypervisors['hypervisor'][0]['name']
|
||||
return self.hypervisor
|
||||
|
||||
for h in hypervisors['hypervisor']:
|
||||
if hypervisor.lower() == h['name'].lower():
|
||||
self.hypervisor = h['name']
|
||||
return self.hypervisor
|
||||
self.fail_json(msg="Hypervisor '%s' not found" % hypervisor)
|
||||
|
||||
def get_account(self, key=None):
|
||||
if self.account:
|
||||
return self._get_by_key(key, self.account)
|
||||
|
||||
account = self.module.params.get('account')
|
||||
if not account:
|
||||
account = os.environ.get('CLOUDSTACK_ACCOUNT')
|
||||
if not account:
|
||||
return None
|
||||
|
||||
domain = self.module.params.get('domain')
|
||||
if not domain:
|
||||
self.fail_json(msg="Account must be specified with Domain")
|
||||
|
||||
args = {
|
||||
'name': account,
|
||||
'domainid': self.get_domain(key='id'),
|
||||
'listall': True
|
||||
}
|
||||
accounts = self.query_api('listAccounts', **args)
|
||||
if accounts:
|
||||
self.account = accounts['account'][0]
|
||||
self.result['account'] = self.account['name']
|
||||
return self._get_by_key(key, self.account)
|
||||
self.fail_json(msg="Account '%s' not found" % account)
|
||||
|
||||
def get_domain(self, key=None):
|
||||
if self.domain:
|
||||
return self._get_by_key(key, self.domain)
|
||||
|
||||
domain = self.module.params.get('domain')
|
||||
if not domain:
|
||||
domain = os.environ.get('CLOUDSTACK_DOMAIN')
|
||||
if not domain:
|
||||
return None
|
||||
|
||||
args = {
|
||||
'listall': True,
|
||||
}
|
||||
domains = self.query_api('listDomains', **args)
|
||||
if domains:
|
||||
for d in domains['domain']:
|
||||
if d['path'].lower() in [domain.lower(), "root/" + domain.lower(), "root" + domain.lower()]:
|
||||
self.domain = d
|
||||
self.result['domain'] = d['path']
|
||||
return self._get_by_key(key, self.domain)
|
||||
self.fail_json(msg="Domain '%s' not found" % domain)
|
||||
|
||||
def query_tags(self, resource, resource_type):
|
||||
args = {
|
||||
'resourceid': resource['id'],
|
||||
'resourcetype': resource_type,
|
||||
}
|
||||
tags = self.query_api('listTags', **args)
|
||||
return self.get_tags(resource=tags, key='tag')
|
||||
|
||||
def get_tags(self, resource=None, key='tags'):
|
||||
existing_tags = []
|
||||
for tag in resource.get(key) or []:
|
||||
existing_tags.append({'key': tag['key'], 'value': tag['value']})
|
||||
return existing_tags
|
||||
|
||||
def _process_tags(self, resource, resource_type, tags, operation="create"):
|
||||
if tags:
|
||||
self.result['changed'] = True
|
||||
if not self.module.check_mode:
|
||||
args = {
|
||||
'resourceids': resource['id'],
|
||||
'resourcetype': resource_type,
|
||||
'tags': tags,
|
||||
}
|
||||
if operation == "create":
|
||||
response = self.query_api('createTags', **args)
|
||||
else:
|
||||
response = self.query_api('deleteTags', **args)
|
||||
self.poll_job(response)
|
||||
|
||||
def _tags_that_should_exist_or_be_updated(self, resource, tags):
|
||||
existing_tags = self.get_tags(resource)
|
||||
return [tag for tag in tags if tag not in existing_tags]
|
||||
|
||||
def _tags_that_should_not_exist(self, resource, tags):
|
||||
existing_tags = self.get_tags(resource)
|
||||
return [tag for tag in existing_tags if tag not in tags]
|
||||
|
||||
def ensure_tags(self, resource, resource_type=None):
|
||||
if not resource_type or not resource:
|
||||
self.fail_json(msg="Error: Missing resource or resource_type for tags.")
|
||||
|
||||
if 'tags' in resource:
|
||||
tags = self.module.params.get('tags')
|
||||
if tags is not None:
|
||||
self._process_tags(resource, resource_type, self._tags_that_should_not_exist(resource, tags), operation="delete")
|
||||
self._process_tags(resource, resource_type, self._tags_that_should_exist_or_be_updated(resource, tags))
|
||||
resource['tags'] = self.query_tags(resource=resource, resource_type=resource_type)
|
||||
return resource
|
||||
|
||||
def get_capabilities(self, key=None):
|
||||
if self.capabilities:
|
||||
return self._get_by_key(key, self.capabilities)
|
||||
capabilities = self.query_api('listCapabilities')
|
||||
self.capabilities = capabilities['capability']
|
||||
return self._get_by_key(key, self.capabilities)
|
||||
|
||||
def poll_job(self, job=None, key=None):
|
||||
if 'jobid' in job:
|
||||
while True:
|
||||
res = self.query_api('queryAsyncJobResult', jobid=job['jobid'])
|
||||
if res['jobstatus'] != 0 and 'jobresult' in res:
|
||||
|
||||
if 'errortext' in res['jobresult']:
|
||||
self.fail_json(msg="Failed: '%s'" % res['jobresult']['errortext'])
|
||||
|
||||
if key and key in res['jobresult']:
|
||||
job = res['jobresult'][key]
|
||||
|
||||
break
|
||||
time.sleep(2)
|
||||
return job
|
||||
|
||||
def update_result(self, resource, result=None):
|
||||
if result is None:
|
||||
result = dict()
|
||||
if resource:
|
||||
returns = self.common_returns.copy()
|
||||
returns.update(self.returns)
|
||||
for search_key, return_key in returns.items():
|
||||
if search_key in resource:
|
||||
result[return_key] = resource[search_key]
|
||||
|
||||
# Bad bad API does not always return int when it should.
|
||||
for search_key, return_key in self.returns_to_int.items():
|
||||
if search_key in resource:
|
||||
result[return_key] = int(resource[search_key])
|
||||
|
||||
if 'tags' in resource:
|
||||
result['tags'] = resource['tags']
|
||||
return result
|
||||
|
||||
def get_result(self, resource):
|
||||
return self.update_result(resource, self.result)
|
||||
|
||||
def get_result_and_facts(self, facts_name, resource):
|
||||
result = self.get_result(resource)
|
||||
|
||||
ansible_facts = {
|
||||
facts_name: result.copy()
|
||||
}
|
||||
for k in ['diff', 'changed']:
|
||||
if k in ansible_facts[facts_name]:
|
||||
del ansible_facts[facts_name][k]
|
||||
|
||||
result.update(ansible_facts=ansible_facts)
|
||||
return result
|
||||
142
plugins/module_utils/database.py
Normal file
142
plugins/module_utils/database.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
class SQLParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnclosedQuoteError(SQLParseError):
|
||||
pass
|
||||
|
||||
|
||||
# maps a type of identifier to the maximum number of dot levels that are
|
||||
# allowed to specify that identifier. For example, a database column can be
|
||||
# specified by up to 4 levels: database.schema.table.column
|
||||
_PG_IDENTIFIER_TO_DOT_LEVEL = dict(
|
||||
database=1,
|
||||
schema=2,
|
||||
table=3,
|
||||
column=4,
|
||||
role=1,
|
||||
tablespace=1,
|
||||
sequence=3,
|
||||
publication=1,
|
||||
)
|
||||
_MYSQL_IDENTIFIER_TO_DOT_LEVEL = dict(database=1, table=2, column=3, role=1, vars=1)
|
||||
|
||||
|
||||
def _find_end_quote(identifier, quote_char):
|
||||
accumulate = 0
|
||||
while True:
|
||||
try:
|
||||
quote = identifier.index(quote_char)
|
||||
except ValueError:
|
||||
raise UnclosedQuoteError
|
||||
accumulate = accumulate + quote
|
||||
try:
|
||||
next_char = identifier[quote + 1]
|
||||
except IndexError:
|
||||
return accumulate
|
||||
if next_char == quote_char:
|
||||
try:
|
||||
identifier = identifier[quote + 2:]
|
||||
accumulate = accumulate + 2
|
||||
except IndexError:
|
||||
raise UnclosedQuoteError
|
||||
else:
|
||||
return accumulate
|
||||
|
||||
|
||||
def _identifier_parse(identifier, quote_char):
|
||||
if not identifier:
|
||||
raise SQLParseError('Identifier name unspecified or unquoted trailing dot')
|
||||
|
||||
already_quoted = False
|
||||
if identifier.startswith(quote_char):
|
||||
already_quoted = True
|
||||
try:
|
||||
end_quote = _find_end_quote(identifier[1:], quote_char=quote_char) + 1
|
||||
except UnclosedQuoteError:
|
||||
already_quoted = False
|
||||
else:
|
||||
if end_quote < len(identifier) - 1:
|
||||
if identifier[end_quote + 1] == '.':
|
||||
dot = end_quote + 1
|
||||
first_identifier = identifier[:dot]
|
||||
next_identifier = identifier[dot + 1:]
|
||||
further_identifiers = _identifier_parse(next_identifier, quote_char)
|
||||
further_identifiers.insert(0, first_identifier)
|
||||
else:
|
||||
raise SQLParseError('User escaped identifiers must escape extra quotes')
|
||||
else:
|
||||
further_identifiers = [identifier]
|
||||
|
||||
if not already_quoted:
|
||||
try:
|
||||
dot = identifier.index('.')
|
||||
except ValueError:
|
||||
identifier = identifier.replace(quote_char, quote_char * 2)
|
||||
identifier = ''.join((quote_char, identifier, quote_char))
|
||||
further_identifiers = [identifier]
|
||||
else:
|
||||
if dot == 0 or dot >= len(identifier) - 1:
|
||||
identifier = identifier.replace(quote_char, quote_char * 2)
|
||||
identifier = ''.join((quote_char, identifier, quote_char))
|
||||
further_identifiers = [identifier]
|
||||
else:
|
||||
first_identifier = identifier[:dot]
|
||||
next_identifier = identifier[dot + 1:]
|
||||
further_identifiers = _identifier_parse(next_identifier, quote_char)
|
||||
first_identifier = first_identifier.replace(quote_char, quote_char * 2)
|
||||
first_identifier = ''.join((quote_char, first_identifier, quote_char))
|
||||
further_identifiers.insert(0, first_identifier)
|
||||
|
||||
return further_identifiers
|
||||
|
||||
|
||||
def pg_quote_identifier(identifier, id_type):
|
||||
identifier_fragments = _identifier_parse(identifier, quote_char='"')
|
||||
if len(identifier_fragments) > _PG_IDENTIFIER_TO_DOT_LEVEL[id_type]:
|
||||
raise SQLParseError('PostgreSQL does not support %s with more than %i dots' % (id_type, _PG_IDENTIFIER_TO_DOT_LEVEL[id_type]))
|
||||
return '.'.join(identifier_fragments)
|
||||
|
||||
|
||||
def mysql_quote_identifier(identifier, id_type):
|
||||
identifier_fragments = _identifier_parse(identifier, quote_char='`')
|
||||
if (len(identifier_fragments) - 1) > _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type]:
|
||||
raise SQLParseError('MySQL does not support %s with more than %i dots' % (id_type, _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type]))
|
||||
|
||||
special_cased_fragments = []
|
||||
for fragment in identifier_fragments:
|
||||
if fragment == '`*`':
|
||||
special_cased_fragments.append('*')
|
||||
else:
|
||||
special_cased_fragments.append(fragment)
|
||||
|
||||
return '.'.join(special_cased_fragments)
|
||||
147
plugins/module_utils/digital_ocean.py
Normal file
147
plugins/module_utils/digital_ocean.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Ansible Project 2017
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import json
|
||||
import os
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
|
||||
|
||||
class Response(object):
|
||||
|
||||
def __init__(self, resp, info):
|
||||
self.body = None
|
||||
if resp:
|
||||
self.body = resp.read()
|
||||
self.info = info
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if not self.body:
|
||||
if "body" in self.info:
|
||||
return json.loads(to_text(self.info["body"]))
|
||||
return None
|
||||
try:
|
||||
return json.loads(to_text(self.body))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def status_code(self):
|
||||
return self.info["status"]
|
||||
|
||||
|
||||
class DigitalOceanHelper:
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.baseurl = 'https://api.digitalocean.com/v2'
|
||||
self.timeout = module.params.get('timeout', 30)
|
||||
self.oauth_token = module.params.get('oauth_token')
|
||||
self.headers = {'Authorization': 'Bearer {0}'.format(self.oauth_token),
|
||||
'Content-type': 'application/json'}
|
||||
|
||||
# Check if api_token is valid or not
|
||||
response = self.get('account')
|
||||
if response.status_code == 401:
|
||||
self.module.fail_json(msg='Failed to login using API token, please verify validity of API token.')
|
||||
|
||||
def _url_builder(self, path):
|
||||
if path[0] == '/':
|
||||
path = path[1:]
|
||||
return '%s/%s' % (self.baseurl, path)
|
||||
|
||||
def send(self, method, path, data=None):
|
||||
url = self._url_builder(path)
|
||||
data = self.module.jsonify(data)
|
||||
|
||||
resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method, timeout=self.timeout)
|
||||
|
||||
return Response(resp, info)
|
||||
|
||||
def get(self, path, data=None):
|
||||
return self.send('GET', path, data)
|
||||
|
||||
def put(self, path, data=None):
|
||||
return self.send('PUT', path, data)
|
||||
|
||||
def post(self, path, data=None):
|
||||
return self.send('POST', path, data)
|
||||
|
||||
def delete(self, path, data=None):
|
||||
return self.send('DELETE', path, data)
|
||||
|
||||
@staticmethod
|
||||
def digital_ocean_argument_spec():
|
||||
return dict(
|
||||
validate_certs=dict(type='bool', required=False, default=True),
|
||||
oauth_token=dict(
|
||||
no_log=True,
|
||||
# Support environment variable for DigitalOcean OAuth Token
|
||||
fallback=(env_fallback, ['DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN', 'OAUTH_TOKEN']),
|
||||
required=False,
|
||||
aliases=['api_token'],
|
||||
),
|
||||
timeout=dict(type='int', default=30),
|
||||
)
|
||||
|
||||
def get_paginated_data(self, base_url=None, data_key_name=None, data_per_page=40, expected_status_code=200):
|
||||
"""
|
||||
Function to get all paginated data from given URL
|
||||
Args:
|
||||
base_url: Base URL to get data from
|
||||
data_key_name: Name of data key value
|
||||
data_per_page: Number results per page (Default: 40)
|
||||
expected_status_code: Expected returned code from DigitalOcean (Default: 200)
|
||||
Returns: List of data
|
||||
|
||||
"""
|
||||
page = 1
|
||||
has_next = True
|
||||
ret_data = []
|
||||
status_code = None
|
||||
response = None
|
||||
while has_next or status_code != expected_status_code:
|
||||
required_url = "{0}page={1}&per_page={2}".format(base_url, page, data_per_page)
|
||||
response = self.get(required_url)
|
||||
status_code = response.status_code
|
||||
# stop if any error during pagination
|
||||
if status_code != expected_status_code:
|
||||
break
|
||||
page += 1
|
||||
ret_data.extend(response.json[data_key_name])
|
||||
has_next = "pages" in response.json["links"] and "next" in response.json["links"]["pages"]
|
||||
|
||||
if status_code != expected_status_code:
|
||||
msg = "Failed to fetch %s from %s" % (data_key_name, base_url)
|
||||
if response:
|
||||
msg += " due to error : %s" % response.json['message']
|
||||
self.module.fail_json(msg=msg)
|
||||
|
||||
return ret_data
|
||||
338
plugins/module_utils/dimensiondata.py
Normal file
338
plugins/module_utils/dimensiondata.py
Normal file
@@ -0,0 +1,338 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2016 Dimension Data
|
||||
#
|
||||
# This module 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.
|
||||
#
|
||||
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Authors:
|
||||
# - Aimon Bustardo <aimon.bustardo@dimensiondata.com>
|
||||
# - Mark Maglana <mmaglana@gmail.com>
|
||||
# - Adam Friedman <tintoy@tintoy.io>
|
||||
#
|
||||
# Common functionality to be used by versious module components
|
||||
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.six.moves import configparser
|
||||
from os.path import expanduser
|
||||
from uuid import UUID
|
||||
|
||||
LIBCLOUD_IMP_ERR = None
|
||||
try:
|
||||
from libcloud.common.dimensiondata import API_ENDPOINTS, DimensionDataAPIException, DimensionDataStatus
|
||||
from libcloud.compute.base import Node, NodeLocation
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.compute.types import Provider
|
||||
|
||||
import libcloud.security
|
||||
|
||||
HAS_LIBCLOUD = True
|
||||
except ImportError:
|
||||
LIBCLOUD_IMP_ERR = traceback.format_exc()
|
||||
HAS_LIBCLOUD = False
|
||||
|
||||
# MCP 2.x version patten for location (datacenter) names.
|
||||
#
|
||||
# Note that this is not a totally reliable way of determining MCP version.
|
||||
# Unfortunately, libcloud's NodeLocation currently makes no provision for extended properties.
|
||||
# At some point we may therefore want to either enhance libcloud or enable overriding mcp_version
|
||||
# by specifying it in the module parameters.
|
||||
MCP_2_LOCATION_NAME_PATTERN = re.compile(r".*MCP\s?2.*")
|
||||
|
||||
|
||||
class DimensionDataModule(object):
|
||||
"""
|
||||
The base class containing common functionality used by Dimension Data modules for Ansible.
|
||||
"""
|
||||
|
||||
def __init__(self, module):
|
||||
"""
|
||||
Create a new DimensionDataModule.
|
||||
|
||||
Will fail if Apache libcloud is not present.
|
||||
|
||||
:param module: The underlying Ansible module.
|
||||
:type module: AnsibleModule
|
||||
"""
|
||||
|
||||
self.module = module
|
||||
|
||||
if not HAS_LIBCLOUD:
|
||||
self.module.fail_json(msg=missing_required_lib('libcloud'), exception=LIBCLOUD_IMP_ERR)
|
||||
|
||||
# Credentials are common to all Dimension Data modules.
|
||||
credentials = self.get_credentials()
|
||||
self.user_id = credentials['user_id']
|
||||
self.key = credentials['key']
|
||||
|
||||
# Region and location are common to all Dimension Data modules.
|
||||
region = self.module.params['region']
|
||||
self.region = 'dd-{0}'.format(region)
|
||||
self.location = self.module.params['location']
|
||||
|
||||
libcloud.security.VERIFY_SSL_CERT = self.module.params['validate_certs']
|
||||
|
||||
self.driver = get_driver(Provider.DIMENSIONDATA)(
|
||||
self.user_id,
|
||||
self.key,
|
||||
region=self.region
|
||||
)
|
||||
|
||||
# Determine the MCP API version (this depends on the target datacenter).
|
||||
self.mcp_version = self.get_mcp_version(self.location)
|
||||
|
||||
# Optional "wait-for-completion" arguments
|
||||
if 'wait' in self.module.params:
|
||||
self.wait = self.module.params['wait']
|
||||
self.wait_time = self.module.params['wait_time']
|
||||
self.wait_poll_interval = self.module.params['wait_poll_interval']
|
||||
else:
|
||||
self.wait = False
|
||||
self.wait_time = 0
|
||||
self.wait_poll_interval = 0
|
||||
|
||||
def get_credentials(self):
|
||||
"""
|
||||
Get user_id and key from module configuration, environment, or dotfile.
|
||||
Order of priority is module, environment, dotfile.
|
||||
|
||||
To set in environment:
|
||||
|
||||
export MCP_USER='myusername'
|
||||
export MCP_PASSWORD='mypassword'
|
||||
|
||||
To set in dot file place a file at ~/.dimensiondata with
|
||||
the following contents:
|
||||
|
||||
[dimensiondatacloud]
|
||||
MCP_USER: myusername
|
||||
MCP_PASSWORD: mypassword
|
||||
"""
|
||||
|
||||
if not HAS_LIBCLOUD:
|
||||
self.module.fail_json(msg='libcloud is required for this module.')
|
||||
|
||||
user_id = None
|
||||
key = None
|
||||
|
||||
# First, try the module configuration
|
||||
if 'mcp_user' in self.module.params:
|
||||
if 'mcp_password' not in self.module.params:
|
||||
self.module.fail_json(
|
||||
msg='"mcp_user" parameter was specified, but not "mcp_password" (either both must be specified, or neither).'
|
||||
)
|
||||
|
||||
user_id = self.module.params['mcp_user']
|
||||
key = self.module.params['mcp_password']
|
||||
|
||||
# Fall back to environment
|
||||
if not user_id or not key:
|
||||
user_id = os.environ.get('MCP_USER', None)
|
||||
key = os.environ.get('MCP_PASSWORD', None)
|
||||
|
||||
# Finally, try dotfile (~/.dimensiondata)
|
||||
if not user_id or not key:
|
||||
home = expanduser('~')
|
||||
config = configparser.RawConfigParser()
|
||||
config.read("%s/.dimensiondata" % home)
|
||||
|
||||
try:
|
||||
user_id = config.get("dimensiondatacloud", "MCP_USER")
|
||||
key = config.get("dimensiondatacloud", "MCP_PASSWORD")
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
pass
|
||||
|
||||
# One or more credentials not found. Function can't recover from this
|
||||
# so it has to raise an error instead of fail silently.
|
||||
if not user_id:
|
||||
raise MissingCredentialsError("Dimension Data user id not found")
|
||||
elif not key:
|
||||
raise MissingCredentialsError("Dimension Data key not found")
|
||||
|
||||
# Both found, return data
|
||||
return dict(user_id=user_id, key=key)
|
||||
|
||||
def get_mcp_version(self, location):
|
||||
"""
|
||||
Get the MCP version for the specified location.
|
||||
"""
|
||||
|
||||
location = self.driver.ex_get_location_by_id(location)
|
||||
if MCP_2_LOCATION_NAME_PATTERN.match(location.name):
|
||||
return '2.0'
|
||||
|
||||
return '1.0'
|
||||
|
||||
def get_network_domain(self, locator, location):
|
||||
"""
|
||||
Retrieve a network domain by its name or Id.
|
||||
"""
|
||||
|
||||
if is_uuid(locator):
|
||||
network_domain = self.driver.ex_get_network_domain(locator)
|
||||
else:
|
||||
matching_network_domains = [
|
||||
network_domain for network_domain in self.driver.ex_list_network_domains(location=location)
|
||||
if network_domain.name == locator
|
||||
]
|
||||
|
||||
if matching_network_domains:
|
||||
network_domain = matching_network_domains[0]
|
||||
else:
|
||||
network_domain = None
|
||||
|
||||
if network_domain:
|
||||
return network_domain
|
||||
|
||||
raise UnknownNetworkError("Network '%s' could not be found" % locator)
|
||||
|
||||
def get_vlan(self, locator, location, network_domain):
|
||||
"""
|
||||
Get a VLAN object by its name or id
|
||||
"""
|
||||
if is_uuid(locator):
|
||||
vlan = self.driver.ex_get_vlan(locator)
|
||||
else:
|
||||
matching_vlans = [
|
||||
vlan for vlan in self.driver.ex_list_vlans(location, network_domain)
|
||||
if vlan.name == locator
|
||||
]
|
||||
|
||||
if matching_vlans:
|
||||
vlan = matching_vlans[0]
|
||||
else:
|
||||
vlan = None
|
||||
|
||||
if vlan:
|
||||
return vlan
|
||||
|
||||
raise UnknownVLANError("VLAN '%s' could not be found" % locator)
|
||||
|
||||
@staticmethod
|
||||
def argument_spec(**additional_argument_spec):
|
||||
"""
|
||||
Build an argument specification for a Dimension Data module.
|
||||
:param additional_argument_spec: An optional dictionary representing the specification for additional module arguments (if any).
|
||||
:return: A dict containing the argument specification.
|
||||
"""
|
||||
|
||||
spec = dict(
|
||||
region=dict(type='str', default='na'),
|
||||
mcp_user=dict(type='str', required=False),
|
||||
mcp_password=dict(type='str', required=False, no_log=True),
|
||||
location=dict(type='str', required=True),
|
||||
validate_certs=dict(type='bool', required=False, default=True)
|
||||
)
|
||||
|
||||
if additional_argument_spec:
|
||||
spec.update(additional_argument_spec)
|
||||
|
||||
return spec
|
||||
|
||||
@staticmethod
|
||||
def argument_spec_with_wait(**additional_argument_spec):
|
||||
"""
|
||||
Build an argument specification for a Dimension Data module that includes "wait for completion" arguments.
|
||||
:param additional_argument_spec: An optional dictionary representing the specification for additional module arguments (if any).
|
||||
:return: A dict containing the argument specification.
|
||||
"""
|
||||
|
||||
spec = DimensionDataModule.argument_spec(
|
||||
wait=dict(type='bool', required=False, default=False),
|
||||
wait_time=dict(type='int', required=False, default=600),
|
||||
wait_poll_interval=dict(type='int', required=False, default=2)
|
||||
)
|
||||
|
||||
if additional_argument_spec:
|
||||
spec.update(additional_argument_spec)
|
||||
|
||||
return spec
|
||||
|
||||
@staticmethod
|
||||
def required_together(*additional_required_together):
|
||||
"""
|
||||
Get the basic argument specification for Dimension Data modules indicating which arguments are must be specified together.
|
||||
:param additional_required_together: An optional list representing the specification for additional module arguments that must be specified together.
|
||||
:return: An array containing the argument specifications.
|
||||
"""
|
||||
|
||||
required_together = [
|
||||
['mcp_user', 'mcp_password']
|
||||
]
|
||||
|
||||
if additional_required_together:
|
||||
required_together.extend(additional_required_together)
|
||||
|
||||
return required_together
|
||||
|
||||
|
||||
class LibcloudNotFound(Exception):
|
||||
"""
|
||||
Exception raised when Apache libcloud cannot be found.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingCredentialsError(Exception):
|
||||
"""
|
||||
Exception raised when credentials for Dimension Data CloudControl cannot be found.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownNetworkError(Exception):
|
||||
"""
|
||||
Exception raised when a network or network domain cannot be found.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownVLANError(Exception):
|
||||
"""
|
||||
Exception raised when a VLAN cannot be found.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def get_dd_regions():
|
||||
"""
|
||||
Get the list of available regions whose vendor is Dimension Data.
|
||||
"""
|
||||
|
||||
# Get endpoints
|
||||
all_regions = API_ENDPOINTS.keys()
|
||||
|
||||
# Only Dimension Data endpoints (no prefix)
|
||||
regions = [region[3:] for region in all_regions if region.startswith('dd-')]
|
||||
|
||||
return regions
|
||||
|
||||
|
||||
def is_uuid(u, version=4):
|
||||
"""
|
||||
Test if valid v4 UUID
|
||||
"""
|
||||
try:
|
||||
uuid_obj = UUID(u, version=version)
|
||||
|
||||
return str(uuid_obj) == u
|
||||
except ValueError:
|
||||
return False
|
||||
0
plugins/module_utils/docker/__init__.py
Normal file
0
plugins/module_utils/docker/__init__.py
Normal file
1022
plugins/module_utils/docker/common.py
Normal file
1022
plugins/module_utils/docker/common.py
Normal file
File diff suppressed because it is too large
Load Diff
280
plugins/module_utils/docker/swarm.py
Normal file
280
plugins/module_utils/docker/swarm.py
Normal file
@@ -0,0 +1,280 @@
|
||||
# (c) 2019 Piotr Wojciechowski (@wojciechowskipiotr) <piotr@it-playground.pl>
|
||||
# (c) Thierry Bouvet (@tbouvet)
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import json
|
||||
from time import sleep
|
||||
|
||||
try:
|
||||
from docker.errors import APIError, NotFound
|
||||
except ImportError:
|
||||
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
|
||||
pass
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible_collections.community.general.plugins.module_utils.docker.common import (
|
||||
AnsibleDockerClient,
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AnsibleDockerSwarmClient, self).__init__(**kwargs)
|
||||
|
||||
def get_swarm_node_id(self):
|
||||
"""
|
||||
Get the 'NodeID' of the Swarm node or 'None' if host is not in Swarm. It returns the NodeID
|
||||
of Docker host the module is executed on
|
||||
:return:
|
||||
NodeID of host or 'None' if not part of Swarm
|
||||
"""
|
||||
|
||||
try:
|
||||
info = self.info()
|
||||
except APIError as exc:
|
||||
self.fail("Failed to get node information for %s" % to_native(exc))
|
||||
|
||||
if info:
|
||||
json_str = json.dumps(info, ensure_ascii=False)
|
||||
swarm_info = json.loads(json_str)
|
||||
if swarm_info['Swarm']['NodeID']:
|
||||
return swarm_info['Swarm']['NodeID']
|
||||
return None
|
||||
|
||||
def check_if_swarm_node(self, node_id=None):
|
||||
"""
|
||||
Checking if host is part of Docker Swarm. If 'node_id' is not provided it reads the Docker host
|
||||
system information looking if specific key in output exists. If 'node_id' is provided then it tries to
|
||||
read node information assuming it is run on Swarm manager. The get_node_inspect() method handles exception if
|
||||
it is not executed on Swarm manager
|
||||
|
||||
:param node_id: Node identifier
|
||||
:return:
|
||||
bool: True if node is part of Swarm, False otherwise
|
||||
"""
|
||||
|
||||
if node_id is None:
|
||||
try:
|
||||
info = self.info()
|
||||
except APIError:
|
||||
self.fail("Failed to get host information.")
|
||||
|
||||
if info:
|
||||
json_str = json.dumps(info, ensure_ascii=False)
|
||||
swarm_info = json.loads(json_str)
|
||||
if swarm_info['Swarm']['NodeID']:
|
||||
return True
|
||||
if swarm_info['Swarm']['LocalNodeState'] in ('active', 'pending', 'locked'):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
node_info = self.get_node_inspect(node_id=node_id)
|
||||
except APIError:
|
||||
return
|
||||
|
||||
if node_info['ID'] is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_if_swarm_manager(self):
|
||||
"""
|
||||
Checks if node role is set as Manager in Swarm. The node is the docker host on which module action
|
||||
is performed. The inspect_swarm() will fail if node is not a manager
|
||||
|
||||
:return: True if node is Swarm Manager, False otherwise
|
||||
"""
|
||||
|
||||
try:
|
||||
self.inspect_swarm()
|
||||
return True
|
||||
except APIError:
|
||||
return False
|
||||
|
||||
def fail_task_if_not_swarm_manager(self):
|
||||
"""
|
||||
If host is not a swarm manager then Ansible task on this host should end with 'failed' state
|
||||
"""
|
||||
if not self.check_if_swarm_manager():
|
||||
self.fail("Error running docker swarm module: must run on swarm manager node")
|
||||
|
||||
def check_if_swarm_worker(self):
|
||||
"""
|
||||
Checks if node role is set as Worker in Swarm. The node is the docker host on which module action
|
||||
is performed. Will fail if run on host that is not part of Swarm via check_if_swarm_node()
|
||||
|
||||
:return: True if node is Swarm Worker, False otherwise
|
||||
"""
|
||||
|
||||
if self.check_if_swarm_node() and not self.check_if_swarm_manager():
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_if_swarm_node_is_down(self, node_id=None, repeat_check=1):
|
||||
"""
|
||||
Checks if node status on Swarm manager is 'down'. If node_id is provided it query manager about
|
||||
node specified in parameter, otherwise it query manager itself. If run on Swarm Worker node or
|
||||
host that is not part of Swarm it will fail the playbook
|
||||
|
||||
:param repeat_check: number of check attempts with 5 seconds delay between them, by default check only once
|
||||
:param node_id: node ID or name, if None then method will try to get node_id of host module run on
|
||||
:return:
|
||||
True if node is part of swarm but its state is down, False otherwise
|
||||
"""
|
||||
|
||||
if repeat_check < 1:
|
||||
repeat_check = 1
|
||||
|
||||
if node_id is None:
|
||||
node_id = self.get_swarm_node_id()
|
||||
|
||||
for retry in range(0, repeat_check):
|
||||
if retry > 0:
|
||||
sleep(5)
|
||||
node_info = self.get_node_inspect(node_id=node_id)
|
||||
if node_info['Status']['State'] == 'down':
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_node_inspect(self, node_id=None, skip_missing=False):
|
||||
"""
|
||||
Returns Swarm node info as in 'docker node inspect' command about single node
|
||||
|
||||
:param skip_missing: if True then function will return None instead of failing the task
|
||||
:param node_id: node ID or name, if None then method will try to get node_id of host module run on
|
||||
:return:
|
||||
Single node information structure
|
||||
"""
|
||||
|
||||
if node_id is None:
|
||||
node_id = self.get_swarm_node_id()
|
||||
|
||||
if node_id is None:
|
||||
self.fail("Failed to get node information.")
|
||||
|
||||
try:
|
||||
node_info = self.inspect_node(node_id=node_id)
|
||||
except APIError as exc:
|
||||
if exc.status_code == 503:
|
||||
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
|
||||
if exc.status_code == 404:
|
||||
if skip_missing:
|
||||
return None
|
||||
self.fail("Error while reading from Swarm manager: %s" % to_native(exc))
|
||||
except Exception as exc:
|
||||
self.fail("Error inspecting swarm node: %s" % exc)
|
||||
|
||||
json_str = json.dumps(node_info, ensure_ascii=False)
|
||||
node_info = json.loads(json_str)
|
||||
|
||||
if 'ManagerStatus' in node_info:
|
||||
if node_info['ManagerStatus'].get('Leader'):
|
||||
# This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0
|
||||
# Check moby/moby#35437 for details
|
||||
count_colons = node_info['ManagerStatus']['Addr'].count(":")
|
||||
if count_colons == 1:
|
||||
swarm_leader_ip = node_info['ManagerStatus']['Addr'].split(":", 1)[0] or node_info['Status']['Addr']
|
||||
else:
|
||||
swarm_leader_ip = node_info['Status']['Addr']
|
||||
node_info['Status']['Addr'] = swarm_leader_ip
|
||||
return node_info
|
||||
|
||||
def get_all_nodes_inspect(self):
|
||||
"""
|
||||
Returns Swarm node info as in 'docker node inspect' command about all registered nodes
|
||||
|
||||
:return:
|
||||
Structure with information about all nodes
|
||||
"""
|
||||
try:
|
||||
node_info = self.nodes()
|
||||
except APIError as exc:
|
||||
if exc.status_code == 503:
|
||||
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
|
||||
self.fail("Error while reading from Swarm manager: %s" % to_native(exc))
|
||||
except Exception as exc:
|
||||
self.fail("Error inspecting swarm node: %s" % exc)
|
||||
|
||||
json_str = json.dumps(node_info, ensure_ascii=False)
|
||||
node_info = json.loads(json_str)
|
||||
return node_info
|
||||
|
||||
def get_all_nodes_list(self, output='short'):
|
||||
"""
|
||||
Returns list of nodes registered in Swarm
|
||||
|
||||
:param output: Defines format of returned data
|
||||
:return:
|
||||
If 'output' is 'short' then return data is list of nodes hostnames registered in Swarm,
|
||||
if 'output' is 'long' then returns data is list of dict containing the attributes as in
|
||||
output of command 'docker node ls'
|
||||
"""
|
||||
nodes_list = []
|
||||
|
||||
nodes_inspect = self.get_all_nodes_inspect()
|
||||
if nodes_inspect is None:
|
||||
return None
|
||||
|
||||
if output == 'short':
|
||||
for node in nodes_inspect:
|
||||
nodes_list.append(node['Description']['Hostname'])
|
||||
elif output == 'long':
|
||||
for node in nodes_inspect:
|
||||
node_property = {}
|
||||
|
||||
node_property.update({'ID': node['ID']})
|
||||
node_property.update({'Hostname': node['Description']['Hostname']})
|
||||
node_property.update({'Status': node['Status']['State']})
|
||||
node_property.update({'Availability': node['Spec']['Availability']})
|
||||
if 'ManagerStatus' in node:
|
||||
if node['ManagerStatus']['Leader'] is True:
|
||||
node_property.update({'Leader': True})
|
||||
node_property.update({'ManagerStatus': node['ManagerStatus']['Reachability']})
|
||||
node_property.update({'EngineVersion': node['Description']['Engine']['EngineVersion']})
|
||||
|
||||
nodes_list.append(node_property)
|
||||
else:
|
||||
return None
|
||||
|
||||
return nodes_list
|
||||
|
||||
def get_node_name_by_id(self, nodeid):
|
||||
return self.get_node_inspect(nodeid)['Description']['Hostname']
|
||||
|
||||
def get_unlock_key(self):
|
||||
if self.docker_py_version < LooseVersion('2.7.0'):
|
||||
return None
|
||||
return super(AnsibleDockerSwarmClient, self).get_unlock_key()
|
||||
|
||||
def get_service_inspect(self, service_id, skip_missing=False):
|
||||
"""
|
||||
Returns Swarm service info as in 'docker service inspect' command about single service
|
||||
|
||||
:param service_id: service ID or name
|
||||
:param skip_missing: if True then function will return None instead of failing the task
|
||||
:return:
|
||||
Single service information structure
|
||||
"""
|
||||
try:
|
||||
service_info = self.inspect_service(service_id)
|
||||
except NotFound as exc:
|
||||
if skip_missing is False:
|
||||
self.fail("Error while reading from Swarm manager: %s" % to_native(exc))
|
||||
else:
|
||||
return None
|
||||
except APIError as exc:
|
||||
if exc.status_code == 503:
|
||||
self.fail("Cannot inspect service: To inspect service execute module on Swarm Manager")
|
||||
self.fail("Error inspecting swarm service: %s" % exc)
|
||||
except Exception as exc:
|
||||
self.fail("Error inspecting swarm service: %s" % exc)
|
||||
|
||||
json_str = json.dumps(service_info, ensure_ascii=False)
|
||||
service_info = json.loads(json_str)
|
||||
return service_info
|
||||
139
plugins/module_utils/exoscale.py
Normal file
139
plugins/module_utils/exoscale.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016, René Moser <mail@renemoser.net>
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from ansible.module_utils.six.moves import configparser
|
||||
from ansible.module_utils.six import integer_types, string_types
|
||||
from ansible.module_utils._text import to_native, to_text
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
EXO_DNS_BASEURL = "https://api.exoscale.ch/dns/v1"
|
||||
|
||||
|
||||
def exo_dns_argument_spec():
|
||||
return dict(
|
||||
api_key=dict(default=os.environ.get('CLOUDSTACK_KEY'), no_log=True),
|
||||
api_secret=dict(default=os.environ.get('CLOUDSTACK_SECRET'), no_log=True),
|
||||
api_timeout=dict(type='int', default=os.environ.get('CLOUDSTACK_TIMEOUT') or 10),
|
||||
api_region=dict(default=os.environ.get('CLOUDSTACK_REGION') or 'cloudstack'),
|
||||
validate_certs=dict(default=True, type='bool'),
|
||||
)
|
||||
|
||||
|
||||
def exo_dns_required_together():
|
||||
return [['api_key', 'api_secret']]
|
||||
|
||||
|
||||
class ExoDns(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.api_key = self.module.params.get('api_key')
|
||||
self.api_secret = self.module.params.get('api_secret')
|
||||
if not (self.api_key and self.api_secret):
|
||||
try:
|
||||
region = self.module.params.get('api_region')
|
||||
config = self.read_config(ini_group=region)
|
||||
self.api_key = config['key']
|
||||
self.api_secret = config['secret']
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Error while processing config: %s" % to_native(e))
|
||||
|
||||
self.headers = {
|
||||
'X-DNS-Token': "%s:%s" % (self.api_key, self.api_secret),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
self.result = {
|
||||
'changed': False,
|
||||
'diff': {
|
||||
'before': {},
|
||||
'after': {},
|
||||
}
|
||||
}
|
||||
|
||||
def read_config(self, ini_group=None):
|
||||
if not ini_group:
|
||||
ini_group = os.environ.get('CLOUDSTACK_REGION', 'cloudstack')
|
||||
|
||||
keys = ['key', 'secret']
|
||||
env_conf = {}
|
||||
for key in keys:
|
||||
if 'CLOUDSTACK_%s' % key.upper() not in os.environ:
|
||||
break
|
||||
else:
|
||||
env_conf[key] = os.environ['CLOUDSTACK_%s' % key.upper()]
|
||||
else:
|
||||
return env_conf
|
||||
|
||||
# Config file: $PWD/cloudstack.ini or $HOME/.cloudstack.ini
|
||||
# Last read wins in configparser
|
||||
paths = (
|
||||
os.path.join(os.path.expanduser('~'), '.cloudstack.ini'),
|
||||
os.path.join(os.getcwd(), 'cloudstack.ini'),
|
||||
)
|
||||
# Look at CLOUDSTACK_CONFIG first if present
|
||||
if 'CLOUDSTACK_CONFIG' in os.environ:
|
||||
paths += (os.path.expanduser(os.environ['CLOUDSTACK_CONFIG']),)
|
||||
if not any([os.path.exists(c) for c in paths]):
|
||||
self.module.fail_json(msg="Config file not found. Tried : %s" % ", ".join(paths))
|
||||
|
||||
conf = configparser.ConfigParser()
|
||||
conf.read(paths)
|
||||
return dict(conf.items(ini_group))
|
||||
|
||||
def api_query(self, resource="/domains", method="GET", data=None):
|
||||
url = EXO_DNS_BASEURL + resource
|
||||
if data:
|
||||
data = self.module.jsonify(data)
|
||||
|
||||
response, info = fetch_url(
|
||||
module=self.module,
|
||||
url=url,
|
||||
data=data,
|
||||
method=method,
|
||||
headers=self.headers,
|
||||
timeout=self.module.params.get('api_timeout'),
|
||||
)
|
||||
|
||||
if info['status'] not in (200, 201, 204):
|
||||
self.module.fail_json(msg="%s returned %s, with body: %s" % (url, info['status'], info['msg']))
|
||||
|
||||
try:
|
||||
return self.module.from_json(to_text(response.read()))
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not process response into json: %s" % to_native(e))
|
||||
|
||||
def has_changed(self, want_dict, current_dict, only_keys=None):
|
||||
changed = False
|
||||
for key, value in want_dict.items():
|
||||
# Optionally limit by a list of keys
|
||||
if only_keys and key not in only_keys:
|
||||
continue
|
||||
# Skip None values
|
||||
if value is None:
|
||||
continue
|
||||
if key in current_dict:
|
||||
if isinstance(current_dict[key], integer_types):
|
||||
if value != current_dict[key]:
|
||||
self.result['diff']['before'][key] = current_dict[key]
|
||||
self.result['diff']['after'][key] = value
|
||||
changed = True
|
||||
elif isinstance(current_dict[key], string_types):
|
||||
if value.lower() != current_dict[key].lower():
|
||||
self.result['diff']['before'][key] = current_dict[key]
|
||||
self.result['diff']['after'][key] = value
|
||||
changed = True
|
||||
else:
|
||||
self.module.fail_json(msg="Unable to determine comparison for key %s" % key)
|
||||
else:
|
||||
self.result['diff']['after'][key] = value
|
||||
changed = True
|
||||
return changed
|
||||
383
plugins/module_utils/f5_utils.py
Normal file
383
plugins/module_utils/f5_utils.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#
|
||||
# Copyright 2016 F5 Networks 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# Legacy
|
||||
|
||||
try:
|
||||
import bigsuds
|
||||
bigsuds_found = True
|
||||
except ImportError:
|
||||
bigsuds_found = False
|
||||
|
||||
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
|
||||
|
||||
def f5_argument_spec():
|
||||
return dict(
|
||||
server=dict(
|
||||
type='str',
|
||||
required=True,
|
||||
fallback=(env_fallback, ['F5_SERVER'])
|
||||
),
|
||||
user=dict(
|
||||
type='str',
|
||||
required=True,
|
||||
fallback=(env_fallback, ['F5_USER'])
|
||||
),
|
||||
password=dict(
|
||||
type='str',
|
||||
aliases=['pass', 'pwd'],
|
||||
required=True,
|
||||
no_log=True,
|
||||
fallback=(env_fallback, ['F5_PASSWORD'])
|
||||
),
|
||||
validate_certs=dict(
|
||||
default='yes',
|
||||
type='bool',
|
||||
fallback=(env_fallback, ['F5_VALIDATE_CERTS'])
|
||||
),
|
||||
server_port=dict(
|
||||
type='int',
|
||||
default=443,
|
||||
fallback=(env_fallback, ['F5_SERVER_PORT'])
|
||||
),
|
||||
state=dict(
|
||||
type='str',
|
||||
default='present',
|
||||
choices=['present', 'absent']
|
||||
),
|
||||
partition=dict(
|
||||
type='str',
|
||||
default='Common',
|
||||
fallback=(env_fallback, ['F5_PARTITION'])
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def f5_parse_arguments(module):
|
||||
if not bigsuds_found:
|
||||
module.fail_json(msg="the python bigsuds module is required")
|
||||
|
||||
if module.params['validate_certs']:
|
||||
import ssl
|
||||
if not hasattr(ssl, 'SSLContext'):
|
||||
module.fail_json(
|
||||
msg="bigsuds does not support verifying certificates with python < 2.7.9."
|
||||
"Either update python or set validate_certs=False on the task'")
|
||||
|
||||
return (
|
||||
module.params['server'],
|
||||
module.params['user'],
|
||||
module.params['password'],
|
||||
module.params['state'],
|
||||
module.params['partition'],
|
||||
module.params['validate_certs'],
|
||||
module.params['server_port']
|
||||
)
|
||||
|
||||
|
||||
def bigip_api(bigip, user, password, validate_certs, port=443):
|
||||
try:
|
||||
if bigsuds.__version__ >= '1.0.4':
|
||||
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password, verify=validate_certs, port=port)
|
||||
elif bigsuds.__version__ == '1.0.3':
|
||||
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password, verify=validate_certs)
|
||||
else:
|
||||
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
|
||||
except TypeError:
|
||||
# bigsuds < 1.0.3, no verify param
|
||||
if validate_certs:
|
||||
# Note: verified we have SSLContext when we parsed params
|
||||
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
|
||||
else:
|
||||
import ssl
|
||||
if hasattr(ssl, 'SSLContext'):
|
||||
# Really, you should never do this. It disables certificate
|
||||
# verification *globally*. But since older bigip libraries
|
||||
# don't give us a way to toggle verification we need to
|
||||
# disable it at the global level.
|
||||
# From https://www.python.org/dev/peps/pep-0476/#id29
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
|
||||
|
||||
return api
|
||||
|
||||
|
||||
# Fully Qualified name (with the partition)
|
||||
def fq_name(partition, name):
|
||||
if name is not None and not name.startswith('/'):
|
||||
return '/%s/%s' % (partition, name)
|
||||
return name
|
||||
|
||||
|
||||
# Fully Qualified name (with partition) for a list
|
||||
def fq_list_names(partition, list_names):
|
||||
if list_names is None:
|
||||
return None
|
||||
return map(lambda x: fq_name(partition, x), list_names)
|
||||
|
||||
|
||||
def to_commands(module, commands):
|
||||
spec = {
|
||||
'command': dict(key=True),
|
||||
'prompt': dict(),
|
||||
'answer': dict()
|
||||
}
|
||||
transform = ComplexList(spec, module)
|
||||
return transform(commands)
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
responses = list()
|
||||
commands = to_commands(module, to_list(commands))
|
||||
for cmd in commands:
|
||||
cmd = module.jsonify(cmd)
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if check_rc and rc != 0:
|
||||
module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), rc=rc)
|
||||
responses.append(to_text(out, errors='surrogate_then_replace'))
|
||||
return responses
|
||||
|
||||
|
||||
# New style
|
||||
|
||||
from abc import ABCMeta, abstractproperty
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
from f5.bigip import ManagementRoot as BigIpMgmt
|
||||
from f5.bigip.contexts import TransactionContextManager as BigIpTxContext
|
||||
|
||||
from f5.bigiq import ManagementRoot as BigIqMgmt
|
||||
|
||||
from f5.iworkflow import ManagementRoot as iWorkflowMgmt
|
||||
from icontrol.exceptions import iControlUnexpectedHTTPError
|
||||
HAS_F5SDK = True
|
||||
except ImportError:
|
||||
HAS_F5SDK = False
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six import iteritems, with_metaclass
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.connection import exec_command
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
F5_COMMON_ARGS = dict(
|
||||
server=dict(
|
||||
type='str',
|
||||
required=True,
|
||||
fallback=(env_fallback, ['F5_SERVER'])
|
||||
),
|
||||
user=dict(
|
||||
type='str',
|
||||
required=True,
|
||||
fallback=(env_fallback, ['F5_USER'])
|
||||
),
|
||||
password=dict(
|
||||
type='str',
|
||||
aliases=['pass', 'pwd'],
|
||||
required=True,
|
||||
no_log=True,
|
||||
fallback=(env_fallback, ['F5_PASSWORD'])
|
||||
),
|
||||
validate_certs=dict(
|
||||
default='yes',
|
||||
type='bool',
|
||||
fallback=(env_fallback, ['F5_VALIDATE_CERTS'])
|
||||
),
|
||||
server_port=dict(
|
||||
type='int',
|
||||
default=443,
|
||||
fallback=(env_fallback, ['F5_SERVER_PORT'])
|
||||
),
|
||||
state=dict(
|
||||
type='str',
|
||||
default='present',
|
||||
choices=['present', 'absent']
|
||||
),
|
||||
partition=dict(
|
||||
type='str',
|
||||
default='Common',
|
||||
fallback=(env_fallback, ['F5_PARTITION'])
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AnsibleF5Client(object):
|
||||
def __init__(self, argument_spec=None, supports_check_mode=False,
|
||||
mutually_exclusive=None, required_together=None,
|
||||
required_if=None, required_one_of=None, add_file_common_args=False,
|
||||
f5_product_name='bigip', sans_state=False, sans_partition=False):
|
||||
|
||||
self.f5_product_name = f5_product_name
|
||||
|
||||
merged_arg_spec = dict()
|
||||
merged_arg_spec.update(F5_COMMON_ARGS)
|
||||
if argument_spec:
|
||||
merged_arg_spec.update(argument_spec)
|
||||
if sans_state:
|
||||
del merged_arg_spec['state']
|
||||
if sans_partition:
|
||||
del merged_arg_spec['partition']
|
||||
self.arg_spec = merged_arg_spec
|
||||
|
||||
mutually_exclusive_params = []
|
||||
if mutually_exclusive:
|
||||
mutually_exclusive_params += mutually_exclusive
|
||||
|
||||
required_together_params = []
|
||||
if required_together:
|
||||
required_together_params += required_together
|
||||
|
||||
self.module = AnsibleModule(
|
||||
argument_spec=merged_arg_spec,
|
||||
supports_check_mode=supports_check_mode,
|
||||
mutually_exclusive=mutually_exclusive_params,
|
||||
required_together=required_together_params,
|
||||
required_if=required_if,
|
||||
required_one_of=required_one_of,
|
||||
add_file_common_args=add_file_common_args
|
||||
)
|
||||
|
||||
self.check_mode = self.module.check_mode
|
||||
self._connect_params = self._get_connect_params()
|
||||
|
||||
if 'transport' not in self.module.params or self.module.params['transport'] != 'cli':
|
||||
try:
|
||||
self.api = self._get_mgmt_root(
|
||||
f5_product_name, **self._connect_params
|
||||
)
|
||||
except iControlUnexpectedHTTPError as exc:
|
||||
self.fail(str(exc))
|
||||
|
||||
def fail(self, msg):
|
||||
self.module.fail_json(msg=msg)
|
||||
|
||||
def _get_connect_params(self):
|
||||
params = dict(
|
||||
user=self.module.params['user'],
|
||||
password=self.module.params['password'],
|
||||
server=self.module.params['server'],
|
||||
server_port=self.module.params['server_port'],
|
||||
validate_certs=self.module.params['validate_certs']
|
||||
)
|
||||
return params
|
||||
|
||||
def _get_mgmt_root(self, type, **kwargs):
|
||||
if type == 'bigip':
|
||||
return BigIpMgmt(
|
||||
kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'],
|
||||
token='tmos'
|
||||
)
|
||||
elif type == 'iworkflow':
|
||||
return iWorkflowMgmt(
|
||||
kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'],
|
||||
token='local'
|
||||
)
|
||||
elif type == 'bigiq':
|
||||
return BigIqMgmt(
|
||||
kwargs['server'],
|
||||
kwargs['user'],
|
||||
kwargs['password'],
|
||||
port=kwargs['server_port'],
|
||||
auth_provider='local'
|
||||
)
|
||||
|
||||
def reconnect(self):
|
||||
"""Attempts to reconnect to a device
|
||||
|
||||
The existing token from a ManagementRoot can become invalid if you,
|
||||
for example, upgrade the device (such as is done in the *_software
|
||||
module.
|
||||
|
||||
This method can be used to reconnect to a remote device without
|
||||
having to re-instantiate the ArgumentSpec and AnsibleF5Client classes
|
||||
it will use the same values that were initially provided to those
|
||||
classes
|
||||
|
||||
:return:
|
||||
:raises iControlUnexpectedHTTPError
|
||||
"""
|
||||
self.api = self._get_mgmt_root(
|
||||
self.f5_product_name, **self._connect_params
|
||||
)
|
||||
|
||||
|
||||
class AnsibleF5Parameters(object):
|
||||
def __init__(self, params=None):
|
||||
self._values = defaultdict(lambda: None)
|
||||
self._values['__warnings'] = []
|
||||
if params:
|
||||
self.update(params=params)
|
||||
|
||||
def update(self, params=None):
|
||||
if params:
|
||||
for k, v in iteritems(params):
|
||||
if self.api_map is not None and k in self.api_map:
|
||||
dict_to_use = self.api_map
|
||||
map_key = self.api_map[k]
|
||||
else:
|
||||
dict_to_use = self._values
|
||||
map_key = k
|
||||
|
||||
# Handle weird API parameters like `dns.proxy.__iter__` by
|
||||
# using a map provided by the module developer
|
||||
class_attr = getattr(type(self), map_key, None)
|
||||
if isinstance(class_attr, property):
|
||||
# There is a mapped value for the api_map key
|
||||
if class_attr.fset is None:
|
||||
# If the mapped value does not have an associated setter
|
||||
self._values[map_key] = v
|
||||
else:
|
||||
# The mapped value has a setter
|
||||
setattr(self, map_key, v)
|
||||
else:
|
||||
# If the mapped value is not a @property
|
||||
self._values[map_key] = v
|
||||
|
||||
def __getattr__(self, item):
|
||||
# Ensures that properties that weren't defined, and therefore stashed
|
||||
# in the `_values` dict, will be retrievable.
|
||||
return self._values[item]
|
||||
|
||||
@property
|
||||
def partition(self):
|
||||
if self._values['partition'] is None:
|
||||
return 'Common'
|
||||
return self._values['partition'].strip('/')
|
||||
|
||||
@partition.setter
|
||||
def partition(self, value):
|
||||
self._values['partition'] = value
|
||||
|
||||
def _filter_params(self, params):
|
||||
return dict((k, v) for k, v in iteritems(params) if v is not None)
|
||||
|
||||
|
||||
class F5ModuleError(Exception):
|
||||
pass
|
||||
316
plugins/module_utils/firewalld.py
Normal file
316
plugins/module_utils/firewalld.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2013-2018, Adam Miller (maxamillion@fedoraproject.org)
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# Imports and info for sanity checking
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
FW_VERSION = None
|
||||
fw = None
|
||||
fw_offline = False
|
||||
import_failure = True
|
||||
try:
|
||||
import firewall.config
|
||||
FW_VERSION = firewall.config.VERSION
|
||||
|
||||
from firewall.client import FirewallClient
|
||||
from firewall.client import FirewallClientZoneSettings
|
||||
from firewall.errors import FirewallError
|
||||
import_failure = False
|
||||
|
||||
try:
|
||||
fw = FirewallClient()
|
||||
fw.getDefaultZone()
|
||||
|
||||
except (AttributeError, FirewallError):
|
||||
# Firewalld is not currently running, permanent-only operations
|
||||
fw_offline = True
|
||||
|
||||
# Import other required parts of the firewalld API
|
||||
#
|
||||
# NOTE:
|
||||
# online and offline operations do not share a common firewalld API
|
||||
try:
|
||||
from firewall.core.fw_test import Firewall_test
|
||||
fw = Firewall_test()
|
||||
except (ModuleNotFoundError):
|
||||
# In firewalld version 0.7.0 this behavior changed
|
||||
from firewall.core.fw import Firewall
|
||||
fw = Firewall(offline=True)
|
||||
|
||||
fw.start()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class FirewallTransaction(object):
|
||||
"""
|
||||
FirewallTransaction
|
||||
|
||||
This is the base class for all firewalld transactions we might want to have
|
||||
"""
|
||||
|
||||
def __init__(self, module, action_args=(), zone=None, desired_state=None,
|
||||
permanent=False, immediate=False, enabled_values=None, disabled_values=None):
|
||||
# type: (firewall.client, tuple, str, bool, bool, bool)
|
||||
"""
|
||||
initializer the transaction
|
||||
|
||||
:module: AnsibleModule, instance of AnsibleModule
|
||||
:action_args: tuple, args to pass for the action to take place
|
||||
:zone: str, firewall zone
|
||||
:desired_state: str, the desired state (enabled, disabled, etc)
|
||||
:permanent: bool, action should be permanent
|
||||
:immediate: bool, action should take place immediately
|
||||
:enabled_values: str[], acceptable values for enabling something (default: enabled)
|
||||
:disabled_values: str[], acceptable values for disabling something (default: disabled)
|
||||
"""
|
||||
|
||||
self.module = module
|
||||
self.fw = fw
|
||||
self.action_args = action_args
|
||||
|
||||
if zone:
|
||||
self.zone = zone
|
||||
else:
|
||||
if fw_offline:
|
||||
self.zone = fw.get_default_zone()
|
||||
else:
|
||||
self.zone = fw.getDefaultZone()
|
||||
|
||||
self.desired_state = desired_state
|
||||
self.permanent = permanent
|
||||
self.immediate = immediate
|
||||
self.fw_offline = fw_offline
|
||||
self.enabled_values = enabled_values or ["enabled"]
|
||||
self.disabled_values = disabled_values or ["disabled"]
|
||||
|
||||
# List of messages that we'll call module.fail_json or module.exit_json
|
||||
# with.
|
||||
self.msgs = []
|
||||
|
||||
# Allow for custom messages to be added for certain subclass transaction
|
||||
# types
|
||||
self.enabled_msg = None
|
||||
self.disabled_msg = None
|
||||
|
||||
#####################
|
||||
# exception handling
|
||||
#
|
||||
def action_handler(self, action_func, action_func_args):
|
||||
"""
|
||||
Function to wrap calls to make actions on firewalld in try/except
|
||||
logic and emit (hopefully) useful error messages
|
||||
"""
|
||||
|
||||
try:
|
||||
return action_func(*action_func_args)
|
||||
except Exception as e:
|
||||
|
||||
# If there are any commonly known errors that we should provide more
|
||||
# context for to help the users diagnose what's wrong. Handle that here
|
||||
if "INVALID_SERVICE" in "%s" % e:
|
||||
self.msgs.append("Services are defined by port/tcp relationship and named as they are in /etc/services (on most systems)")
|
||||
|
||||
if len(self.msgs) > 0:
|
||||
self.module.fail_json(
|
||||
msg='ERROR: Exception caught: %s %s' % (e, ', '.join(self.msgs))
|
||||
)
|
||||
else:
|
||||
self.module.fail_json(msg='ERROR: Exception caught: %s' % e)
|
||||
|
||||
def get_fw_zone_settings(self):
|
||||
if self.fw_offline:
|
||||
fw_zone = self.fw.config.get_zone(self.zone)
|
||||
fw_settings = FirewallClientZoneSettings(
|
||||
list(self.fw.config.get_zone_config(fw_zone))
|
||||
)
|
||||
else:
|
||||
fw_zone = self.fw.config().getZoneByName(self.zone)
|
||||
fw_settings = fw_zone.getSettings()
|
||||
|
||||
return (fw_zone, fw_settings)
|
||||
|
||||
def update_fw_settings(self, fw_zone, fw_settings):
|
||||
if self.fw_offline:
|
||||
self.fw.config.set_zone_config(fw_zone, fw_settings.settings)
|
||||
else:
|
||||
fw_zone.update(fw_settings)
|
||||
|
||||
def get_enabled_immediate(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_enabled_permanent(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_enabled_immediate(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_enabled_permanent(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_disabled_immediate(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_disabled_permanent(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
run
|
||||
|
||||
This function contains the "transaction logic" where as all operations
|
||||
follow a similar pattern in order to perform their action but simply
|
||||
call different functions to carry that action out.
|
||||
"""
|
||||
|
||||
self.changed = False
|
||||
|
||||
if self.immediate and self.permanent:
|
||||
is_enabled_permanent = self.action_handler(
|
||||
self.get_enabled_permanent,
|
||||
self.action_args
|
||||
)
|
||||
is_enabled_immediate = self.action_handler(
|
||||
self.get_enabled_immediate,
|
||||
self.action_args
|
||||
)
|
||||
self.msgs.append('Permanent and Non-Permanent(immediate) operation')
|
||||
|
||||
if self.desired_state in self.enabled_values:
|
||||
if not is_enabled_permanent or not is_enabled_immediate:
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True)
|
||||
if not is_enabled_permanent:
|
||||
self.action_handler(
|
||||
self.set_enabled_permanent,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if not is_enabled_immediate:
|
||||
self.action_handler(
|
||||
self.set_enabled_immediate,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if self.changed and self.enabled_msg:
|
||||
self.msgs.append(self.enabled_msg)
|
||||
|
||||
elif self.desired_state in self.disabled_values:
|
||||
if is_enabled_permanent or is_enabled_immediate:
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True)
|
||||
if is_enabled_permanent:
|
||||
self.action_handler(
|
||||
self.set_disabled_permanent,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if is_enabled_immediate:
|
||||
self.action_handler(
|
||||
self.set_disabled_immediate,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if self.changed and self.disabled_msg:
|
||||
self.msgs.append(self.disabled_msg)
|
||||
|
||||
elif self.permanent and not self.immediate:
|
||||
is_enabled = self.action_handler(
|
||||
self.get_enabled_permanent,
|
||||
self.action_args
|
||||
)
|
||||
self.msgs.append('Permanent operation')
|
||||
|
||||
if self.desired_state in self.enabled_values:
|
||||
if not is_enabled:
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True)
|
||||
|
||||
self.action_handler(
|
||||
self.set_enabled_permanent,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if self.changed and self.enabled_msg:
|
||||
self.msgs.append(self.enabled_msg)
|
||||
|
||||
elif self.desired_state in self.disabled_values:
|
||||
if is_enabled:
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True)
|
||||
|
||||
self.action_handler(
|
||||
self.set_disabled_permanent,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if self.changed and self.disabled_msg:
|
||||
self.msgs.append(self.disabled_msg)
|
||||
|
||||
elif self.immediate and not self.permanent:
|
||||
is_enabled = self.action_handler(
|
||||
self.get_enabled_immediate,
|
||||
self.action_args
|
||||
)
|
||||
self.msgs.append('Non-permanent operation')
|
||||
|
||||
if self.desired_state in self.enabled_values:
|
||||
if not is_enabled:
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True)
|
||||
|
||||
self.action_handler(
|
||||
self.set_enabled_immediate,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if self.changed and self.enabled_msg:
|
||||
self.msgs.append(self.enabled_msg)
|
||||
|
||||
elif self.desired_state in self.disabled_values:
|
||||
if is_enabled:
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True)
|
||||
|
||||
self.action_handler(
|
||||
self.set_disabled_immediate,
|
||||
self.action_args
|
||||
)
|
||||
self.changed = True
|
||||
if self.changed and self.disabled_msg:
|
||||
self.msgs.append(self.disabled_msg)
|
||||
|
||||
return (self.changed, self.msgs)
|
||||
|
||||
@staticmethod
|
||||
def sanity_check(module):
|
||||
"""
|
||||
Perform sanity checking, version checks, etc
|
||||
|
||||
:module: AnsibleModule instance
|
||||
"""
|
||||
|
||||
if FW_VERSION and fw_offline:
|
||||
# Pre-run version checking
|
||||
if LooseVersion(FW_VERSION) < LooseVersion("0.3.9"):
|
||||
module.fail_json(msg='unsupported version of firewalld, offline operations require >= 0.3.9 - found: {0}'.format(FW_VERSION))
|
||||
elif FW_VERSION and not fw_offline:
|
||||
# Pre-run version checking
|
||||
if LooseVersion(FW_VERSION) < LooseVersion("0.2.11"):
|
||||
module.fail_json(msg='unsupported version of firewalld, requires >= 0.2.11 - found: {0}'.format(FW_VERSION))
|
||||
|
||||
# Check for firewalld running
|
||||
try:
|
||||
if fw.connected is False:
|
||||
module.fail_json(msg='firewalld service must be running, or try with offline=true')
|
||||
except AttributeError:
|
||||
module.fail_json(msg="firewalld connection can't be established,\
|
||||
installed version (%s) likely too old. Requires firewalld >= 0.2.11" % FW_VERSION)
|
||||
|
||||
if import_failure:
|
||||
module.fail_json(
|
||||
msg='Python Module not found: firewalld and its python module are required for this module, \
|
||||
version 0.2.11 or newer required (0.3.9 or newer for offline operations)'
|
||||
)
|
||||
55
plugins/module_utils/gcdns.py
Normal file
55
plugins/module_utils/gcdns.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Franck Cuny <franck.cuny@gmail.com>, 2014
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
try:
|
||||
from libcloud.dns.types import Provider
|
||||
from libcloud.dns.providers import get_driver
|
||||
HAS_LIBCLOUD_BASE = True
|
||||
except ImportError:
|
||||
HAS_LIBCLOUD_BASE = False
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gcp import gcp_connect
|
||||
from ansible_collections.community.general.plugins.module_utils.gcp import unexpected_error_msg as gcp_error
|
||||
|
||||
USER_AGENT_PRODUCT = "Ansible-gcdns"
|
||||
USER_AGENT_VERSION = "v1"
|
||||
|
||||
|
||||
def gcdns_connect(module, provider=None):
|
||||
"""Return a GCP connection for Google Cloud DNS."""
|
||||
if not HAS_LIBCLOUD_BASE:
|
||||
module.fail_json(msg='libcloud must be installed to use this module')
|
||||
|
||||
provider = provider or Provider.GOOGLE
|
||||
return gcp_connect(module, provider, get_driver, USER_AGENT_PRODUCT, USER_AGENT_VERSION)
|
||||
|
||||
|
||||
def unexpected_error_msg(error):
|
||||
"""Create an error string based on passed in error."""
|
||||
return gcp_error(error)
|
||||
54
plugins/module_utils/gce.py
Normal file
54
plugins/module_utils/gce.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Franck Cuny <franck.cuny@gmail.com>, 2014
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
try:
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.compute.providers import get_driver
|
||||
HAS_LIBCLOUD_BASE = True
|
||||
except ImportError:
|
||||
HAS_LIBCLOUD_BASE = False
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gcp import gcp_connect
|
||||
from ansible_collections.community.general.plugins.module_utils.gcp import unexpected_error_msg as gcp_error
|
||||
|
||||
USER_AGENT_PRODUCT = "Ansible-gce"
|
||||
USER_AGENT_VERSION = "v1"
|
||||
|
||||
|
||||
def gce_connect(module, provider=None):
|
||||
"""Return a GCP connection for Google Compute Engine."""
|
||||
if not HAS_LIBCLOUD_BASE:
|
||||
module.fail_json(msg='libcloud must be installed to use this module')
|
||||
provider = provider or Provider.GCE
|
||||
|
||||
return gcp_connect(module, provider, get_driver, USER_AGENT_PRODUCT, USER_AGENT_VERSION)
|
||||
|
||||
|
||||
def unexpected_error_msg(error):
|
||||
"""Create an error string based on passed in error."""
|
||||
return gcp_error(error)
|
||||
815
plugins/module_utils/gcp.py
Normal file
815
plugins/module_utils/gcp.py
Normal file
@@ -0,0 +1,815 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Franck Cuny <franck.cuny@gmail.com>, 2014
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
# libcloud
|
||||
try:
|
||||
import libcloud
|
||||
HAS_LIBCLOUD_BASE = True
|
||||
except ImportError:
|
||||
HAS_LIBCLOUD_BASE = False
|
||||
|
||||
# google-auth
|
||||
try:
|
||||
import google.auth
|
||||
from google.oauth2 import service_account
|
||||
HAS_GOOGLE_AUTH = True
|
||||
except ImportError:
|
||||
HAS_GOOGLE_AUTH = False
|
||||
|
||||
# google-python-api
|
||||
try:
|
||||
import google_auth_httplib2
|
||||
from httplib2 import Http
|
||||
from googleapiclient.http import set_user_agent
|
||||
from googleapiclient.errors import HttpError
|
||||
from apiclient.discovery import build
|
||||
HAS_GOOGLE_API_LIB = True
|
||||
except ImportError:
|
||||
HAS_GOOGLE_API_LIB = False
|
||||
|
||||
|
||||
import ansible.module_utils.six.moves.urllib.parse as urlparse
|
||||
|
||||
GCP_DEFAULT_SCOPES = ['https://www.googleapis.com/auth/cloud-platform']
|
||||
|
||||
|
||||
def _get_gcp_ansible_credentials(module):
|
||||
"""Helper to fetch creds from AnsibleModule object."""
|
||||
service_account_email = module.params.get('service_account_email', None)
|
||||
# Note: pem_file is discouraged and will be deprecated
|
||||
credentials_file = module.params.get('pem_file', None) or module.params.get(
|
||||
'credentials_file', None)
|
||||
project_id = module.params.get('project_id', None)
|
||||
|
||||
return (service_account_email, credentials_file, project_id)
|
||||
|
||||
|
||||
def _get_gcp_environ_var(var_name, default_value):
|
||||
"""Wrapper around os.environ.get call."""
|
||||
return os.environ.get(
|
||||
var_name, default_value)
|
||||
|
||||
|
||||
def _get_gcp_environment_credentials(service_account_email, credentials_file, project_id):
|
||||
"""Helper to look in environment variables for credentials."""
|
||||
# If any of the values are not given as parameters, check the appropriate
|
||||
# environment variables.
|
||||
if not service_account_email:
|
||||
service_account_email = _get_gcp_environ_var('GCE_EMAIL', None)
|
||||
if not credentials_file:
|
||||
credentials_file = _get_gcp_environ_var(
|
||||
'GCE_CREDENTIALS_FILE_PATH', None) or _get_gcp_environ_var(
|
||||
'GOOGLE_APPLICATION_CREDENTIALS', None) or _get_gcp_environ_var(
|
||||
'GCE_PEM_FILE_PATH', None)
|
||||
if not project_id:
|
||||
project_id = _get_gcp_environ_var('GCE_PROJECT', None) or _get_gcp_environ_var(
|
||||
'GOOGLE_CLOUD_PROJECT', None)
|
||||
return (service_account_email, credentials_file, project_id)
|
||||
|
||||
|
||||
def _get_gcp_credentials(module, require_valid_json=True, check_libcloud=False):
|
||||
"""
|
||||
Obtain GCP credentials by trying various methods.
|
||||
|
||||
There are 3 ways to specify GCP credentials:
|
||||
1. Specify via Ansible module parameters (recommended).
|
||||
2. Specify via environment variables. Two sets of env vars are available:
|
||||
a) GOOGLE_CLOUD_PROJECT, GOOGLE_CREDENTIALS_APPLICATION (preferred)
|
||||
b) GCE_PROJECT, GCE_CREDENTIAL_FILE_PATH, GCE_EMAIL (legacy, not recommended; req'd if
|
||||
using p12 key)
|
||||
3. Specify via libcloud secrets.py file (deprecated).
|
||||
|
||||
There are 3 helper functions to assist in the above.
|
||||
|
||||
Regardless of method, the user also has the option of specifying a JSON
|
||||
file or a p12 file as the credentials file. JSON is strongly recommended and
|
||||
p12 will be removed in the future.
|
||||
|
||||
Additionally, flags may be set to require valid json and check the libcloud
|
||||
version.
|
||||
|
||||
AnsibleModule.fail_json is called only if the project_id cannot be found.
|
||||
|
||||
:param module: initialized Ansible module object
|
||||
:type module: `class AnsibleModule`
|
||||
|
||||
:param require_valid_json: If true, require credentials to be valid JSON. Default is True.
|
||||
:type require_valid_json: ``bool``
|
||||
|
||||
:params check_libcloud: If true, check the libcloud version available to see if
|
||||
JSON creds are supported.
|
||||
:type check_libcloud: ``bool``
|
||||
|
||||
:return: {'service_account_email': service_account_email,
|
||||
'credentials_file': credentials_file,
|
||||
'project_id': project_id}
|
||||
:rtype: ``dict``
|
||||
"""
|
||||
(service_account_email,
|
||||
credentials_file,
|
||||
project_id) = _get_gcp_ansible_credentials(module)
|
||||
|
||||
# If any of the values are not given as parameters, check the appropriate
|
||||
# environment variables.
|
||||
(service_account_email,
|
||||
credentials_file,
|
||||
project_id) = _get_gcp_environment_credentials(service_account_email,
|
||||
credentials_file, project_id)
|
||||
|
||||
if credentials_file is None or project_id is None or service_account_email is None:
|
||||
if check_libcloud is True:
|
||||
if project_id is None:
|
||||
# TODO(supertom): this message is legacy and integration tests
|
||||
# depend on it.
|
||||
module.fail_json(msg='Missing GCE connection parameters in libcloud '
|
||||
'secrets file.')
|
||||
else:
|
||||
if project_id is None:
|
||||
module.fail_json(msg=('GCP connection error: unable to determine project (%s) or '
|
||||
'credentials file (%s)' % (project_id, credentials_file)))
|
||||
# Set these fields to empty strings if they are None
|
||||
# consumers of this will make the distinction between an empty string
|
||||
# and None.
|
||||
if credentials_file is None:
|
||||
credentials_file = ''
|
||||
if service_account_email is None:
|
||||
service_account_email = ''
|
||||
|
||||
# ensure the credentials file is found and is in the proper format.
|
||||
if credentials_file:
|
||||
_validate_credentials_file(module, credentials_file,
|
||||
require_valid_json=require_valid_json,
|
||||
check_libcloud=check_libcloud)
|
||||
|
||||
return {'service_account_email': service_account_email,
|
||||
'credentials_file': credentials_file,
|
||||
'project_id': project_id}
|
||||
|
||||
|
||||
def _validate_credentials_file(module, credentials_file, require_valid_json=True, check_libcloud=False):
|
||||
"""
|
||||
Check for valid credentials file.
|
||||
|
||||
Optionally check for JSON format and if libcloud supports JSON.
|
||||
|
||||
:param module: initialized Ansible module object
|
||||
:type module: `class AnsibleModule`
|
||||
|
||||
:param credentials_file: path to file on disk
|
||||
:type credentials_file: ``str``. Complete path to file on disk.
|
||||
|
||||
:param require_valid_json: This argument is ignored as of Ansible 2.7.
|
||||
:type require_valid_json: ``bool``
|
||||
|
||||
:params check_libcloud: If true, check the libcloud version available to see if
|
||||
JSON creds are supported.
|
||||
:type check_libcloud: ``bool``
|
||||
|
||||
:returns: True
|
||||
:rtype: ``bool``
|
||||
"""
|
||||
try:
|
||||
# Try to read credentials as JSON
|
||||
with open(credentials_file) as credentials:
|
||||
json.loads(credentials.read())
|
||||
# If the credentials are proper JSON and we do not have the minimum
|
||||
# required libcloud version, bail out and return a descriptive
|
||||
# error
|
||||
if check_libcloud and LooseVersion(libcloud.__version__) < '0.17.0':
|
||||
module.fail_json(msg='Using JSON credentials but libcloud minimum version not met. '
|
||||
'Upgrade to libcloud>=0.17.0.')
|
||||
return True
|
||||
except IOError as e:
|
||||
module.fail_json(msg='GCP Credentials File %s not found.' %
|
||||
credentials_file, changed=False)
|
||||
return False
|
||||
except ValueError as e:
|
||||
module.fail_json(
|
||||
msg='Non-JSON credentials file provided. Please generate a new JSON key from the Google Cloud console',
|
||||
changed=False)
|
||||
|
||||
|
||||
def gcp_connect(module, provider, get_driver, user_agent_product, user_agent_version):
|
||||
"""Return a Google libcloud driver connection."""
|
||||
if not HAS_LIBCLOUD_BASE:
|
||||
module.fail_json(msg='libcloud must be installed to use this module')
|
||||
|
||||
creds = _get_gcp_credentials(module,
|
||||
require_valid_json=False,
|
||||
check_libcloud=True)
|
||||
try:
|
||||
gcp = get_driver(provider)(creds['service_account_email'], creds['credentials_file'],
|
||||
datacenter=module.params.get('zone', None),
|
||||
project=creds['project_id'])
|
||||
gcp.connection.user_agent_append("%s/%s" % (
|
||||
user_agent_product, user_agent_version))
|
||||
except (RuntimeError, ValueError) as e:
|
||||
module.fail_json(msg=str(e), changed=False)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
|
||||
return gcp
|
||||
|
||||
|
||||
def get_google_cloud_credentials(module, scopes=None):
|
||||
"""
|
||||
Get credentials object for use with Google Cloud client.
|
||||
|
||||
Attempts to obtain credentials by calling _get_gcp_credentials. If those are
|
||||
not present will attempt to connect via Application Default Credentials.
|
||||
|
||||
To connect via libcloud, don't use this function, use gcp_connect instead. For
|
||||
Google Python API Client, see get_google_api_auth for how to connect.
|
||||
|
||||
For more information on Google's client library options for Python, see:
|
||||
U(https://cloud.google.com/apis/docs/client-libraries-explained#google_api_client_libraries)
|
||||
|
||||
Google Cloud example:
|
||||
creds, params = get_google_cloud_credentials(module, scopes, user_agent_product, user_agent_version)
|
||||
pubsub_client = pubsub.Client(project=params['project_id'], credentials=creds)
|
||||
pubsub_client.user_agent = 'ansible-pubsub-0.1'
|
||||
...
|
||||
|
||||
:param module: initialized Ansible module object
|
||||
:type module: `class AnsibleModule`
|
||||
|
||||
:param scopes: list of scopes
|
||||
:type module: ``list`` of URIs
|
||||
|
||||
:returns: A tuple containing (google authorized) credentials object and
|
||||
params dict {'service_account_email': '...', 'credentials_file': '...', 'project_id': ...}
|
||||
:rtype: ``tuple``
|
||||
"""
|
||||
scopes = [] if scopes is None else scopes
|
||||
|
||||
if not HAS_GOOGLE_AUTH:
|
||||
module.fail_json(msg='Please install google-auth.')
|
||||
|
||||
conn_params = _get_gcp_credentials(module,
|
||||
require_valid_json=True,
|
||||
check_libcloud=False)
|
||||
try:
|
||||
if conn_params['credentials_file']:
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
conn_params['credentials_file'])
|
||||
if scopes:
|
||||
credentials = credentials.with_scopes(scopes)
|
||||
else:
|
||||
(credentials, project_id) = google.auth.default(
|
||||
scopes=scopes)
|
||||
if project_id is not None:
|
||||
conn_params['project_id'] = project_id
|
||||
|
||||
return (credentials, conn_params)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
return (None, None)
|
||||
|
||||
|
||||
def get_google_api_auth(module, scopes=None, user_agent_product='ansible-python-api', user_agent_version='NA'):
|
||||
"""
|
||||
Authentication for use with google-python-api-client.
|
||||
|
||||
Function calls get_google_cloud_credentials, which attempts to assemble the credentials
|
||||
from various locations. Next it attempts to authenticate with Google.
|
||||
|
||||
This function returns an httplib2 (compatible) object that can be provided to the Google Python API client.
|
||||
|
||||
For libcloud, don't use this function, use gcp_connect instead. For Google Cloud, See
|
||||
get_google_cloud_credentials for how to connect.
|
||||
|
||||
For more information on Google's client library options for Python, see:
|
||||
U(https://cloud.google.com/apis/docs/client-libraries-explained#google_api_client_libraries)
|
||||
|
||||
Google API example:
|
||||
http_auth, conn_params = get_google_api_auth(module, scopes, user_agent_product, user_agent_version)
|
||||
service = build('myservice', 'v1', http=http_auth)
|
||||
...
|
||||
|
||||
:param module: initialized Ansible module object
|
||||
:type module: `class AnsibleModule`
|
||||
|
||||
:param scopes: list of scopes
|
||||
:type scopes: ``list`` of URIs
|
||||
|
||||
:param user_agent_product: User agent product. eg: 'ansible-python-api'
|
||||
:type user_agent_product: ``str``
|
||||
|
||||
:param user_agent_version: Version string to append to product. eg: 'NA' or '0.1'
|
||||
:type user_agent_version: ``str``
|
||||
|
||||
:returns: A tuple containing (google authorized) httplib2 request object and a
|
||||
params dict {'service_account_email': '...', 'credentials_file': '...', 'project_id': ...}
|
||||
:rtype: ``tuple``
|
||||
"""
|
||||
scopes = [] if scopes is None else scopes
|
||||
|
||||
if not HAS_GOOGLE_API_LIB:
|
||||
module.fail_json(msg="Please install google-api-python-client library")
|
||||
if not scopes:
|
||||
scopes = GCP_DEFAULT_SCOPES
|
||||
try:
|
||||
(credentials, conn_params) = get_google_cloud_credentials(module, scopes)
|
||||
http = set_user_agent(Http(), '%s-%s' %
|
||||
(user_agent_product, user_agent_version))
|
||||
http_auth = google_auth_httplib2.AuthorizedHttp(credentials, http=http)
|
||||
|
||||
return (http_auth, conn_params)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
||||
return (None, None)
|
||||
|
||||
|
||||
def get_google_api_client(module, service, user_agent_product, user_agent_version,
|
||||
scopes=None, api_version='v1'):
|
||||
"""
|
||||
Get the discovery-based python client. Use when a cloud client is not available.
|
||||
|
||||
client = get_google_api_client(module, 'compute', user_agent_product=USER_AGENT_PRODUCT,
|
||||
user_agent_version=USER_AGENT_VERSION)
|
||||
|
||||
:returns: A tuple containing the authorized client to the specified service and a
|
||||
params dict {'service_account_email': '...', 'credentials_file': '...', 'project_id': ...}
|
||||
:rtype: ``tuple``
|
||||
"""
|
||||
if not scopes:
|
||||
scopes = GCP_DEFAULT_SCOPES
|
||||
|
||||
http_auth, conn_params = get_google_api_auth(module, scopes=scopes,
|
||||
user_agent_product=user_agent_product,
|
||||
user_agent_version=user_agent_version)
|
||||
client = build(service, api_version, http=http_auth)
|
||||
|
||||
return (client, conn_params)
|
||||
|
||||
|
||||
def check_min_pkg_version(pkg_name, minimum_version):
|
||||
"""Minimum required version is >= installed version."""
|
||||
from pkg_resources import get_distribution
|
||||
try:
|
||||
installed_version = get_distribution(pkg_name).version
|
||||
return LooseVersion(installed_version) >= minimum_version
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
|
||||
def unexpected_error_msg(error):
|
||||
"""Create an error string based on passed in error."""
|
||||
return 'Unexpected response: (%s). Detail: %s' % (str(error), traceback.format_exc())
|
||||
|
||||
|
||||
def get_valid_location(module, driver, location, location_type='zone'):
|
||||
if location_type == 'zone':
|
||||
l = driver.ex_get_zone(location)
|
||||
else:
|
||||
l = driver.ex_get_region(location)
|
||||
if l is None:
|
||||
link = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones#available'
|
||||
module.fail_json(msg=('%s %s is invalid. Please see the list of '
|
||||
'available %s at %s' % (
|
||||
location_type, location, location_type, link)),
|
||||
changed=False)
|
||||
return l
|
||||
|
||||
|
||||
def check_params(params, field_list):
|
||||
"""
|
||||
Helper to validate params.
|
||||
|
||||
Use this in function definitions if they require specific fields
|
||||
to be present.
|
||||
|
||||
:param params: structure that contains the fields
|
||||
:type params: ``dict``
|
||||
|
||||
:param field_list: list of dict representing the fields
|
||||
[{'name': str, 'required': True/False', 'type': cls}]
|
||||
:type field_list: ``list`` of ``dict``
|
||||
|
||||
:return True or raises ValueError
|
||||
:rtype: ``bool`` or `class:ValueError`
|
||||
"""
|
||||
for d in field_list:
|
||||
if not d['name'] in params:
|
||||
if 'required' in d and d['required'] is True:
|
||||
raise ValueError(("%s is required and must be of type: %s" %
|
||||
(d['name'], str(d['type']))))
|
||||
else:
|
||||
if not isinstance(params[d['name']], d['type']):
|
||||
raise ValueError(("%s must be of type: %s. %s (%s) provided." % (
|
||||
d['name'], str(d['type']), params[d['name']],
|
||||
type(params[d['name']]))))
|
||||
if 'values' in d:
|
||||
if params[d['name']] not in d['values']:
|
||||
raise ValueError(("%s must be one of: %s" % (
|
||||
d['name'], ','.join(d['values']))))
|
||||
if isinstance(params[d['name']], int):
|
||||
if 'min' in d:
|
||||
if params[d['name']] < d['min']:
|
||||
raise ValueError(("%s must be greater than or equal to: %s" % (
|
||||
d['name'], d['min'])))
|
||||
if 'max' in d:
|
||||
if params[d['name']] > d['max']:
|
||||
raise ValueError("%s must be less than or equal to: %s" % (
|
||||
d['name'], d['max']))
|
||||
return True
|
||||
|
||||
|
||||
class GCPUtils(object):
|
||||
"""
|
||||
Helper utilities for GCP.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def underscore_to_camel(txt):
|
||||
return txt.split('_')[0] + ''.join(x.capitalize() or '_' for x in txt.split('_')[1:])
|
||||
|
||||
@staticmethod
|
||||
def remove_non_gcp_params(params):
|
||||
"""
|
||||
Remove params if found.
|
||||
"""
|
||||
params_to_remove = ['state']
|
||||
for p in params_to_remove:
|
||||
if p in params:
|
||||
del params[p]
|
||||
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def params_to_gcp_dict(params, resource_name=None):
|
||||
"""
|
||||
Recursively convert ansible params to GCP Params.
|
||||
|
||||
Keys are converted from snake to camelCase
|
||||
ex: default_service to defaultService
|
||||
|
||||
Handles lists, dicts and strings
|
||||
|
||||
special provision for the resource name
|
||||
"""
|
||||
if not isinstance(params, dict):
|
||||
return params
|
||||
gcp_dict = {}
|
||||
params = GCPUtils.remove_non_gcp_params(params)
|
||||
for k, v in params.items():
|
||||
gcp_key = GCPUtils.underscore_to_camel(k)
|
||||
if isinstance(v, dict):
|
||||
retval = GCPUtils.params_to_gcp_dict(v)
|
||||
gcp_dict[gcp_key] = retval
|
||||
elif isinstance(v, list):
|
||||
gcp_dict[gcp_key] = [GCPUtils.params_to_gcp_dict(x) for x in v]
|
||||
else:
|
||||
if resource_name and k == resource_name:
|
||||
gcp_dict['name'] = v
|
||||
else:
|
||||
gcp_dict[gcp_key] = v
|
||||
return gcp_dict
|
||||
|
||||
@staticmethod
|
||||
def execute_api_client_req(req, client=None, raw=True,
|
||||
operation_timeout=180, poll_interval=5,
|
||||
raise_404=True):
|
||||
"""
|
||||
General python api client interaction function.
|
||||
|
||||
For use with google-api-python-client, or clients created
|
||||
with get_google_api_client function
|
||||
Not for use with Google Cloud client libraries
|
||||
|
||||
For long-running operations, we make an immediate query and then
|
||||
sleep poll_interval before re-querying. After the request is done
|
||||
we rebuild the request with a get method and return the result.
|
||||
|
||||
"""
|
||||
try:
|
||||
resp = req.execute()
|
||||
|
||||
if not resp:
|
||||
return None
|
||||
|
||||
if raw:
|
||||
return resp
|
||||
|
||||
if resp['kind'] == 'compute#operation':
|
||||
resp = GCPUtils.execute_api_client_operation_req(req, resp,
|
||||
client,
|
||||
operation_timeout,
|
||||
poll_interval)
|
||||
|
||||
if 'items' in resp:
|
||||
return resp['items']
|
||||
|
||||
return resp
|
||||
except HttpError as h:
|
||||
# Note: 404s can be generated (incorrectly) for dependent
|
||||
# resources not existing. We let the caller determine if
|
||||
# they want 404s raised for their invocation.
|
||||
if h.resp.status == 404 and not raise_404:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def execute_api_client_operation_req(orig_req, op_resp, client,
|
||||
operation_timeout=180, poll_interval=5):
|
||||
"""
|
||||
Poll an operation for a result.
|
||||
"""
|
||||
parsed_url = GCPUtils.parse_gcp_url(orig_req.uri)
|
||||
project_id = parsed_url['project']
|
||||
resource_name = GCPUtils.get_gcp_resource_from_methodId(
|
||||
orig_req.methodId)
|
||||
resource = GCPUtils.build_resource_from_name(client, resource_name)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
complete = False
|
||||
attempts = 1
|
||||
while not complete:
|
||||
if start_time + operation_timeout >= time.time():
|
||||
op_req = client.globalOperations().get(
|
||||
project=project_id, operation=op_resp['name'])
|
||||
op_resp = op_req.execute()
|
||||
if op_resp['status'] != 'DONE':
|
||||
time.sleep(poll_interval)
|
||||
attempts += 1
|
||||
else:
|
||||
complete = True
|
||||
if op_resp['operationType'] == 'delete':
|
||||
# don't wait for the delete
|
||||
return True
|
||||
elif op_resp['operationType'] in ['insert', 'update', 'patch']:
|
||||
# TODO(supertom): Isolate 'build-new-request' stuff.
|
||||
resource_name_singular = GCPUtils.get_entity_name_from_resource_name(
|
||||
resource_name)
|
||||
if op_resp['operationType'] == 'insert' or 'entity_name' not in parsed_url:
|
||||
parsed_url['entity_name'] = GCPUtils.parse_gcp_url(op_resp['targetLink'])[
|
||||
'entity_name']
|
||||
args = {'project': project_id,
|
||||
resource_name_singular: parsed_url['entity_name']}
|
||||
new_req = resource.get(**args)
|
||||
resp = new_req.execute()
|
||||
return resp
|
||||
else:
|
||||
# assuming multiple entities, do a list call.
|
||||
new_req = resource.list(project=project_id)
|
||||
resp = new_req.execute()
|
||||
return resp
|
||||
else:
|
||||
# operation didn't complete on time.
|
||||
raise GCPOperationTimeoutError("Operation timed out: %s" % (
|
||||
op_resp['targetLink']))
|
||||
|
||||
@staticmethod
|
||||
def build_resource_from_name(client, resource_name):
|
||||
try:
|
||||
method = getattr(client, resource_name)
|
||||
return method()
|
||||
except AttributeError:
|
||||
raise NotImplementedError('%s is not an attribute of %s' % (resource_name,
|
||||
client))
|
||||
|
||||
@staticmethod
|
||||
def get_gcp_resource_from_methodId(methodId):
|
||||
try:
|
||||
parts = methodId.split('.')
|
||||
if len(parts) != 3:
|
||||
return None
|
||||
else:
|
||||
return parts[1]
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_entity_name_from_resource_name(resource_name):
|
||||
if not resource_name:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Chop off global or region prefixes
|
||||
if resource_name.startswith('global'):
|
||||
resource_name = resource_name.replace('global', '')
|
||||
elif resource_name.startswith('regional'):
|
||||
resource_name = resource_name.replace('region', '')
|
||||
|
||||
# ensure we have a lower case first letter
|
||||
resource_name = resource_name[0].lower() + resource_name[1:]
|
||||
|
||||
if resource_name[-3:] == 'ies':
|
||||
return resource_name.replace(
|
||||
resource_name[-3:], 'y')
|
||||
if resource_name[-1] == 's':
|
||||
return resource_name[:-1]
|
||||
|
||||
return resource_name
|
||||
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_gcp_url(url):
|
||||
"""
|
||||
Parse GCP urls and return dict of parts.
|
||||
|
||||
Supported URL structures:
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/RESOURCE
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/RESOURCE/ENTITY_NAME
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/RESOURCE/ENTITY_NAME/METHOD_NAME
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/'global'/RESOURCE
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/'global'/RESOURCE/ENTITY_NAME
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/'global'/RESOURCE/ENTITY_NAME/METHOD_NAME
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/LOCATION_TYPE/LOCATION/RESOURCE
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/LOCATION_TYPE/LOCATION/RESOURCE/ENTITY_NAME
|
||||
/SERVICE/VERSION/'projects'/PROJECT_ID/LOCATION_TYPE/LOCATION/RESOURCE/ENTITY_NAME/METHOD_NAME
|
||||
|
||||
:param url: GCP-generated URL, such as a selflink or resource location.
|
||||
:type url: ``str``
|
||||
|
||||
:return: dictionary of parts. Includes stanard components of urlparse, plus
|
||||
GCP-specific 'service', 'api_version', 'project' and
|
||||
'resource_name' keys. Optionally, 'zone', 'region', 'entity_name'
|
||||
and 'method_name', if applicable.
|
||||
:rtype: ``dict``
|
||||
"""
|
||||
|
||||
p = urlparse.urlparse(url)
|
||||
if not p:
|
||||
return None
|
||||
else:
|
||||
# we add extra items such as
|
||||
# zone, region and resource_name
|
||||
url_parts = {}
|
||||
url_parts['scheme'] = p.scheme
|
||||
url_parts['host'] = p.netloc
|
||||
url_parts['path'] = p.path
|
||||
if p.path.find('/') == 0:
|
||||
url_parts['path'] = p.path[1:]
|
||||
url_parts['params'] = p.params
|
||||
url_parts['fragment'] = p.fragment
|
||||
url_parts['query'] = p.query
|
||||
url_parts['project'] = None
|
||||
url_parts['service'] = None
|
||||
url_parts['api_version'] = None
|
||||
|
||||
path_parts = url_parts['path'].split('/')
|
||||
url_parts['service'] = path_parts[0]
|
||||
url_parts['api_version'] = path_parts[1]
|
||||
if path_parts[2] == 'projects':
|
||||
url_parts['project'] = path_parts[3]
|
||||
else:
|
||||
# invalid URL
|
||||
raise GCPInvalidURLError('unable to parse: %s' % url)
|
||||
|
||||
if 'global' in path_parts:
|
||||
url_parts['global'] = True
|
||||
idx = path_parts.index('global')
|
||||
if len(path_parts) - idx == 4:
|
||||
# we have a resource, entity and method_name
|
||||
url_parts['resource_name'] = path_parts[idx + 1]
|
||||
url_parts['entity_name'] = path_parts[idx + 2]
|
||||
url_parts['method_name'] = path_parts[idx + 3]
|
||||
|
||||
if len(path_parts) - idx == 3:
|
||||
# we have a resource and entity
|
||||
url_parts['resource_name'] = path_parts[idx + 1]
|
||||
url_parts['entity_name'] = path_parts[idx + 2]
|
||||
|
||||
if len(path_parts) - idx == 2:
|
||||
url_parts['resource_name'] = path_parts[idx + 1]
|
||||
|
||||
if len(path_parts) - idx < 2:
|
||||
# invalid URL
|
||||
raise GCPInvalidURLError('unable to parse: %s' % url)
|
||||
|
||||
elif 'regions' in path_parts or 'zones' in path_parts:
|
||||
idx = -1
|
||||
if 'regions' in path_parts:
|
||||
idx = path_parts.index('regions')
|
||||
url_parts['region'] = path_parts[idx + 1]
|
||||
else:
|
||||
idx = path_parts.index('zones')
|
||||
url_parts['zone'] = path_parts[idx + 1]
|
||||
|
||||
if len(path_parts) - idx == 5:
|
||||
# we have a resource, entity and method_name
|
||||
url_parts['resource_name'] = path_parts[idx + 2]
|
||||
url_parts['entity_name'] = path_parts[idx + 3]
|
||||
url_parts['method_name'] = path_parts[idx + 4]
|
||||
|
||||
if len(path_parts) - idx == 4:
|
||||
# we have a resource and entity
|
||||
url_parts['resource_name'] = path_parts[idx + 2]
|
||||
url_parts['entity_name'] = path_parts[idx + 3]
|
||||
|
||||
if len(path_parts) - idx == 3:
|
||||
url_parts['resource_name'] = path_parts[idx + 2]
|
||||
|
||||
if len(path_parts) - idx < 3:
|
||||
# invalid URL
|
||||
raise GCPInvalidURLError('unable to parse: %s' % url)
|
||||
|
||||
else:
|
||||
# no location in URL.
|
||||
idx = path_parts.index('projects')
|
||||
if len(path_parts) - idx == 5:
|
||||
# we have a resource, entity and method_name
|
||||
url_parts['resource_name'] = path_parts[idx + 2]
|
||||
url_parts['entity_name'] = path_parts[idx + 3]
|
||||
url_parts['method_name'] = path_parts[idx + 4]
|
||||
|
||||
if len(path_parts) - idx == 4:
|
||||
# we have a resource and entity
|
||||
url_parts['resource_name'] = path_parts[idx + 2]
|
||||
url_parts['entity_name'] = path_parts[idx + 3]
|
||||
|
||||
if len(path_parts) - idx == 3:
|
||||
url_parts['resource_name'] = path_parts[idx + 2]
|
||||
|
||||
if len(path_parts) - idx < 3:
|
||||
# invalid URL
|
||||
raise GCPInvalidURLError('unable to parse: %s' % url)
|
||||
|
||||
return url_parts
|
||||
|
||||
@staticmethod
|
||||
def build_googleapi_url(project, api_version='v1', service='compute'):
|
||||
return 'https://www.googleapis.com/%s/%s/projects/%s' % (service, api_version, project)
|
||||
|
||||
@staticmethod
|
||||
def filter_gcp_fields(params, excluded_fields=None):
|
||||
new_params = {}
|
||||
if not excluded_fields:
|
||||
excluded_fields = ['creationTimestamp', 'id', 'kind',
|
||||
'selfLink', 'fingerprint', 'description']
|
||||
|
||||
if isinstance(params, list):
|
||||
new_params = [GCPUtils.filter_gcp_fields(
|
||||
x, excluded_fields) for x in params]
|
||||
elif isinstance(params, dict):
|
||||
for k in params.keys():
|
||||
if k not in excluded_fields:
|
||||
new_params[k] = GCPUtils.filter_gcp_fields(
|
||||
params[k], excluded_fields)
|
||||
else:
|
||||
new_params = params
|
||||
|
||||
return new_params
|
||||
|
||||
@staticmethod
|
||||
def are_params_equal(p1, p2):
|
||||
"""
|
||||
Check if two params dicts are equal.
|
||||
TODO(supertom): need a way to filter out URLs, or they need to be built
|
||||
"""
|
||||
filtered_p1 = GCPUtils.filter_gcp_fields(p1)
|
||||
filtered_p2 = GCPUtils.filter_gcp_fields(p2)
|
||||
if filtered_p1 != filtered_p2:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class GCPError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GCPOperationTimeoutError(GCPError):
|
||||
pass
|
||||
|
||||
|
||||
class GCPInvalidURLError(GCPError):
|
||||
pass
|
||||
104
plugins/module_utils/gitlab.py
Normal file
104
plugins/module_utils/gitlab.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr)
|
||||
# Copyright: (c) 2018, Marcus Watkins <marwatk@marcuswatkins.net>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import
|
||||
import json
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
from urllib import quote_plus # Python 2.X
|
||||
except ImportError:
|
||||
from urllib.parse import quote_plus # Python 3+
|
||||
|
||||
import traceback
|
||||
|
||||
GITLAB_IMP_ERR = None
|
||||
try:
|
||||
import gitlab
|
||||
HAS_GITLAB_PACKAGE = True
|
||||
except Exception:
|
||||
GITLAB_IMP_ERR = traceback.format_exc()
|
||||
HAS_GITLAB_PACKAGE = False
|
||||
|
||||
|
||||
def request(module, api_url, project, path, access_token, private_token, rawdata='', method='GET'):
|
||||
url = "%s/v4/projects/%s%s" % (api_url, quote_plus(project), path)
|
||||
headers = {}
|
||||
if access_token:
|
||||
headers['Authorization'] = "Bearer %s" % access_token
|
||||
else:
|
||||
headers['Private-Token'] = private_token
|
||||
|
||||
headers['Accept'] = "application/json"
|
||||
headers['Content-Type'] = "application/json"
|
||||
|
||||
response, info = fetch_url(module=module, url=url, headers=headers, data=rawdata, method=method)
|
||||
status = info['status']
|
||||
content = ""
|
||||
if response:
|
||||
content = response.read()
|
||||
if status == 204:
|
||||
return True, content
|
||||
elif status == 200 or status == 201:
|
||||
return True, json.loads(content)
|
||||
else:
|
||||
return False, str(status) + ": " + content
|
||||
|
||||
|
||||
def findProject(gitlab_instance, identifier):
|
||||
try:
|
||||
project = gitlab_instance.projects.get(identifier)
|
||||
except Exception as e:
|
||||
current_user = gitlab_instance.user
|
||||
try:
|
||||
project = gitlab_instance.projects.get(current_user.username + '/' + identifier)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def findGroup(gitlab_instance, identifier):
|
||||
try:
|
||||
project = gitlab_instance.groups.get(identifier)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def gitlabAuthentication(module):
|
||||
gitlab_url = module.params['api_url']
|
||||
validate_certs = module.params['validate_certs']
|
||||
gitlab_user = module.params['api_username']
|
||||
gitlab_password = module.params['api_password']
|
||||
gitlab_token = module.params['api_token']
|
||||
|
||||
if not HAS_GITLAB_PACKAGE:
|
||||
module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR)
|
||||
|
||||
try:
|
||||
# python-gitlab library remove support for username/password authentication since 1.13.0
|
||||
# Changelog : https://github.com/python-gitlab/python-gitlab/releases/tag/v1.13.0
|
||||
# This condition allow to still support older version of the python-gitlab library
|
||||
if StrictVersion(gitlab.__version__) < StrictVersion("1.13.0"):
|
||||
gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password,
|
||||
private_token=gitlab_token, api_version=4)
|
||||
else:
|
||||
gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, private_token=gitlab_token, api_version=4)
|
||||
|
||||
gitlab_instance.auth()
|
||||
except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e:
|
||||
module.fail_json(msg="Failed to connect to GitLab server: %s" % to_native(e))
|
||||
except (gitlab.exceptions.GitlabHttpError) as e:
|
||||
module.fail_json(msg="Failed to connect to GitLab server: %s. \
|
||||
GitLab remove Session API now that private tokens are removed from user API endpoints since version 10.2." % to_native(e))
|
||||
|
||||
return gitlab_instance
|
||||
41
plugins/module_utils/heroku.py
Normal file
41
plugins/module_utils/heroku.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright: (c) 2018, Ansible Project
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import env_fallback, missing_required_lib
|
||||
|
||||
HAS_HEROKU = False
|
||||
HEROKU_IMP_ERR = None
|
||||
try:
|
||||
import heroku3
|
||||
HAS_HEROKU = True
|
||||
except ImportError:
|
||||
HEROKU_IMP_ERR = traceback.format_exc()
|
||||
|
||||
|
||||
class HerokuHelper():
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.check_lib()
|
||||
self.api_key = module.params["api_key"]
|
||||
|
||||
def check_lib(self):
|
||||
if not HAS_HEROKU:
|
||||
self.module.fail_json(msg=missing_required_lib('heroku3'), exception=HEROKU_IMP_ERR)
|
||||
|
||||
@staticmethod
|
||||
def heroku_argument_spec():
|
||||
return dict(
|
||||
api_key=dict(fallback=(env_fallback, ['HEROKU_API_KEY', 'TF_VAR_HEROKU_API_KEY']), type='str', no_log=True))
|
||||
|
||||
def get_heroku_client(self):
|
||||
client = heroku3.from_key(self.api_key)
|
||||
|
||||
if not client.is_authenticated:
|
||||
self.module.fail_json(msg='Heroku authentication failure, please check your API Key')
|
||||
|
||||
return client
|
||||
171
plugins/module_utils/hetzner.py
Normal file
171
plugins/module_utils/hetzner.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Felix Fontein <felix@fontein.de>, 2019
|
||||
#
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
|
||||
import time
|
||||
|
||||
|
||||
HETZNER_DEFAULT_ARGUMENT_SPEC = dict(
|
||||
hetzner_user=dict(type='str', required=True),
|
||||
hetzner_password=dict(type='str', required=True, no_log=True),
|
||||
)
|
||||
|
||||
# The API endpoint is fixed.
|
||||
BASE_URL = "https://robot-ws.your-server.de"
|
||||
|
||||
|
||||
def fetch_url_json(module, url, method='GET', timeout=10, data=None, headers=None, accept_errors=None):
|
||||
'''
|
||||
Make general request to Hetzner's JSON robot API.
|
||||
'''
|
||||
module.params['url_username'] = module.params['hetzner_user']
|
||||
module.params['url_password'] = module.params['hetzner_password']
|
||||
resp, info = fetch_url(module, url, method=method, timeout=timeout, data=data, headers=headers)
|
||||
try:
|
||||
content = resp.read()
|
||||
except AttributeError:
|
||||
content = info.pop('body', None)
|
||||
|
||||
if not content:
|
||||
module.fail_json(msg='Cannot retrieve content from {0}'.format(url))
|
||||
|
||||
try:
|
||||
result = module.from_json(content.decode('utf8'))
|
||||
if 'error' in result:
|
||||
if accept_errors:
|
||||
if result['error']['code'] in accept_errors:
|
||||
return result, result['error']['code']
|
||||
module.fail_json(msg='Request failed: {0} {1} ({2})'.format(
|
||||
result['error']['status'],
|
||||
result['error']['code'],
|
||||
result['error']['message']
|
||||
))
|
||||
return result, None
|
||||
except ValueError:
|
||||
module.fail_json(msg='Cannot decode content retrieved from {0}'.format(url))
|
||||
|
||||
|
||||
class CheckDoneTimeoutException(Exception):
|
||||
def __init__(self, result, error):
|
||||
super(CheckDoneTimeoutException, self).__init__()
|
||||
self.result = result
|
||||
self.error = error
|
||||
|
||||
|
||||
def fetch_url_json_with_retries(module, url, check_done_callback, check_done_delay=10, check_done_timeout=180, skip_first=False, **kwargs):
|
||||
'''
|
||||
Make general request to Hetzner's JSON robot API, with retries until a condition is satisfied.
|
||||
|
||||
The condition is tested by calling ``check_done_callback(result, error)``. If it is not satisfied,
|
||||
it will be retried with delays ``check_done_delay`` (in seconds) until a total timeout of
|
||||
``check_done_timeout`` (in seconds) since the time the first request is started is reached.
|
||||
|
||||
If ``skip_first`` is specified, will assume that a first call has already been made and will
|
||||
directly start with waiting.
|
||||
'''
|
||||
start_time = time.time()
|
||||
if not skip_first:
|
||||
result, error = fetch_url_json(module, url, **kwargs)
|
||||
if check_done_callback(result, error):
|
||||
return result, error
|
||||
while True:
|
||||
elapsed = (time.time() - start_time)
|
||||
left_time = check_done_timeout - elapsed
|
||||
time.sleep(max(min(check_done_delay, left_time), 0))
|
||||
result, error = fetch_url_json(module, url, **kwargs)
|
||||
if check_done_callback(result, error):
|
||||
return result, error
|
||||
if left_time < check_done_delay:
|
||||
raise CheckDoneTimeoutException(result, error)
|
||||
|
||||
|
||||
# #####################################################################################
|
||||
# ## FAILOVER IP ######################################################################
|
||||
|
||||
def get_failover_record(module, ip):
|
||||
'''
|
||||
Get information record of failover IP.
|
||||
|
||||
See https://robot.your-server.de/doc/webservice/en.html#get-failover-failover-ip
|
||||
'''
|
||||
url = "{0}/failover/{1}".format(BASE_URL, ip)
|
||||
result, error = fetch_url_json(module, url)
|
||||
if 'failover' not in result:
|
||||
module.fail_json(msg='Cannot interpret result: {0}'.format(result))
|
||||
return result['failover']
|
||||
|
||||
|
||||
def get_failover(module, ip):
|
||||
'''
|
||||
Get current routing target of failover IP.
|
||||
|
||||
The value ``None`` represents unrouted.
|
||||
|
||||
See https://robot.your-server.de/doc/webservice/en.html#get-failover-failover-ip
|
||||
'''
|
||||
return get_failover_record(module, ip)['active_server_ip']
|
||||
|
||||
|
||||
def set_failover(module, ip, value, timeout=180):
|
||||
'''
|
||||
Set current routing target of failover IP.
|
||||
|
||||
Return a pair ``(value, changed)``. The value ``None`` for ``value`` represents unrouted.
|
||||
|
||||
See https://robot.your-server.de/doc/webservice/en.html#post-failover-failover-ip
|
||||
and https://robot.your-server.de/doc/webservice/en.html#delete-failover-failover-ip
|
||||
'''
|
||||
url = "{0}/failover/{1}".format(BASE_URL, ip)
|
||||
if value is None:
|
||||
result, error = fetch_url_json(
|
||||
module,
|
||||
url,
|
||||
method='DELETE',
|
||||
timeout=timeout,
|
||||
accept_errors=['FAILOVER_ALREADY_ROUTED']
|
||||
)
|
||||
else:
|
||||
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
||||
data = dict(
|
||||
active_server_ip=value,
|
||||
)
|
||||
result, error = fetch_url_json(
|
||||
module,
|
||||
url,
|
||||
method='POST',
|
||||
timeout=timeout,
|
||||
data=urlencode(data),
|
||||
headers=headers,
|
||||
accept_errors=['FAILOVER_ALREADY_ROUTED']
|
||||
)
|
||||
if error is not None:
|
||||
return value, False
|
||||
else:
|
||||
return result['failover']['active_server_ip'], True
|
||||
|
||||
|
||||
def get_failover_state(value):
|
||||
'''
|
||||
Create result dictionary for failover IP's value.
|
||||
|
||||
The value ``None`` represents unrouted.
|
||||
'''
|
||||
return dict(
|
||||
value=value,
|
||||
state='routed' if value else 'unrouted'
|
||||
)
|
||||
438
plugins/module_utils/hwc_utils.py
Normal file
438
plugins/module_utils/hwc_utils.py
Normal file
@@ -0,0 +1,438 @@
|
||||
# Copyright (c), Google Inc, 2017
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or
|
||||
# https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
THIRD_LIBRARIES_IMP_ERR = None
|
||||
try:
|
||||
from keystoneauth1.adapter import Adapter
|
||||
from keystoneauth1.identity import v3
|
||||
from keystoneauth1 import session
|
||||
HAS_THIRD_LIBRARIES = True
|
||||
except ImportError:
|
||||
THIRD_LIBRARIES_IMP_ERR = traceback.format_exc()
|
||||
HAS_THIRD_LIBRARIES = False
|
||||
|
||||
from ansible.module_utils.basic import (AnsibleModule, env_fallback,
|
||||
missing_required_lib)
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
class HwcModuleException(Exception):
|
||||
def __init__(self, message):
|
||||
super(HwcModuleException, self).__init__()
|
||||
|
||||
self._message = message
|
||||
|
||||
def __str__(self):
|
||||
return "[HwcClientException] message=%s" % self._message
|
||||
|
||||
|
||||
class HwcClientException(Exception):
|
||||
def __init__(self, code, message):
|
||||
super(HwcClientException, self).__init__()
|
||||
|
||||
self._code = code
|
||||
self._message = message
|
||||
|
||||
def __str__(self):
|
||||
msg = " code=%s," % str(self._code) if self._code != 0 else ""
|
||||
return "[HwcClientException]%s message=%s" % (
|
||||
msg, self._message)
|
||||
|
||||
|
||||
class HwcClientException404(HwcClientException):
|
||||
def __init__(self, message):
|
||||
super(HwcClientException404, self).__init__(404, message)
|
||||
|
||||
def __str__(self):
|
||||
return "[HwcClientException404] message=%s" % self._message
|
||||
|
||||
|
||||
def session_method_wrapper(f):
|
||||
def _wrap(self, url, *args, **kwargs):
|
||||
try:
|
||||
url = self.endpoint + url
|
||||
r = f(self, url, *args, **kwargs)
|
||||
except Exception as ex:
|
||||
raise HwcClientException(
|
||||
0, "Sending request failed, error=%s" % ex)
|
||||
|
||||
result = None
|
||||
if r.content:
|
||||
try:
|
||||
result = r.json()
|
||||
except Exception as ex:
|
||||
raise HwcClientException(
|
||||
0, "Parsing response to json failed, error: %s" % ex)
|
||||
|
||||
code = r.status_code
|
||||
if code not in [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]:
|
||||
msg = ""
|
||||
for i in ['message', 'error.message']:
|
||||
try:
|
||||
msg = navigate_value(result, i)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
msg = str(result)
|
||||
|
||||
if code == 404:
|
||||
raise HwcClientException404(msg)
|
||||
|
||||
raise HwcClientException(code, msg)
|
||||
|
||||
return result
|
||||
|
||||
return _wrap
|
||||
|
||||
|
||||
class _ServiceClient(object):
|
||||
def __init__(self, client, endpoint, product):
|
||||
self._client = client
|
||||
self._endpoint = endpoint
|
||||
self._default_header = {
|
||||
'User-Agent': "Huawei-Ansible-MM-%s" % product,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
return self._endpoint
|
||||
|
||||
@endpoint.setter
|
||||
def endpoint(self, e):
|
||||
self._endpoint = e
|
||||
|
||||
@session_method_wrapper
|
||||
def get(self, url, body=None, header=None, timeout=None):
|
||||
return self._client.get(url, json=body, timeout=timeout,
|
||||
headers=self._header(header))
|
||||
|
||||
@session_method_wrapper
|
||||
def post(self, url, body=None, header=None, timeout=None):
|
||||
return self._client.post(url, json=body, timeout=timeout,
|
||||
headers=self._header(header))
|
||||
|
||||
@session_method_wrapper
|
||||
def delete(self, url, body=None, header=None, timeout=None):
|
||||
return self._client.delete(url, json=body, timeout=timeout,
|
||||
headers=self._header(header))
|
||||
|
||||
@session_method_wrapper
|
||||
def put(self, url, body=None, header=None, timeout=None):
|
||||
return self._client.put(url, json=body, timeout=timeout,
|
||||
headers=self._header(header))
|
||||
|
||||
def _header(self, header):
|
||||
if header and isinstance(header, dict):
|
||||
for k, v in self._default_header.items():
|
||||
if k not in header:
|
||||
header[k] = v
|
||||
else:
|
||||
header = self._default_header
|
||||
|
||||
return header
|
||||
|
||||
|
||||
class Config(object):
|
||||
def __init__(self, module, product):
|
||||
self._project_client = None
|
||||
self._domain_client = None
|
||||
self._module = module
|
||||
self._product = product
|
||||
self._endpoints = {}
|
||||
|
||||
self._validate()
|
||||
self._gen_provider_client()
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
return self._module
|
||||
|
||||
def client(self, region, service_type, service_level):
|
||||
c = self._project_client
|
||||
if service_level == "domain":
|
||||
c = self._domain_client
|
||||
|
||||
e = self._get_service_endpoint(c, service_type, region)
|
||||
|
||||
return _ServiceClient(c, e, self._product)
|
||||
|
||||
def _gen_provider_client(self):
|
||||
m = self._module
|
||||
p = {
|
||||
"auth_url": m.params['identity_endpoint'],
|
||||
"password": m.params['password'],
|
||||
"username": m.params['user'],
|
||||
"project_name": m.params['project'],
|
||||
"user_domain_name": m.params['domain'],
|
||||
"reauthenticate": True
|
||||
}
|
||||
|
||||
self._project_client = Adapter(
|
||||
session.Session(auth=v3.Password(**p)),
|
||||
raise_exc=False)
|
||||
|
||||
p.pop("project_name")
|
||||
self._domain_client = Adapter(
|
||||
session.Session(auth=v3.Password(**p)),
|
||||
raise_exc=False)
|
||||
|
||||
def _get_service_endpoint(self, client, service_type, region):
|
||||
k = "%s.%s" % (service_type, region if region else "")
|
||||
|
||||
if k in self._endpoints:
|
||||
return self._endpoints.get(k)
|
||||
|
||||
url = None
|
||||
try:
|
||||
url = client.get_endpoint(service_type=service_type,
|
||||
region_name=region, interface="public")
|
||||
except Exception as ex:
|
||||
raise HwcClientException(
|
||||
0, "Getting endpoint failed, error=%s" % ex)
|
||||
|
||||
if url == "":
|
||||
raise HwcClientException(
|
||||
0, "Can not find the enpoint for %s" % service_type)
|
||||
|
||||
if url[-1] != "/":
|
||||
url += "/"
|
||||
|
||||
self._endpoints[k] = url
|
||||
return url
|
||||
|
||||
def _validate(self):
|
||||
if not HAS_THIRD_LIBRARIES:
|
||||
self.module.fail_json(
|
||||
msg=missing_required_lib('keystoneauth1'),
|
||||
exception=THIRD_LIBRARIES_IMP_ERR)
|
||||
|
||||
|
||||
class HwcModule(AnsibleModule):
|
||||
def __init__(self, *args, **kwargs):
|
||||
arg_spec = kwargs.setdefault('argument_spec', {})
|
||||
|
||||
arg_spec.update(
|
||||
dict(
|
||||
identity_endpoint=dict(
|
||||
required=True, type='str',
|
||||
fallback=(env_fallback, ['ANSIBLE_HWC_IDENTITY_ENDPOINT']),
|
||||
),
|
||||
user=dict(
|
||||
required=True, type='str',
|
||||
fallback=(env_fallback, ['ANSIBLE_HWC_USER']),
|
||||
),
|
||||
password=dict(
|
||||
required=True, type='str', no_log=True,
|
||||
fallback=(env_fallback, ['ANSIBLE_HWC_PASSWORD']),
|
||||
),
|
||||
domain=dict(
|
||||
required=True, type='str',
|
||||
fallback=(env_fallback, ['ANSIBLE_HWC_DOMAIN']),
|
||||
),
|
||||
project=dict(
|
||||
required=True, type='str',
|
||||
fallback=(env_fallback, ['ANSIBLE_HWC_PROJECT']),
|
||||
),
|
||||
region=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, ['ANSIBLE_HWC_REGION']),
|
||||
),
|
||||
id=dict(type='str')
|
||||
)
|
||||
)
|
||||
|
||||
super(HwcModule, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class _DictComparison(object):
|
||||
''' This class takes in two dictionaries `a` and `b`.
|
||||
These are dictionaries of arbitrary depth, but made up of standard
|
||||
Python types only.
|
||||
This differ will compare all values in `a` to those in `b`.
|
||||
If value in `a` is None, always returns True, indicating
|
||||
this value is no need to compare.
|
||||
Note: On all lists, order does matter.
|
||||
'''
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._compare_dicts(self.request, other.request)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def _compare_dicts(self, dict1, dict2):
|
||||
if dict1 is None:
|
||||
return True
|
||||
|
||||
if set(dict1.keys()) != set(dict2.keys()):
|
||||
return False
|
||||
|
||||
for k in dict1:
|
||||
if not self._compare_value(dict1.get(k), dict2.get(k)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _compare_lists(self, list1, list2):
|
||||
"""Takes in two lists and compares them."""
|
||||
if list1 is None:
|
||||
return True
|
||||
|
||||
if len(list1) != len(list2):
|
||||
return False
|
||||
|
||||
for i in range(len(list1)):
|
||||
if not self._compare_value(list1[i], list2[i]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _compare_value(self, value1, value2):
|
||||
"""
|
||||
return: True: value1 is same as value2, otherwise False.
|
||||
"""
|
||||
if value1 is None:
|
||||
return True
|
||||
|
||||
if not (value1 and value2):
|
||||
return (not value1) and (not value2)
|
||||
|
||||
# Can assume non-None types at this point.
|
||||
if isinstance(value1, list) and isinstance(value2, list):
|
||||
return self._compare_lists(value1, value2)
|
||||
|
||||
elif isinstance(value1, dict) and isinstance(value2, dict):
|
||||
return self._compare_dicts(value1, value2)
|
||||
|
||||
# Always use to_text values to avoid unicode issues.
|
||||
return (to_text(value1, errors='surrogate_or_strict') == to_text(
|
||||
value2, errors='surrogate_or_strict'))
|
||||
|
||||
|
||||
def wait_to_finish(target, pending, refresh, timeout, min_interval=1, delay=3):
|
||||
is_last_time = False
|
||||
not_found_times = 0
|
||||
wait = 0
|
||||
|
||||
time.sleep(delay)
|
||||
|
||||
end = time.time() + timeout
|
||||
while not is_last_time:
|
||||
if time.time() > end:
|
||||
is_last_time = True
|
||||
|
||||
obj, status = refresh()
|
||||
|
||||
if obj is None:
|
||||
not_found_times += 1
|
||||
|
||||
if not_found_times > 10:
|
||||
raise HwcModuleException(
|
||||
"not found the object for %d times" % not_found_times)
|
||||
else:
|
||||
not_found_times = 0
|
||||
|
||||
if status in target:
|
||||
return obj
|
||||
|
||||
if pending and status not in pending:
|
||||
raise HwcModuleException(
|
||||
"unexpect status(%s) occured" % status)
|
||||
|
||||
if not is_last_time:
|
||||
wait *= 2
|
||||
if wait < min_interval:
|
||||
wait = min_interval
|
||||
elif wait > 10:
|
||||
wait = 10
|
||||
|
||||
time.sleep(wait)
|
||||
|
||||
raise HwcModuleException("asycn wait timeout after %d seconds" % timeout)
|
||||
|
||||
|
||||
def navigate_value(data, index, array_index=None):
|
||||
if array_index and (not isinstance(array_index, dict)):
|
||||
raise HwcModuleException("array_index must be dict")
|
||||
|
||||
d = data
|
||||
for n in range(len(index)):
|
||||
if d is None:
|
||||
return None
|
||||
|
||||
if not isinstance(d, dict):
|
||||
raise HwcModuleException(
|
||||
"can't navigate value from a non-dict object")
|
||||
|
||||
i = index[n]
|
||||
if i not in d:
|
||||
raise HwcModuleException(
|
||||
"navigate value failed: key(%s) is not exist in dict" % i)
|
||||
d = d[i]
|
||||
|
||||
if not array_index:
|
||||
continue
|
||||
|
||||
k = ".".join(index[: (n + 1)])
|
||||
if k not in array_index:
|
||||
continue
|
||||
|
||||
if d is None:
|
||||
return None
|
||||
|
||||
if not isinstance(d, list):
|
||||
raise HwcModuleException(
|
||||
"can't navigate value from a non-list object")
|
||||
|
||||
j = array_index.get(k)
|
||||
if j >= len(d):
|
||||
raise HwcModuleException(
|
||||
"navigate value failed: the index is out of list")
|
||||
d = d[j]
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def build_path(module, path, kv=None):
|
||||
if kv is None:
|
||||
kv = dict()
|
||||
|
||||
v = {}
|
||||
for p in re.findall(r"{[^/]*}", path):
|
||||
n = p[1:][:-1]
|
||||
|
||||
if n in kv:
|
||||
v[n] = str(kv[n])
|
||||
|
||||
else:
|
||||
if n in module.params:
|
||||
v[n] = str(module.params.get(n))
|
||||
else:
|
||||
v[n] = ""
|
||||
|
||||
return path.format(**v)
|
||||
|
||||
|
||||
def get_region(module):
|
||||
if module.params['region']:
|
||||
return module.params['region']
|
||||
|
||||
return module.params['project'].split("_")[0]
|
||||
|
||||
|
||||
def is_empty_value(v):
|
||||
return (not v)
|
||||
|
||||
|
||||
def are_different_dicts(dict1, dict2):
|
||||
return _DictComparison(dict1) != _DictComparison(dict2)
|
||||
94
plugins/module_utils/ibm_sa_utils.py
Normal file
94
plugins/module_utils/ibm_sa_utils.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# Copyright (C) 2018 IBM CORPORATION
|
||||
# Author(s): Tzur Eliyahu <tzure@il.ibm.com>
|
||||
#
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
|
||||
from functools import wraps
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
PYXCLI_INSTALLED = True
|
||||
PYXCLI_IMP_ERR = None
|
||||
try:
|
||||
from pyxcli import client, errors
|
||||
except ImportError:
|
||||
PYXCLI_IMP_ERR = traceback.format_exc()
|
||||
PYXCLI_INSTALLED = False
|
||||
|
||||
AVAILABLE_PYXCLI_FIELDS = ['pool', 'size', 'snapshot_size',
|
||||
'domain', 'perf_class', 'vol',
|
||||
'iscsi_chap_name', 'iscsi_chap_secret',
|
||||
'cluster', 'host', 'lun', 'override',
|
||||
'fcaddress', 'iscsi_name', 'max_dms',
|
||||
'max_cgs', 'ldap_id', 'max_mirrors',
|
||||
'max_pools', 'max_volumes', 'hard_capacity',
|
||||
'soft_capacity']
|
||||
|
||||
|
||||
def xcli_wrapper(func):
|
||||
""" Catch xcli errors and return a proper message"""
|
||||
@wraps(func)
|
||||
def wrapper(module, *args, **kwargs):
|
||||
try:
|
||||
return func(module, *args, **kwargs)
|
||||
except errors.CommandExecutionError as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
return wrapper
|
||||
|
||||
|
||||
@xcli_wrapper
|
||||
def connect_ssl(module):
|
||||
endpoints = module.params['endpoints']
|
||||
username = module.params['username']
|
||||
password = module.params['password']
|
||||
if not (username and password and endpoints):
|
||||
module.fail_json(
|
||||
msg="Username, password or endpoints arguments "
|
||||
"are missing from the module arguments")
|
||||
|
||||
try:
|
||||
return client.XCLIClient.connect_multiendpoint_ssl(username,
|
||||
password,
|
||||
endpoints)
|
||||
except errors.CommandFailedConnectionError as e:
|
||||
module.fail_json(
|
||||
msg="Connection with Spectrum Accelerate system has "
|
||||
"failed: {[0]}.".format(to_native(e)))
|
||||
|
||||
|
||||
def spectrum_accelerate_spec():
|
||||
""" Return arguments spec for AnsibleModule """
|
||||
return dict(
|
||||
endpoints=dict(required=True),
|
||||
username=dict(required=True),
|
||||
password=dict(no_log=True, required=True),
|
||||
)
|
||||
|
||||
|
||||
@xcli_wrapper
|
||||
def execute_pyxcli_command(module, xcli_command, xcli_client):
|
||||
pyxcli_args = build_pyxcli_command(module.params)
|
||||
getattr(xcli_client.cmd, xcli_command)(**(pyxcli_args))
|
||||
return True
|
||||
|
||||
|
||||
def build_pyxcli_command(fields):
|
||||
""" Builds the args for pyxcli using the exact args from ansible"""
|
||||
pyxcli_args = {}
|
||||
for field in fields:
|
||||
if not fields[field]:
|
||||
continue
|
||||
if field in AVAILABLE_PYXCLI_FIELDS and fields[field] != '':
|
||||
pyxcli_args[field] = fields[field]
|
||||
return pyxcli_args
|
||||
|
||||
|
||||
def is_pyxcli_installed(module):
|
||||
if not PYXCLI_INSTALLED:
|
||||
module.fail_json(msg=missing_required_lib('pyxcli'),
|
||||
exception=PYXCLI_IMP_ERR)
|
||||
0
plugins/module_utils/identity/__init__.py
Normal file
0
plugins/module_utils/identity/__init__.py
Normal file
0
plugins/module_utils/identity/keycloak/__init__.py
Normal file
0
plugins/module_utils/identity/keycloak/__init__.py
Normal file
480
plugins/module_utils/identity/keycloak/keycloak.py
Normal file
480
plugins/module_utils/identity/keycloak/keycloak.py
Normal file
@@ -0,0 +1,480 @@
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
from ansible.module_utils.urls import open_url
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
URL_TOKEN = "{url}/realms/{realm}/protocol/openid-connect/token"
|
||||
URL_CLIENT = "{url}/admin/realms/{realm}/clients/{id}"
|
||||
URL_CLIENTS = "{url}/admin/realms/{realm}/clients"
|
||||
URL_CLIENT_ROLES = "{url}/admin/realms/{realm}/clients/{id}/roles"
|
||||
URL_REALM_ROLES = "{url}/admin/realms/{realm}/roles"
|
||||
|
||||
URL_CLIENTTEMPLATE = "{url}/admin/realms/{realm}/client-templates/{id}"
|
||||
URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates"
|
||||
URL_GROUPS = "{url}/admin/realms/{realm}/groups"
|
||||
URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}"
|
||||
|
||||
|
||||
def keycloak_argument_spec():
|
||||
"""
|
||||
Returns argument_spec of options common to keycloak_*-modules
|
||||
|
||||
:return: argument_spec dict
|
||||
"""
|
||||
return dict(
|
||||
auth_keycloak_url=dict(type='str', aliases=['url'], required=True),
|
||||
auth_client_id=dict(type='str', default='admin-cli'),
|
||||
auth_realm=dict(type='str', required=True),
|
||||
auth_client_secret=dict(type='str', default=None),
|
||||
auth_username=dict(type='str', aliases=['username'], required=True),
|
||||
auth_password=dict(type='str', aliases=['password'], required=True, no_log=True),
|
||||
validate_certs=dict(type='bool', default=True)
|
||||
)
|
||||
|
||||
|
||||
def camel(words):
|
||||
return words.split('_')[0] + ''.join(x.capitalize() or '_' for x in words.split('_')[1:])
|
||||
|
||||
|
||||
class KeycloakError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_token(base_url, validate_certs, auth_realm, client_id,
|
||||
auth_username, auth_password, client_secret):
|
||||
auth_url = URL_TOKEN.format(url=base_url, realm=auth_realm)
|
||||
temp_payload = {
|
||||
'grant_type': 'password',
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'username': auth_username,
|
||||
'password': auth_password,
|
||||
}
|
||||
# Remove empty items, for instance missing client_secret
|
||||
payload = dict(
|
||||
(k, v) for k, v in temp_payload.items() if v is not None)
|
||||
try:
|
||||
r = json.loads(to_native(open_url(auth_url, method='POST',
|
||||
validate_certs=validate_certs,
|
||||
data=urlencode(payload)).read()))
|
||||
except ValueError as e:
|
||||
raise KeycloakError(
|
||||
'API returned invalid JSON when trying to obtain access token from %s: %s'
|
||||
% (auth_url, str(e)))
|
||||
except Exception as e:
|
||||
raise KeycloakError('Could not obtain access token from %s: %s'
|
||||
% (auth_url, str(e)))
|
||||
|
||||
try:
|
||||
return {
|
||||
'Authorization': 'Bearer ' + r['access_token'],
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
except KeyError:
|
||||
raise KeycloakError(
|
||||
'Could not obtain access token from %s' % auth_url)
|
||||
|
||||
|
||||
class KeycloakAPI(object):
|
||||
""" Keycloak API access; Keycloak uses OAuth 2.0 to protect its API, an access token for which
|
||||
is obtained through OpenID connect
|
||||
"""
|
||||
def __init__(self, module, connection_header):
|
||||
self.module = module
|
||||
self.baseurl = self.module.params.get('auth_keycloak_url')
|
||||
self.validate_certs = self.module.params.get('validate_certs')
|
||||
self.restheaders = connection_header
|
||||
|
||||
def get_clients(self, realm='master', filter=None):
|
||||
""" Obtains client representations for clients in a realm
|
||||
|
||||
:param realm: realm to be queried
|
||||
:param filter: if defined, only the client with clientId specified in the filter is returned
|
||||
:return: list of dicts of client representations
|
||||
"""
|
||||
clientlist_url = URL_CLIENTS.format(url=self.baseurl, realm=realm)
|
||||
if filter is not None:
|
||||
clientlist_url += '?clientId=%s' % filter
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(clientlist_url, method='GET', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of clients for realm %s: %s'
|
||||
% (realm, str(e)))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not obtain list of clients for realm %s: %s'
|
||||
% (realm, str(e)))
|
||||
|
||||
def get_client_by_clientid(self, client_id, realm='master'):
|
||||
""" Get client representation by clientId
|
||||
:param client_id: The clientId to be queried
|
||||
:param realm: realm from which to obtain the client representation
|
||||
:return: dict with a client representation or None if none matching exist
|
||||
"""
|
||||
r = self.get_clients(realm=realm, filter=client_id)
|
||||
if len(r) > 0:
|
||||
return r[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_client_by_id(self, id, realm='master'):
|
||||
""" Obtain client representation by id
|
||||
|
||||
:param id: id (not clientId) of client to be queried
|
||||
:param realm: client from this realm
|
||||
:return: dict of client representation or None if none matching exist
|
||||
"""
|
||||
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(client_url, method='GET', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
else:
|
||||
self.module.fail_json(msg='Could not obtain client %s for realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain client %s for realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not obtain client %s for realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
|
||||
def get_client_id(self, client_id, realm='master'):
|
||||
""" Obtain id of client by client_id
|
||||
|
||||
:param client_id: client_id of client to be queried
|
||||
:param realm: client template from this realm
|
||||
:return: id of client (usually a UUID)
|
||||
"""
|
||||
result = self.get_client_by_clientid(client_id, realm)
|
||||
if isinstance(result, dict) and 'id' in result:
|
||||
return result['id']
|
||||
else:
|
||||
return None
|
||||
|
||||
def update_client(self, id, clientrep, realm="master"):
|
||||
""" Update an existing client
|
||||
:param id: id (not clientId) of client to be updated in Keycloak
|
||||
:param clientrep: corresponding (partial/full) client representation with updates
|
||||
:param realm: realm the client is in
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(client_url, method='PUT', headers=self.restheaders,
|
||||
data=json.dumps(clientrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update client %s in realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
|
||||
def create_client(self, clientrep, realm="master"):
|
||||
""" Create a client in keycloak
|
||||
:param clientrep: Client representation of client to be created. Must at least contain field clientId
|
||||
:param realm: realm for client to be created
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
client_url = URL_CLIENTS.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return open_url(client_url, method='POST', headers=self.restheaders,
|
||||
data=json.dumps(clientrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create client %s in realm %s: %s'
|
||||
% (clientrep['clientId'], realm, str(e)))
|
||||
|
||||
def delete_client(self, id, realm="master"):
|
||||
""" Delete a client from Keycloak
|
||||
|
||||
:param id: id (not clientId) of client to be deleted
|
||||
:param realm: realm of client to be deleted
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(client_url, method='DELETE', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not delete client %s in realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
|
||||
def get_client_templates(self, realm='master'):
|
||||
""" Obtains client template representations for client templates in a realm
|
||||
|
||||
:param realm: realm to be queried
|
||||
:return: list of dicts of client representations
|
||||
"""
|
||||
url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of client templates for realm %s: %s'
|
||||
% (realm, str(e)))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not obtain list of client templates for realm %s: %s'
|
||||
% (realm, str(e)))
|
||||
|
||||
def get_client_template_by_id(self, id, realm='master'):
|
||||
""" Obtain client template representation by id
|
||||
|
||||
:param id: id (not name) of client template to be queried
|
||||
:param realm: client template from this realm
|
||||
:return: dict of client template representation or None if none matching exist
|
||||
"""
|
||||
url = URL_CLIENTTEMPLATE.format(url=self.baseurl, id=id, realm=realm)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain client templates %s for realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not obtain client template %s for realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
|
||||
def get_client_template_by_name(self, name, realm='master'):
|
||||
""" Obtain client template representation by name
|
||||
|
||||
:param name: name of client template to be queried
|
||||
:param realm: client template from this realm
|
||||
:return: dict of client template representation or None if none matching exist
|
||||
"""
|
||||
result = self.get_client_templates(realm)
|
||||
if isinstance(result, list):
|
||||
result = [x for x in result if x['name'] == name]
|
||||
if len(result) > 0:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
def get_client_template_id(self, name, realm='master'):
|
||||
""" Obtain client template id by name
|
||||
|
||||
:param name: name of client template to be queried
|
||||
:param realm: client template from this realm
|
||||
:return: client template id (usually a UUID)
|
||||
"""
|
||||
result = self.get_client_template_by_name(name, realm)
|
||||
if isinstance(result, dict) and 'id' in result:
|
||||
return result['id']
|
||||
else:
|
||||
return None
|
||||
|
||||
def update_client_template(self, id, clienttrep, realm="master"):
|
||||
""" Update an existing client template
|
||||
:param id: id (not name) of client template to be updated in Keycloak
|
||||
:param clienttrep: corresponding (partial/full) client template representation with updates
|
||||
:param realm: realm the client template is in
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(url, method='PUT', headers=self.restheaders,
|
||||
data=json.dumps(clienttrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update client template %s in realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
|
||||
def create_client_template(self, clienttrep, realm="master"):
|
||||
""" Create a client in keycloak
|
||||
:param clienttrep: Client template representation of client template to be created. Must at least contain field name
|
||||
:param realm: realm for client template to be created in
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return open_url(url, method='POST', headers=self.restheaders,
|
||||
data=json.dumps(clienttrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create client template %s in realm %s: %s'
|
||||
% (clienttrep['clientId'], realm, str(e)))
|
||||
|
||||
def delete_client_template(self, id, realm="master"):
|
||||
""" Delete a client template from Keycloak
|
||||
|
||||
:param id: id (not name) of client to be deleted
|
||||
:param realm: realm of client template to be deleted
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(url, method='DELETE', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not delete client template %s in realm %s: %s'
|
||||
% (id, realm, str(e)))
|
||||
|
||||
def get_groups(self, realm="master"):
|
||||
""" Fetch the name and ID of all groups on the Keycloak server.
|
||||
|
||||
To fetch the full data of the group, make a subsequent call to
|
||||
get_group_by_groupid, passing in the ID of the group you wish to return.
|
||||
|
||||
:param realm: Return the groups of this realm (default "master").
|
||||
"""
|
||||
groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch list of groups in realm %s: %s"
|
||||
% (realm, str(e)))
|
||||
|
||||
def get_group_by_groupid(self, gid, realm="master"):
|
||||
""" Fetch a keycloak group from the provided realm using the group's unique ID.
|
||||
|
||||
If the group does not exist, None is returned.
|
||||
|
||||
gid is a UUID provided by the Keycloak API
|
||||
:param gid: UUID of the group to be returned
|
||||
:param realm: Realm in which the group resides; default 'master'.
|
||||
"""
|
||||
groups_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=gid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
else:
|
||||
self.module.fail_json(msg="Could not fetch group %s in realm %s: %s"
|
||||
% (gid, realm, str(e)))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch group %s in realm %s: %s"
|
||||
% (gid, realm, str(e)))
|
||||
|
||||
def get_group_by_name(self, name, realm="master"):
|
||||
""" Fetch a keycloak group within a realm based on its name.
|
||||
|
||||
The Keycloak API does not allow filtering of the Groups resource by name.
|
||||
As a result, this method first retrieves the entire list of groups - name and ID -
|
||||
then performs a second query to fetch the group.
|
||||
|
||||
If the group does not exist, None is returned.
|
||||
:param name: Name of the group to fetch.
|
||||
:param realm: Realm in which the group resides; default 'master'
|
||||
"""
|
||||
groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
all_groups = self.get_groups(realm=realm)
|
||||
|
||||
for group in all_groups:
|
||||
if group['name'] == name:
|
||||
return self.get_group_by_groupid(group['id'], realm=realm)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch group %s in realm %s: %s"
|
||||
% (name, realm, str(e)))
|
||||
|
||||
def create_group(self, grouprep, realm="master"):
|
||||
""" Create a Keycloak group.
|
||||
|
||||
:param grouprep: a GroupRepresentation of the group to be created. Must contain at minimum the field name.
|
||||
:return: HTTPResponse object on success
|
||||
"""
|
||||
groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return open_url(groups_url, method='POST', headers=self.restheaders,
|
||||
data=json.dumps(grouprep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not create group %s in realm %s: %s"
|
||||
% (grouprep['name'], realm, str(e)))
|
||||
|
||||
def update_group(self, grouprep, realm="master"):
|
||||
""" Update an existing group.
|
||||
|
||||
:param grouprep: A GroupRepresentation of the updated group.
|
||||
:return HTTPResponse object on success
|
||||
"""
|
||||
group_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=grouprep['id'])
|
||||
|
||||
try:
|
||||
return open_url(group_url, method='PUT', headers=self.restheaders,
|
||||
data=json.dumps(grouprep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update group %s in realm %s: %s'
|
||||
% (grouprep['name'], realm, str(e)))
|
||||
|
||||
def delete_group(self, name=None, groupid=None, realm="master"):
|
||||
""" Delete a group. One of name or groupid must be provided.
|
||||
|
||||
Providing the group ID is preferred as it avoids a second lookup to
|
||||
convert a group name to an ID.
|
||||
|
||||
:param name: The name of the group. A lookup will be performed to retrieve the group ID.
|
||||
:param groupid: The ID of the group (preferred to name).
|
||||
:param realm: The realm in which this group resides, default "master".
|
||||
"""
|
||||
|
||||
if groupid is None and name is None:
|
||||
# prefer an exception since this is almost certainly a programming error in the module itself.
|
||||
raise Exception("Unable to delete group - one of group ID or name must be provided.")
|
||||
|
||||
# only lookup the name if groupid isn't provided.
|
||||
# in the case that both are provided, prefer the ID, since it's one
|
||||
# less lookup.
|
||||
if groupid is None and name is not None:
|
||||
for group in self.get_groups(realm=realm):
|
||||
if group['name'] == name:
|
||||
groupid = group['id']
|
||||
break
|
||||
|
||||
# if the group doesn't exist - no problem, nothing to delete.
|
||||
if groupid is None:
|
||||
return None
|
||||
|
||||
# should have a good groupid by here.
|
||||
group_url = URL_GROUP.format(realm=realm, groupid=groupid, url=self.baseurl)
|
||||
try:
|
||||
return open_url(group_url, method='DELETE', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to delete group %s: %s" % (groupid, str(e)))
|
||||
93
plugins/module_utils/infinibox.py
Normal file
93
plugins/module_utils/infinibox.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Gregory Shulov <gregory.shulov@gmail.com>,2016
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
HAS_INFINISDK = True
|
||||
try:
|
||||
from infinisdk import InfiniBox, core
|
||||
except ImportError:
|
||||
HAS_INFINISDK = False
|
||||
|
||||
from functools import wraps
|
||||
from os import environ
|
||||
from os import path
|
||||
|
||||
|
||||
def api_wrapper(func):
|
||||
""" Catch API Errors Decorator"""
|
||||
@wraps(func)
|
||||
def __wrapper(*args, **kwargs):
|
||||
module = args[0]
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except core.exceptions.APICommandException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
except core.exceptions.SystemNotFoundException as e:
|
||||
module.fail_json(msg=e.message)
|
||||
except Exception:
|
||||
raise
|
||||
return __wrapper
|
||||
|
||||
|
||||
@api_wrapper
|
||||
def get_system(module):
|
||||
"""Return System Object or Fail"""
|
||||
box = module.params['system']
|
||||
user = module.params.get('user', None)
|
||||
password = module.params.get('password', None)
|
||||
|
||||
if user and password:
|
||||
system = InfiniBox(box, auth=(user, password))
|
||||
elif environ.get('INFINIBOX_USER') and environ.get('INFINIBOX_PASSWORD'):
|
||||
system = InfiniBox(box, auth=(environ.get('INFINIBOX_USER'), environ.get('INFINIBOX_PASSWORD')))
|
||||
elif path.isfile(path.expanduser('~') + '/.infinidat/infinisdk.ini'):
|
||||
system = InfiniBox(box)
|
||||
else:
|
||||
module.fail_json(msg="You must set INFINIBOX_USER and INFINIBOX_PASSWORD environment variables or set username/password module arguments")
|
||||
|
||||
try:
|
||||
system.login()
|
||||
except Exception:
|
||||
module.fail_json(msg="Infinibox authentication failed. Check your credentials")
|
||||
return system
|
||||
|
||||
|
||||
def infinibox_argument_spec():
|
||||
"""Return standard base dictionary used for the argument_spec argument in AnsibleModule"""
|
||||
|
||||
return dict(
|
||||
system=dict(required=True),
|
||||
user=dict(),
|
||||
password=dict(no_log=True),
|
||||
)
|
||||
|
||||
|
||||
def infinibox_required_together():
|
||||
"""Return the default list used for the required_together argument to AnsibleModule"""
|
||||
return [['user', 'password']]
|
||||
88
plugins/module_utils/influxdb.py
Normal file
88
plugins/module_utils/influxdb.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
import requests.exceptions
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
REQUESTS_IMP_ERR = traceback.format_exc()
|
||||
HAS_REQUESTS = False
|
||||
|
||||
INFLUXDB_IMP_ERR = None
|
||||
try:
|
||||
from influxdb import InfluxDBClient
|
||||
from influxdb import __version__ as influxdb_version
|
||||
from influxdb import exceptions
|
||||
HAS_INFLUXDB = True
|
||||
except ImportError:
|
||||
INFLUXDB_IMP_ERR = traceback.format_exc()
|
||||
HAS_INFLUXDB = False
|
||||
|
||||
|
||||
class InfluxDb():
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.params = self.module.params
|
||||
self.check_lib()
|
||||
self.hostname = self.params['hostname']
|
||||
self.port = self.params['port']
|
||||
self.path = self.params['path']
|
||||
self.username = self.params['username']
|
||||
self.password = self.params['password']
|
||||
self.database_name = self.params.get('database_name')
|
||||
|
||||
def check_lib(self):
|
||||
if not HAS_REQUESTS:
|
||||
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
||||
|
||||
if not HAS_INFLUXDB:
|
||||
self.module.fail_json(msg=missing_required_lib('influxdb'), exception=INFLUXDB_IMP_ERR)
|
||||
|
||||
@staticmethod
|
||||
def influxdb_argument_spec():
|
||||
return dict(
|
||||
hostname=dict(type='str', default='localhost'),
|
||||
port=dict(type='int', default=8086),
|
||||
path=dict(type='str', default=''),
|
||||
username=dict(type='str', default='root', aliases=['login_username']),
|
||||
password=dict(type='str', default='root', no_log=True, aliases=['login_password']),
|
||||
ssl=dict(type='bool', default=False),
|
||||
validate_certs=dict(type='bool', default=True),
|
||||
timeout=dict(type='int'),
|
||||
retries=dict(type='int', default=3),
|
||||
proxies=dict(type='dict', default={}),
|
||||
use_udp=dict(type='bool', default=False),
|
||||
udp_port=dict(type='int', default=4444),
|
||||
)
|
||||
|
||||
def connect_to_influxdb(self):
|
||||
args = dict(
|
||||
host=self.hostname,
|
||||
port=self.port,
|
||||
path=self.path,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
database=self.database_name,
|
||||
ssl=self.params['ssl'],
|
||||
verify_ssl=self.params['validate_certs'],
|
||||
timeout=self.params['timeout'],
|
||||
use_udp=self.params['use_udp'],
|
||||
udp_port=self.params['udp_port'],
|
||||
proxies=self.params['proxies'],
|
||||
)
|
||||
influxdb_api_version = tuple(influxdb_version.split("."))
|
||||
if influxdb_api_version >= ('4', '1', '0'):
|
||||
# retries option is added in version 4.1.0
|
||||
args.update(retries=self.params['retries'])
|
||||
|
||||
return InfluxDBClient(**args)
|
||||
226
plugins/module_utils/ipa.py
Normal file
226
plugins/module_utils/ipa.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c) 2016 Thomas Krahn (@Nosmoht)
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import uuid
|
||||
|
||||
import re
|
||||
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.six import PY3
|
||||
from ansible.module_utils.six.moves.urllib.parse import quote
|
||||
from ansible.module_utils.urls import fetch_url, HAS_GSSAPI
|
||||
from ansible.module_utils.basic import env_fallback, AnsibleFallbackNotFound
|
||||
|
||||
|
||||
def _env_then_dns_fallback(*args, **kwargs):
|
||||
''' Load value from environment or DNS in that order'''
|
||||
try:
|
||||
return env_fallback(*args, **kwargs)
|
||||
except AnsibleFallbackNotFound:
|
||||
# If no host was given, we try to guess it from IPA.
|
||||
# The ipa-ca entry is a standard entry that IPA will have set for
|
||||
# the CA.
|
||||
try:
|
||||
return socket.gethostbyaddr(socket.gethostbyname('ipa-ca'))[0]
|
||||
except Exception:
|
||||
raise AnsibleFallbackNotFound
|
||||
|
||||
|
||||
class IPAClient(object):
|
||||
def __init__(self, module, host, port, protocol):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.protocol = protocol
|
||||
self.module = module
|
||||
self.headers = None
|
||||
self.timeout = module.params.get('ipa_timeout')
|
||||
self.use_gssapi = False
|
||||
|
||||
def get_base_url(self):
|
||||
return '%s://%s/ipa' % (self.protocol, self.host)
|
||||
|
||||
def get_json_url(self):
|
||||
return '%s/session/json' % self.get_base_url()
|
||||
|
||||
def login(self, username, password):
|
||||
if 'KRB5CCNAME' in os.environ and HAS_GSSAPI:
|
||||
self.use_gssapi = True
|
||||
elif 'KRB5_CLIENT_KTNAME' in os.environ and HAS_GSSAPI:
|
||||
ccache = "MEMORY:" + str(uuid.uuid4())
|
||||
os.environ['KRB5CCNAME'] = ccache
|
||||
self.use_gssapi = True
|
||||
else:
|
||||
if not password:
|
||||
if 'KRB5CCNAME' in os.environ or 'KRB5_CLIENT_KTNAME' in os.environ:
|
||||
self.module.warn("In order to use GSSAPI, you need to install 'urllib_gssapi'")
|
||||
self._fail('login', 'Password is required if not using '
|
||||
'GSSAPI. To use GSSAPI, please set the '
|
||||
'KRB5_CLIENT_KTNAME or KRB5CCNAME (or both) '
|
||||
' environment variables.')
|
||||
url = '%s/session/login_password' % self.get_base_url()
|
||||
data = 'user=%s&password=%s' % (quote(username, safe=''), quote(password, safe=''))
|
||||
headers = {'referer': self.get_base_url(),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'text/plain'}
|
||||
try:
|
||||
resp, info = fetch_url(module=self.module, url=url, data=to_bytes(data), headers=headers, timeout=self.timeout)
|
||||
status_code = info['status']
|
||||
if status_code not in [200, 201, 204]:
|
||||
self._fail('login', info['msg'])
|
||||
|
||||
self.headers = {'Cookie': resp.info().get('Set-Cookie')}
|
||||
except Exception as e:
|
||||
self._fail('login', to_native(e))
|
||||
if not self.headers:
|
||||
self.headers = dict()
|
||||
self.headers.update({
|
||||
'referer': self.get_base_url(),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'})
|
||||
|
||||
def _fail(self, msg, e):
|
||||
if 'message' in e:
|
||||
err_string = e.get('message')
|
||||
else:
|
||||
err_string = e
|
||||
self.module.fail_json(msg='%s: %s' % (msg, err_string))
|
||||
|
||||
def get_ipa_version(self):
|
||||
response = self.ping()['summary']
|
||||
ipa_ver_regex = re.compile(r'IPA server version (\d\.\d\.\d).*')
|
||||
version_match = ipa_ver_regex.match(response)
|
||||
ipa_version = None
|
||||
if version_match:
|
||||
ipa_version = version_match.groups()[0]
|
||||
return ipa_version
|
||||
|
||||
def ping(self):
|
||||
return self._post_json(method='ping', name=None)
|
||||
|
||||
def _post_json(self, method, name, item=None):
|
||||
if item is None:
|
||||
item = {}
|
||||
url = '%s/session/json' % self.get_base_url()
|
||||
data = dict(method=method)
|
||||
|
||||
# TODO: We should probably handle this a little better.
|
||||
if method in ('ping', 'config_show'):
|
||||
data['params'] = [[], {}]
|
||||
elif method == 'config_mod':
|
||||
data['params'] = [[], item]
|
||||
else:
|
||||
data['params'] = [[name], item]
|
||||
|
||||
try:
|
||||
resp, info = fetch_url(module=self.module, url=url, data=to_bytes(json.dumps(data)),
|
||||
headers=self.headers, timeout=self.timeout, use_gssapi=self.use_gssapi)
|
||||
status_code = info['status']
|
||||
if status_code not in [200, 201, 204]:
|
||||
self._fail(method, info['msg'])
|
||||
except Exception as e:
|
||||
self._fail('post %s' % method, to_native(e))
|
||||
|
||||
if PY3:
|
||||
charset = resp.headers.get_content_charset('latin-1')
|
||||
else:
|
||||
response_charset = resp.headers.getparam('charset')
|
||||
if response_charset:
|
||||
charset = response_charset
|
||||
else:
|
||||
charset = 'latin-1'
|
||||
resp = json.loads(to_text(resp.read(), encoding=charset), encoding=charset)
|
||||
err = resp.get('error')
|
||||
if err is not None:
|
||||
self._fail('response %s' % method, err)
|
||||
|
||||
if 'result' in resp:
|
||||
result = resp.get('result')
|
||||
if 'result' in result:
|
||||
result = result.get('result')
|
||||
if isinstance(result, list):
|
||||
if len(result) > 0:
|
||||
return result[0]
|
||||
else:
|
||||
return {}
|
||||
return result
|
||||
return None
|
||||
|
||||
def get_diff(self, ipa_data, module_data):
|
||||
result = []
|
||||
for key in module_data.keys():
|
||||
mod_value = module_data.get(key, None)
|
||||
if isinstance(mod_value, list):
|
||||
default = []
|
||||
else:
|
||||
default = None
|
||||
ipa_value = ipa_data.get(key, default)
|
||||
if isinstance(ipa_value, list) and not isinstance(mod_value, list):
|
||||
mod_value = [mod_value]
|
||||
if isinstance(ipa_value, list) and isinstance(mod_value, list):
|
||||
mod_value = sorted(mod_value)
|
||||
ipa_value = sorted(ipa_value)
|
||||
if mod_value != ipa_value:
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
def modify_if_diff(self, name, ipa_list, module_list, add_method, remove_method, item=None):
|
||||
changed = False
|
||||
diff = list(set(ipa_list) - set(module_list))
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not self.module.check_mode:
|
||||
if item:
|
||||
remove_method(name=name, item={item: diff})
|
||||
else:
|
||||
remove_method(name=name, item=diff)
|
||||
|
||||
diff = list(set(module_list) - set(ipa_list))
|
||||
if len(diff) > 0:
|
||||
changed = True
|
||||
if not self.module.check_mode:
|
||||
if item:
|
||||
add_method(name=name, item={item: diff})
|
||||
else:
|
||||
add_method(name=name, item=diff)
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def ipa_argument_spec():
|
||||
return dict(
|
||||
ipa_prot=dict(type='str', default='https', choices=['http', 'https'], fallback=(env_fallback, ['IPA_PROT'])),
|
||||
ipa_host=dict(type='str', default='ipa.example.com', fallback=(_env_then_dns_fallback, ['IPA_HOST'])),
|
||||
ipa_port=dict(type='int', default=443, fallback=(env_fallback, ['IPA_PORT'])),
|
||||
ipa_user=dict(type='str', default='admin', fallback=(env_fallback, ['IPA_USER'])),
|
||||
ipa_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['IPA_PASS'])),
|
||||
ipa_timeout=dict(type='int', default=10, fallback=(env_fallback, ['IPA_TIMEOUT'])),
|
||||
validate_certs=dict(type='bool', default=True),
|
||||
)
|
||||
195
plugins/module_utils/known_hosts.py
Normal file
195
plugins/module_utils/known_hosts.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import hmac
|
||||
import re
|
||||
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
import sha as sha1
|
||||
|
||||
HASHED_KEY_MAGIC = "|1|"
|
||||
|
||||
|
||||
def is_ssh_url(url):
|
||||
|
||||
""" check if url is ssh """
|
||||
|
||||
if "@" in url and "://" not in url:
|
||||
return True
|
||||
for scheme in "ssh://", "git+ssh://", "ssh+git://":
|
||||
if url.startswith(scheme):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_fqdn_and_port(repo_url):
|
||||
|
||||
""" chop the hostname and port out of a url """
|
||||
|
||||
fqdn = None
|
||||
port = None
|
||||
ipv6_re = re.compile(r'(\[[^]]*\])(?::([0-9]+))?')
|
||||
if "@" in repo_url and "://" not in repo_url:
|
||||
# most likely an user@host:path or user@host/path type URL
|
||||
repo_url = repo_url.split("@", 1)[1]
|
||||
match = ipv6_re.match(repo_url)
|
||||
# For this type of URL, colon specifies the path, not the port
|
||||
if match:
|
||||
fqdn, path = match.groups()
|
||||
elif ":" in repo_url:
|
||||
fqdn = repo_url.split(":")[0]
|
||||
elif "/" in repo_url:
|
||||
fqdn = repo_url.split("/")[0]
|
||||
elif "://" in repo_url:
|
||||
# this should be something we can parse with urlparse
|
||||
parts = urlparse(repo_url)
|
||||
# parts[1] will be empty on python2.4 on ssh:// or git:// urls, so
|
||||
# ensure we actually have a parts[1] before continuing.
|
||||
if parts[1] != '':
|
||||
fqdn = parts[1]
|
||||
if "@" in fqdn:
|
||||
fqdn = fqdn.split("@", 1)[1]
|
||||
match = ipv6_re.match(fqdn)
|
||||
if match:
|
||||
fqdn, port = match.groups()
|
||||
elif ":" in fqdn:
|
||||
fqdn, port = fqdn.split(":")[0:2]
|
||||
return fqdn, port
|
||||
|
||||
|
||||
def check_hostkey(module, fqdn):
|
||||
return not not_in_host_file(module, fqdn)
|
||||
|
||||
|
||||
# this is a variant of code found in connection_plugins/paramiko.py and we should modify
|
||||
# the paramiko code to import and use this.
|
||||
|
||||
def not_in_host_file(self, host):
|
||||
|
||||
if 'USER' in os.environ:
|
||||
user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
|
||||
else:
|
||||
user_host_file = "~/.ssh/known_hosts"
|
||||
user_host_file = os.path.expanduser(user_host_file)
|
||||
|
||||
host_file_list = []
|
||||
host_file_list.append(user_host_file)
|
||||
host_file_list.append("/etc/ssh/ssh_known_hosts")
|
||||
host_file_list.append("/etc/ssh/ssh_known_hosts2")
|
||||
host_file_list.append("/etc/openssh/ssh_known_hosts")
|
||||
|
||||
hfiles_not_found = 0
|
||||
for hf in host_file_list:
|
||||
if not os.path.exists(hf):
|
||||
hfiles_not_found += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
host_fh = open(hf)
|
||||
except IOError:
|
||||
hfiles_not_found += 1
|
||||
continue
|
||||
else:
|
||||
data = host_fh.read()
|
||||
host_fh.close()
|
||||
|
||||
for line in data.split("\n"):
|
||||
if line is None or " " not in line:
|
||||
continue
|
||||
tokens = line.split()
|
||||
if tokens[0].find(HASHED_KEY_MAGIC) == 0:
|
||||
# this is a hashed known host entry
|
||||
try:
|
||||
(kn_salt, kn_host) = tokens[0][len(HASHED_KEY_MAGIC):].split("|", 2)
|
||||
hash = hmac.new(kn_salt.decode('base64'), digestmod=sha1)
|
||||
hash.update(host)
|
||||
if hash.digest() == kn_host.decode('base64'):
|
||||
return False
|
||||
except Exception:
|
||||
# invalid hashed host key, skip it
|
||||
continue
|
||||
else:
|
||||
# standard host file entry
|
||||
if host in tokens[0]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def add_host_key(module, fqdn, port=22, key_type="rsa", create_dir=False):
|
||||
|
||||
""" use ssh-keyscan to add the hostkey """
|
||||
|
||||
keyscan_cmd = module.get_bin_path('ssh-keyscan', True)
|
||||
|
||||
if 'USER' in os.environ:
|
||||
user_ssh_dir = os.path.expandvars("~${USER}/.ssh/")
|
||||
user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
|
||||
else:
|
||||
user_ssh_dir = "~/.ssh/"
|
||||
user_host_file = "~/.ssh/known_hosts"
|
||||
user_ssh_dir = os.path.expanduser(user_ssh_dir)
|
||||
|
||||
if not os.path.exists(user_ssh_dir):
|
||||
if create_dir:
|
||||
try:
|
||||
os.makedirs(user_ssh_dir, int('700', 8))
|
||||
except Exception:
|
||||
module.fail_json(msg="failed to create host key directory: %s" % user_ssh_dir)
|
||||
else:
|
||||
module.fail_json(msg="%s does not exist" % user_ssh_dir)
|
||||
elif not os.path.isdir(user_ssh_dir):
|
||||
module.fail_json(msg="%s is not a directory" % user_ssh_dir)
|
||||
|
||||
if port:
|
||||
this_cmd = "%s -t %s -p %s %s" % (keyscan_cmd, key_type, port, fqdn)
|
||||
else:
|
||||
this_cmd = "%s -t %s %s" % (keyscan_cmd, key_type, fqdn)
|
||||
|
||||
rc, out, err = module.run_command(this_cmd)
|
||||
# ssh-keyscan gives a 0 exit code and prints nothing on timeout
|
||||
if rc != 0 or not out:
|
||||
msg = 'failed to retrieve hostkey'
|
||||
if not out:
|
||||
msg += '. "%s" returned no matches.' % this_cmd
|
||||
else:
|
||||
msg += ' using command "%s". [stdout]: %s' % (this_cmd, out)
|
||||
|
||||
if err:
|
||||
msg += ' [stderr]: %s' % err
|
||||
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
module.append_to_file(user_host_file, out)
|
||||
|
||||
return rc, out, err
|
||||
462
plugins/module_utils/kubevirt.py
Normal file
462
plugins/module_utils/kubevirt.py
Normal file
@@ -0,0 +1,462 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
# Copyright (c) 2018, KubeVirt Team <@kubevirt>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from collections import defaultdict
|
||||
from distutils.version import Version
|
||||
|
||||
from ansible.module_utils.common import dict_transformations
|
||||
from ansible.module_utils.common._collections_compat import Sequence
|
||||
from ansible_collections.community.kubernetes.plugins.module_utils.k8s.common import list_dict_str
|
||||
from ansible_collections.community.kubernetes.plugins.module_utils.k8s.raw import KubernetesRawModule
|
||||
|
||||
import copy
|
||||
import re
|
||||
|
||||
MAX_SUPPORTED_API_VERSION = 'v1alpha3'
|
||||
API_GROUP = 'kubevirt.io'
|
||||
|
||||
|
||||
# Put all args that (can) modify 'spec:' here:
|
||||
VM_SPEC_DEF_ARG_SPEC = {
|
||||
'resource_definition': {
|
||||
'type': 'dict',
|
||||
'aliases': ['definition', 'inline']
|
||||
},
|
||||
'memory': {'type': 'str'},
|
||||
'memory_limit': {'type': 'str'},
|
||||
'cpu_cores': {'type': 'int'},
|
||||
'disks': {'type': 'list'},
|
||||
'labels': {'type': 'dict'},
|
||||
'interfaces': {'type': 'list'},
|
||||
'machine_type': {'type': 'str'},
|
||||
'cloud_init_nocloud': {'type': 'dict'},
|
||||
'bootloader': {'type': 'str'},
|
||||
'smbios_uuid': {'type': 'str'},
|
||||
'cpu_model': {'type': 'str'},
|
||||
'headless': {'type': 'str'},
|
||||
'hugepage_size': {'type': 'str'},
|
||||
'tablets': {'type': 'list'},
|
||||
'cpu_limit': {'type': 'int'},
|
||||
'cpu_shares': {'type': 'int'},
|
||||
'cpu_features': {'type': 'list'},
|
||||
'affinity': {'type': 'dict'},
|
||||
'anti_affinity': {'type': 'dict'},
|
||||
'node_affinity': {'type': 'dict'},
|
||||
}
|
||||
# And other common args go here:
|
||||
VM_COMMON_ARG_SPEC = {
|
||||
'name': {'required': True},
|
||||
'namespace': {'required': True},
|
||||
'hostname': {'type': 'str'},
|
||||
'subdomain': {'type': 'str'},
|
||||
'state': {
|
||||
'default': 'present',
|
||||
'choices': ['present', 'absent'],
|
||||
},
|
||||
'force': {
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
'merge_type': {'type': 'list', 'choices': ['json', 'merge', 'strategic-merge']},
|
||||
'wait': {'type': 'bool', 'default': True},
|
||||
'wait_timeout': {'type': 'int', 'default': 120},
|
||||
'wait_sleep': {'type': 'int', 'default': 5},
|
||||
}
|
||||
VM_COMMON_ARG_SPEC.update(VM_SPEC_DEF_ARG_SPEC)
|
||||
|
||||
|
||||
def virtdict():
|
||||
"""
|
||||
This function create dictionary, with defaults to dictionary.
|
||||
"""
|
||||
return defaultdict(virtdict)
|
||||
|
||||
|
||||
class KubeAPIVersion(Version):
|
||||
component_re = re.compile(r'(\d+ | [a-z]+)', re.VERBOSE)
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def parse(self, vstring):
|
||||
self.vstring = vstring
|
||||
components = [x for x in self.component_re.split(vstring) if x]
|
||||
for i, obj in enumerate(components):
|
||||
try:
|
||||
components[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
errmsg = "version '{0}' does not conform to kubernetes api versioning guidelines".format(vstring)
|
||||
c = components
|
||||
|
||||
if len(c) not in (2, 4) or c[0] != 'v' or not isinstance(c[1], int):
|
||||
raise ValueError(errmsg)
|
||||
if len(c) == 4 and (c[2] not in ('alpha', 'beta') or not isinstance(c[3], int)):
|
||||
raise ValueError(errmsg)
|
||||
|
||||
self.version = components
|
||||
|
||||
def __str__(self):
|
||||
return self.vstring
|
||||
|
||||
def __repr__(self):
|
||||
return "KubeAPIVersion ('{0}')".format(str(self))
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = KubeAPIVersion(other)
|
||||
|
||||
myver = self.version
|
||||
otherver = other.version
|
||||
|
||||
for ver in myver, otherver:
|
||||
if len(ver) == 2:
|
||||
ver.extend(['zeta', 9999])
|
||||
|
||||
if myver == otherver:
|
||||
return 0
|
||||
if myver < otherver:
|
||||
return -1
|
||||
if myver > otherver:
|
||||
return 1
|
||||
|
||||
# python2 compatibility
|
||||
def __cmp__(self, other):
|
||||
return self._cmp(other)
|
||||
|
||||
|
||||
class KubeVirtRawModule(KubernetesRawModule):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(KubeVirtRawModule, self).__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def merge_dicts(base_dict, merging_dicts):
|
||||
"""This function merges a base dictionary with one or more other dictionaries.
|
||||
The base dictionary takes precedence when there is a key collision.
|
||||
merging_dicts can be a dict or a list or tuple of dicts. In the latter case, the
|
||||
dictionaries at the front of the list have higher precedence over the ones at the end.
|
||||
"""
|
||||
if not merging_dicts:
|
||||
merging_dicts = ({},)
|
||||
|
||||
if not isinstance(merging_dicts, Sequence):
|
||||
merging_dicts = (merging_dicts,)
|
||||
|
||||
new_dict = {}
|
||||
for d in reversed(merging_dicts):
|
||||
new_dict = dict_transformations.dict_merge(new_dict, d)
|
||||
|
||||
new_dict = dict_transformations.dict_merge(new_dict, base_dict)
|
||||
|
||||
return new_dict
|
||||
|
||||
def get_resource(self, resource):
|
||||
try:
|
||||
existing = resource.get(name=self.name, namespace=self.namespace)
|
||||
except Exception:
|
||||
existing = None
|
||||
|
||||
return existing
|
||||
|
||||
def _define_datavolumes(self, datavolumes, spec):
|
||||
"""
|
||||
Takes datavoulmes parameter of Ansible and create kubevirt API datavolumesTemplateSpec
|
||||
structure from it
|
||||
"""
|
||||
if not datavolumes:
|
||||
return
|
||||
|
||||
spec['dataVolumeTemplates'] = []
|
||||
for dv in datavolumes:
|
||||
# Add datavolume to datavolumetemplates spec:
|
||||
dvt = virtdict()
|
||||
dvt['metadata']['name'] = dv.get('name')
|
||||
dvt['spec']['pvc'] = {
|
||||
'accessModes': dv.get('pvc').get('accessModes'),
|
||||
'resources': {
|
||||
'requests': {
|
||||
'storage': dv.get('pvc').get('storage'),
|
||||
}
|
||||
}
|
||||
}
|
||||
dvt['spec']['source'] = dv.get('source')
|
||||
spec['dataVolumeTemplates'].append(dvt)
|
||||
|
||||
# Add datavolume to disks spec:
|
||||
if not spec['template']['spec']['domain']['devices']['disks']:
|
||||
spec['template']['spec']['domain']['devices']['disks'] = []
|
||||
|
||||
spec['template']['spec']['domain']['devices']['disks'].append(
|
||||
{
|
||||
'name': dv.get('name'),
|
||||
'disk': dv.get('disk', {'bus': 'virtio'}),
|
||||
}
|
||||
)
|
||||
|
||||
# Add datavolume to volumes spec:
|
||||
if not spec['template']['spec']['volumes']:
|
||||
spec['template']['spec']['volumes'] = []
|
||||
|
||||
spec['template']['spec']['volumes'].append(
|
||||
{
|
||||
'dataVolume': {
|
||||
'name': dv.get('name')
|
||||
},
|
||||
'name': dv.get('name'),
|
||||
}
|
||||
)
|
||||
|
||||
def _define_cloud_init(self, cloud_init_nocloud, template_spec):
|
||||
"""
|
||||
Takes the user's cloud_init_nocloud parameter and fill it in kubevirt
|
||||
API strucuture. The name for disk is hardcoded to ansiblecloudinitdisk.
|
||||
"""
|
||||
if cloud_init_nocloud:
|
||||
if not template_spec['volumes']:
|
||||
template_spec['volumes'] = []
|
||||
if not template_spec['domain']['devices']['disks']:
|
||||
template_spec['domain']['devices']['disks'] = []
|
||||
|
||||
template_spec['volumes'].append({'name': 'ansiblecloudinitdisk', 'cloudInitNoCloud': cloud_init_nocloud})
|
||||
template_spec['domain']['devices']['disks'].append({
|
||||
'name': 'ansiblecloudinitdisk',
|
||||
'disk': {'bus': 'virtio'},
|
||||
})
|
||||
|
||||
def _define_interfaces(self, interfaces, template_spec, defaults):
|
||||
"""
|
||||
Takes interfaces parameter of Ansible and create kubevirt API interfaces
|
||||
and networks strucutre out from it.
|
||||
"""
|
||||
if not interfaces and defaults and 'interfaces' in defaults:
|
||||
interfaces = copy.deepcopy(defaults['interfaces'])
|
||||
for d in interfaces:
|
||||
d['network'] = defaults['networks'][0]
|
||||
|
||||
if interfaces:
|
||||
# Extract interfaces k8s specification from interfaces list passed to Ansible:
|
||||
spec_interfaces = []
|
||||
for i in interfaces:
|
||||
spec_interfaces.append(
|
||||
self.merge_dicts(dict((k, v) for k, v in i.items() if k != 'network'), defaults['interfaces'])
|
||||
)
|
||||
if 'interfaces' not in template_spec['domain']['devices']:
|
||||
template_spec['domain']['devices']['interfaces'] = []
|
||||
template_spec['domain']['devices']['interfaces'].extend(spec_interfaces)
|
||||
|
||||
# Extract networks k8s specification from interfaces list passed to Ansible:
|
||||
spec_networks = []
|
||||
for i in interfaces:
|
||||
net = i['network']
|
||||
net['name'] = i['name']
|
||||
spec_networks.append(self.merge_dicts(net, defaults['networks']))
|
||||
if 'networks' not in template_spec:
|
||||
template_spec['networks'] = []
|
||||
template_spec['networks'].extend(spec_networks)
|
||||
|
||||
def _define_disks(self, disks, template_spec, defaults):
|
||||
"""
|
||||
Takes disks parameter of Ansible and create kubevirt API disks and
|
||||
volumes strucutre out from it.
|
||||
"""
|
||||
if not disks and defaults and 'disks' in defaults:
|
||||
disks = copy.deepcopy(defaults['disks'])
|
||||
for d in disks:
|
||||
d['volume'] = defaults['volumes'][0]
|
||||
|
||||
if disks:
|
||||
# Extract k8s specification from disks list passed to Ansible:
|
||||
spec_disks = []
|
||||
for d in disks:
|
||||
spec_disks.append(
|
||||
self.merge_dicts(dict((k, v) for k, v in d.items() if k != 'volume'), defaults['disks'])
|
||||
)
|
||||
if 'disks' not in template_spec['domain']['devices']:
|
||||
template_spec['domain']['devices']['disks'] = []
|
||||
template_spec['domain']['devices']['disks'].extend(spec_disks)
|
||||
|
||||
# Extract volumes k8s specification from disks list passed to Ansible:
|
||||
spec_volumes = []
|
||||
for d in disks:
|
||||
volume = d['volume']
|
||||
volume['name'] = d['name']
|
||||
spec_volumes.append(self.merge_dicts(volume, defaults['volumes']))
|
||||
if 'volumes' not in template_spec:
|
||||
template_spec['volumes'] = []
|
||||
template_spec['volumes'].extend(spec_volumes)
|
||||
|
||||
def find_supported_resource(self, kind):
|
||||
results = self.client.resources.search(kind=kind, group=API_GROUP)
|
||||
if not results:
|
||||
self.fail('Failed to find resource {0} in {1}'.format(kind, API_GROUP))
|
||||
sr = sorted(results, key=lambda r: KubeAPIVersion(r.api_version), reverse=True)
|
||||
for r in sr:
|
||||
if KubeAPIVersion(r.api_version) <= KubeAPIVersion(MAX_SUPPORTED_API_VERSION):
|
||||
return r
|
||||
self.fail("API versions {0} are too recent. Max supported is {1}/{2}.".format(
|
||||
str([r.api_version for r in sr]), API_GROUP, MAX_SUPPORTED_API_VERSION))
|
||||
|
||||
def _construct_vm_definition(self, kind, definition, template, params, defaults=None):
|
||||
self.client = self.get_api_client()
|
||||
|
||||
disks = params.get('disks', [])
|
||||
memory = params.get('memory')
|
||||
memory_limit = params.get('memory_limit')
|
||||
cpu_cores = params.get('cpu_cores')
|
||||
cpu_model = params.get('cpu_model')
|
||||
cpu_features = params.get('cpu_features')
|
||||
labels = params.get('labels')
|
||||
datavolumes = params.get('datavolumes')
|
||||
interfaces = params.get('interfaces')
|
||||
bootloader = params.get('bootloader')
|
||||
cloud_init_nocloud = params.get('cloud_init_nocloud')
|
||||
machine_type = params.get('machine_type')
|
||||
headless = params.get('headless')
|
||||
smbios_uuid = params.get('smbios_uuid')
|
||||
hugepage_size = params.get('hugepage_size')
|
||||
tablets = params.get('tablets')
|
||||
cpu_shares = params.get('cpu_shares')
|
||||
cpu_limit = params.get('cpu_limit')
|
||||
node_affinity = params.get('node_affinity')
|
||||
vm_affinity = params.get('affinity')
|
||||
vm_anti_affinity = params.get('anti_affinity')
|
||||
hostname = params.get('hostname')
|
||||
subdomain = params.get('subdomain')
|
||||
template_spec = template['spec']
|
||||
|
||||
# Merge additional flat parameters:
|
||||
if memory:
|
||||
template_spec['domain']['resources']['requests']['memory'] = memory
|
||||
|
||||
if cpu_shares:
|
||||
template_spec['domain']['resources']['requests']['cpu'] = cpu_shares
|
||||
|
||||
if cpu_limit:
|
||||
template_spec['domain']['resources']['limits']['cpu'] = cpu_limit
|
||||
|
||||
if tablets:
|
||||
for tablet in tablets:
|
||||
tablet['type'] = 'tablet'
|
||||
template_spec['domain']['devices']['inputs'] = tablets
|
||||
|
||||
if memory_limit:
|
||||
template_spec['domain']['resources']['limits']['memory'] = memory_limit
|
||||
|
||||
if hugepage_size is not None:
|
||||
template_spec['domain']['memory']['hugepages']['pageSize'] = hugepage_size
|
||||
|
||||
if cpu_features is not None:
|
||||
template_spec['domain']['cpu']['features'] = cpu_features
|
||||
|
||||
if cpu_cores is not None:
|
||||
template_spec['domain']['cpu']['cores'] = cpu_cores
|
||||
|
||||
if cpu_model:
|
||||
template_spec['domain']['cpu']['model'] = cpu_model
|
||||
|
||||
if labels:
|
||||
template['metadata']['labels'] = self.merge_dicts(labels, template['metadata']['labels'])
|
||||
|
||||
if machine_type:
|
||||
template_spec['domain']['machine']['type'] = machine_type
|
||||
|
||||
if bootloader:
|
||||
template_spec['domain']['firmware']['bootloader'] = {bootloader: {}}
|
||||
|
||||
if smbios_uuid:
|
||||
template_spec['domain']['firmware']['uuid'] = smbios_uuid
|
||||
|
||||
if headless is not None:
|
||||
template_spec['domain']['devices']['autoattachGraphicsDevice'] = not headless
|
||||
|
||||
if vm_affinity or vm_anti_affinity:
|
||||
vms_affinity = vm_affinity or vm_anti_affinity
|
||||
affinity_name = 'podAffinity' if vm_affinity else 'podAntiAffinity'
|
||||
for affinity in vms_affinity.get('soft', []):
|
||||
if not template_spec['affinity'][affinity_name]['preferredDuringSchedulingIgnoredDuringExecution']:
|
||||
template_spec['affinity'][affinity_name]['preferredDuringSchedulingIgnoredDuringExecution'] = []
|
||||
template_spec['affinity'][affinity_name]['preferredDuringSchedulingIgnoredDuringExecution'].append({
|
||||
'weight': affinity.get('weight'),
|
||||
'podAffinityTerm': {
|
||||
'labelSelector': {
|
||||
'matchExpressions': affinity.get('term').get('match_expressions'),
|
||||
},
|
||||
'topologyKey': affinity.get('topology_key'),
|
||||
},
|
||||
})
|
||||
for affinity in vms_affinity.get('hard', []):
|
||||
if not template_spec['affinity'][affinity_name]['requiredDuringSchedulingIgnoredDuringExecution']:
|
||||
template_spec['affinity'][affinity_name]['requiredDuringSchedulingIgnoredDuringExecution'] = []
|
||||
template_spec['affinity'][affinity_name]['requiredDuringSchedulingIgnoredDuringExecution'].append({
|
||||
'labelSelector': {
|
||||
'matchExpressions': affinity.get('term').get('match_expressions'),
|
||||
},
|
||||
'topologyKey': affinity.get('topology_key'),
|
||||
})
|
||||
|
||||
if node_affinity:
|
||||
for affinity in node_affinity.get('soft', []):
|
||||
if not template_spec['affinity']['nodeAffinity']['preferredDuringSchedulingIgnoredDuringExecution']:
|
||||
template_spec['affinity']['nodeAffinity']['preferredDuringSchedulingIgnoredDuringExecution'] = []
|
||||
template_spec['affinity']['nodeAffinity']['preferredDuringSchedulingIgnoredDuringExecution'].append({
|
||||
'weight': affinity.get('weight'),
|
||||
'preference': {
|
||||
'matchExpressions': affinity.get('term').get('match_expressions'),
|
||||
}
|
||||
})
|
||||
for affinity in node_affinity.get('hard', []):
|
||||
if not template_spec['affinity']['nodeAffinity']['requiredDuringSchedulingIgnoredDuringExecution']['nodeSelectorTerms']:
|
||||
template_spec['affinity']['nodeAffinity']['requiredDuringSchedulingIgnoredDuringExecution']['nodeSelectorTerms'] = []
|
||||
template_spec['affinity']['nodeAffinity']['requiredDuringSchedulingIgnoredDuringExecution']['nodeSelectorTerms'].append({
|
||||
'matchExpressions': affinity.get('term').get('match_expressions'),
|
||||
})
|
||||
|
||||
if hostname:
|
||||
template_spec['hostname'] = hostname
|
||||
|
||||
if subdomain:
|
||||
template_spec['subdomain'] = subdomain
|
||||
|
||||
# Define disks
|
||||
self._define_disks(disks, template_spec, defaults)
|
||||
|
||||
# Define cloud init disk if defined:
|
||||
# Note, that this must be called after _define_disks, so the cloud_init
|
||||
# is not first in order and it's not used as boot disk:
|
||||
self._define_cloud_init(cloud_init_nocloud, template_spec)
|
||||
|
||||
# Define interfaces:
|
||||
self._define_interfaces(interfaces, template_spec, defaults)
|
||||
|
||||
# Define datavolumes:
|
||||
self._define_datavolumes(datavolumes, definition['spec'])
|
||||
|
||||
return self.merge_dicts(definition, self.resource_definitions[0])
|
||||
|
||||
def construct_vm_definition(self, kind, definition, template, defaults=None):
|
||||
definition = self._construct_vm_definition(kind, definition, template, self.params, defaults)
|
||||
resource = self.find_supported_resource(kind)
|
||||
definition = self.set_defaults(resource, definition)
|
||||
return resource, definition
|
||||
|
||||
def construct_vm_template_definition(self, kind, definition, template, params):
|
||||
definition = self._construct_vm_definition(kind, definition, template, params)
|
||||
resource = self.find_resource(kind, definition['apiVersion'], fail=True)
|
||||
|
||||
# Set defaults:
|
||||
definition['kind'] = kind
|
||||
definition['metadata']['name'] = params.get('name')
|
||||
definition['metadata']['namespace'] = params.get('namespace')
|
||||
|
||||
return resource, definition
|
||||
|
||||
def execute_crud(self, kind, definition):
|
||||
""" Module execution """
|
||||
resource = self.find_supported_resource(kind)
|
||||
definition = self.set_defaults(resource, definition)
|
||||
return self.perform_action(resource, definition)
|
||||
78
plugins/module_utils/ldap.py
Normal file
78
plugins/module_utils/ldap.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016, Peter Sagerson <psagers@ignorare.net>
|
||||
# Copyright: (c) 2016, Jiri Tyr <jiri.tyr@gmail.com>
|
||||
# Copyright: (c) 2017-2018 Keller Fuchs (@KellerFuchs) <kellerfuchs@hashbang.sh>
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
|
||||
HAS_LDAP = True
|
||||
except ImportError:
|
||||
HAS_LDAP = False
|
||||
|
||||
|
||||
def gen_specs(**specs):
|
||||
specs.update({
|
||||
'bind_dn': dict(),
|
||||
'bind_pw': dict(default='', no_log=True),
|
||||
'dn': dict(required=True),
|
||||
'server_uri': dict(default='ldapi:///'),
|
||||
'start_tls': dict(default=False, type='bool'),
|
||||
'validate_certs': dict(default=True, type='bool'),
|
||||
})
|
||||
|
||||
return specs
|
||||
|
||||
|
||||
class LdapGeneric(object):
|
||||
def __init__(self, module):
|
||||
# Shortcuts
|
||||
self.module = module
|
||||
self.bind_dn = self.module.params['bind_dn']
|
||||
self.bind_pw = self.module.params['bind_pw']
|
||||
self.dn = self.module.params['dn']
|
||||
self.server_uri = self.module.params['server_uri']
|
||||
self.start_tls = self.module.params['start_tls']
|
||||
self.verify_cert = self.module.params['validate_certs']
|
||||
|
||||
# Establish connection
|
||||
self.connection = self._connect_to_ldap()
|
||||
|
||||
def fail(self, msg, exn):
|
||||
self.module.fail_json(
|
||||
msg=msg,
|
||||
details=to_native(exn),
|
||||
exception=traceback.format_exc()
|
||||
)
|
||||
|
||||
def _connect_to_ldap(self):
|
||||
if not self.verify_cert:
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
|
||||
connection = ldap.initialize(self.server_uri)
|
||||
|
||||
if self.start_tls:
|
||||
try:
|
||||
connection.start_tls_s()
|
||||
except ldap.LDAPError as e:
|
||||
self.fail("Cannot start TLS.", e)
|
||||
|
||||
try:
|
||||
if self.bind_dn is not None:
|
||||
connection.simple_bind_s(self.bind_dn, self.bind_pw)
|
||||
else:
|
||||
connection.sasl_interactive_bind_s('', ldap.sasl.external())
|
||||
except ldap.LDAPError as e:
|
||||
self.fail("Cannot bind to the server.", e)
|
||||
|
||||
return connection
|
||||
37
plugins/module_utils/linode.py
Normal file
37
plugins/module_utils/linode.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Luke Murphy @decentral1se
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
def get_user_agent(module):
|
||||
"""Retrieve a user-agent to send with LinodeClient requests."""
|
||||
try:
|
||||
from ansible.module_utils.ansible_release import __version__ as ansible_version
|
||||
except ImportError:
|
||||
ansible_version = 'unknown'
|
||||
return 'Ansible-%s/%s' % (module, ansible_version)
|
||||
142
plugins/module_utils/lxd.py
Normal file
142
plugins/module_utils/lxd.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016, Hiroaki Nakamura <hnakamur@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from ansible.module_utils.urls import generic_urlparse
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
from ansible.module_utils.six.moves import http_client
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
# httplib/http.client connection using unix domain socket
|
||||
HTTPConnection = http_client.HTTPConnection
|
||||
HTTPSConnection = http_client.HTTPSConnection
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class UnixHTTPConnection(HTTPConnection):
|
||||
def __init__(self, path):
|
||||
HTTPConnection.__init__(self, 'localhost')
|
||||
self.path = path
|
||||
|
||||
def connect(self):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(self.path)
|
||||
self.sock = sock
|
||||
|
||||
|
||||
class LXDClientException(Exception):
|
||||
def __init__(self, msg, **kwargs):
|
||||
self.msg = msg
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
class LXDClient(object):
|
||||
def __init__(self, url, key_file=None, cert_file=None, debug=False):
|
||||
"""LXD Client.
|
||||
|
||||
:param url: The URL of the LXD server. (e.g. unix:/var/lib/lxd/unix.socket or https://127.0.0.1)
|
||||
:type url: ``str``
|
||||
:param key_file: The path of the client certificate key file.
|
||||
:type key_file: ``str``
|
||||
:param cert_file: The path of the client certificate file.
|
||||
:type cert_file: ``str``
|
||||
:param debug: The debug flag. The request and response are stored in logs when debug is true.
|
||||
:type debug: ``bool``
|
||||
"""
|
||||
self.url = url
|
||||
self.debug = debug
|
||||
self.logs = []
|
||||
if url.startswith('https:'):
|
||||
self.cert_file = cert_file
|
||||
self.key_file = key_file
|
||||
parts = generic_urlparse(urlparse(self.url))
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ctx.load_cert_chain(cert_file, keyfile=key_file)
|
||||
self.connection = HTTPSConnection(parts.get('netloc'), context=ctx)
|
||||
elif url.startswith('unix:'):
|
||||
unix_socket_path = url[len('unix:'):]
|
||||
self.connection = UnixHTTPConnection(unix_socket_path)
|
||||
else:
|
||||
raise LXDClientException('URL scheme must be unix: or https:')
|
||||
|
||||
def do(self, method, url, body_json=None, ok_error_codes=None, timeout=None):
|
||||
resp_json = self._send_request(method, url, body_json=body_json, ok_error_codes=ok_error_codes, timeout=timeout)
|
||||
if resp_json['type'] == 'async':
|
||||
url = '{0}/wait'.format(resp_json['operation'])
|
||||
resp_json = self._send_request('GET', url)
|
||||
if resp_json['metadata']['status'] != 'Success':
|
||||
self._raise_err_from_json(resp_json)
|
||||
return resp_json
|
||||
|
||||
def authenticate(self, trust_password):
|
||||
body_json = {'type': 'client', 'password': trust_password}
|
||||
return self._send_request('POST', '/1.0/certificates', body_json=body_json)
|
||||
|
||||
def _send_request(self, method, url, body_json=None, ok_error_codes=None, timeout=None):
|
||||
try:
|
||||
body = json.dumps(body_json)
|
||||
self.connection.request(method, url, body=body)
|
||||
resp = self.connection.getresponse()
|
||||
resp_data = resp.read()
|
||||
resp_data = to_text(resp_data, errors='surrogate_or_strict')
|
||||
resp_json = json.loads(resp_data)
|
||||
self.logs.append({
|
||||
'type': 'sent request',
|
||||
'request': {'method': method, 'url': url, 'json': body_json, 'timeout': timeout},
|
||||
'response': {'json': resp_json}
|
||||
})
|
||||
resp_type = resp_json.get('type', None)
|
||||
if resp_type == 'error':
|
||||
if ok_error_codes is not None and resp_json['error_code'] in ok_error_codes:
|
||||
return resp_json
|
||||
if resp_json['error'] == "Certificate already in trust store":
|
||||
return resp_json
|
||||
self._raise_err_from_json(resp_json)
|
||||
return resp_json
|
||||
except socket.error as e:
|
||||
raise LXDClientException('cannot connect to the LXD server', err=e)
|
||||
|
||||
def _raise_err_from_json(self, resp_json):
|
||||
err_params = {}
|
||||
if self.debug:
|
||||
err_params['logs'] = self.logs
|
||||
raise LXDClientException(self._get_err_from_resp_json(resp_json), **err_params)
|
||||
|
||||
@staticmethod
|
||||
def _get_err_from_resp_json(resp_json):
|
||||
err = None
|
||||
metadata = resp_json.get('metadata', None)
|
||||
if metadata is not None:
|
||||
err = metadata.get('err', None)
|
||||
if err is None:
|
||||
err = resp_json.get('error', None)
|
||||
return err
|
||||
170
plugins/module_utils/manageiq.py
Normal file
170
plugins/module_utils/manageiq.py
Normal file
@@ -0,0 +1,170 @@
|
||||
#
|
||||
# Copyright (c) 2017, Daniel Korn <korndaniel1@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
CLIENT_IMP_ERR = None
|
||||
try:
|
||||
from manageiq_client.api import ManageIQClient
|
||||
HAS_CLIENT = True
|
||||
except ImportError:
|
||||
CLIENT_IMP_ERR = traceback.format_exc()
|
||||
HAS_CLIENT = False
|
||||
|
||||
|
||||
def manageiq_argument_spec():
|
||||
options = dict(
|
||||
url=dict(default=os.environ.get('MIQ_URL', None)),
|
||||
username=dict(default=os.environ.get('MIQ_USERNAME', None)),
|
||||
password=dict(default=os.environ.get('MIQ_PASSWORD', None), no_log=True),
|
||||
token=dict(default=os.environ.get('MIQ_TOKEN', None), no_log=True),
|
||||
validate_certs=dict(default=True, type='bool', aliases=['verify_ssl']),
|
||||
ca_cert=dict(required=False, default=None, aliases=['ca_bundle_path']),
|
||||
)
|
||||
|
||||
return dict(
|
||||
manageiq_connection=dict(type='dict',
|
||||
apply_defaults=True,
|
||||
options=options),
|
||||
)
|
||||
|
||||
|
||||
def check_client(module):
|
||||
if not HAS_CLIENT:
|
||||
module.fail_json(msg=missing_required_lib('manageiq-client'), exception=CLIENT_IMP_ERR)
|
||||
|
||||
|
||||
def validate_connection_params(module):
|
||||
params = module.params['manageiq_connection']
|
||||
error_str = "missing required argument: manageiq_connection[{}]"
|
||||
url = params['url']
|
||||
token = params['token']
|
||||
username = params['username']
|
||||
password = params['password']
|
||||
|
||||
if (url and username and password) or (url and token):
|
||||
return params
|
||||
for arg in ['url', 'username', 'password']:
|
||||
if params[arg] in (None, ''):
|
||||
module.fail_json(msg=error_str.format(arg))
|
||||
|
||||
|
||||
def manageiq_entities():
|
||||
return {
|
||||
'provider': 'providers', 'host': 'hosts', 'vm': 'vms',
|
||||
'category': 'categories', 'cluster': 'clusters', 'data store': 'data_stores',
|
||||
'group': 'groups', 'resource pool': 'resource_pools', 'service': 'services',
|
||||
'service template': 'service_templates', 'template': 'templates',
|
||||
'tenant': 'tenants', 'user': 'users', 'blueprint': 'blueprints'
|
||||
}
|
||||
|
||||
|
||||
class ManageIQ(object):
|
||||
"""
|
||||
class encapsulating ManageIQ API client.
|
||||
"""
|
||||
|
||||
def __init__(self, module):
|
||||
# handle import errors
|
||||
check_client(module)
|
||||
|
||||
params = validate_connection_params(module)
|
||||
|
||||
url = params['url']
|
||||
username = params['username']
|
||||
password = params['password']
|
||||
token = params['token']
|
||||
verify_ssl = params['validate_certs']
|
||||
ca_bundle_path = params['ca_cert']
|
||||
|
||||
self._module = module
|
||||
self._api_url = url + '/api'
|
||||
self._auth = dict(user=username, password=password, token=token)
|
||||
try:
|
||||
self._client = ManageIQClient(self._api_url, self._auth, verify_ssl=verify_ssl, ca_bundle_path=ca_bundle_path)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="failed to open connection (%s): %s" % (url, str(e)))
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
""" Ansible module module
|
||||
|
||||
Returns:
|
||||
the ansible module
|
||||
"""
|
||||
return self._module
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
""" Base ManageIQ API
|
||||
|
||||
Returns:
|
||||
the base ManageIQ API
|
||||
"""
|
||||
return self._api_url
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
""" ManageIQ client
|
||||
|
||||
Returns:
|
||||
the ManageIQ client
|
||||
"""
|
||||
return self._client
|
||||
|
||||
def find_collection_resource_by(self, collection_name, **params):
|
||||
""" Searches the collection resource by the collection name and the param passed.
|
||||
|
||||
Returns:
|
||||
the resource as an object if it exists in manageiq, None otherwise.
|
||||
"""
|
||||
try:
|
||||
entity = self.client.collections.__getattribute__(collection_name).get(**params)
|
||||
except ValueError:
|
||||
return None
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="failed to find resource {error}".format(error=e))
|
||||
return vars(entity)
|
||||
|
||||
def find_collection_resource_or_fail(self, collection_name, **params):
|
||||
""" Searches the collection resource by the collection name and the param passed.
|
||||
|
||||
Returns:
|
||||
the resource as an object if it exists in manageiq, Fail otherwise.
|
||||
"""
|
||||
resource = self.find_collection_resource_by(collection_name, **params)
|
||||
if resource:
|
||||
return resource
|
||||
else:
|
||||
msg = "{collection_name} where {params} does not exist in manageiq".format(
|
||||
collection_name=collection_name, params=str(params))
|
||||
self.module.fail_json(msg=msg)
|
||||
151
plugins/module_utils/memset.py
Normal file
151
plugins/module_utils/memset.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c) 2018, Simon Weald <ansible@simonweald.com>
|
||||
#
|
||||
# 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.
|
||||
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.urls import open_url, urllib_error
|
||||
from ansible.module_utils.basic import json
|
||||
|
||||
|
||||
class Response(object):
|
||||
'''
|
||||
Create a response object to mimic that of requests.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.content = None
|
||||
self.status_code = None
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.content)
|
||||
|
||||
|
||||
def memset_api_call(api_key, api_method, payload=None):
|
||||
'''
|
||||
Generic function which returns results back to calling function.
|
||||
|
||||
Requires an API key and an API method to assemble the API URL.
|
||||
Returns response text to be analysed.
|
||||
'''
|
||||
# instantiate a response object
|
||||
response = Response()
|
||||
|
||||
# if we've already started preloading the payload then copy it
|
||||
# and use that, otherwise we need to isntantiate it.
|
||||
if payload is None:
|
||||
payload = dict()
|
||||
else:
|
||||
payload = payload.copy()
|
||||
|
||||
# set some sane defaults
|
||||
has_failed = False
|
||||
msg = None
|
||||
|
||||
data = urlencode(payload)
|
||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
api_uri_base = 'https://api.memset.com/v1/json/'
|
||||
api_uri = '{0}{1}/' . format(api_uri_base, api_method)
|
||||
|
||||
try:
|
||||
resp = open_url(api_uri, data=data, headers=headers, method="POST", force_basic_auth=True, url_username=api_key)
|
||||
response.content = resp.read().decode('utf-8')
|
||||
response.status_code = resp.getcode()
|
||||
except urllib_error.HTTPError as e:
|
||||
try:
|
||||
errorcode = e.code
|
||||
except AttributeError:
|
||||
errorcode = None
|
||||
|
||||
has_failed = True
|
||||
response.content = e.read().decode('utf8')
|
||||
response.status_code = errorcode
|
||||
|
||||
if response.status_code is not None:
|
||||
msg = "Memset API returned a {0} response ({1}, {2})." . format(response.status_code, response.json()['error_type'], response.json()['error'])
|
||||
else:
|
||||
msg = "Memset API returned an error ({0}, {1})." . format(response.json()['error_type'], response.json()['error'])
|
||||
|
||||
if msg is None:
|
||||
msg = response.json()
|
||||
|
||||
return(has_failed, msg, response)
|
||||
|
||||
|
||||
def check_zone_domain(data, domain):
|
||||
'''
|
||||
Returns true if domain already exists, and false if not.
|
||||
'''
|
||||
exists = False
|
||||
|
||||
if data.status_code in [201, 200]:
|
||||
for zone_domain in data.json():
|
||||
if zone_domain['domain'] == domain:
|
||||
exists = True
|
||||
|
||||
return(exists)
|
||||
|
||||
|
||||
def check_zone(data, name):
|
||||
'''
|
||||
Returns true if zone already exists, and false if not.
|
||||
'''
|
||||
counter = 0
|
||||
exists = False
|
||||
|
||||
if data.status_code in [201, 200]:
|
||||
for zone in data.json():
|
||||
if zone['nickname'] == name:
|
||||
counter += 1
|
||||
if counter == 1:
|
||||
exists = True
|
||||
|
||||
return(exists, counter)
|
||||
|
||||
|
||||
def get_zone_id(zone_name, current_zones):
|
||||
'''
|
||||
Returns the zone's id if it exists and is unique
|
||||
'''
|
||||
zone_exists = False
|
||||
zone_id, msg = None, None
|
||||
zone_list = []
|
||||
|
||||
for zone in current_zones:
|
||||
if zone['nickname'] == zone_name:
|
||||
zone_list.append(zone['id'])
|
||||
|
||||
counter = len(zone_list)
|
||||
|
||||
if counter == 0:
|
||||
msg = 'No matching zone found'
|
||||
elif counter == 1:
|
||||
zone_id = zone_list[0]
|
||||
zone_exists = True
|
||||
elif counter > 1:
|
||||
zone_id = None
|
||||
msg = 'Zone ID could not be returned as duplicate zone names were detected'
|
||||
|
||||
return(zone_exists, msg, counter, zone_id)
|
||||
106
plugins/module_utils/mysql.py
Normal file
106
plugins/module_utils/mysql.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Jonathan Mainguy <jon@soh.re>, 2015
|
||||
# Most of this was originally added by Sven Schliesing @muffl0n in the mysql_user.py module
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
import pymysql as mysql_driver
|
||||
_mysql_cursor_param = 'cursor'
|
||||
except ImportError:
|
||||
try:
|
||||
import MySQLdb as mysql_driver
|
||||
import MySQLdb.cursors
|
||||
_mysql_cursor_param = 'cursorclass'
|
||||
except ImportError:
|
||||
mysql_driver = None
|
||||
|
||||
mysql_driver_fail_msg = 'The PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) module is required.'
|
||||
|
||||
|
||||
def mysql_connect(module, login_user=None, login_password=None, config_file='', ssl_cert=None, ssl_key=None, ssl_ca=None, db=None, cursor_class=None,
|
||||
connect_timeout=30, autocommit=False):
|
||||
config = {}
|
||||
|
||||
if ssl_ca is not None or ssl_key is not None or ssl_cert is not None:
|
||||
config['ssl'] = {}
|
||||
|
||||
if module.params['login_unix_socket']:
|
||||
config['unix_socket'] = module.params['login_unix_socket']
|
||||
else:
|
||||
config['host'] = module.params['login_host']
|
||||
config['port'] = module.params['login_port']
|
||||
|
||||
if os.path.exists(config_file):
|
||||
config['read_default_file'] = config_file
|
||||
|
||||
# If login_user or login_password are given, they should override the
|
||||
# config file
|
||||
if login_user is not None:
|
||||
config['user'] = login_user
|
||||
if login_password is not None:
|
||||
config['passwd'] = login_password
|
||||
if ssl_cert is not None:
|
||||
config['ssl']['cert'] = ssl_cert
|
||||
if ssl_key is not None:
|
||||
config['ssl']['key'] = ssl_key
|
||||
if ssl_ca is not None:
|
||||
config['ssl']['ca'] = ssl_ca
|
||||
if db is not None:
|
||||
config['db'] = db
|
||||
if connect_timeout is not None:
|
||||
config['connect_timeout'] = connect_timeout
|
||||
|
||||
if _mysql_cursor_param == 'cursor':
|
||||
# In case of PyMySQL driver:
|
||||
db_connection = mysql_driver.connect(autocommit=autocommit, **config)
|
||||
else:
|
||||
# In case of MySQLdb driver
|
||||
db_connection = mysql_driver.connect(**config)
|
||||
if autocommit:
|
||||
db_connection.autocommit(True)
|
||||
|
||||
if cursor_class == 'DictCursor':
|
||||
return db_connection.cursor(**{_mysql_cursor_param: mysql_driver.cursors.DictCursor}), db_connection
|
||||
else:
|
||||
return db_connection.cursor(), db_connection
|
||||
|
||||
|
||||
def mysql_common_argument_spec():
|
||||
return dict(
|
||||
login_user=dict(type='str', default=None),
|
||||
login_password=dict(type='str', no_log=True),
|
||||
login_host=dict(type='str', default='localhost'),
|
||||
login_port=dict(type='int', default=3306),
|
||||
login_unix_socket=dict(type='str'),
|
||||
config_file=dict(type='path', default='~/.my.cnf'),
|
||||
connect_timeout=dict(type='int', default=30),
|
||||
client_cert=dict(type='path', aliases=['ssl_cert']),
|
||||
client_key=dict(type='path', aliases=['ssl_key']),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca']),
|
||||
)
|
||||
0
plugins/module_utils/net_tools/__init__.py
Normal file
0
plugins/module_utils/net_tools/__init__.py
Normal file
0
plugins/module_utils/net_tools/netbox/__init__.py
Normal file
0
plugins/module_utils/net_tools/netbox/__init__.py
Normal file
0
plugins/module_utils/net_tools/nios/__init__.py
Normal file
0
plugins/module_utils/net_tools/nios/__init__.py
Normal file
601
plugins/module_utils/net_tools/nios/api.py
Normal file
601
plugins/module_utils/net_tools/nios/api.py
Normal file
@@ -0,0 +1,601 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2018 Red Hat Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import os
|
||||
from functools import partial
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
|
||||
try:
|
||||
from infoblox_client.connector import Connector
|
||||
from infoblox_client.exceptions import InfobloxException
|
||||
HAS_INFOBLOX_CLIENT = True
|
||||
except ImportError:
|
||||
HAS_INFOBLOX_CLIENT = False
|
||||
|
||||
# defining nios constants
|
||||
NIOS_DNS_VIEW = 'view'
|
||||
NIOS_NETWORK_VIEW = 'networkview'
|
||||
NIOS_HOST_RECORD = 'record:host'
|
||||
NIOS_IPV4_NETWORK = 'network'
|
||||
NIOS_IPV6_NETWORK = 'ipv6network'
|
||||
NIOS_ZONE = 'zone_auth'
|
||||
NIOS_PTR_RECORD = 'record:ptr'
|
||||
NIOS_A_RECORD = 'record:a'
|
||||
NIOS_AAAA_RECORD = 'record:aaaa'
|
||||
NIOS_CNAME_RECORD = 'record:cname'
|
||||
NIOS_MX_RECORD = 'record:mx'
|
||||
NIOS_SRV_RECORD = 'record:srv'
|
||||
NIOS_NAPTR_RECORD = 'record:naptr'
|
||||
NIOS_TXT_RECORD = 'record:txt'
|
||||
NIOS_NSGROUP = 'nsgroup'
|
||||
NIOS_IPV4_FIXED_ADDRESS = 'fixedaddress'
|
||||
NIOS_IPV6_FIXED_ADDRESS = 'ipv6fixedaddress'
|
||||
NIOS_NEXT_AVAILABLE_IP = 'func:nextavailableip'
|
||||
NIOS_IPV4_NETWORK_CONTAINER = 'networkcontainer'
|
||||
NIOS_IPV6_NETWORK_CONTAINER = 'ipv6networkcontainer'
|
||||
NIOS_MEMBER = 'member'
|
||||
|
||||
NIOS_PROVIDER_SPEC = {
|
||||
'host': dict(fallback=(env_fallback, ['INFOBLOX_HOST'])),
|
||||
'username': dict(fallback=(env_fallback, ['INFOBLOX_USERNAME'])),
|
||||
'password': dict(fallback=(env_fallback, ['INFOBLOX_PASSWORD']), no_log=True),
|
||||
'validate_certs': dict(type='bool', default=False, fallback=(env_fallback, ['INFOBLOX_SSL_VERIFY']), aliases=['ssl_verify']),
|
||||
'silent_ssl_warnings': dict(type='bool', default=True),
|
||||
'http_request_timeout': dict(type='int', default=10, fallback=(env_fallback, ['INFOBLOX_HTTP_REQUEST_TIMEOUT'])),
|
||||
'http_pool_connections': dict(type='int', default=10),
|
||||
'http_pool_maxsize': dict(type='int', default=10),
|
||||
'max_retries': dict(type='int', default=3, fallback=(env_fallback, ['INFOBLOX_MAX_RETRIES'])),
|
||||
'wapi_version': dict(default='2.1', fallback=(env_fallback, ['INFOBLOX_WAP_VERSION'])),
|
||||
'max_results': dict(type='int', default=1000, fallback=(env_fallback, ['INFOBLOX_MAX_RETRIES']))
|
||||
}
|
||||
|
||||
|
||||
def get_connector(*args, **kwargs):
|
||||
''' Returns an instance of infoblox_client.connector.Connector
|
||||
:params args: positional arguments are silently ignored
|
||||
:params kwargs: dict that is passed to Connector init
|
||||
:returns: Connector
|
||||
'''
|
||||
if not HAS_INFOBLOX_CLIENT:
|
||||
raise Exception('infoblox-client is required but does not appear '
|
||||
'to be installed. It can be installed using the '
|
||||
'command `pip install infoblox-client`')
|
||||
|
||||
if not set(kwargs.keys()).issubset(list(NIOS_PROVIDER_SPEC.keys()) + ['ssl_verify']):
|
||||
raise Exception('invalid or unsupported keyword argument for connector')
|
||||
for key, value in iteritems(NIOS_PROVIDER_SPEC):
|
||||
if key not in kwargs:
|
||||
# apply default values from NIOS_PROVIDER_SPEC since we cannot just
|
||||
# assume the provider values are coming from AnsibleModule
|
||||
if 'default' in value:
|
||||
kwargs[key] = value['default']
|
||||
|
||||
# override any values with env variables unless they were
|
||||
# explicitly set
|
||||
env = ('INFOBLOX_%s' % key).upper()
|
||||
if env in os.environ:
|
||||
kwargs[key] = os.environ.get(env)
|
||||
|
||||
if 'validate_certs' in kwargs.keys():
|
||||
kwargs['ssl_verify'] = kwargs['validate_certs']
|
||||
kwargs.pop('validate_certs', None)
|
||||
|
||||
return Connector(kwargs)
|
||||
|
||||
|
||||
def normalize_extattrs(value):
|
||||
''' Normalize extattrs field to expected format
|
||||
The module accepts extattrs as key/value pairs. This method will
|
||||
transform the key/value pairs into a structure suitable for
|
||||
sending across WAPI in the format of:
|
||||
extattrs: {
|
||||
key: {
|
||||
value: <value>
|
||||
}
|
||||
}
|
||||
'''
|
||||
return dict([(k, {'value': v}) for k, v in iteritems(value)])
|
||||
|
||||
|
||||
def flatten_extattrs(value):
|
||||
''' Flatten the key/value struct for extattrs
|
||||
WAPI returns extattrs field as a dict in form of:
|
||||
extattrs: {
|
||||
key: {
|
||||
value: <value>
|
||||
}
|
||||
}
|
||||
This method will flatten the structure to:
|
||||
extattrs: {
|
||||
key: value
|
||||
}
|
||||
'''
|
||||
return dict([(k, v['value']) for k, v in iteritems(value)])
|
||||
|
||||
|
||||
def member_normalize(member_spec):
|
||||
''' Transforms the member module arguments into a valid WAPI struct
|
||||
This function will transform the arguments into a structure that
|
||||
is a valid WAPI structure in the format of:
|
||||
{
|
||||
key: <value>,
|
||||
}
|
||||
It will remove any arguments that are set to None since WAPI will error on
|
||||
that condition.
|
||||
The remainder of the value validation is performed by WAPI
|
||||
Some parameters in ib_spec are passed as a list in order to pass the validation for elements.
|
||||
In this function, they are converted to dictionary.
|
||||
'''
|
||||
member_elements = ['vip_setting', 'ipv6_setting', 'lan2_port_setting', 'mgmt_port_setting',
|
||||
'pre_provisioning', 'network_setting', 'v6_network_setting',
|
||||
'ha_port_setting', 'lan_port_setting', 'lan2_physical_setting',
|
||||
'lan_ha_port_setting', 'mgmt_network_setting', 'v6_mgmt_network_setting']
|
||||
for key in member_spec.keys():
|
||||
if key in member_elements and member_spec[key] is not None:
|
||||
member_spec[key] = member_spec[key][0]
|
||||
if isinstance(member_spec[key], dict):
|
||||
member_spec[key] = member_normalize(member_spec[key])
|
||||
elif isinstance(member_spec[key], list):
|
||||
for x in member_spec[key]:
|
||||
if isinstance(x, dict):
|
||||
x = member_normalize(x)
|
||||
elif member_spec[key] is None:
|
||||
del member_spec[key]
|
||||
return member_spec
|
||||
|
||||
|
||||
class WapiBase(object):
|
||||
''' Base class for implementing Infoblox WAPI API '''
|
||||
provider_spec = {'provider': dict(type='dict', options=NIOS_PROVIDER_SPEC)}
|
||||
|
||||
def __init__(self, provider):
|
||||
self.connector = get_connector(**provider)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.__dict__[name]
|
||||
except KeyError:
|
||||
if name.startswith('_'):
|
||||
raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))
|
||||
return partial(self._invoke_method, name)
|
||||
|
||||
def _invoke_method(self, name, *args, **kwargs):
|
||||
try:
|
||||
method = getattr(self.connector, name)
|
||||
return method(*args, **kwargs)
|
||||
except InfobloxException as exc:
|
||||
if hasattr(self, 'handle_exception'):
|
||||
self.handle_exception(name, exc)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class WapiLookup(WapiBase):
|
||||
''' Implements WapiBase for lookup plugins '''
|
||||
def handle_exception(self, method_name, exc):
|
||||
if ('text' in exc.response):
|
||||
raise Exception(exc.response['text'])
|
||||
else:
|
||||
raise Exception(exc)
|
||||
|
||||
|
||||
class WapiInventory(WapiBase):
|
||||
''' Implements WapiBase for dynamic inventory script '''
|
||||
pass
|
||||
|
||||
|
||||
class WapiModule(WapiBase):
|
||||
''' Implements WapiBase for executing a NIOS module '''
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
provider = module.params['provider']
|
||||
try:
|
||||
super(WapiModule, self).__init__(provider)
|
||||
except Exception as exc:
|
||||
self.module.fail_json(msg=to_text(exc))
|
||||
|
||||
def handle_exception(self, method_name, exc):
|
||||
''' Handles any exceptions raised
|
||||
This method will be called if an InfobloxException is raised for
|
||||
any call to the instance of Connector and also, in case of generic
|
||||
exception. This method will then gracefully fail the module.
|
||||
:args exc: instance of InfobloxException
|
||||
'''
|
||||
if ('text' in exc.response):
|
||||
self.module.fail_json(
|
||||
msg=exc.response['text'],
|
||||
type=exc.response['Error'].split(':')[0],
|
||||
code=exc.response.get('code'),
|
||||
operation=method_name
|
||||
)
|
||||
else:
|
||||
self.module.fail_json(msg=to_native(exc))
|
||||
|
||||
def run(self, ib_obj_type, ib_spec):
|
||||
''' Runs the module and performans configuration tasks
|
||||
:args ib_obj_type: the WAPI object type to operate against
|
||||
:args ib_spec: the specification for the WAPI object as a dict
|
||||
:returns: a results dict
|
||||
'''
|
||||
|
||||
update = new_name = None
|
||||
state = self.module.params['state']
|
||||
if state not in ('present', 'absent'):
|
||||
self.module.fail_json(msg='state must be one of `present`, `absent`, got `%s`' % state)
|
||||
|
||||
result = {'changed': False}
|
||||
|
||||
obj_filter = dict([(k, self.module.params[k]) for k, v in iteritems(ib_spec) if v.get('ib_req')])
|
||||
|
||||
# get object reference
|
||||
ib_obj_ref, update, new_name = self.get_object_ref(self.module, ib_obj_type, obj_filter, ib_spec)
|
||||
proposed_object = {}
|
||||
for key, value in iteritems(ib_spec):
|
||||
if self.module.params[key] is not None:
|
||||
if 'transform' in value:
|
||||
proposed_object[key] = value['transform'](self.module)
|
||||
else:
|
||||
proposed_object[key] = self.module.params[key]
|
||||
|
||||
# If configure_by_dns is set to False, then delete the default dns set in the param else throw exception
|
||||
if not proposed_object.get('configure_for_dns') and proposed_object.get('view') == 'default'\
|
||||
and ib_obj_type == NIOS_HOST_RECORD:
|
||||
del proposed_object['view']
|
||||
elif not proposed_object.get('configure_for_dns') and proposed_object.get('view') != 'default'\
|
||||
and ib_obj_type == NIOS_HOST_RECORD:
|
||||
self.module.fail_json(msg='DNS Bypass is not allowed if DNS view is set other than \'default\'')
|
||||
|
||||
if ib_obj_ref:
|
||||
if len(ib_obj_ref) > 1:
|
||||
for each in ib_obj_ref:
|
||||
# To check for existing A_record with same name with input A_record by IP
|
||||
if each.get('ipv4addr') and each.get('ipv4addr') == proposed_object.get('ipv4addr'):
|
||||
current_object = each
|
||||
# To check for existing Host_record with same name with input Host_record by IP
|
||||
elif each.get('ipv4addrs')[0].get('ipv4addr') and each.get('ipv4addrs')[0].get('ipv4addr')\
|
||||
== proposed_object.get('ipv4addrs')[0].get('ipv4addr'):
|
||||
current_object = each
|
||||
# Else set the current_object with input value
|
||||
else:
|
||||
current_object = obj_filter
|
||||
ref = None
|
||||
else:
|
||||
current_object = ib_obj_ref[0]
|
||||
if 'extattrs' in current_object:
|
||||
current_object['extattrs'] = flatten_extattrs(current_object['extattrs'])
|
||||
if current_object.get('_ref'):
|
||||
ref = current_object.pop('_ref')
|
||||
else:
|
||||
current_object = obj_filter
|
||||
ref = None
|
||||
# checks if the object type is member to normalize the attributes being passed
|
||||
if (ib_obj_type == NIOS_MEMBER):
|
||||
proposed_object = member_normalize(proposed_object)
|
||||
|
||||
# checks if the name's field has been updated
|
||||
if update and new_name:
|
||||
proposed_object['name'] = new_name
|
||||
|
||||
check_remove = []
|
||||
if (ib_obj_type == NIOS_HOST_RECORD):
|
||||
# this check is for idempotency, as if the same ip address shall be passed
|
||||
# add param will be removed, and same exists true for remove case as well.
|
||||
if 'ipv4addrs' in [current_object and proposed_object]:
|
||||
for each in current_object['ipv4addrs']:
|
||||
if each['ipv4addr'] == proposed_object['ipv4addrs'][0]['ipv4addr']:
|
||||
if 'add' in proposed_object['ipv4addrs'][0]:
|
||||
del proposed_object['ipv4addrs'][0]['add']
|
||||
break
|
||||
check_remove += each.values()
|
||||
if proposed_object['ipv4addrs'][0]['ipv4addr'] not in check_remove:
|
||||
if 'remove' in proposed_object['ipv4addrs'][0]:
|
||||
del proposed_object['ipv4addrs'][0]['remove']
|
||||
|
||||
res = None
|
||||
modified = not self.compare_objects(current_object, proposed_object)
|
||||
if 'extattrs' in proposed_object:
|
||||
proposed_object['extattrs'] = normalize_extattrs(proposed_object['extattrs'])
|
||||
|
||||
# Checks if nios_next_ip param is passed in ipv4addrs/ipv4addr args
|
||||
proposed_object = self.check_if_nios_next_ip_exists(proposed_object)
|
||||
|
||||
if state == 'present':
|
||||
if ref is None:
|
||||
if not self.module.check_mode:
|
||||
self.create_object(ib_obj_type, proposed_object)
|
||||
result['changed'] = True
|
||||
# Check if NIOS_MEMBER and the flag to call function create_token is set
|
||||
elif (ib_obj_type == NIOS_MEMBER) and (proposed_object['create_token']):
|
||||
proposed_object = None
|
||||
# the function creates a token that can be used by a pre-provisioned member to join the grid
|
||||
result['api_results'] = self.call_func('create_token', ref, proposed_object)
|
||||
result['changed'] = True
|
||||
elif modified:
|
||||
if 'ipv4addrs' in proposed_object:
|
||||
if ('add' not in proposed_object['ipv4addrs'][0]) and ('remove' not in proposed_object['ipv4addrs'][0]):
|
||||
self.check_if_recordname_exists(obj_filter, ib_obj_ref, ib_obj_type, current_object, proposed_object)
|
||||
|
||||
if (ib_obj_type in (NIOS_HOST_RECORD, NIOS_NETWORK_VIEW, NIOS_DNS_VIEW)):
|
||||
run_update = True
|
||||
proposed_object = self.on_update(proposed_object, ib_spec)
|
||||
if 'ipv4addrs' in proposed_object:
|
||||
if ('add' or 'remove') in proposed_object['ipv4addrs'][0]:
|
||||
run_update, proposed_object = self.check_if_add_remove_ip_arg_exists(proposed_object)
|
||||
if run_update:
|
||||
res = self.update_object(ref, proposed_object)
|
||||
result['changed'] = True
|
||||
else:
|
||||
res = ref
|
||||
if (ib_obj_type in (NIOS_A_RECORD, NIOS_AAAA_RECORD, NIOS_PTR_RECORD, NIOS_SRV_RECORD)):
|
||||
# popping 'view' key as update of 'view' is not supported with respect to a:record/aaaa:record/srv:record/ptr:record
|
||||
proposed_object = self.on_update(proposed_object, ib_spec)
|
||||
del proposed_object['view']
|
||||
if not self.module.check_mode:
|
||||
res = self.update_object(ref, proposed_object)
|
||||
result['changed'] = True
|
||||
elif 'network_view' in proposed_object:
|
||||
proposed_object.pop('network_view')
|
||||
result['changed'] = True
|
||||
if not self.module.check_mode and res is None:
|
||||
proposed_object = self.on_update(proposed_object, ib_spec)
|
||||
self.update_object(ref, proposed_object)
|
||||
result['changed'] = True
|
||||
|
||||
elif state == 'absent':
|
||||
if ref is not None:
|
||||
if 'ipv4addrs' in proposed_object:
|
||||
if 'remove' in proposed_object['ipv4addrs'][0]:
|
||||
self.check_if_add_remove_ip_arg_exists(proposed_object)
|
||||
self.update_object(ref, proposed_object)
|
||||
result['changed'] = True
|
||||
elif not self.module.check_mode:
|
||||
self.delete_object(ref)
|
||||
result['changed'] = True
|
||||
|
||||
return result
|
||||
|
||||
def check_if_recordname_exists(self, obj_filter, ib_obj_ref, ib_obj_type, current_object, proposed_object):
|
||||
''' Send POST request if host record input name and retrieved ref name is same,
|
||||
but input IP and retrieved IP is different'''
|
||||
|
||||
if 'name' in (obj_filter and ib_obj_ref[0]) and ib_obj_type == NIOS_HOST_RECORD:
|
||||
obj_host_name = obj_filter['name']
|
||||
ref_host_name = ib_obj_ref[0]['name']
|
||||
if 'ipv4addrs' in (current_object and proposed_object):
|
||||
current_ip_addr = current_object['ipv4addrs'][0]['ipv4addr']
|
||||
proposed_ip_addr = proposed_object['ipv4addrs'][0]['ipv4addr']
|
||||
elif 'ipv6addrs' in (current_object and proposed_object):
|
||||
current_ip_addr = current_object['ipv6addrs'][0]['ipv6addr']
|
||||
proposed_ip_addr = proposed_object['ipv6addrs'][0]['ipv6addr']
|
||||
|
||||
if obj_host_name == ref_host_name and current_ip_addr != proposed_ip_addr:
|
||||
self.create_object(ib_obj_type, proposed_object)
|
||||
|
||||
def check_if_nios_next_ip_exists(self, proposed_object):
|
||||
''' Check if nios_next_ip argument is passed in ipaddr while creating
|
||||
host record, if yes then format proposed object ipv4addrs and pass
|
||||
func:nextavailableip and ipaddr range to create hostrecord with next
|
||||
available ip in one call to avoid any race condition '''
|
||||
|
||||
if 'ipv4addrs' in proposed_object:
|
||||
if 'nios_next_ip' in proposed_object['ipv4addrs'][0]['ipv4addr']:
|
||||
ip_range = self.module._check_type_dict(proposed_object['ipv4addrs'][0]['ipv4addr'])['nios_next_ip']
|
||||
proposed_object['ipv4addrs'][0]['ipv4addr'] = NIOS_NEXT_AVAILABLE_IP + ':' + ip_range
|
||||
elif 'ipv4addr' in proposed_object:
|
||||
if 'nios_next_ip' in proposed_object['ipv4addr']:
|
||||
ip_range = self.module._check_type_dict(proposed_object['ipv4addr'])['nios_next_ip']
|
||||
proposed_object['ipv4addr'] = NIOS_NEXT_AVAILABLE_IP + ':' + ip_range
|
||||
|
||||
return proposed_object
|
||||
|
||||
def check_if_add_remove_ip_arg_exists(self, proposed_object):
|
||||
'''
|
||||
This function shall check if add/remove param is set to true and
|
||||
is passed in the args, then we will update the proposed dictionary
|
||||
to add/remove IP to existing host_record, if the user passes false
|
||||
param with the argument nothing shall be done.
|
||||
:returns: True if param is changed based on add/remove, and also the
|
||||
changed proposed_object.
|
||||
'''
|
||||
update = False
|
||||
if 'add' in proposed_object['ipv4addrs'][0]:
|
||||
if proposed_object['ipv4addrs'][0]['add']:
|
||||
proposed_object['ipv4addrs+'] = proposed_object['ipv4addrs']
|
||||
del proposed_object['ipv4addrs']
|
||||
del proposed_object['ipv4addrs+'][0]['add']
|
||||
update = True
|
||||
else:
|
||||
del proposed_object['ipv4addrs'][0]['add']
|
||||
elif 'remove' in proposed_object['ipv4addrs'][0]:
|
||||
if proposed_object['ipv4addrs'][0]['remove']:
|
||||
proposed_object['ipv4addrs-'] = proposed_object['ipv4addrs']
|
||||
del proposed_object['ipv4addrs']
|
||||
del proposed_object['ipv4addrs-'][0]['remove']
|
||||
update = True
|
||||
else:
|
||||
del proposed_object['ipv4addrs'][0]['remove']
|
||||
return update, proposed_object
|
||||
|
||||
def issubset(self, item, objects):
|
||||
''' Checks if item is a subset of objects
|
||||
:args item: the subset item to validate
|
||||
:args objects: superset list of objects to validate against
|
||||
:returns: True if item is a subset of one entry in objects otherwise
|
||||
this method will return None
|
||||
'''
|
||||
for obj in objects:
|
||||
if isinstance(item, dict):
|
||||
if all(entry in obj.items() for entry in item.items()):
|
||||
return True
|
||||
else:
|
||||
if item in obj:
|
||||
return True
|
||||
|
||||
def compare_objects(self, current_object, proposed_object):
|
||||
for key, proposed_item in iteritems(proposed_object):
|
||||
current_item = current_object.get(key)
|
||||
|
||||
# if proposed has a key that current doesn't then the objects are
|
||||
# not equal and False will be immediately returned
|
||||
if current_item is None:
|
||||
return False
|
||||
|
||||
elif isinstance(proposed_item, list):
|
||||
for subitem in proposed_item:
|
||||
if not self.issubset(subitem, current_item):
|
||||
return False
|
||||
|
||||
elif isinstance(proposed_item, dict):
|
||||
return self.compare_objects(current_item, proposed_item)
|
||||
|
||||
else:
|
||||
if current_item != proposed_item:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_object_ref(self, module, ib_obj_type, obj_filter, ib_spec):
|
||||
''' this function gets the reference object of pre-existing nios objects '''
|
||||
|
||||
update = False
|
||||
old_name = new_name = None
|
||||
if ('name' in obj_filter):
|
||||
# gets and returns the current object based on name/old_name passed
|
||||
try:
|
||||
name_obj = self.module._check_type_dict(obj_filter['name'])
|
||||
old_name = name_obj['old_name']
|
||||
new_name = name_obj['new_name']
|
||||
except TypeError:
|
||||
name = obj_filter['name']
|
||||
|
||||
if old_name and new_name:
|
||||
if (ib_obj_type == NIOS_HOST_RECORD):
|
||||
test_obj_filter = dict([('name', old_name), ('view', obj_filter['view'])])
|
||||
elif (ib_obj_type in (NIOS_AAAA_RECORD, NIOS_A_RECORD)):
|
||||
test_obj_filter = obj_filter
|
||||
else:
|
||||
test_obj_filter = dict([('name', old_name)])
|
||||
# get the object reference
|
||||
ib_obj = self.get_object(ib_obj_type, test_obj_filter, return_fields=ib_spec.keys())
|
||||
if ib_obj:
|
||||
obj_filter['name'] = new_name
|
||||
else:
|
||||
test_obj_filter['name'] = new_name
|
||||
ib_obj = self.get_object(ib_obj_type, test_obj_filter, return_fields=ib_spec.keys())
|
||||
update = True
|
||||
return ib_obj, update, new_name
|
||||
if (ib_obj_type == NIOS_HOST_RECORD):
|
||||
# to check only by name if dns bypassing is set
|
||||
if not obj_filter['configure_for_dns']:
|
||||
test_obj_filter = dict([('name', name)])
|
||||
else:
|
||||
test_obj_filter = dict([('name', name), ('view', obj_filter['view'])])
|
||||
elif (ib_obj_type == NIOS_IPV4_FIXED_ADDRESS or ib_obj_type == NIOS_IPV6_FIXED_ADDRESS and 'mac' in obj_filter):
|
||||
test_obj_filter = dict([['mac', obj_filter['mac']]])
|
||||
elif (ib_obj_type == NIOS_A_RECORD):
|
||||
# resolves issue where a_record with uppercase name was returning null and was failing
|
||||
test_obj_filter = obj_filter
|
||||
test_obj_filter['name'] = test_obj_filter['name'].lower()
|
||||
# resolves issue where multiple a_records with same name and different IP address
|
||||
try:
|
||||
ipaddr_obj = self.module._check_type_dict(obj_filter['ipv4addr'])
|
||||
ipaddr = ipaddr_obj['old_ipv4addr']
|
||||
except TypeError:
|
||||
ipaddr = obj_filter['ipv4addr']
|
||||
test_obj_filter['ipv4addr'] = ipaddr
|
||||
elif (ib_obj_type == NIOS_TXT_RECORD):
|
||||
# resolves issue where multiple txt_records with same name and different text
|
||||
test_obj_filter = obj_filter
|
||||
try:
|
||||
text_obj = self.module._check_type_dict(obj_filter['text'])
|
||||
txt = text_obj['old_text']
|
||||
except TypeError:
|
||||
txt = obj_filter['text']
|
||||
test_obj_filter['text'] = txt
|
||||
# check if test_obj_filter is empty copy passed obj_filter
|
||||
else:
|
||||
test_obj_filter = obj_filter
|
||||
ib_obj = self.get_object(ib_obj_type, test_obj_filter.copy(), return_fields=ib_spec.keys())
|
||||
elif (ib_obj_type == NIOS_A_RECORD):
|
||||
# resolves issue where multiple a_records with same name and different IP address
|
||||
test_obj_filter = obj_filter
|
||||
try:
|
||||
ipaddr_obj = self.module._check_type_dict(obj_filter['ipv4addr'])
|
||||
ipaddr = ipaddr_obj['old_ipv4addr']
|
||||
except TypeError:
|
||||
ipaddr = obj_filter['ipv4addr']
|
||||
test_obj_filter['ipv4addr'] = ipaddr
|
||||
ib_obj = self.get_object(ib_obj_type, test_obj_filter.copy(), return_fields=ib_spec.keys())
|
||||
elif (ib_obj_type == NIOS_TXT_RECORD):
|
||||
# resolves issue where multiple txt_records with same name and different text
|
||||
test_obj_filter = obj_filter
|
||||
try:
|
||||
text_obj = self.module._check_type_dict(obj_filter['text'])
|
||||
txt = text_obj['old_text']
|
||||
except TypeError:
|
||||
txt = obj_filter['text']
|
||||
test_obj_filter['text'] = txt
|
||||
ib_obj = self.get_object(ib_obj_type, test_obj_filter.copy(), return_fields=ib_spec.keys())
|
||||
elif (ib_obj_type == NIOS_ZONE):
|
||||
# del key 'restart_if_needed' as nios_zone get_object fails with the key present
|
||||
temp = ib_spec['restart_if_needed']
|
||||
del ib_spec['restart_if_needed']
|
||||
ib_obj = self.get_object(ib_obj_type, obj_filter.copy(), return_fields=ib_spec.keys())
|
||||
# reinstate restart_if_needed if ib_obj is none, meaning there's no existing nios_zone ref
|
||||
if not ib_obj:
|
||||
ib_spec['restart_if_needed'] = temp
|
||||
elif (ib_obj_type == NIOS_MEMBER):
|
||||
# del key 'create_token' as nios_member get_object fails with the key present
|
||||
temp = ib_spec['create_token']
|
||||
del ib_spec['create_token']
|
||||
ib_obj = self.get_object(ib_obj_type, obj_filter.copy(), return_fields=ib_spec.keys())
|
||||
if temp:
|
||||
# reinstate 'create_token' key
|
||||
ib_spec['create_token'] = temp
|
||||
else:
|
||||
ib_obj = self.get_object(ib_obj_type, obj_filter.copy(), return_fields=ib_spec.keys())
|
||||
return ib_obj, update, new_name
|
||||
|
||||
def on_update(self, proposed_object, ib_spec):
|
||||
''' Event called before the update is sent to the API endpoing
|
||||
This method will allow the final proposed object to be changed
|
||||
and/or keys filtered before it is sent to the API endpoint to
|
||||
be processed.
|
||||
:args proposed_object: A dict item that will be encoded and sent
|
||||
the API endpoint with the updated data structure
|
||||
:returns: updated object to be sent to API endpoint
|
||||
'''
|
||||
keys = set()
|
||||
for key, value in iteritems(proposed_object):
|
||||
update = ib_spec[key].get('update', True)
|
||||
if not update:
|
||||
keys.add(key)
|
||||
return dict([(k, v) for k, v in iteritems(proposed_object) if k not in keys])
|
||||
0
plugins/module_utils/network/__init__.py
Normal file
0
plugins/module_utils/network/__init__.py
Normal file
0
plugins/module_utils/network/a10/__init__.py
Normal file
0
plugins/module_utils/network/a10/__init__.py
Normal file
153
plugins/module_utils/network/a10/a10.py
Normal file
153
plugins/module_utils/network/a10/a10.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import json
|
||||
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
|
||||
AXAPI_PORT_PROTOCOLS = {
|
||||
'tcp': 2,
|
||||
'udp': 3,
|
||||
}
|
||||
|
||||
AXAPI_VPORT_PROTOCOLS = {
|
||||
'tcp': 2,
|
||||
'udp': 3,
|
||||
'fast-http': 9,
|
||||
'http': 11,
|
||||
'https': 12,
|
||||
}
|
||||
|
||||
|
||||
def a10_argument_spec():
|
||||
return dict(
|
||||
host=dict(type='str', required=True),
|
||||
username=dict(type='str', aliases=['user', 'admin'], required=True),
|
||||
password=dict(type='str', aliases=['pass', 'pwd'], required=True, no_log=True),
|
||||
write_config=dict(type='bool', default=False)
|
||||
)
|
||||
|
||||
|
||||
def axapi_failure(result):
|
||||
if 'response' in result and result['response'].get('status') == 'fail':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def axapi_call(module, url, post=None):
|
||||
'''
|
||||
Returns a datastructure based on the result of the API call
|
||||
'''
|
||||
rsp, info = fetch_url(module, url, data=post)
|
||||
if not rsp or info['status'] >= 400:
|
||||
module.fail_json(msg="failed to connect (status code %s), error was %s" % (info['status'], info.get('msg', 'no error given')))
|
||||
try:
|
||||
raw_data = rsp.read()
|
||||
data = json.loads(raw_data)
|
||||
except ValueError:
|
||||
# at least one API call (system.action.write_config) returns
|
||||
# XML even when JSON is requested, so do some minimal handling
|
||||
# here to prevent failing even when the call succeeded
|
||||
if 'status="ok"' in raw_data.lower():
|
||||
data = {"response": {"status": "OK"}}
|
||||
else:
|
||||
data = {"response": {"status": "fail", "err": {"msg": raw_data}}}
|
||||
except Exception:
|
||||
module.fail_json(msg="could not read the result from the host")
|
||||
finally:
|
||||
rsp.close()
|
||||
return data
|
||||
|
||||
|
||||
def axapi_authenticate(module, base_url, username, password):
|
||||
url = '%s&method=authenticate&username=%s&password=%s' % (base_url, username, password)
|
||||
result = axapi_call(module, url)
|
||||
if axapi_failure(result):
|
||||
return module.fail_json(msg=result['response']['err']['msg'])
|
||||
sessid = result['session_id']
|
||||
return base_url + '&session_id=' + sessid
|
||||
|
||||
|
||||
def axapi_authenticate_v3(module, base_url, username, password):
|
||||
url = base_url
|
||||
auth_payload = {"credentials": {"username": username, "password": password}}
|
||||
result = axapi_call_v3(module, url, method='POST', body=json.dumps(auth_payload))
|
||||
if axapi_failure(result):
|
||||
return module.fail_json(msg=result['response']['err']['msg'])
|
||||
signature = result['authresponse']['signature']
|
||||
return signature
|
||||
|
||||
|
||||
def axapi_call_v3(module, url, method=None, body=None, signature=None):
|
||||
'''
|
||||
Returns a datastructure based on the result of the API call
|
||||
'''
|
||||
if signature:
|
||||
headers = {'content-type': 'application/json', 'Authorization': 'A10 %s' % signature}
|
||||
else:
|
||||
headers = {'content-type': 'application/json'}
|
||||
rsp, info = fetch_url(module, url, method=method, data=body, headers=headers)
|
||||
if not rsp or info['status'] >= 400:
|
||||
module.fail_json(msg="failed to connect (status code %s), error was %s" % (info['status'], info.get('msg', 'no error given')))
|
||||
try:
|
||||
raw_data = rsp.read()
|
||||
data = json.loads(raw_data)
|
||||
except ValueError:
|
||||
# at least one API call (system.action.write_config) returns
|
||||
# XML even when JSON is requested, so do some minimal handling
|
||||
# here to prevent failing even when the call succeeded
|
||||
if 'status="ok"' in raw_data.lower():
|
||||
data = {"response": {"status": "OK"}}
|
||||
else:
|
||||
data = {"response": {"status": "fail", "err": {"msg": raw_data}}}
|
||||
except Exception:
|
||||
module.fail_json(msg="could not read the result from the host")
|
||||
finally:
|
||||
rsp.close()
|
||||
return data
|
||||
|
||||
|
||||
def axapi_enabled_disabled(flag):
|
||||
'''
|
||||
The axapi uses 0/1 integer values for flags, rather than strings
|
||||
or booleans, so convert the given flag to a 0 or 1. For now, params
|
||||
are specified as strings only so thats what we check.
|
||||
'''
|
||||
if flag == 'enabled':
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def axapi_get_port_protocol(protocol):
|
||||
return AXAPI_PORT_PROTOCOLS.get(protocol.lower(), None)
|
||||
|
||||
|
||||
def axapi_get_vport_protocol(protocol):
|
||||
return AXAPI_VPORT_PROTOCOLS.get(protocol.lower(), None)
|
||||
0
plugins/module_utils/network/aci/__init__.py
Normal file
0
plugins/module_utils/network/aci/__init__.py
Normal file
0
plugins/module_utils/network/aireos/__init__.py
Normal file
0
plugins/module_utils/network/aireos/__init__.py
Normal file
129
plugins/module_utils/network/aireos/aireos.py
Normal file
129
plugins/module_utils/network/aireos/aireos.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2016 Red Hat Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.connection import exec_command
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
|
||||
aireos_provider_spec = {
|
||||
'host': dict(),
|
||||
'port': dict(type='int'),
|
||||
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
|
||||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
|
||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
|
||||
'timeout': dict(type='int'),
|
||||
}
|
||||
aireos_argument_spec = {
|
||||
'provider': dict(type='dict', options=aireos_provider_spec)
|
||||
}
|
||||
|
||||
aireos_top_spec = {
|
||||
'host': dict(removed_in_version=2.9),
|
||||
'port': dict(removed_in_version=2.9, type='int'),
|
||||
'username': dict(removed_in_version=2.9),
|
||||
'password': dict(removed_in_version=2.9, no_log=True),
|
||||
'ssh_keyfile': dict(removed_in_version=2.9, type='path'),
|
||||
'timeout': dict(removed_in_version=2.9, type='int'),
|
||||
}
|
||||
aireos_argument_spec.update(aireos_top_spec)
|
||||
|
||||
|
||||
def sanitize(resp):
|
||||
# Takes response from device and strips whitespace from all lines
|
||||
# Aireos adds in extra preceding whitespace which netcfg parses as children/parents, which Aireos does not do
|
||||
# Aireos also adds in trailing whitespace that is unused
|
||||
cleaned = []
|
||||
for line in resp.splitlines():
|
||||
cleaned.append(line.strip())
|
||||
return '\n'.join(cleaned).strip()
|
||||
|
||||
|
||||
def get_provider_argspec():
|
||||
return aireos_provider_spec
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
pass
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
cmd = 'show run-config commands '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
try:
|
||||
return _DEVICE_CONFIGS[cmd]
|
||||
except KeyError:
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg='unable to retrieve current config', stderr=to_text(err, errors='surrogate_then_replace'))
|
||||
cfg = sanitize(to_text(out, errors='surrogate_then_replace').strip())
|
||||
_DEVICE_CONFIGS[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
|
||||
def to_commands(module, commands):
|
||||
spec = {
|
||||
'command': dict(key=True),
|
||||
'prompt': dict(),
|
||||
'answer': dict()
|
||||
}
|
||||
transform = ComplexList(spec, module)
|
||||
return transform(commands)
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
responses = list()
|
||||
commands = to_commands(module, to_list(commands))
|
||||
for cmd in commands:
|
||||
cmd = module.jsonify(cmd)
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if check_rc and rc != 0:
|
||||
module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), rc=rc)
|
||||
responses.append(sanitize(to_text(out, errors='surrogate_then_replace')))
|
||||
return responses
|
||||
|
||||
|
||||
def load_config(module, commands):
|
||||
|
||||
rc, out, err = exec_command(module, 'config')
|
||||
if rc != 0:
|
||||
module.fail_json(msg='unable to enter configuration mode', err=to_text(out, errors='surrogate_then_replace'))
|
||||
|
||||
for command in to_list(commands):
|
||||
if command == 'end':
|
||||
continue
|
||||
rc, out, err = exec_command(module, command)
|
||||
if rc != 0:
|
||||
module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc)
|
||||
|
||||
exec_command(module, 'end')
|
||||
0
plugins/module_utils/network/aos/__init__.py
Normal file
0
plugins/module_utils/network/aos/__init__.py
Normal file
180
plugins/module_utils/network/aos/aos.py
Normal file
180
plugins/module_utils/network/aos/aos.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#
|
||||
# Copyright (c) 2017 Apstra Inc, <community@apstra.com>
|
||||
#
|
||||
# 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.network.aos.aos import (check_aos_version, get_aos_session, find_collection_item,
|
||||
content_to_dict, do_load_resource)
|
||||
|
||||
"""
|
||||
import json
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
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
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
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 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=<token>,
|
||||
server=<ip>,
|
||||
port=<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.safe_load(content)
|
||||
|
||||
if not isinstance(content_dict, dict):
|
||||
raise Exception()
|
||||
|
||||
# Check if dict is empty and return an error if it's
|
||||
if not content_dict:
|
||||
raise Exception()
|
||||
|
||||
except Exception:
|
||||
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 Exception:
|
||||
module.fail_json(msg="An error occurred 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 Exception as e:
|
||||
module.fail_json(msg="Unable to write item content : %r" % to_native(e))
|
||||
|
||||
module.exit_json(changed=True, name=item.name, id=item.id, value=item.value)
|
||||
0
plugins/module_utils/network/apconos/__init__.py
Normal file
0
plugins/module_utils/network/apconos/__init__.py
Normal file
113
plugins/module_utils/network/apconos/apconos.py
Normal file
113
plugins/module_utils/network/apconos/apconos.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (C) 2019 APCON, Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Contains utility methods
|
||||
# APCON Networking
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import EntityCollection
|
||||
from ansible.module_utils.connection import Connection, exec_command
|
||||
from ansible.module_utils.connection import ConnectionError
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
_CONNECTION = None
|
||||
|
||||
|
||||
command_spec = {
|
||||
'command': dict(key=True),
|
||||
}
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
pass
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
global _CONNECTION
|
||||
if _CONNECTION:
|
||||
return _CONNECTION
|
||||
_CONNECTION = Connection(module._socket_path)
|
||||
|
||||
return _CONNECTION
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
cmd = ' '.join(flags).strip()
|
||||
|
||||
try:
|
||||
return _DEVICE_CONFIGS[cmd]
|
||||
except KeyError:
|
||||
conn = get_connection(module)
|
||||
out = conn.get(cmd)
|
||||
cfg = to_text(out, errors='surrogate_then_replace').strip()
|
||||
_DEVICE_CONFIGS[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
connection = get_connection(module)
|
||||
transform = EntityCollection(module, command_spec)
|
||||
commands = transform(commands)
|
||||
|
||||
responses = list()
|
||||
|
||||
for cmd in commands:
|
||||
out = connection.get(**cmd)
|
||||
responses.append(to_text(out, errors='surrogate_then_replace'))
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
def load_config(module, config):
|
||||
try:
|
||||
conn = get_connection(module)
|
||||
conn.edit_config(config)
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
|
||||
|
||||
def get_defaults_flag(module):
|
||||
rc, out, err = exec_command(module, 'display running-config ?')
|
||||
out = to_text(out, errors='surrogate_then_replace')
|
||||
|
||||
commands = set()
|
||||
for line in out.splitlines():
|
||||
if line:
|
||||
commands.add(line.strip().split()[0])
|
||||
|
||||
if 'all' in commands:
|
||||
return 'all'
|
||||
else:
|
||||
return 'full'
|
||||
0
plugins/module_utils/network/aruba/__init__.py
Normal file
0
plugins/module_utils/network/aruba/__init__.py
Normal file
131
plugins/module_utils/network/aruba/aruba.py
Normal file
131
plugins/module_utils/network/aruba/aruba.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2016 Red Hat Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.connection import exec_command
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
|
||||
aruba_provider_spec = {
|
||||
'host': dict(),
|
||||
'port': dict(type='int'),
|
||||
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
|
||||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
|
||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
|
||||
'timeout': dict(type='int'),
|
||||
}
|
||||
aruba_argument_spec = {
|
||||
'provider': dict(type='dict', options=aruba_provider_spec)
|
||||
}
|
||||
|
||||
aruba_top_spec = {
|
||||
'host': dict(removed_in_version=2.9),
|
||||
'port': dict(removed_in_version=2.9, type='int'),
|
||||
'username': dict(removed_in_version=2.9),
|
||||
'password': dict(removed_in_version=2.9, no_log=True),
|
||||
'ssh_keyfile': dict(removed_in_version=2.9, type='path'),
|
||||
'timeout': dict(removed_in_version=2.9, type='int'),
|
||||
}
|
||||
|
||||
aruba_argument_spec.update(aruba_top_spec)
|
||||
|
||||
|
||||
def get_provider_argspec():
|
||||
return aruba_provider_spec
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
pass
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
cmd = 'show running-config '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
try:
|
||||
return _DEVICE_CONFIGS[cmd]
|
||||
except KeyError:
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg='unable to retrieve current config', stderr=to_text(err, errors='surrogate_then_replace'))
|
||||
cfg = sanitize(to_text(out, errors='surrogate_then_replace').strip())
|
||||
_DEVICE_CONFIGS[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
|
||||
def sanitize(resp):
|
||||
# Takes response from device and adjusts leading whitespace to just 1 space
|
||||
cleaned = []
|
||||
for line in resp.splitlines():
|
||||
cleaned.append(re.sub(r"^\s+", " ", line))
|
||||
return '\n'.join(cleaned).strip()
|
||||
|
||||
|
||||
def to_commands(module, commands):
|
||||
spec = {
|
||||
'command': dict(key=True),
|
||||
'prompt': dict(),
|
||||
'answer': dict()
|
||||
}
|
||||
transform = ComplexList(spec, module)
|
||||
return transform(commands)
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
responses = list()
|
||||
commands = to_commands(module, to_list(commands))
|
||||
for cmd in commands:
|
||||
cmd = module.jsonify(cmd)
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if check_rc and rc != 0:
|
||||
module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), rc=rc)
|
||||
responses.append(to_text(out, errors='surrogate_then_replace'))
|
||||
return responses
|
||||
|
||||
|
||||
def load_config(module, commands):
|
||||
|
||||
rc, out, err = exec_command(module, 'configure terminal')
|
||||
if rc != 0:
|
||||
module.fail_json(msg='unable to enter configuration mode', err=to_text(out, errors='surrogate_then_replace'))
|
||||
|
||||
for command in to_list(commands):
|
||||
if command == 'end':
|
||||
continue
|
||||
rc, out, err = exec_command(module, command)
|
||||
if rc != 0:
|
||||
module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc)
|
||||
|
||||
exec_command(module, 'end')
|
||||
0
plugins/module_utils/network/avi/__init__.py
Normal file
0
plugins/module_utils/network/avi/__init__.py
Normal file
572
plugins/module_utils/network/avi/ansible_utils.py
Normal file
572
plugins/module_utils/network/avi/ansible_utils.py
Normal file
@@ -0,0 +1,572 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
"""
|
||||
Created on Aug 16, 2016
|
||||
|
||||
@author: Gaurav Rastogi (grastogi@avinetworks.com)
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.avi.avi_api import (
|
||||
ApiSession, ObjectNotFound, avi_sdk_syslog_logger, AviCredentials, HAS_AVI)
|
||||
except ImportError:
|
||||
HAS_AVI = False
|
||||
|
||||
|
||||
if os.environ.get('AVI_LOG_HANDLER', '') != 'syslog':
|
||||
log = logging.getLogger(__name__)
|
||||
else:
|
||||
# Ansible does not allow logging from the modules.
|
||||
log = avi_sdk_syslog_logger()
|
||||
|
||||
|
||||
def _check_type_string(x):
|
||||
"""
|
||||
:param x:
|
||||
:return: True if it is of type string
|
||||
"""
|
||||
if isinstance(x, str):
|
||||
return True
|
||||
if sys.version_info[0] < 3:
|
||||
try:
|
||||
return isinstance(x, unicode)
|
||||
except NameError:
|
||||
return False
|
||||
|
||||
|
||||
class AviCheckModeResponse(object):
|
||||
"""
|
||||
Class to support ansible check mode.
|
||||
"""
|
||||
|
||||
def __init__(self, obj, status_code=200):
|
||||
self.obj = obj
|
||||
self.status_code = status_code
|
||||
|
||||
def json(self):
|
||||
return self.obj
|
||||
|
||||
|
||||
def ansible_return(module, rsp, changed, req=None, existing_obj=None,
|
||||
api_context=None):
|
||||
"""
|
||||
:param module: AnsibleModule
|
||||
:param rsp: ApiResponse from avi_api
|
||||
:param changed: boolean
|
||||
:param req: ApiRequest to avi_api
|
||||
:param existing_obj: object to be passed debug output
|
||||
:param api_context: api login context
|
||||
|
||||
helper function to return the right ansible based on the error code and
|
||||
changed
|
||||
Returns: specific ansible module exit function
|
||||
"""
|
||||
|
||||
if rsp is not None and rsp.status_code > 299:
|
||||
return module.fail_json(
|
||||
msg='Error %d Msg %s req: %s api_context:%s ' % (
|
||||
rsp.status_code, rsp.text, req, api_context))
|
||||
api_creds = AviCredentials()
|
||||
api_creds.update_from_ansible_module(module)
|
||||
key = '%s:%s:%s' % (api_creds.controller, api_creds.username,
|
||||
api_creds.port)
|
||||
disable_fact = module.params.get('avi_disable_session_cache_as_fact')
|
||||
|
||||
fact_context = None
|
||||
if not disable_fact:
|
||||
fact_context = module.params.get('api_context', {})
|
||||
if fact_context:
|
||||
fact_context.update({key: api_context})
|
||||
else:
|
||||
fact_context = {key: api_context}
|
||||
|
||||
obj_val = rsp.json() if rsp else existing_obj
|
||||
|
||||
if (obj_val and module.params.get("obj_username", None) and
|
||||
"username" in obj_val):
|
||||
obj_val["obj_username"] = obj_val["username"]
|
||||
if (obj_val and module.params.get("obj_password", None) and
|
||||
"password" in obj_val):
|
||||
obj_val["obj_password"] = obj_val["password"]
|
||||
old_obj_val = existing_obj if changed and existing_obj else None
|
||||
api_context_val = api_context if disable_fact else None
|
||||
ansible_facts_val = dict(
|
||||
avi_api_context=fact_context) if not disable_fact else {}
|
||||
|
||||
return module.exit_json(
|
||||
changed=changed, obj=obj_val, old_obj=old_obj_val,
|
||||
ansible_facts=ansible_facts_val, api_context=api_context_val)
|
||||
|
||||
|
||||
def purge_optional_fields(obj, module):
|
||||
"""
|
||||
It purges the optional arguments to be sent to the controller.
|
||||
:param obj: dictionary of the ansible object passed as argument.
|
||||
:param module: AnsibleModule
|
||||
return modified obj
|
||||
"""
|
||||
purge_fields = []
|
||||
for param, spec in module.argument_spec.items():
|
||||
if not spec.get('required', False):
|
||||
if param not in obj:
|
||||
# these are ansible common items
|
||||
continue
|
||||
if obj[param] is None:
|
||||
purge_fields.append(param)
|
||||
log.debug('purging fields %s', purge_fields)
|
||||
for param in purge_fields:
|
||||
obj.pop(param, None)
|
||||
return obj
|
||||
|
||||
|
||||
def cleanup_absent_fields(obj):
|
||||
"""
|
||||
cleans up any field that is marked as state: absent. It needs to be removed
|
||||
from the object if it is present.
|
||||
:param obj:
|
||||
:return: Purged object
|
||||
"""
|
||||
if type(obj) != dict:
|
||||
return obj
|
||||
cleanup_keys = []
|
||||
for k, v in obj.items():
|
||||
if type(v) == dict:
|
||||
if (('state' in v and v['state'] == 'absent') or
|
||||
(v == "{'state': 'absent'}")):
|
||||
cleanup_keys.append(k)
|
||||
else:
|
||||
cleanup_absent_fields(v)
|
||||
if not v:
|
||||
cleanup_keys.append(k)
|
||||
elif type(v) == list:
|
||||
new_list = []
|
||||
for elem in v:
|
||||
elem = cleanup_absent_fields(elem)
|
||||
if elem:
|
||||
# remove the item from list
|
||||
new_list.append(elem)
|
||||
if new_list:
|
||||
obj[k] = new_list
|
||||
else:
|
||||
cleanup_keys.append(k)
|
||||
elif isinstance(v, str) or isinstance(v, str):
|
||||
if v == "{'state': 'absent'}":
|
||||
cleanup_keys.append(k)
|
||||
for k in cleanup_keys:
|
||||
del obj[k]
|
||||
return obj
|
||||
|
||||
|
||||
RE_REF_MATCH = re.compile(r'^/api/[\w/]+\?name\=[\w]+[^#<>]*$')
|
||||
# if HTTP ref match then strip out the #name
|
||||
HTTP_REF_MATCH = re.compile(r'https://[\w.0-9:-]+/api/.+')
|
||||
HTTP_REF_W_NAME_MATCH = re.compile(r'https://[\w.0-9:-]+/api/.*#.+')
|
||||
|
||||
|
||||
def ref_n_str_cmp(x, y):
|
||||
"""
|
||||
compares two references
|
||||
1. check for exact reference
|
||||
2. check for obj_type/uuid
|
||||
3. check for name
|
||||
|
||||
if x is ref=name then extract uuid and name from y and use it.
|
||||
if x is http_ref then
|
||||
strip x and y
|
||||
compare them.
|
||||
|
||||
if x and y are urls then match with split on #
|
||||
if x is a RE_REF_MATCH then extract name
|
||||
if y is a REF_MATCH then extract name
|
||||
:param x: first string
|
||||
:param y: second string from controller's object
|
||||
|
||||
Returns
|
||||
True if they are equivalent else False
|
||||
"""
|
||||
if type(y) in (int, float, bool, int, complex):
|
||||
y = str(y)
|
||||
x = str(x)
|
||||
if not (_check_type_string(x) and _check_type_string(y)):
|
||||
return False
|
||||
y_uuid = y_name = str(y)
|
||||
x = str(x)
|
||||
if RE_REF_MATCH.match(x):
|
||||
x = x.split('name=')[1]
|
||||
elif HTTP_REF_MATCH.match(x):
|
||||
x = x.rsplit('#', 1)[0]
|
||||
y = y.rsplit('#', 1)[0]
|
||||
elif RE_REF_MATCH.match(y):
|
||||
y = y.split('name=')[1]
|
||||
|
||||
if HTTP_REF_W_NAME_MATCH.match(y):
|
||||
path = y.split('api/', 1)[1]
|
||||
# Fetching name or uuid from path /xxxx_xx/xx/xx_x/uuid_or_name
|
||||
uuid_or_name = path.split('/')[-1]
|
||||
parts = uuid_or_name.rsplit('#', 1)
|
||||
y_uuid = parts[0]
|
||||
y_name = parts[1] if len(parts) > 1 else ''
|
||||
# is just string but y is a url so match either uuid or name
|
||||
result = (x in (y, y_name, y_uuid))
|
||||
if not result:
|
||||
log.debug('x: %s y: %s y_name %s y_uuid %s',
|
||||
x, y, y_name, y_uuid)
|
||||
return result
|
||||
|
||||
|
||||
def avi_obj_cmp(x, y, sensitive_fields=None):
|
||||
"""
|
||||
compares whether x is fully contained in y. The comparision is different
|
||||
from a simple dictionary compare for following reasons
|
||||
1. Some fields could be references. The object in controller returns the
|
||||
full URL for those references. However, the ansible script would have
|
||||
it specified as /api/pool?name=blah. So, the reference fields need
|
||||
to match uuid, relative reference based on name and actual reference.
|
||||
|
||||
2. Optional fields with defaults: In case there are optional fields with
|
||||
defaults then controller automatically fills it up. This would
|
||||
cause the comparison with Ansible object specification to always return
|
||||
changed.
|
||||
|
||||
3. Optional fields without defaults: This is most tricky. The issue is
|
||||
how to specify deletion of such objects from ansible script. If the
|
||||
ansible playbook has object specified as Null then Avi controller will
|
||||
reject for non Message(dict) type fields. In addition, to deal with the
|
||||
defaults=null issue all the fields that are set with None are purged
|
||||
out before comparing with Avi controller's version
|
||||
|
||||
So, the solution is to pass state: absent if any optional field needs
|
||||
to be deleted from the configuration. The script would return changed
|
||||
=true if it finds a key in the controller version and it is marked with
|
||||
state: absent in ansible playbook. Alternatively, it would return
|
||||
false if key is not present in the controller object. Before, doing
|
||||
put or post it would purge the fields that are marked state: absent.
|
||||
|
||||
:param x: first string
|
||||
:param y: second string from controller's object
|
||||
:param sensitive_fields: sensitive fields to ignore for diff
|
||||
|
||||
Returns:
|
||||
True if x is subset of y else False
|
||||
"""
|
||||
if not sensitive_fields:
|
||||
sensitive_fields = set()
|
||||
if isinstance(x, str) or isinstance(x, str):
|
||||
# Special handling for strings as they can be references.
|
||||
return ref_n_str_cmp(x, y)
|
||||
if type(x) not in [list, dict]:
|
||||
# if it is not list or dict or string then simply compare the values
|
||||
return x == y
|
||||
if type(x) == list:
|
||||
# should compare each item in the list and that should match
|
||||
if len(x) != len(y):
|
||||
log.debug('x has %d items y has %d', len(x), len(y))
|
||||
return False
|
||||
for i in zip(x, y):
|
||||
if not avi_obj_cmp(i[0], i[1], sensitive_fields=sensitive_fields):
|
||||
# no need to continue
|
||||
return False
|
||||
|
||||
if type(x) == dict:
|
||||
x.pop('_last_modified', None)
|
||||
x.pop('tenant', None)
|
||||
y.pop('_last_modified', None)
|
||||
x.pop('api_version', None)
|
||||
y.pop('api_verison', None)
|
||||
d_xks = [k for k in x.keys() if k in sensitive_fields]
|
||||
|
||||
if d_xks:
|
||||
# if there is sensitive field then always return changed
|
||||
return False
|
||||
# pop the keys that are marked deleted but not present in y
|
||||
# return false if item is marked absent and is present in y
|
||||
d_x_absent_ks = []
|
||||
for k, v in x.items():
|
||||
if v is None:
|
||||
d_x_absent_ks.append(k)
|
||||
continue
|
||||
if isinstance(v, dict):
|
||||
if ('state' in v) and (v['state'] == 'absent'):
|
||||
if type(y) == dict and k not in y:
|
||||
d_x_absent_ks.append(k)
|
||||
else:
|
||||
return False
|
||||
elif not v:
|
||||
d_x_absent_ks.append(k)
|
||||
elif isinstance(v, list) and not v:
|
||||
d_x_absent_ks.append(k)
|
||||
# Added condition to check key in dict.
|
||||
elif isinstance(v, str) or (k in y and isinstance(y[k], str)):
|
||||
# this is the case when ansible converts the dictionary into a
|
||||
# string.
|
||||
if v == "{'state': 'absent'}" and k not in y:
|
||||
d_x_absent_ks.append(k)
|
||||
elif not v and k not in y:
|
||||
# this is the case when x has set the value that qualifies
|
||||
# as not but y does not have that value
|
||||
d_x_absent_ks.append(k)
|
||||
for k in d_x_absent_ks:
|
||||
x.pop(k)
|
||||
x_keys = set(x.keys())
|
||||
y_keys = set(y.keys())
|
||||
if not x_keys.issubset(y_keys):
|
||||
# log.debug('x has %s and y has %s keys', len(x_keys), len(y_keys))
|
||||
return False
|
||||
for k, v in x.items():
|
||||
if k not in y:
|
||||
# log.debug('k %s is not in y %s', k, y)
|
||||
return False
|
||||
if not avi_obj_cmp(v, y[k], sensitive_fields=sensitive_fields):
|
||||
# log.debug('k %s v %s did not match in y %s', k, v, y[k])
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
POP_FIELDS = ['state', 'controller', 'username', 'password', 'api_version',
|
||||
'avi_credentials', 'avi_api_update_method', 'avi_api_patch_op',
|
||||
'api_context', 'tenant', 'tenant_uuid', 'avi_disable_session_cache_as_fact']
|
||||
|
||||
|
||||
def get_api_context(module, api_creds):
|
||||
api_context = module.params.get('api_context')
|
||||
if api_context and module.params.get('avi_disable_session_cache_as_fact'):
|
||||
return api_context
|
||||
elif api_context and not module.params.get(
|
||||
'avi_disable_session_cache_as_fact'):
|
||||
key = '%s:%s:%s' % (api_creds.controller, api_creds.username,
|
||||
api_creds.port)
|
||||
return api_context.get(key)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def avi_ansible_api(module, obj_type, sensitive_fields):
|
||||
"""
|
||||
This converts the Ansible module into AVI object and invokes APIs
|
||||
:param module: Ansible module
|
||||
:param obj_type: string representing Avi object type
|
||||
:param sensitive_fields: sensitive fields to be excluded for comparison
|
||||
purposes.
|
||||
Returns:
|
||||
success: module.exit_json with obj=avi object
|
||||
faliure: module.fail_json
|
||||
"""
|
||||
|
||||
api_creds = AviCredentials()
|
||||
api_creds.update_from_ansible_module(module)
|
||||
api_context = get_api_context(module, api_creds)
|
||||
if api_context:
|
||||
api = ApiSession.get_session(
|
||||
api_creds.controller,
|
||||
api_creds.username,
|
||||
password=api_creds.password,
|
||||
timeout=api_creds.timeout,
|
||||
tenant=api_creds.tenant,
|
||||
tenant_uuid=api_creds.tenant_uuid,
|
||||
token=api_context['csrftoken'],
|
||||
port=api_creds.port,
|
||||
session_id=api_context['session_id'],
|
||||
csrftoken=api_context['csrftoken'])
|
||||
else:
|
||||
api = ApiSession.get_session(
|
||||
api_creds.controller,
|
||||
api_creds.username,
|
||||
password=api_creds.password,
|
||||
timeout=api_creds.timeout,
|
||||
tenant=api_creds.tenant,
|
||||
tenant_uuid=api_creds.tenant_uuid,
|
||||
token=api_creds.token,
|
||||
port=api_creds.port)
|
||||
state = module.params['state']
|
||||
# Get the api version.
|
||||
avi_update_method = module.params.get('avi_api_update_method', 'put')
|
||||
avi_patch_op = module.params.get('avi_api_patch_op', 'add')
|
||||
|
||||
api_version = api_creds.api_version
|
||||
name = module.params.get('name', None)
|
||||
# Added Support to get uuid
|
||||
uuid = module.params.get('uuid', None)
|
||||
check_mode = module.check_mode
|
||||
if uuid and obj_type != 'cluster':
|
||||
obj_path = '%s/%s' % (obj_type, uuid)
|
||||
else:
|
||||
obj_path = '%s/' % obj_type
|
||||
obj = deepcopy(module.params)
|
||||
tenant = obj.pop('tenant', '')
|
||||
tenant_uuid = obj.pop('tenant_uuid', '')
|
||||
# obj.pop('cloud_ref', None)
|
||||
for k in POP_FIELDS:
|
||||
obj.pop(k, None)
|
||||
purge_optional_fields(obj, module)
|
||||
|
||||
# Special code to handle situation where object has a field
|
||||
# named username. This is used in case of api/user
|
||||
# The following code copies the username and password
|
||||
# from the obj_username and obj_password fields.
|
||||
if 'obj_username' in obj:
|
||||
obj['username'] = obj['obj_username']
|
||||
obj.pop('obj_username')
|
||||
if 'obj_password' in obj:
|
||||
obj['password'] = obj['obj_password']
|
||||
obj.pop('obj_password')
|
||||
if 'full_name' not in obj and 'name' in obj and obj_type == "user":
|
||||
obj['full_name'] = obj['name']
|
||||
# Special case as name represent full_name in user module
|
||||
# As per API response, name is always same as username regardless of full_name
|
||||
obj['name'] = obj['username']
|
||||
|
||||
log.info('passed object %s ', obj)
|
||||
|
||||
if uuid:
|
||||
# Get the object based on uuid.
|
||||
try:
|
||||
existing_obj = api.get(
|
||||
obj_path, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
params={'include_refs': '', 'include_name': ''},
|
||||
api_version=api_version)
|
||||
existing_obj = existing_obj.json()
|
||||
except ObjectNotFound:
|
||||
existing_obj = None
|
||||
elif name:
|
||||
params = {'include_refs': '', 'include_name': ''}
|
||||
if obj.get('cloud_ref', None):
|
||||
# this is the case when gets have to be scoped with cloud
|
||||
cloud = obj['cloud_ref'].split('name=')[1]
|
||||
params['cloud_ref.name'] = cloud
|
||||
existing_obj = api.get_object_by_name(
|
||||
obj_type, name, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
params=params, api_version=api_version)
|
||||
|
||||
# Need to check if tenant_ref was provided and the object returned
|
||||
# is actually in admin tenant.
|
||||
if existing_obj and 'tenant_ref' in obj and 'tenant_ref' in existing_obj:
|
||||
# https://10.10.25.42/api/tenant/admin#admin
|
||||
existing_obj_tenant = existing_obj['tenant_ref'].split('#')[1]
|
||||
obj_tenant = obj['tenant_ref'].split('name=')[1]
|
||||
if obj_tenant != existing_obj_tenant:
|
||||
existing_obj = None
|
||||
else:
|
||||
# added api version to avi api call.
|
||||
existing_obj = api.get(obj_path, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
params={'include_refs': '', 'include_name': ''},
|
||||
api_version=api_version).json()
|
||||
|
||||
if state == 'absent':
|
||||
rsp = None
|
||||
changed = False
|
||||
err = False
|
||||
if not check_mode and existing_obj:
|
||||
try:
|
||||
if name is not None:
|
||||
# added api version to avi api call.
|
||||
rsp = api.delete_by_name(
|
||||
obj_type, name, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
api_version=api_version)
|
||||
else:
|
||||
# added api version to avi api call.
|
||||
rsp = api.delete(
|
||||
obj_path, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
api_version=api_version)
|
||||
except ObjectNotFound:
|
||||
pass
|
||||
if check_mode and existing_obj:
|
||||
changed = True
|
||||
|
||||
if rsp:
|
||||
if rsp.status_code == 204:
|
||||
changed = True
|
||||
else:
|
||||
err = True
|
||||
if not err:
|
||||
return ansible_return(
|
||||
module, rsp, changed, existing_obj=existing_obj,
|
||||
api_context=api.get_context())
|
||||
elif rsp:
|
||||
return module.fail_json(msg=rsp.text)
|
||||
|
||||
rsp = None
|
||||
req = None
|
||||
if existing_obj:
|
||||
# this is case of modify as object exists. should find out
|
||||
# if changed is true or not
|
||||
if name is not None and obj_type != 'cluster':
|
||||
obj_uuid = existing_obj['uuid']
|
||||
obj_path = '%s/%s' % (obj_type, obj_uuid)
|
||||
if avi_update_method == 'put':
|
||||
changed = not avi_obj_cmp(obj, existing_obj, sensitive_fields)
|
||||
obj = cleanup_absent_fields(obj)
|
||||
if changed:
|
||||
req = obj
|
||||
if check_mode:
|
||||
# No need to process any further.
|
||||
rsp = AviCheckModeResponse(obj=existing_obj)
|
||||
else:
|
||||
rsp = api.put(
|
||||
obj_path, data=req, tenant=tenant,
|
||||
tenant_uuid=tenant_uuid, api_version=api_version)
|
||||
elif check_mode:
|
||||
rsp = AviCheckModeResponse(obj=existing_obj)
|
||||
else:
|
||||
if check_mode:
|
||||
# No need to process any further.
|
||||
rsp = AviCheckModeResponse(obj=existing_obj)
|
||||
changed = True
|
||||
else:
|
||||
obj.pop('name', None)
|
||||
patch_data = {avi_patch_op: obj}
|
||||
rsp = api.patch(
|
||||
obj_path, data=patch_data, tenant=tenant,
|
||||
tenant_uuid=tenant_uuid, api_version=api_version)
|
||||
obj = rsp.json()
|
||||
changed = not avi_obj_cmp(obj, existing_obj)
|
||||
if changed:
|
||||
log.debug('EXISTING OBJ %s', existing_obj)
|
||||
log.debug('NEW OBJ %s', obj)
|
||||
else:
|
||||
changed = True
|
||||
req = obj
|
||||
if check_mode:
|
||||
rsp = AviCheckModeResponse(obj=None)
|
||||
else:
|
||||
rsp = api.post(obj_type, data=obj, tenant=tenant,
|
||||
tenant_uuid=tenant_uuid, api_version=api_version)
|
||||
return ansible_return(module, rsp, changed, req, existing_obj=existing_obj,
|
||||
api_context=api.get_context())
|
||||
|
||||
|
||||
def avi_common_argument_spec():
|
||||
"""
|
||||
Returns common arguments for all Avi modules
|
||||
:return: dict
|
||||
"""
|
||||
credentials_spec = dict(
|
||||
controller=dict(fallback=(env_fallback, ['AVI_CONTROLLER'])),
|
||||
username=dict(fallback=(env_fallback, ['AVI_USERNAME'])),
|
||||
password=dict(fallback=(env_fallback, ['AVI_PASSWORD']), no_log=True),
|
||||
api_version=dict(default='16.4.4', type='str'),
|
||||
tenant=dict(default='admin'),
|
||||
tenant_uuid=dict(default='', type='str'),
|
||||
port=dict(type='int'),
|
||||
timeout=dict(default=300, type='int'),
|
||||
token=dict(default='', type='str', no_log=True),
|
||||
session_id=dict(default='', type='str', no_log=True),
|
||||
csrftoken=dict(default='', type='str', no_log=True)
|
||||
)
|
||||
|
||||
return dict(
|
||||
controller=dict(fallback=(env_fallback, ['AVI_CONTROLLER'])),
|
||||
username=dict(fallback=(env_fallback, ['AVI_USERNAME'])),
|
||||
password=dict(fallback=(env_fallback, ['AVI_PASSWORD']), no_log=True),
|
||||
tenant=dict(default='admin'),
|
||||
tenant_uuid=dict(default=''),
|
||||
api_version=dict(default='16.4.4', type='str'),
|
||||
avi_credentials=dict(default=None, type='dict',
|
||||
options=credentials_spec),
|
||||
api_context=dict(type='dict'),
|
||||
avi_disable_session_cache_as_fact=dict(default=False, type='bool'))
|
||||
38
plugins/module_utils/network/avi/avi.py
Normal file
38
plugins/module_utils/network/avi/avi.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c), Gaurav Rastogi <grastogi@avinetworks.com>, 2017
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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 initially matched the namespace of network module avi. However,
|
||||
# that causes namespace import error when other modules from avi namespaces
|
||||
# are imported. Added import of absolute_import to avoid import collisions for
|
||||
# avi.sdk.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.network.avi.ansible_utils import (
|
||||
avi_ansible_api, avi_common_argument_spec, ansible_return,
|
||||
avi_obj_cmp, cleanup_absent_fields, AviCheckModeResponse, HAS_AVI)
|
||||
972
plugins/module_utils/network/avi/avi_api.py
Normal file
972
plugins/module_utils/network/avi/avi_api.py
Normal file
@@ -0,0 +1,972 @@
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
import sys
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from ssl import SSLError
|
||||
|
||||
|
||||
class MockResponse(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise Exception("Requests library Response object not found. Using fake one.")
|
||||
|
||||
|
||||
class MockRequestsConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MockSession(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise Exception("Requests library Session object not found. Using fake one.")
|
||||
|
||||
|
||||
HAS_AVI = True
|
||||
try:
|
||||
from requests import ConnectionError as RequestsConnectionError
|
||||
from requests import Response
|
||||
from requests.sessions import Session
|
||||
except ImportError:
|
||||
HAS_AVI = False
|
||||
Response = MockResponse
|
||||
RequestsConnectionError = MockRequestsConnectionError
|
||||
Session = MockSession
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sessionDict = {}
|
||||
|
||||
|
||||
def avi_timedelta(td):
|
||||
'''
|
||||
This is a wrapper class to workaround python 2.6 builtin datetime.timedelta
|
||||
does not have total_seconds method
|
||||
:param timedelta object
|
||||
'''
|
||||
if type(td) != timedelta:
|
||||
raise TypeError()
|
||||
if sys.version_info >= (2, 7):
|
||||
ts = td.total_seconds()
|
||||
else:
|
||||
ts = td.seconds + (24 * 3600 * td.days)
|
||||
return ts
|
||||
|
||||
|
||||
def avi_sdk_syslog_logger(logger_name='avi.sdk'):
|
||||
# The following sets up syslog module to log underlying avi SDK messages
|
||||
# based on the environment variables:
|
||||
# AVI_LOG_HANDLER: names the logging handler to use. Only syslog is
|
||||
# supported.
|
||||
# AVI_LOG_LEVEL: Logging level used for the avi SDK. Default is DEBUG
|
||||
# AVI_SYSLOG_ADDRESS: Destination address for the syslog handler.
|
||||
# Default is /dev/log
|
||||
from logging.handlers import SysLogHandler
|
||||
lf = '[%(asctime)s] %(levelname)s [%(module)s.%(funcName)s:%(lineno)d] %(message)s'
|
||||
log = logging.getLogger(logger_name)
|
||||
log_level = os.environ.get('AVI_LOG_LEVEL', 'DEBUG')
|
||||
if log_level:
|
||||
log.setLevel(getattr(logging, log_level))
|
||||
formatter = logging.Formatter(lf)
|
||||
sh = SysLogHandler(address=os.environ.get('AVI_SYSLOG_ADDRESS', '/dev/log'))
|
||||
sh.setFormatter(formatter)
|
||||
log.addHandler(sh)
|
||||
return log
|
||||
|
||||
|
||||
class ObjectNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
def __init__(self, arg, rsp=None):
|
||||
self.args = [arg, rsp]
|
||||
self.rsp = rsp
|
||||
|
||||
|
||||
class AviServerError(APIError):
|
||||
def __init__(self, arg, rsp=None):
|
||||
super(AviServerError, self).__init__(arg, rsp)
|
||||
|
||||
|
||||
class APINotImplemented(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ApiResponse(Response):
|
||||
"""
|
||||
Returns copy of the requests.Response object provides additional helper
|
||||
routines
|
||||
1. obj: returns dictionary of Avi Object
|
||||
"""
|
||||
def __init__(self, rsp):
|
||||
super(ApiResponse, self).__init__()
|
||||
for k, v in list(rsp.__dict__.items()):
|
||||
setattr(self, k, v)
|
||||
|
||||
def json(self):
|
||||
"""
|
||||
Extends the session default json interface to handle special errors
|
||||
and raise Exceptions
|
||||
returns the Avi object as a dictionary from rsp.text
|
||||
"""
|
||||
if self.status_code in (200, 201):
|
||||
if not self.text:
|
||||
# In cases like status_code == 201 the response text could be
|
||||
# empty string.
|
||||
return None
|
||||
return super(ApiResponse, self).json()
|
||||
elif self.status_code == 204:
|
||||
# No response needed; e.g., delete operation
|
||||
return None
|
||||
elif self.status_code == 404:
|
||||
raise ObjectNotFound('HTTP Error: %s Error Msg %s' % (
|
||||
self.status_code, self.text), self)
|
||||
elif self.status_code >= 500:
|
||||
raise AviServerError('HTTP Error: %s Error Msg %s' % (
|
||||
self.status_code, self.text), self)
|
||||
else:
|
||||
raise APIError('HTTP Error: %s Error Msg %s' % (
|
||||
self.status_code, self.text), self)
|
||||
|
||||
def count(self):
|
||||
"""
|
||||
return the number of objects in the collection response. If it is not
|
||||
a collection response then it would simply return 1.
|
||||
"""
|
||||
obj = self.json()
|
||||
if 'count' in obj:
|
||||
# this was a resposne to collection
|
||||
return obj['count']
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def to_avi_response(resp):
|
||||
if type(resp) == Response:
|
||||
return ApiResponse(resp)
|
||||
return resp
|
||||
|
||||
|
||||
class AviCredentials(object):
|
||||
controller = ''
|
||||
username = ''
|
||||
password = ''
|
||||
api_version = '16.4.4'
|
||||
tenant = None
|
||||
tenant_uuid = None
|
||||
token = None
|
||||
port = None
|
||||
timeout = 300
|
||||
session_id = None
|
||||
csrftoken = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def update_from_ansible_module(self, m):
|
||||
"""
|
||||
:param m: ansible module
|
||||
:return:
|
||||
"""
|
||||
if m.params.get('avi_credentials'):
|
||||
for k, v in m.params['avi_credentials'].items():
|
||||
if hasattr(self, k):
|
||||
setattr(self, k, v)
|
||||
if m.params['controller']:
|
||||
self.controller = m.params['controller']
|
||||
if m.params['username']:
|
||||
self.username = m.params['username']
|
||||
if m.params['password']:
|
||||
self.password = m.params['password']
|
||||
if (m.params['api_version'] and
|
||||
(m.params['api_version'] != '16.4.4')):
|
||||
self.api_version = m.params['api_version']
|
||||
if m.params['tenant']:
|
||||
self.tenant = m.params['tenant']
|
||||
if m.params['tenant_uuid']:
|
||||
self.tenant_uuid = m.params['tenant_uuid']
|
||||
if m.params.get('session_id'):
|
||||
self.session_id = m.params['session_id']
|
||||
if m.params.get('csrftoken'):
|
||||
self.csrftoken = m.params['csrftoken']
|
||||
|
||||
def __str__(self):
|
||||
return 'controller %s user %s api %s tenant %s' % (
|
||||
self.controller, self.username, self.api_version, self.tenant)
|
||||
|
||||
|
||||
class ApiSession(Session):
|
||||
"""
|
||||
Extends the Request library's session object to provide helper
|
||||
utilities to work with Avi Controller like authentication, api massaging
|
||||
etc.
|
||||
"""
|
||||
|
||||
# This keeps track of the process which created the cache.
|
||||
# At anytime the pid of the process changes then it would create
|
||||
# a new cache for that process.
|
||||
AVI_SLUG = 'Slug'
|
||||
SESSION_CACHE_EXPIRY = 20 * 60
|
||||
SHARED_USER_HDRS = ['X-CSRFToken', 'Session-Id', 'Referer', 'Content-Type']
|
||||
MAX_API_RETRIES = 3
|
||||
|
||||
def __init__(self, controller_ip=None, username=None, password=None,
|
||||
token=None, tenant=None, tenant_uuid=None, verify=False,
|
||||
port=None, timeout=60, api_version=None,
|
||||
retry_conxn_errors=True, data_log=False,
|
||||
avi_credentials=None, session_id=None, csrftoken=None,
|
||||
lazy_authentication=False, max_api_retries=None):
|
||||
"""
|
||||
ApiSession takes ownership of avi_credentials and may update the
|
||||
information inside it.
|
||||
|
||||
Initialize new session object with authenticated token from login api.
|
||||
It also keeps a cache of user sessions that are cleaned up if inactive
|
||||
for more than 20 mins.
|
||||
|
||||
Notes:
|
||||
01. If mode is https and port is none or 443, we don't embed the
|
||||
port in the prefix. The prefix would be 'https://ip'. If port
|
||||
is a non-default value then we concatenate https://ip:port
|
||||
in the prefix.
|
||||
02. If mode is http and the port is none or 80, we don't embed the
|
||||
port in the prefix. The prefix would be 'http://ip'. If port is
|
||||
a non-default value, then we concatenate http://ip:port in
|
||||
the prefix.
|
||||
"""
|
||||
super(ApiSession, self).__init__()
|
||||
if not avi_credentials:
|
||||
tenant = tenant if tenant else "admin"
|
||||
self.avi_credentials = AviCredentials(
|
||||
controller=controller_ip, username=username, password=password,
|
||||
api_version=api_version, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
token=token, port=port, timeout=timeout,
|
||||
session_id=session_id, csrftoken=csrftoken)
|
||||
else:
|
||||
self.avi_credentials = avi_credentials
|
||||
self.headers = {}
|
||||
self.verify = verify
|
||||
self.retry_conxn_errors = retry_conxn_errors
|
||||
self.remote_api_version = {}
|
||||
self.session_cookie_name = ''
|
||||
self.user_hdrs = {}
|
||||
self.data_log = data_log
|
||||
self.num_session_retries = 0
|
||||
self.retry_wait_time = 0
|
||||
self.max_session_retries = (
|
||||
self.MAX_API_RETRIES if max_api_retries is None
|
||||
else int(max_api_retries))
|
||||
# Refer Notes 01 and 02
|
||||
k_port = port if port else 443
|
||||
if self.avi_credentials.controller.startswith('http'):
|
||||
k_port = 80 if not self.avi_credentials.port else k_port
|
||||
if self.avi_credentials.port is None or self.avi_credentials.port\
|
||||
== 80:
|
||||
self.prefix = self.avi_credentials.controller
|
||||
else:
|
||||
self.prefix = '{x}:{y}'.format(
|
||||
x=self.avi_credentials.controller,
|
||||
y=self.avi_credentials.port)
|
||||
else:
|
||||
if port is None or port == 443:
|
||||
self.prefix = 'https://{x}'.format(
|
||||
x=self.avi_credentials.controller)
|
||||
else:
|
||||
self.prefix = 'https://{x}:{y}'.format(
|
||||
x=self.avi_credentials.controller,
|
||||
y=self.avi_credentials.port)
|
||||
self.timeout = timeout
|
||||
self.key = '%s:%s:%s' % (self.avi_credentials.controller,
|
||||
self.avi_credentials.username, k_port)
|
||||
# Added api token and session id to sessionDict for handle single
|
||||
# session
|
||||
if self.avi_credentials.csrftoken:
|
||||
sessionDict[self.key] = {
|
||||
'api': self,
|
||||
"csrftoken": self.avi_credentials.csrftoken,
|
||||
"session_id": self.avi_credentials.session_id,
|
||||
"last_used": datetime.utcnow()
|
||||
}
|
||||
elif lazy_authentication:
|
||||
sessionDict.get(self.key, {}).update(
|
||||
{'api': self, "last_used": datetime.utcnow()})
|
||||
else:
|
||||
self.authenticate_session()
|
||||
|
||||
self.num_session_retries = 0
|
||||
self.pid = os.getpid()
|
||||
ApiSession._clean_inactive_sessions()
|
||||
return
|
||||
|
||||
@property
|
||||
def controller_ip(self):
|
||||
return self.avi_credentials.controller
|
||||
|
||||
@controller_ip.setter
|
||||
def controller_ip(self, controller_ip):
|
||||
self.avi_credentials.controller = controller_ip
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self.avi_credentials.username
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return sessionDict.get(self.key, {}).get('connected', False)
|
||||
|
||||
@username.setter
|
||||
def username(self, username):
|
||||
self.avi_credentials.username = username
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self.avi_credentials.password
|
||||
|
||||
@password.setter
|
||||
def password(self, password):
|
||||
self.avi_credentials.password = password
|
||||
|
||||
@property
|
||||
def keystone_token(self):
|
||||
return sessionDict.get(self.key, {}).get('csrftoken', None)
|
||||
|
||||
@keystone_token.setter
|
||||
def keystone_token(self, token):
|
||||
sessionDict[self.key]['csrftoken'] = token
|
||||
|
||||
@property
|
||||
def tenant_uuid(self):
|
||||
self.avi_credentials.tenant_uuid
|
||||
|
||||
@tenant_uuid.setter
|
||||
def tenant_uuid(self, tenant_uuid):
|
||||
self.avi_credentials.tenant_uuid = tenant_uuid
|
||||
|
||||
@property
|
||||
def tenant(self):
|
||||
return self.avi_credentials.tenant
|
||||
|
||||
@tenant.setter
|
||||
def tenant(self, tenant):
|
||||
if tenant:
|
||||
self.avi_credentials.tenant = tenant
|
||||
else:
|
||||
self.avi_credentials.tenant = 'admin'
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
self.avi_credentials.port
|
||||
|
||||
@port.setter
|
||||
def port(self, port):
|
||||
self.avi_credentials.port = port
|
||||
|
||||
@property
|
||||
def api_version(self):
|
||||
return self.avi_credentials.api_version
|
||||
|
||||
@api_version.setter
|
||||
def api_version(self, api_version):
|
||||
self.avi_credentials.api_version = api_version
|
||||
|
||||
@property
|
||||
def session_id(self):
|
||||
return sessionDict[self.key]['session_id']
|
||||
|
||||
def get_context(self):
|
||||
return {
|
||||
'session_id': sessionDict[self.key]['session_id'],
|
||||
'csrftoken': sessionDict[self.key]['csrftoken']
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def clear_cached_sessions():
|
||||
global sessionDict
|
||||
sessionDict = {}
|
||||
|
||||
@staticmethod
|
||||
def get_session(
|
||||
controller_ip=None, username=None, password=None, token=None, tenant=None,
|
||||
tenant_uuid=None, verify=False, port=None, timeout=60,
|
||||
retry_conxn_errors=True, api_version=None, data_log=False,
|
||||
avi_credentials=None, session_id=None, csrftoken=None,
|
||||
lazy_authentication=False, max_api_retries=None):
|
||||
"""
|
||||
returns the session object for same user and tenant
|
||||
calls init if session dose not exist and adds it to session cache
|
||||
:param controller_ip: controller IP address
|
||||
:param username:
|
||||
:param password:
|
||||
:param token: Token to use; example, a valid keystone token
|
||||
:param tenant: Name of the tenant on Avi Controller
|
||||
:param tenant_uuid: Don't specify tenant when using tenant_id
|
||||
:param port: Rest-API may use a different port other than 443
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param retry_conxn_errors: retry on connection errors
|
||||
:param api_version: Controller API version
|
||||
"""
|
||||
if not avi_credentials:
|
||||
tenant = tenant if tenant else "admin"
|
||||
avi_credentials = AviCredentials(
|
||||
controller=controller_ip, username=username, password=password,
|
||||
api_version=api_version, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
token=token, port=port, timeout=timeout,
|
||||
session_id=session_id, csrftoken=csrftoken)
|
||||
|
||||
k_port = avi_credentials.port if avi_credentials.port else 443
|
||||
if avi_credentials.controller.startswith('http'):
|
||||
k_port = 80 if not avi_credentials.port else k_port
|
||||
key = '%s:%s:%s' % (avi_credentials.controller,
|
||||
avi_credentials.username, k_port)
|
||||
cached_session = sessionDict.get(key)
|
||||
if cached_session:
|
||||
user_session = cached_session['api']
|
||||
if not (user_session.avi_credentials.csrftoken or
|
||||
lazy_authentication):
|
||||
user_session.authenticate_session()
|
||||
else:
|
||||
user_session = ApiSession(
|
||||
controller_ip, username, password, token=token, tenant=tenant,
|
||||
tenant_uuid=tenant_uuid, verify=verify, port=port,
|
||||
timeout=timeout, retry_conxn_errors=retry_conxn_errors,
|
||||
api_version=api_version, data_log=data_log,
|
||||
avi_credentials=avi_credentials,
|
||||
lazy_authentication=lazy_authentication,
|
||||
max_api_retries=max_api_retries)
|
||||
ApiSession._clean_inactive_sessions()
|
||||
return user_session
|
||||
|
||||
def reset_session(self):
|
||||
"""
|
||||
resets and re-authenticates the current session.
|
||||
"""
|
||||
sessionDict[self.key]['connected'] = False
|
||||
logger.info('resetting session for %s', self.key)
|
||||
self.user_hdrs = {}
|
||||
for k, v in self.headers.items():
|
||||
if k not in self.SHARED_USER_HDRS:
|
||||
self.user_hdrs[k] = v
|
||||
self.headers = {}
|
||||
self.authenticate_session()
|
||||
|
||||
def authenticate_session(self):
|
||||
"""
|
||||
Performs session authentication with Avi controller and stores
|
||||
session cookies and sets header options like tenant.
|
||||
"""
|
||||
body = {"username": self.avi_credentials.username}
|
||||
if self.avi_credentials.password:
|
||||
body["password"] = self.avi_credentials.password
|
||||
elif self.avi_credentials.token:
|
||||
body["token"] = self.avi_credentials.token
|
||||
else:
|
||||
raise APIError("Neither user password or token provided")
|
||||
logger.debug('authenticating user %s prefix %s',
|
||||
self.avi_credentials.username, self.prefix)
|
||||
self.cookies.clear()
|
||||
err = None
|
||||
try:
|
||||
rsp = super(ApiSession, self).post(
|
||||
self.prefix + "/login", body, timeout=self.timeout, verify=self.verify)
|
||||
|
||||
if rsp.status_code == 200:
|
||||
self.num_session_retries = 0
|
||||
self.remote_api_version = rsp.json().get('version', {})
|
||||
self.session_cookie_name = rsp.json().get('session_cookie_name', 'sessionid')
|
||||
self.headers.update(self.user_hdrs)
|
||||
if rsp.cookies and 'csrftoken' in rsp.cookies:
|
||||
csrftoken = rsp.cookies['csrftoken']
|
||||
sessionDict[self.key] = {
|
||||
'csrftoken': csrftoken,
|
||||
'session_id': rsp.cookies[self.session_cookie_name],
|
||||
'last_used': datetime.utcnow(),
|
||||
'api': self,
|
||||
'connected': True
|
||||
}
|
||||
logger.debug("authentication success for user %s",
|
||||
self.avi_credentials.username)
|
||||
return
|
||||
# Check for bad request and invalid credentials response code
|
||||
elif rsp.status_code in [401, 403]:
|
||||
logger.error('Status Code %s msg %s', rsp.status_code, rsp.text)
|
||||
err = APIError('Status Code %s msg %s' % (
|
||||
rsp.status_code, rsp.text), rsp)
|
||||
raise err
|
||||
else:
|
||||
logger.error("Error status code %s msg %s", rsp.status_code,
|
||||
rsp.text)
|
||||
err = APIError('Status Code %s msg %s' % (
|
||||
rsp.status_code, rsp.text), rsp)
|
||||
except (RequestsConnectionError, SSLError) as e:
|
||||
if not self.retry_conxn_errors:
|
||||
raise
|
||||
logger.warning('Connection error retrying %s', e)
|
||||
err = e
|
||||
# comes here only if there was either exception or login was not
|
||||
# successful
|
||||
if self.retry_wait_time:
|
||||
time.sleep(self.retry_wait_time)
|
||||
self.num_session_retries += 1
|
||||
if self.num_session_retries > self.max_session_retries:
|
||||
self.num_session_retries = 0
|
||||
logger.error("giving up after %d retries connection failure %s",
|
||||
self.max_session_retries, True)
|
||||
ret_err = (
|
||||
err if err else APIError("giving up after %d retries connection failure %s" %
|
||||
(self.max_session_retries, True)))
|
||||
raise ret_err
|
||||
self.authenticate_session()
|
||||
return
|
||||
|
||||
def _get_api_headers(self, tenant, tenant_uuid, timeout, headers,
|
||||
api_version):
|
||||
"""
|
||||
returns the headers that are passed to the requests.Session api calls.
|
||||
"""
|
||||
api_hdrs = copy.deepcopy(self.headers)
|
||||
api_hdrs.update({
|
||||
"Referer": self.prefix,
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
api_hdrs['timeout'] = str(timeout)
|
||||
if self.key in sessionDict and 'csrftoken' in sessionDict.get(self.key):
|
||||
api_hdrs['X-CSRFToken'] = sessionDict.get(self.key)['csrftoken']
|
||||
else:
|
||||
self.authenticate_session()
|
||||
api_hdrs['X-CSRFToken'] = sessionDict.get(self.key)['csrftoken']
|
||||
if api_version:
|
||||
api_hdrs['X-Avi-Version'] = api_version
|
||||
elif self.avi_credentials.api_version:
|
||||
api_hdrs['X-Avi-Version'] = self.avi_credentials.api_version
|
||||
if tenant:
|
||||
tenant_uuid = None
|
||||
elif tenant_uuid:
|
||||
tenant = None
|
||||
else:
|
||||
tenant = self.avi_credentials.tenant
|
||||
tenant_uuid = self.avi_credentials.tenant_uuid
|
||||
if tenant_uuid:
|
||||
api_hdrs.update({"X-Avi-Tenant-UUID": "%s" % tenant_uuid})
|
||||
api_hdrs.pop("X-Avi-Tenant", None)
|
||||
elif tenant:
|
||||
api_hdrs.update({"X-Avi-Tenant": "%s" % tenant})
|
||||
api_hdrs.pop("X-Avi-Tenant-UUID", None)
|
||||
# Override any user headers that were passed by users. We don't know
|
||||
# when the user had updated the user_hdrs
|
||||
if self.user_hdrs:
|
||||
api_hdrs.update(self.user_hdrs)
|
||||
if headers:
|
||||
# overwrite the headers passed via the API calls.
|
||||
api_hdrs.update(headers)
|
||||
return api_hdrs
|
||||
|
||||
def _api(self, api_name, path, tenant, tenant_uuid, data=None,
|
||||
headers=None, timeout=None, api_version=None, **kwargs):
|
||||
"""
|
||||
It calls the requests.Session APIs and handles session expiry
|
||||
and other situations where session needs to be reset.
|
||||
returns ApiResponse object
|
||||
:param path: takes relative path to the AVI api.
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param headers: dictionary of headers that override the session
|
||||
headers.
|
||||
"""
|
||||
if self.pid != os.getpid():
|
||||
logger.info('pid %d change detected new %d. Closing session',
|
||||
self.pid, os.getpid())
|
||||
self.close()
|
||||
self.pid = os.getpid()
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
fullpath = self._get_api_path(path)
|
||||
fn = getattr(super(ApiSession, self), api_name)
|
||||
api_hdrs = self._get_api_headers(tenant, tenant_uuid, timeout, headers,
|
||||
api_version)
|
||||
connection_error = False
|
||||
err = None
|
||||
cookies = {
|
||||
'csrftoken': api_hdrs['X-CSRFToken'],
|
||||
}
|
||||
try:
|
||||
if self.session_cookie_name:
|
||||
cookies[self.session_cookie_name] = sessionDict[self.key]['session_id']
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
if (data is not None) and (type(data) == dict):
|
||||
resp = fn(fullpath, data=json.dumps(data), headers=api_hdrs,
|
||||
timeout=timeout, cookies=cookies, **kwargs)
|
||||
else:
|
||||
resp = fn(fullpath, data=data, headers=api_hdrs,
|
||||
timeout=timeout, cookies=cookies, **kwargs)
|
||||
except (RequestsConnectionError, SSLError) as e:
|
||||
logger.warning('Connection error retrying %s', e)
|
||||
if not self.retry_conxn_errors:
|
||||
raise
|
||||
connection_error = True
|
||||
err = e
|
||||
except Exception as e:
|
||||
logger.error('Error in Requests library %s', e)
|
||||
raise
|
||||
if not connection_error:
|
||||
logger.debug('path: %s http_method: %s hdrs: %s params: '
|
||||
'%s data: %s rsp: %s', fullpath, api_name.upper(),
|
||||
api_hdrs, kwargs, data,
|
||||
(resp.text if self.data_log else 'None'))
|
||||
if connection_error or resp.status_code in (401, 419):
|
||||
if connection_error:
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
# ignoring exception in cleanup path
|
||||
pass
|
||||
logger.warning('Connection failed, retrying.')
|
||||
# Adding sleep before retrying
|
||||
if self.retry_wait_time:
|
||||
time.sleep(self.retry_wait_time)
|
||||
else:
|
||||
logger.info('received error %d %s so resetting connection',
|
||||
resp.status_code, resp.text)
|
||||
ApiSession.reset_session(self)
|
||||
self.num_session_retries += 1
|
||||
if self.num_session_retries > self.max_session_retries:
|
||||
# Added this such that any code which re-tries can succeed
|
||||
# eventually.
|
||||
self.num_session_retries = 0
|
||||
if not connection_error:
|
||||
err = APIError('Status Code %s msg %s' % (
|
||||
resp.status_code, resp.text), resp)
|
||||
logger.error(
|
||||
"giving up after %d retries conn failure %s err %s",
|
||||
self.max_session_retries, connection_error, err)
|
||||
ret_err = (
|
||||
err if err else APIError("giving up after %d retries connection failure %s" %
|
||||
(self.max_session_retries, True)))
|
||||
raise ret_err
|
||||
# should restore the updated_hdrs to one passed down
|
||||
resp = self._api(api_name, path, tenant, tenant_uuid, data,
|
||||
headers=headers, api_version=api_version,
|
||||
timeout=timeout, **kwargs)
|
||||
self.num_session_retries = 0
|
||||
|
||||
if resp.cookies and 'csrftoken' in resp.cookies:
|
||||
csrftoken = resp.cookies['csrftoken']
|
||||
self.headers.update({"X-CSRFToken": csrftoken})
|
||||
self._update_session_last_used()
|
||||
return ApiResponse.to_avi_response(resp)
|
||||
|
||||
def get_controller_details(self):
|
||||
result = {
|
||||
"controller_ip": self.controller_ip,
|
||||
"controller_api_version": self.remote_api_version
|
||||
}
|
||||
return result
|
||||
|
||||
def get(self, path, tenant='', tenant_uuid='', timeout=None, params=None,
|
||||
api_version=None, **kwargs):
|
||||
"""
|
||||
It extends the Session Library interface to add AVI API prefixes,
|
||||
handle session exceptions related to authentication and update
|
||||
the global user session cache.
|
||||
:param path: takes relative path to the AVI api.
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
get method takes relative path to service and kwargs as per Session
|
||||
class get method
|
||||
returns session's response object
|
||||
"""
|
||||
return self._api('get', path, tenant, tenant_uuid, timeout=timeout,
|
||||
params=params, api_version=api_version, **kwargs)
|
||||
|
||||
def get_object_by_name(self, path, name, tenant='', tenant_uuid='',
|
||||
timeout=None, params=None, api_version=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Helper function to access Avi REST Objects using object
|
||||
type and name. It behaves like python dictionary interface where it
|
||||
returns None when the object is not present in the AviController.
|
||||
Internally, it transforms the request to api/path?name=<name>...
|
||||
:param path: relative path to service
|
||||
:param name: name of the object
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
returns dictionary object if successful else None
|
||||
"""
|
||||
obj = None
|
||||
if not params:
|
||||
params = {}
|
||||
params['name'] = name
|
||||
resp = self.get(path, tenant=tenant, tenant_uuid=tenant_uuid,
|
||||
timeout=timeout,
|
||||
params=params, api_version=api_version, **kwargs)
|
||||
if resp.status_code in (401, 419):
|
||||
ApiSession.reset_session(self)
|
||||
resp = self.get_object_by_name(
|
||||
path, name, tenant, tenant_uuid, timeout=timeout,
|
||||
params=params, **kwargs)
|
||||
if resp.status_code > 499 or 'Invalid version' in resp.text:
|
||||
logger.error('Error in get object by name for %s named %s. '
|
||||
'Error: %s', path, name, resp.text)
|
||||
raise AviServerError(resp.text, rsp=resp)
|
||||
elif resp.status_code > 299:
|
||||
return obj
|
||||
try:
|
||||
if 'results' in resp.json():
|
||||
obj = resp.json()['results'][0]
|
||||
else:
|
||||
# For apis returning single object eg. api/cluster
|
||||
obj = resp.json()
|
||||
except IndexError:
|
||||
logger.warning('Warning: Object Not found for %s named %s',
|
||||
path, name)
|
||||
obj = None
|
||||
self._update_session_last_used()
|
||||
return obj
|
||||
|
||||
def post(self, path, data=None, tenant='', tenant_uuid='', timeout=None,
|
||||
force_uuid=None, params=None, api_version=None, **kwargs):
|
||||
"""
|
||||
It extends the Session Library interface to add AVI API prefixes,
|
||||
handle session exceptions related to authentication and update
|
||||
the global user session cache.
|
||||
:param path: takes relative path to the AVI api.It is modified by
|
||||
the library to conform to AVI Controller's REST API interface
|
||||
:param data: dictionary of the data. Support for json string
|
||||
is deprecated
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
returns session's response object
|
||||
"""
|
||||
if force_uuid is not None:
|
||||
headers = kwargs.get('headers', {})
|
||||
headers[self.AVI_SLUG] = force_uuid
|
||||
kwargs['headers'] = headers
|
||||
return self._api('post', path, tenant, tenant_uuid, data=data,
|
||||
timeout=timeout, params=params,
|
||||
api_version=api_version, **kwargs)
|
||||
|
||||
def put(self, path, data=None, tenant='', tenant_uuid='',
|
||||
timeout=None, params=None, api_version=None, **kwargs):
|
||||
"""
|
||||
It extends the Session Library interface to add AVI API prefixes,
|
||||
handle session exceptions related to authentication and update
|
||||
the global user session cache.
|
||||
:param path: takes relative path to the AVI api.It is modified by
|
||||
the library to conform to AVI Controller's REST API interface
|
||||
:param data: dictionary of the data. Support for json string
|
||||
is deprecated
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
returns session's response object
|
||||
"""
|
||||
return self._api('put', path, tenant, tenant_uuid, data=data,
|
||||
timeout=timeout, params=params,
|
||||
api_version=api_version, **kwargs)
|
||||
|
||||
def patch(self, path, data=None, tenant='', tenant_uuid='',
|
||||
timeout=None, params=None, api_version=None, **kwargs):
|
||||
"""
|
||||
It extends the Session Library interface to add AVI API prefixes,
|
||||
handle session exceptions related to authentication and update
|
||||
the global user session cache.
|
||||
:param path: takes relative path to the AVI api.It is modified by
|
||||
the library to conform to AVI Controller's REST API interface
|
||||
:param data: dictionary of the data. Support for json string
|
||||
is deprecated
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
returns session's response object
|
||||
"""
|
||||
return self._api('patch', path, tenant, tenant_uuid, data=data,
|
||||
timeout=timeout, params=params,
|
||||
api_version=api_version, **kwargs)
|
||||
|
||||
def put_by_name(self, path, name, data=None, tenant='',
|
||||
tenant_uuid='', timeout=None, params=None,
|
||||
api_version=None, **kwargs):
|
||||
"""
|
||||
Helper function to perform HTTP PUT on Avi REST Objects using object
|
||||
type and name.
|
||||
Internally, it transforms the request to api/path?name=<name>...
|
||||
:param path: relative path to service
|
||||
:param name: name of the object
|
||||
:param data: dictionary of the data. Support for json string
|
||||
is deprecated
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
returns session's response object
|
||||
"""
|
||||
uuid = self._get_uuid_by_name(
|
||||
path, name, tenant, tenant_uuid, api_version=api_version)
|
||||
path = '%s/%s' % (path, uuid)
|
||||
return self.put(path, data, tenant, tenant_uuid, timeout=timeout,
|
||||
params=params, api_version=api_version, **kwargs)
|
||||
|
||||
def delete(self, path, tenant='', tenant_uuid='', timeout=None, params=None,
|
||||
data=None, api_version=None, **kwargs):
|
||||
"""
|
||||
It extends the Session Library interface to add AVI API prefixes,
|
||||
handle session exceptions related to authentication and update
|
||||
the global user session cache.
|
||||
:param path: takes relative path to the AVI api.It is modified by
|
||||
the library to conform to AVI Controller's REST API interface
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param data: dictionary of the data. Support for json string
|
||||
is deprecated
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
returns session's response object
|
||||
"""
|
||||
return self._api('delete', path, tenant, tenant_uuid, data=data,
|
||||
timeout=timeout, params=params,
|
||||
api_version=api_version, **kwargs)
|
||||
|
||||
def delete_by_name(self, path, name, tenant='', tenant_uuid='',
|
||||
timeout=None, params=None, api_version=None, **kwargs):
|
||||
"""
|
||||
Helper function to perform HTTP DELETE on Avi REST Objects using object
|
||||
type and name.Internally, it transforms the request to
|
||||
api/path?name=<name>...
|
||||
:param path: relative path to service
|
||||
:param name: name of the object
|
||||
:param tenant: overrides the tenant used during session creation
|
||||
:param tenant_uuid: overrides the tenant or tenant_uuid during session
|
||||
creation
|
||||
:param timeout: timeout for API calls; Default value is 60 seconds
|
||||
:param params: dictionary of key value pairs to be sent as query
|
||||
parameters
|
||||
:param api_version: overrides x-avi-header in request header during
|
||||
session creation
|
||||
returns session's response object
|
||||
"""
|
||||
uuid = self._get_uuid_by_name(path, name, tenant, tenant_uuid,
|
||||
api_version=api_version)
|
||||
if not uuid:
|
||||
raise ObjectNotFound("%s/?name=%s" % (path, name))
|
||||
path = '%s/%s' % (path, uuid)
|
||||
return self.delete(path, tenant, tenant_uuid, timeout=timeout,
|
||||
params=params, api_version=api_version, **kwargs)
|
||||
|
||||
def get_obj_ref(self, obj):
|
||||
"""returns reference url from dict object"""
|
||||
if not obj:
|
||||
return None
|
||||
if isinstance(obj, Response):
|
||||
obj = json.loads(obj.text)
|
||||
if obj.get(0, None):
|
||||
return obj[0]['url']
|
||||
elif obj.get('url', None):
|
||||
return obj['url']
|
||||
elif obj.get('results', None):
|
||||
return obj['results'][0]['url']
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_obj_uuid(self, obj):
|
||||
"""returns uuid from dict object"""
|
||||
if not obj:
|
||||
raise ObjectNotFound('Object %s Not found' % (obj))
|
||||
if isinstance(obj, Response):
|
||||
obj = json.loads(obj.text)
|
||||
if obj.get(0, None):
|
||||
return obj[0]['uuid']
|
||||
elif obj.get('uuid', None):
|
||||
return obj['uuid']
|
||||
elif obj.get('results', None):
|
||||
return obj['results'][0]['uuid']
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_api_path(self, path, uuid=None):
|
||||
"""
|
||||
This function returns the full url from relative path and uuid.
|
||||
"""
|
||||
if path == 'logout':
|
||||
return self.prefix + '/' + path
|
||||
elif uuid:
|
||||
return self.prefix + '/api/' + path + '/' + uuid
|
||||
else:
|
||||
return self.prefix + '/api/' + path
|
||||
|
||||
def _get_uuid_by_name(self, path, name, tenant='admin',
|
||||
tenant_uuid='', api_version=None):
|
||||
"""gets object by name and service path and returns uuid"""
|
||||
resp = self.get_object_by_name(
|
||||
path, name, tenant, tenant_uuid, api_version=api_version)
|
||||
if not resp:
|
||||
raise ObjectNotFound("%s/%s" % (path, name))
|
||||
return self.get_obj_uuid(resp)
|
||||
|
||||
def _update_session_last_used(self):
|
||||
if self.key in sessionDict:
|
||||
sessionDict[self.key]["last_used"] = datetime.utcnow()
|
||||
|
||||
@staticmethod
|
||||
def _clean_inactive_sessions():
|
||||
"""Removes sessions which are inactive more than 20 min"""
|
||||
session_cache = sessionDict
|
||||
logger.debug("cleaning inactive sessions in pid %d num elem %d",
|
||||
os.getpid(), len(session_cache))
|
||||
keys_to_delete = []
|
||||
for key, session in list(session_cache.items()):
|
||||
tdiff = avi_timedelta(datetime.utcnow() - session["last_used"])
|
||||
if tdiff < ApiSession.SESSION_CACHE_EXPIRY:
|
||||
continue
|
||||
keys_to_delete.append(key)
|
||||
for key in keys_to_delete:
|
||||
del session_cache[key]
|
||||
logger.debug("Removed session for : %s", key)
|
||||
|
||||
def delete_session(self):
|
||||
""" Removes the session for cleanup"""
|
||||
logger.debug("Removed session for : %s", self.key)
|
||||
sessionDict.pop(self.key, None)
|
||||
return
|
||||
# End of file
|
||||
0
plugins/module_utils/network/bigswitch/__init__.py
Normal file
0
plugins/module_utils/network/bigswitch/__init__.py
Normal file
91
plugins/module_utils/network/bigswitch/bigswitch.py
Normal file
91
plugins/module_utils/network/bigswitch/bigswitch.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2016, Ted Elhourani <ted@bigswitch.com>
|
||||
#
|
||||
#
|
||||
# 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.
|
||||
|
||||
import json
|
||||
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
|
||||
class Response(object):
|
||||
|
||||
def __init__(self, resp, info):
|
||||
self.body = None
|
||||
if resp:
|
||||
self.body = resp.read()
|
||||
self.info = info
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if not self.body:
|
||||
if "body" in self.info:
|
||||
return json.loads(self.info["body"])
|
||||
return None
|
||||
try:
|
||||
return json.loads(self.body)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def status_code(self):
|
||||
return self.info["status"]
|
||||
|
||||
|
||||
class Rest(object):
|
||||
|
||||
def __init__(self, module, headers, baseurl):
|
||||
self.module = module
|
||||
self.headers = headers
|
||||
self.baseurl = baseurl
|
||||
|
||||
def _url_builder(self, path):
|
||||
if path[0] == '/':
|
||||
path = path[1:]
|
||||
return '%s/%s' % (self.baseurl, path)
|
||||
|
||||
def send(self, method, path, data=None, headers=None):
|
||||
url = self._url_builder(path)
|
||||
data = self.module.jsonify(data)
|
||||
|
||||
resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method)
|
||||
|
||||
return Response(resp, info)
|
||||
|
||||
def get(self, path, data=None, headers=None):
|
||||
return self.send('GET', path, data, headers)
|
||||
|
||||
def put(self, path, data=None, headers=None):
|
||||
return self.send('PUT', path, data, headers)
|
||||
|
||||
def post(self, path, data=None, headers=None):
|
||||
return self.send('POST', path, data, headers)
|
||||
|
||||
def patch(self, path, data=None, headers=None):
|
||||
return self.send('PATCH', path, data, headers)
|
||||
|
||||
def delete(self, path, data=None, headers=None):
|
||||
return self.send('DELETE', path, data, headers)
|
||||
0
plugins/module_utils/network/checkpoint/__init__.py
Normal file
0
plugins/module_utils/network/checkpoint/__init__.py
Normal file
421
plugins/module_utils/network/cloudengine/ce.py
Normal file
421
plugins/module_utils/network/cloudengine/ce.py
Normal file
@@ -0,0 +1,421 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# (c) 2017 Red Hat, Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.connection import exec_command, ConnectionError
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import NetconfConnection
|
||||
|
||||
|
||||
try:
|
||||
from ncclient.xml_ import to_xml, new_ele_ns
|
||||
HAS_NCCLIENT = True
|
||||
except ImportError:
|
||||
HAS_NCCLIENT = False
|
||||
|
||||
|
||||
try:
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
from xml.etree import ElementTree as etree
|
||||
|
||||
_DEVICE_CLI_CONNECTION = None
|
||||
_DEVICE_NC_CONNECTION = None
|
||||
|
||||
ce_provider_spec = {
|
||||
'host': dict(),
|
||||
'port': dict(type='int'),
|
||||
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
|
||||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
|
||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
|
||||
'use_ssl': dict(type='bool'),
|
||||
'validate_certs': dict(type='bool'),
|
||||
'timeout': dict(type='int'),
|
||||
'transport': dict(default='cli', choices=['cli', 'netconf']),
|
||||
}
|
||||
ce_argument_spec = {
|
||||
'provider': dict(type='dict', options=ce_provider_spec),
|
||||
}
|
||||
ce_top_spec = {
|
||||
'host': dict(removed_in_version=2.9),
|
||||
'port': dict(removed_in_version=2.9, type='int'),
|
||||
'username': dict(removed_in_version=2.9),
|
||||
'password': dict(removed_in_version=2.9, no_log=True),
|
||||
'ssh_keyfile': dict(removed_in_version=2.9, type='path'),
|
||||
'use_ssl': dict(removed_in_version=2.9, type='bool'),
|
||||
'validate_certs': dict(removed_in_version=2.9, type='bool'),
|
||||
'timeout': dict(removed_in_version=2.9, type='int'),
|
||||
'transport': dict(removed_in_version=2.9, choices=['cli', 'netconf']),
|
||||
}
|
||||
ce_argument_spec.update(ce_top_spec)
|
||||
|
||||
|
||||
def to_string(data):
|
||||
return re.sub(r'<data\s+.+?(/>|>)', r'<data\1', data)
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
pass
|
||||
|
||||
|
||||
def load_params(module):
|
||||
"""load_params"""
|
||||
provider = module.params.get('provider') or dict()
|
||||
for key, value in iteritems(provider):
|
||||
if key in ce_argument_spec:
|
||||
if module.params.get(key) is None and value is not None:
|
||||
module.params[key] = value
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
"""get_connection"""
|
||||
global _DEVICE_CLI_CONNECTION
|
||||
if not _DEVICE_CLI_CONNECTION:
|
||||
load_params(module)
|
||||
conn = Cli(module)
|
||||
_DEVICE_CLI_CONNECTION = conn
|
||||
return _DEVICE_CLI_CONNECTION
|
||||
|
||||
|
||||
def rm_config_prefix(cfg):
|
||||
if not cfg:
|
||||
return cfg
|
||||
|
||||
cmds = cfg.split("\n")
|
||||
for i in range(len(cmds)):
|
||||
if not cmds[i]:
|
||||
continue
|
||||
if '~' in cmds[i]:
|
||||
index = cmds[i].index('~')
|
||||
if cmds[i][:index] == ' ' * index:
|
||||
cmds[i] = cmds[i].replace("~", "", 1)
|
||||
return '\n'.join(cmds)
|
||||
|
||||
|
||||
class Cli:
|
||||
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._device_configs = {}
|
||||
|
||||
def exec_command(self, command):
|
||||
if isinstance(command, dict):
|
||||
command = self._module.jsonify(command)
|
||||
|
||||
return exec_command(self._module, command)
|
||||
|
||||
def get_config(self, flags=None):
|
||||
"""Retrieves the current config from the device or cache
|
||||
"""
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
cmd = 'display current-configuration '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
try:
|
||||
return self._device_configs[cmd]
|
||||
except KeyError:
|
||||
rc, out, err = self.exec_command(cmd)
|
||||
if rc != 0:
|
||||
self._module.fail_json(msg=err)
|
||||
cfg = str(out).strip()
|
||||
# remove default configuration prefix '~'
|
||||
for flag in flags:
|
||||
if "include-default" in flag:
|
||||
cfg = rm_config_prefix(cfg)
|
||||
break
|
||||
|
||||
self._device_configs[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
def run_commands(self, commands, check_rc=True):
|
||||
"""Run list of commands on remote device and return results
|
||||
"""
|
||||
responses = list()
|
||||
|
||||
for item in to_list(commands):
|
||||
|
||||
rc, out, err = self.exec_command(item)
|
||||
|
||||
if check_rc and rc != 0:
|
||||
self._module.fail_json(msg=cli_err_msg(item['command'].strip(), err))
|
||||
|
||||
try:
|
||||
out = self._module.from_json(out)
|
||||
except ValueError:
|
||||
out = str(out).strip()
|
||||
|
||||
responses.append(out)
|
||||
return responses
|
||||
|
||||
def load_config(self, config):
|
||||
"""Sends configuration commands to the remote device
|
||||
"""
|
||||
rc, out, err = self.exec_command('mmi-mode enable')
|
||||
if rc != 0:
|
||||
self._module.fail_json(msg='unable to set mmi-mode enable', output=err)
|
||||
rc, out, err = self.exec_command('system-view immediately')
|
||||
if rc != 0:
|
||||
self._module.fail_json(msg='unable to enter system-view', output=err)
|
||||
|
||||
for cmd in config:
|
||||
rc, out, err = self.exec_command(cmd)
|
||||
if rc != 0:
|
||||
self._module.fail_json(msg=cli_err_msg(cmd.strip(), err))
|
||||
|
||||
self.exec_command('return')
|
||||
|
||||
|
||||
def cli_err_msg(cmd, err):
|
||||
""" get cli exception message"""
|
||||
|
||||
if not err:
|
||||
return "Error: Fail to get cli exception message."
|
||||
|
||||
msg = list()
|
||||
err_list = str(err).split("\r\n")
|
||||
for err in err_list:
|
||||
err = err.strip('.,\r\n\t ')
|
||||
if not err:
|
||||
continue
|
||||
if cmd and cmd == err:
|
||||
continue
|
||||
if " at '^' position" in err:
|
||||
err = err.replace(" at '^' position", "").strip()
|
||||
err = err.strip('.,\r\n\t ')
|
||||
if err == "^":
|
||||
continue
|
||||
if len(err) > 2 and err[0] in ["<", "["] and err[-1] in [">", "]"]:
|
||||
continue
|
||||
err.strip('.,\r\n\t ')
|
||||
if err:
|
||||
msg.append(err)
|
||||
|
||||
if cmd:
|
||||
msg.insert(0, "Command: %s" % cmd)
|
||||
|
||||
return ", ".join(msg).capitalize() + "."
|
||||
|
||||
|
||||
def to_command(module, commands):
|
||||
default_output = 'text'
|
||||
transform = ComplexList(dict(
|
||||
command=dict(key=True),
|
||||
output=dict(default=default_output),
|
||||
prompt=dict(),
|
||||
answer=dict()
|
||||
), module)
|
||||
|
||||
commands = transform(to_list(commands))
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
conn = get_connection(module)
|
||||
return conn.get_config(flags)
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
conn = get_connection(module)
|
||||
return conn.run_commands(to_command(module, commands), check_rc)
|
||||
|
||||
|
||||
def load_config(module, config):
|
||||
"""load_config"""
|
||||
conn = get_connection(module)
|
||||
return conn.load_config(config)
|
||||
|
||||
|
||||
def ce_unknown_host_cb(host, fingerprint):
|
||||
""" ce_unknown_host_cb """
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_nc_set_id(xml_str):
|
||||
"""get netconf set-id value"""
|
||||
|
||||
result = re.findall(r'<rpc-reply.+?set-id=\"(\d+)\"', xml_str)
|
||||
if not result:
|
||||
return None
|
||||
return result[0]
|
||||
|
||||
|
||||
def get_xml_line(xml_list, index):
|
||||
"""get xml specified line valid string data"""
|
||||
|
||||
ele = None
|
||||
while xml_list and not ele:
|
||||
if index >= 0 and index >= len(xml_list):
|
||||
return None
|
||||
if index < 0 and abs(index) > len(xml_list):
|
||||
return None
|
||||
|
||||
ele = xml_list[index]
|
||||
if not ele.replace(" ", ""):
|
||||
xml_list.pop(index)
|
||||
ele = None
|
||||
return ele
|
||||
|
||||
|
||||
def merge_nc_xml(xml1, xml2):
|
||||
"""merge xml1 and xml2"""
|
||||
|
||||
xml1_list = xml1.split("</data>")[0].split("\n")
|
||||
xml2_list = xml2.split("<data>")[1].split("\n")
|
||||
|
||||
while True:
|
||||
xml1_ele1 = get_xml_line(xml1_list, -1)
|
||||
xml1_ele2 = get_xml_line(xml1_list, -2)
|
||||
xml2_ele1 = get_xml_line(xml2_list, 0)
|
||||
xml2_ele2 = get_xml_line(xml2_list, 1)
|
||||
if not xml1_ele1 or not xml1_ele2 or not xml2_ele1 or not xml2_ele2:
|
||||
return xml1
|
||||
|
||||
if "xmlns" in xml2_ele1:
|
||||
xml2_ele1 = xml2_ele1.lstrip().split(" ")[0] + ">"
|
||||
if "xmlns" in xml2_ele2:
|
||||
xml2_ele2 = xml2_ele2.lstrip().split(" ")[0] + ">"
|
||||
if xml1_ele1.replace(" ", "").replace("/", "") == xml2_ele1.replace(" ", "").replace("/", ""):
|
||||
if xml1_ele2.replace(" ", "").replace("/", "") == xml2_ele2.replace(" ", "").replace("/", ""):
|
||||
xml1_list.pop()
|
||||
xml2_list.pop(0)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
return "\n".join(xml1_list + xml2_list)
|
||||
|
||||
|
||||
def get_nc_connection(module):
|
||||
global _DEVICE_NC_CONNECTION
|
||||
if not _DEVICE_NC_CONNECTION:
|
||||
load_params(module)
|
||||
conn = NetconfConnection(module._socket_path)
|
||||
_DEVICE_NC_CONNECTION = conn
|
||||
return _DEVICE_NC_CONNECTION
|
||||
|
||||
|
||||
def set_nc_config(module, xml_str):
|
||||
""" set_config """
|
||||
|
||||
conn = get_nc_connection(module)
|
||||
try:
|
||||
out = conn.edit_config(target='running', config=xml_str, default_operation='merge',
|
||||
error_option='rollback-on-error')
|
||||
finally:
|
||||
# conn.unlock(target = 'candidate')
|
||||
pass
|
||||
return to_string(to_xml(out))
|
||||
|
||||
|
||||
def get_nc_next(module, xml_str):
|
||||
""" get_nc_next for exchange capability """
|
||||
|
||||
conn = get_nc_connection(module)
|
||||
result = None
|
||||
if xml_str is not None:
|
||||
response = conn.get(xml_str, if_rpc_reply=True)
|
||||
result = response.find('./*')
|
||||
set_id = response.get('set-id')
|
||||
while True and set_id is not None:
|
||||
try:
|
||||
fetch_node = new_ele_ns('get-next', 'http://www.huawei.com/netconf/capability/base/1.0', {'set-id': set_id})
|
||||
next_xml = conn.dispatch_rpc(etree.tostring(fetch_node))
|
||||
if next_xml is not None:
|
||||
result.extend(next_xml.find('./*'))
|
||||
set_id = next_xml.get('set-id')
|
||||
except ConnectionError:
|
||||
break
|
||||
if result is not None:
|
||||
return etree.tostring(result)
|
||||
return result
|
||||
|
||||
|
||||
def get_nc_config(module, xml_str):
|
||||
""" get_config """
|
||||
|
||||
conn = get_nc_connection(module)
|
||||
if xml_str is not None:
|
||||
response = conn.get(xml_str)
|
||||
else:
|
||||
return None
|
||||
|
||||
return to_string(to_xml(response))
|
||||
|
||||
|
||||
def execute_nc_action(module, xml_str):
|
||||
""" huawei execute-action """
|
||||
|
||||
conn = get_nc_connection(module)
|
||||
response = conn.execute_action(xml_str)
|
||||
return to_string(to_xml(response))
|
||||
|
||||
|
||||
def execute_nc_cli(module, xml_str):
|
||||
""" huawei execute-cli """
|
||||
|
||||
if xml_str is not None:
|
||||
try:
|
||||
conn = get_nc_connection(module)
|
||||
out = conn.execute_nc_cli(command=xml_str)
|
||||
return to_string(to_xml(out))
|
||||
except Exception as exc:
|
||||
raise Exception(exc)
|
||||
|
||||
|
||||
def check_ip_addr(ipaddr):
|
||||
""" check ip address, Supports IPv4 and IPv6 """
|
||||
|
||||
if not ipaddr or '\x00' in ipaddr:
|
||||
return False
|
||||
|
||||
try:
|
||||
res = socket.getaddrinfo(ipaddr, 0, socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM,
|
||||
0, socket.AI_NUMERICHOST)
|
||||
return bool(res)
|
||||
except socket.gaierror:
|
||||
err = sys.exc_info()[1]
|
||||
if err.args[0] == socket.EAI_NONAME:
|
||||
return False
|
||||
raise
|
||||
0
plugins/module_utils/network/cnos/__init__.py
Normal file
0
plugins/module_utils/network/cnos/__init__.py
Normal file
660
plugins/module_utils/network/cnos/cnos.py
Normal file
660
plugins/module_utils/network/cnos/cnos.py
Normal file
@@ -0,0 +1,660 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Contains utility methods
|
||||
# Lenovo Networking
|
||||
|
||||
import time
|
||||
import socket
|
||||
import re
|
||||
import json
|
||||
try:
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos_errorcodes
|
||||
from ansible_collections.community.general.plugins.module_utils.network.cnos import cnos_devicerules
|
||||
HAS_LIB = True
|
||||
except Exception:
|
||||
HAS_LIB = False
|
||||
from distutils.cmd import Command
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, EntityCollection
|
||||
from ansible.module_utils.connection import Connection, exec_command
|
||||
from ansible.module_utils.connection import ConnectionError
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
_CONNECTION = None
|
||||
_VALID_USER_ROLES = ['network-admin', 'network-operator']
|
||||
|
||||
cnos_provider_spec = {
|
||||
'host': dict(),
|
||||
'port': dict(type='int'),
|
||||
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
|
||||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']),
|
||||
no_log=True),
|
||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']),
|
||||
type='path'),
|
||||
'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']),
|
||||
type='bool'),
|
||||
'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']),
|
||||
no_log=True),
|
||||
'timeout': dict(type='int'),
|
||||
'context': dict(),
|
||||
'passwords': dict()
|
||||
}
|
||||
|
||||
cnos_argument_spec = {
|
||||
'provider': dict(type='dict', options=cnos_provider_spec),
|
||||
}
|
||||
|
||||
command_spec = {
|
||||
'command': dict(key=True),
|
||||
'prompt': dict(),
|
||||
'answer': dict(),
|
||||
'check_all': dict()
|
||||
}
|
||||
|
||||
|
||||
def get_provider_argspec():
|
||||
return cnos_provider_spec
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
pass
|
||||
|
||||
|
||||
def get_user_roles():
|
||||
return _VALID_USER_ROLES
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
global _CONNECTION
|
||||
if _CONNECTION:
|
||||
return _CONNECTION
|
||||
_CONNECTION = Connection(module._socket_path)
|
||||
|
||||
context = None
|
||||
try:
|
||||
context = module.params['context']
|
||||
except KeyError:
|
||||
context = None
|
||||
|
||||
if context:
|
||||
if context == 'system':
|
||||
command = 'changeto system'
|
||||
else:
|
||||
command = 'changeto context %s' % context
|
||||
_CONNECTION.get(command)
|
||||
|
||||
return _CONNECTION
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
passwords = None
|
||||
try:
|
||||
passwords = module.params['passwords']
|
||||
except KeyError:
|
||||
passwords = None
|
||||
if passwords:
|
||||
cmd = 'more system:running-config'
|
||||
else:
|
||||
cmd = 'display running-config '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
try:
|
||||
return _DEVICE_CONFIGS[cmd]
|
||||
except KeyError:
|
||||
conn = get_connection(module)
|
||||
out = conn.get(cmd)
|
||||
cfg = to_text(out, errors='surrogate_then_replace').strip()
|
||||
_DEVICE_CONFIGS[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
|
||||
def to_commands(module, commands):
|
||||
if not isinstance(commands, list):
|
||||
raise AssertionError('argument must be of type <list>')
|
||||
|
||||
transform = EntityCollection(module, command_spec)
|
||||
commands = transform(commands)
|
||||
|
||||
for index, item in enumerate(commands):
|
||||
if module.check_mode and not item['command'].startswith('show'):
|
||||
module.warn('only show commands are supported when using check '
|
||||
'mode, not executing `%s`' % item['command'])
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
connection = get_connection(module)
|
||||
connection.get('enable')
|
||||
commands = to_commands(module, to_list(commands))
|
||||
|
||||
responses = list()
|
||||
|
||||
for cmd in commands:
|
||||
out = connection.get(**cmd)
|
||||
responses.append(to_text(out, errors='surrogate_then_replace'))
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
def run_cnos_commands(module, commands, check_rc=True):
|
||||
retVal = ''
|
||||
enter_config = {'command': 'configure terminal', 'prompt': None,
|
||||
'answer': None}
|
||||
exit_config = {'command': 'end', 'prompt': None, 'answer': None}
|
||||
commands.insert(0, enter_config)
|
||||
commands.append(exit_config)
|
||||
for cmd in commands:
|
||||
retVal = retVal + '>> ' + cmd['command'] + '\n'
|
||||
try:
|
||||
responses = run_commands(module, commands, check_rc)
|
||||
for response in responses:
|
||||
retVal = retVal + '<< ' + response + '\n'
|
||||
except Exception as e:
|
||||
errMsg = ''
|
||||
if hasattr(e, 'message'):
|
||||
errMsg = e.message
|
||||
else:
|
||||
errMsg = str(e)
|
||||
# Exception in Exceptions
|
||||
if 'VLAN_ACCESS_MAP' in errMsg:
|
||||
return retVal + '<<' + errMsg + '\n'
|
||||
if 'confederation identifier' in errMsg:
|
||||
return retVal + '<<' + errMsg + '\n'
|
||||
# Add more here if required
|
||||
retVal = retVal + '<< ' + 'Error-101 ' + errMsg + '\n'
|
||||
return str(retVal)
|
||||
|
||||
|
||||
def get_capabilities(module):
|
||||
if hasattr(module, '_cnos_capabilities'):
|
||||
return module._cnos_capabilities
|
||||
try:
|
||||
capabilities = Connection(module._socket_path).get_capabilities()
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
module._cnos_capabilities = json.loads(capabilities)
|
||||
return module._cnos_capabilities
|
||||
|
||||
|
||||
def load_config(module, config):
|
||||
try:
|
||||
conn = get_connection(module)
|
||||
conn.get('enable')
|
||||
resp = conn.edit_config(config)
|
||||
return resp.get('response')
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
|
||||
|
||||
def get_defaults_flag(module):
|
||||
rc, out, err = exec_command(module, 'display running-config ?')
|
||||
out = to_text(out, errors='surrogate_then_replace')
|
||||
|
||||
commands = set()
|
||||
for line in out.splitlines():
|
||||
if line:
|
||||
commands.add(line.strip().split()[0])
|
||||
|
||||
if 'all' in commands:
|
||||
return 'all'
|
||||
else:
|
||||
return 'full'
|
||||
|
||||
|
||||
def enterEnableModeForDevice(enablePassword, timeout, obj):
|
||||
command = "enable\n"
|
||||
pwdPrompt = "password:"
|
||||
# debugOutput(enablePassword)
|
||||
# debugOutput('\n')
|
||||
obj.settimeout(int(timeout))
|
||||
# Executing enable
|
||||
obj.send(command)
|
||||
flag = False
|
||||
retVal = ""
|
||||
count = 5
|
||||
while not flag:
|
||||
# If wait time is execeeded.
|
||||
if(count == 0):
|
||||
flag = True
|
||||
else:
|
||||
count = count - 1
|
||||
# A delay of one second
|
||||
time.sleep(1)
|
||||
try:
|
||||
buffByte = obj.recv(9999)
|
||||
buff = buffByte.decode()
|
||||
retVal = retVal + buff
|
||||
# debugOutput(buff)
|
||||
gotit = buff.find(pwdPrompt)
|
||||
if(gotit != -1):
|
||||
time.sleep(1)
|
||||
if(enablePassword is None or enablePassword == ""):
|
||||
return "\n Error-106"
|
||||
obj.send(enablePassword)
|
||||
obj.send("\r")
|
||||
obj.send("\n")
|
||||
time.sleep(1)
|
||||
innerBuffByte = obj.recv(9999)
|
||||
innerBuff = innerBuffByte.decode()
|
||||
retVal = retVal + innerBuff
|
||||
# debugOutput(innerBuff)
|
||||
innerGotit = innerBuff.find("#")
|
||||
if(innerGotit != -1):
|
||||
return retVal
|
||||
else:
|
||||
gotit = buff.find("#")
|
||||
if(gotit != -1):
|
||||
return retVal
|
||||
except Exception:
|
||||
retVal = retVal + "\n Error-101"
|
||||
flag = True
|
||||
if(retVal == ""):
|
||||
retVal = "\n Error-101"
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def waitForDeviceResponse(command, prompt, timeout, obj):
|
||||
obj.settimeout(int(timeout))
|
||||
obj.send(command)
|
||||
flag = False
|
||||
retVal = ""
|
||||
while not flag:
|
||||
time.sleep(1)
|
||||
try:
|
||||
buffByte = obj.recv(9999)
|
||||
buff = buffByte.decode()
|
||||
retVal = retVal + buff
|
||||
# debugOutput(retVal)
|
||||
gotit = buff.find(prompt)
|
||||
if(gotit != -1):
|
||||
flag = True
|
||||
except Exception:
|
||||
# debugOutput(prompt)
|
||||
if prompt == "(yes/no)?":
|
||||
pass
|
||||
elif prompt == "Password:":
|
||||
pass
|
||||
else:
|
||||
retVal = retVal + "\n Error-101"
|
||||
flag = True
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def checkOutputForError(output):
|
||||
retVal = ""
|
||||
index = output.lower().find('error')
|
||||
startIndex = index + 6
|
||||
if(index == -1):
|
||||
index = output.lower().find('invalid')
|
||||
startIndex = index + 8
|
||||
if(index == -1):
|
||||
index = output.lower().find('cannot be enabled in l2 interface')
|
||||
startIndex = index + 34
|
||||
if(index == -1):
|
||||
index = output.lower().find('incorrect')
|
||||
startIndex = index + 10
|
||||
if(index == -1):
|
||||
index = output.lower().find('failure')
|
||||
startIndex = index + 8
|
||||
if(index == -1):
|
||||
return None
|
||||
|
||||
endIndex = startIndex + 3
|
||||
errorCode = output[startIndex:endIndex]
|
||||
result = errorCode.isdigit()
|
||||
if(result is not True):
|
||||
return "Device returned an Error. Please check Results for more \
|
||||
information"
|
||||
|
||||
errorFile = "dictionary/ErrorCodes.lvo"
|
||||
try:
|
||||
# with open(errorFile, 'r') as f:
|
||||
f = open(errorFile, 'r')
|
||||
for line in f:
|
||||
if('=' in line):
|
||||
data = line.split('=')
|
||||
if(data[0].strip() == errorCode):
|
||||
errorString = data[1].strip()
|
||||
return errorString
|
||||
except Exception:
|
||||
errorString = cnos_errorcodes.getErrorString(errorCode)
|
||||
errorString = errorString.strip()
|
||||
return errorString
|
||||
return "Error Code Not Found"
|
||||
# EOM
|
||||
|
||||
|
||||
def checkSanityofVariable(deviceType, variableId, variableValue):
|
||||
retVal = ""
|
||||
ruleFile = "dictionary/" + deviceType + "_rules.lvo"
|
||||
ruleString = getRuleStringForVariable(deviceType, ruleFile, variableId)
|
||||
retVal = validateValueAgainstRule(ruleString, variableValue)
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def getRuleStringForVariable(deviceType, ruleFile, variableId):
|
||||
retVal = ""
|
||||
try:
|
||||
# with open(ruleFile, 'r') as f:
|
||||
f = open(ruleFile, 'r')
|
||||
for line in f:
|
||||
# debugOutput(line)
|
||||
if(':' in line):
|
||||
data = line.split(':')
|
||||
# debugOutput(data[0])
|
||||
if(data[0].strip() == variableId):
|
||||
retVal = line
|
||||
except Exception:
|
||||
ruleString = cnos_devicerules.getRuleString(deviceType, variableId)
|
||||
retVal = ruleString.strip()
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def validateValueAgainstRule(ruleString, variableValue):
|
||||
|
||||
retVal = ""
|
||||
if(ruleString == ""):
|
||||
return 1
|
||||
rules = ruleString.split(':')
|
||||
variableType = rules[1].strip()
|
||||
varRange = rules[2].strip()
|
||||
if(variableType == "INTEGER"):
|
||||
result = checkInteger(variableValue)
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-111"
|
||||
elif(variableType == "FLOAT"):
|
||||
result = checkFloat(variableValue)
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-112"
|
||||
|
||||
elif(variableType == "INTEGER_VALUE"):
|
||||
int_range = varRange.split('-')
|
||||
r = range(int(int_range[0].strip()), int(int_range[1].strip()))
|
||||
if(checkInteger(variableValue) is not True):
|
||||
return "Error-111"
|
||||
result = int(variableValue) in r
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-113"
|
||||
|
||||
elif(variableType == "INTEGER_VALUE_RANGE"):
|
||||
int_range = varRange.split('-')
|
||||
varLower = int_range[0].strip()
|
||||
varHigher = int_range[1].strip()
|
||||
r = range(int(varLower), int(varHigher))
|
||||
val_range = variableValue.split('-')
|
||||
try:
|
||||
valLower = val_range[0].strip()
|
||||
valHigher = val_range[1].strip()
|
||||
except Exception:
|
||||
return "Error-113"
|
||||
if((checkInteger(valLower) is not True) or
|
||||
(checkInteger(valHigher) is not True)):
|
||||
# debugOutput("Error-114")
|
||||
return "Error-114"
|
||||
result = (int(valLower) in r) and (int(valHigher)in r) \
|
||||
and (int(valLower) < int(valHigher))
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
# debugOutput("Error-113")
|
||||
return "Error-113"
|
||||
|
||||
elif(variableType == "INTEGER_OPTIONS"):
|
||||
int_options = varRange.split(',')
|
||||
if(checkInteger(variableValue) is not True):
|
||||
return "Error-111"
|
||||
for opt in int_options:
|
||||
if(opt.strip() is variableValue):
|
||||
result = True
|
||||
break
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-115"
|
||||
|
||||
elif(variableType == "LONG"):
|
||||
result = checkLong(variableValue)
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-116"
|
||||
|
||||
elif(variableType == "LONG_VALUE"):
|
||||
long_range = varRange.split('-')
|
||||
r = range(int(long_range[0].strip()), int(long_range[1].strip()))
|
||||
if(checkLong(variableValue) is not True):
|
||||
# debugOutput(variableValue)
|
||||
return "Error-116"
|
||||
result = int(variableValue) in r
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-113"
|
||||
|
||||
elif(variableType == "LONG_VALUE_RANGE"):
|
||||
long_range = varRange.split('-')
|
||||
r = range(int(long_range[0].strip()), int(long_range[1].strip()))
|
||||
val_range = variableValue.split('-')
|
||||
if((checkLong(val_range[0]) is not True) or
|
||||
(checkLong(val_range[1]) is not True)):
|
||||
return "Error-117"
|
||||
result = (val_range[0] in r) and (
|
||||
val_range[1] in r) and (val_range[0] < val_range[1])
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-113"
|
||||
elif(variableType == "LONG_OPTIONS"):
|
||||
long_options = varRange.split(',')
|
||||
if(checkLong(variableValue) is not True):
|
||||
return "Error-116"
|
||||
for opt in long_options:
|
||||
if(opt.strip() == variableValue):
|
||||
result = True
|
||||
break
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-115"
|
||||
|
||||
elif(variableType == "TEXT"):
|
||||
if(variableValue == ""):
|
||||
return "Error-118"
|
||||
if(True is isinstance(variableValue, str)):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-119"
|
||||
|
||||
elif(variableType == "NO_VALIDATION"):
|
||||
if(variableValue == ""):
|
||||
return "Error-118"
|
||||
else:
|
||||
return "ok"
|
||||
|
||||
elif(variableType == "TEXT_OR_EMPTY"):
|
||||
if(variableValue is None or variableValue == ""):
|
||||
return "ok"
|
||||
if(result == isinstance(variableValue, str)):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-119"
|
||||
|
||||
elif(variableType == "MATCH_TEXT"):
|
||||
if(variableValue == ""):
|
||||
return "Error-118"
|
||||
if(isinstance(variableValue, str)):
|
||||
if(varRange == variableValue):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-120"
|
||||
else:
|
||||
return "Error-119"
|
||||
|
||||
elif(variableType == "MATCH_TEXT_OR_EMPTY"):
|
||||
if(variableValue is None or variableValue == ""):
|
||||
return "ok"
|
||||
if(isinstance(variableValue, str)):
|
||||
if(varRange == variableValue):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-120"
|
||||
else:
|
||||
return "Error-119"
|
||||
|
||||
elif(variableType == "TEXT_OPTIONS"):
|
||||
str_options = varRange.split(',')
|
||||
if(isinstance(variableValue, str) is not True):
|
||||
return "Error-119"
|
||||
result = False
|
||||
for opt in str_options:
|
||||
if(opt.strip() == variableValue):
|
||||
result = True
|
||||
break
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-115"
|
||||
|
||||
elif(variableType == "TEXT_OPTIONS_OR_EMPTY"):
|
||||
if(variableValue is None or variableValue == ""):
|
||||
return "ok"
|
||||
str_options = varRange.split(',')
|
||||
if(isinstance(variableValue, str) is not True):
|
||||
return "Error-119"
|
||||
for opt in str_options:
|
||||
if(opt.strip() == variableValue):
|
||||
result = True
|
||||
break
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-115"
|
||||
|
||||
elif(variableType == "IPV4Address"):
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, variableValue)
|
||||
result = True
|
||||
except socket.error:
|
||||
result = False
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-121"
|
||||
elif(variableType == "IPV4AddressWithMask"):
|
||||
if(variableValue is None or variableValue == ""):
|
||||
return "Error-119"
|
||||
str_options = variableValue.split('/')
|
||||
ipaddr = str_options[0]
|
||||
mask = str_options[1]
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, ipaddr)
|
||||
if(checkInteger(mask) is True):
|
||||
result = True
|
||||
else:
|
||||
result = False
|
||||
except socket.error:
|
||||
result = False
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-121"
|
||||
|
||||
elif(variableType == "IPV6Address"):
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, variableValue)
|
||||
result = True
|
||||
except socket.error:
|
||||
result = False
|
||||
if(result is True):
|
||||
return "ok"
|
||||
else:
|
||||
return "Error-122"
|
||||
|
||||
return retVal
|
||||
# EOM
|
||||
|
||||
|
||||
def disablePaging(remote_conn):
|
||||
remote_conn.send("terminal length 0\n")
|
||||
time.sleep(1)
|
||||
# Clear the buffer on the screen
|
||||
outputByte = remote_conn.recv(1000)
|
||||
output = outputByte.decode()
|
||||
return output
|
||||
# EOM
|
||||
|
||||
|
||||
def checkInteger(s):
|
||||
try:
|
||||
int(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
# EOM
|
||||
|
||||
|
||||
def checkFloat(s):
|
||||
try:
|
||||
float(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
# EOM
|
||||
|
||||
|
||||
def checkLong(s):
|
||||
try:
|
||||
int(s)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def debugOutput(command):
|
||||
f = open('debugOutput.txt', 'a')
|
||||
f.write(str(command)) # python will convert \n to os.linesep
|
||||
f.close() # you can omit in most cases as the destructor will call it
|
||||
# EOM
|
||||
1921
plugins/module_utils/network/cnos/cnos_devicerules.py
Normal file
1921
plugins/module_utils/network/cnos/cnos_devicerules.py
Normal file
File diff suppressed because it is too large
Load Diff
256
plugins/module_utils/network/cnos/cnos_errorcodes.py
Normal file
256
plugins/module_utils/network/cnos/cnos_errorcodes.py
Normal file
@@ -0,0 +1,256 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo, Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Contains error codes and methods
|
||||
# Lenovo Networking
|
||||
|
||||
errorDict = {0: 'Success',
|
||||
1: 'NOK',
|
||||
101: 'Device Response Timed out',
|
||||
102: 'Command Not supported - Use CLI command',
|
||||
103: 'Invalid Context',
|
||||
104: 'Command Value Not Supported as of Now. Use vlan Id only',
|
||||
105: 'Invalid interface Range',
|
||||
106: 'Please provide Enable Password.',
|
||||
108: '',
|
||||
109: '',
|
||||
110: 'Invalid protocol option',
|
||||
111: 'The Value is not Integer',
|
||||
112: 'The Value is not Float',
|
||||
113: 'Value is not in Range',
|
||||
114: 'Range value is not Integer',
|
||||
115: 'Value is not in Options',
|
||||
116: 'The Value is not Long',
|
||||
117: 'Range value is not Long',
|
||||
118: 'The Value cannot be empty',
|
||||
119: 'The Value is not String',
|
||||
120: 'The Value is not Matching',
|
||||
121: 'The Value is not IPV4 Address',
|
||||
122: 'The Value is not IPV6 Address',
|
||||
123: '',
|
||||
124: '',
|
||||
125: '',
|
||||
126: '',
|
||||
127: '',
|
||||
128: '',
|
||||
129: '',
|
||||
130: 'Invalid Access Map Name',
|
||||
131: 'Invalid Vlan Dot1q Tag',
|
||||
132: 'Invalid Vlan filter value',
|
||||
133: 'Invalid Vlan Range Value',
|
||||
134: 'Invalid Vlan Id',
|
||||
135: 'Invalid Vlan Access Map Action',
|
||||
136: 'Invalid Vlan Access Map Name',
|
||||
137: 'Invalid Access List',
|
||||
138: 'Invalid Vlan Access Map parameter',
|
||||
139: 'Invalid Vlan Name',
|
||||
140: 'Invalid Vlan Flood value,',
|
||||
141: 'Invalid Vlan State Value',
|
||||
142: 'Invalid Vlan Last Member query Interval',
|
||||
143: 'Invalid Querier IP address',
|
||||
144: 'Invalid Querier Time out',
|
||||
145: 'Invalid Query Interval',
|
||||
146: 'Invalid Vlan query max response time',
|
||||
147: 'Invalid vlan robustness variable',
|
||||
148: 'Invalid Vlan Startup Query count',
|
||||
149: 'Invalid vlan Startup Query Interval',
|
||||
150: 'Invalid Vlan snooping version',
|
||||
151: 'Invalid Vlan Ethernet Interface',
|
||||
152: 'Invalid Vlan Port Tag Number',
|
||||
153: 'Invalid mrouter option',
|
||||
154: 'Invalid Vlan Option',
|
||||
155: '',
|
||||
156: '',
|
||||
157: '',
|
||||
158: '',
|
||||
159: '',
|
||||
160: 'Invalid Vlag Auto Recovery Value',
|
||||
161: 'Invalid Vlag Config Consistency Value',
|
||||
162: 'Invalid Vlag Port Aggregation Number',
|
||||
163: 'Invalid Vlag Priority Value',
|
||||
164: 'Invalid Vlag Startup delay value',
|
||||
165: 'Invalid Vlag Trie Id',
|
||||
166: 'Invalid Vlag Instance Option',
|
||||
167: 'Invalid Vlag Keep Alive Attempts',
|
||||
168: 'Invalid Vlag Keep Alive Interval',
|
||||
169: 'Invalid Vlag Retry Interval',
|
||||
170: 'Invalid Vlag Peer Ip VRF Value',
|
||||
171: 'Invalid Vlag Health Check Options',
|
||||
172: 'Invalid Vlag Option',
|
||||
173: '',
|
||||
174: '',
|
||||
175: '',
|
||||
176: 'Invalid BGP As Number',
|
||||
177: 'Invalid Routing protocol option',
|
||||
178: 'Invalid BGP Address Family',
|
||||
179: 'Invalid AS Path options',
|
||||
180: 'Invalid BGP med options',
|
||||
181: 'Invalid Best Path option',
|
||||
182: 'Invalid BGP Local count number',
|
||||
183: 'Cluster Id has to either IP or AS Number',
|
||||
184: 'Invalid confederation identifier',
|
||||
185: 'Invalid Confederation Peer AS Value',
|
||||
186: 'Invalid Confederation Option',
|
||||
187: 'Invalid state path relay value',
|
||||
188: 'Invalid Maxas Limit AS Value',
|
||||
189: 'Invalid Neighbor IP Address or Neighbor AS Number',
|
||||
190: 'Invalid Router Id',
|
||||
191: 'Invalid BGP Keep Alive Interval',
|
||||
192: 'Invalid BGP Hold time',
|
||||
193: 'Invalid BGP Option',
|
||||
194: 'Invalid BGP Address Family option',
|
||||
195: 'Invalid BGP Address Family Redistribution option. ',
|
||||
196: 'Invalid BGP Address Family Route Map Name',
|
||||
197: 'Invalid Next Hop Critical Delay',
|
||||
198: 'Invalid Next Hop Non Critical Delay',
|
||||
199: 'Invalid Multipath Number Value',
|
||||
200: 'Invalid Aggegation Group Mode',
|
||||
201: 'Invalid Aggregation Group No',
|
||||
202: 'Invalid BFD Access Vlan',
|
||||
203: 'Invalid CFD Bridgeport Mode',
|
||||
204: 'Invalid Trunk Option',
|
||||
205: 'Invalid BFD Option',
|
||||
206: 'Invalid Portchannel description',
|
||||
207: 'Invalid Portchannel duplex option',
|
||||
208: 'Invalid Flow control option state',
|
||||
209: 'Invalid Flow control option',
|
||||
210: 'Invalid LACP Port priority',
|
||||
211: 'Invalid LACP Time out options',
|
||||
212: 'Invalid LACP Command options',
|
||||
213: 'Invalid LLDP TLV Option',
|
||||
214: 'Invalid LLDP Option',
|
||||
215: 'Invalid Load interval delay',
|
||||
216: 'Invalid Load interval Counter Number',
|
||||
217: 'Invalid Load Interval option',
|
||||
218: 'Invalid Mac Access Group Name',
|
||||
219: 'Invalid Mac Address',
|
||||
220: 'Invalid Microburst threshold value',
|
||||
221: 'Invalid MTU Value',
|
||||
222: 'Invalid Service instance value',
|
||||
223: 'Invalid service policy name',
|
||||
224: 'Invalid service policy options',
|
||||
225: 'Invalid Interface speed value',
|
||||
226: 'Invalid Storm control level value',
|
||||
227: 'Invalid Storm control option',
|
||||
228: 'Invalid Portchannel dot1q tag',
|
||||
229: 'Invalid VRRP Id Value',
|
||||
230: 'Invalid VRRP Options',
|
||||
231: 'Invalid portchannel source interface option',
|
||||
232: 'Invalid portchannel load balance options',
|
||||
233: 'Invalid Portchannel configuration attribute',
|
||||
234: 'Invalid BFD Interval Value',
|
||||
235: 'Invalid BFD minrx Value',
|
||||
236: 'Invalid BFD multiplier Value',
|
||||
237: 'Invalid Key Chain Value',
|
||||
238: 'Invalid key name option',
|
||||
239: 'Invalid key id value',
|
||||
240: 'Invalid Key Option',
|
||||
241: 'Invalid authentication option',
|
||||
242: 'Invalid destination Ip',
|
||||
243: 'Invalid source Ip',
|
||||
244: 'Invalid IP Option',
|
||||
245: 'Invalid Access group option',
|
||||
246: 'Invalid Access group name',
|
||||
247: 'Invalid ARP MacAddress Value',
|
||||
248: 'Invalid ARP timeout value',
|
||||
249: 'Invalid ARP Option',
|
||||
250: 'Invalid dhcp request option',
|
||||
251: 'Invalid dhcp Client option',
|
||||
252: 'Invalid relay Ip Address',
|
||||
253: 'Invalid dhcp Option',
|
||||
254: 'Invalid OSPF Option',
|
||||
255: 'Invalid OSPF Id IP Address Value',
|
||||
256: 'Invalid Ip Router Option',
|
||||
257: 'Invalid Spanning tree bpdufilter Options',
|
||||
258: 'Invalid Spanning tree bpduguard Options',
|
||||
259: 'Invalid Spanning tree cost Options',
|
||||
260: 'Invalid Spanning tree guard Options',
|
||||
261: 'Invalid Spanning tree link-type Options',
|
||||
262: 'Invalid Spanning tree link-type Options',
|
||||
263: 'Invalid Spanning tree options',
|
||||
264: 'Port-priority in increments of 32 is required',
|
||||
265: 'Invalid Spanning tree vlan options',
|
||||
266: 'Invalid IPv6 option',
|
||||
267: 'Invalid IPV6 neighbor IP Address',
|
||||
268: 'Invalid IPV6 neighbor mac address',
|
||||
269: 'Invalid IPV6 dhcp option',
|
||||
270: 'Invalid IPV6 relay address option',
|
||||
271: 'Invalid IPV6 Ethernet option',
|
||||
272: 'Invalid IPV6 Vlan option',
|
||||
273: 'Invalid IPV6 Link Local option',
|
||||
274: 'Invalid IPV6 dhcp option',
|
||||
275: 'Invalid IPV6 Address',
|
||||
276: 'Invalid IPV6 Address option',
|
||||
277: 'Invalid BFD neighbor options',
|
||||
278: 'Invalid Secondary option',
|
||||
289: 'Invalid PortChannel IPV4 address',
|
||||
290: 'Invalid Max Path Options',
|
||||
291: 'Invalid Distance Local Route value',
|
||||
292: 'Invalid Distance Internal AS value',
|
||||
293: 'Invalid Distance External AS value',
|
||||
294: 'Invalid BGP Reachability Half Life',
|
||||
295: 'Invalid BGP Dampening parameter',
|
||||
296: 'Invalid BGP Aggregate Prefix value',
|
||||
297: 'Invalid BGP Aggregate Prefix Option',
|
||||
298: 'Invalid BGP Address Family Route Map Name',
|
||||
299: 'Invalid BGP Net IP Mask Value',
|
||||
300: 'Invalid BGP Net IP Prefix Value',
|
||||
301: 'Invalid BGP Neighbor configuration option',
|
||||
302: 'Invalid BGP Neighbor Weight Value',
|
||||
303: 'Invalid Neigbor update source option',
|
||||
304: 'Invalid Ethernet slot/chassis number',
|
||||
305: 'Invalid Loopback Interface number',
|
||||
306: 'Invalid vlan id',
|
||||
307: 'Invalid Number of hops',
|
||||
308: 'Invalid Neighbor Keepalive interval',
|
||||
309: 'Invalid Neighbor timer hold time',
|
||||
310: 'Invalid neighbor password ',
|
||||
311: 'Invalid Max peer limit',
|
||||
312: 'Invalid Local AS Number',
|
||||
313: 'Invalid maximum hop count',
|
||||
314: 'Invalid neighbor description',
|
||||
315: 'Invalid Neighbor connect timer value',
|
||||
316: 'Invalid Neighbor address family option',
|
||||
317: 'Invalid neighbor address family option',
|
||||
318: 'Invalid route-map name',
|
||||
319: 'Invalid route-map',
|
||||
320: 'Invalid Name of a prefix list',
|
||||
321: 'Invalid Filter incoming option',
|
||||
322: 'Invalid AS path access-list name',
|
||||
323: 'Invalid Filter route option',
|
||||
324: 'Invalid route-map name',
|
||||
325: 'Invalid Number of occurrences of AS number',
|
||||
326: 'Invalid Prefix Limit'}
|
||||
|
||||
|
||||
def getErrorString(errorCode):
|
||||
retVal = errorDict[int(errorCode)]
|
||||
return retVal
|
||||
# EOM
|
||||
0
plugins/module_utils/network/edgeos/__init__.py
Normal file
0
plugins/module_utils/network/edgeos/__init__.py
Normal file
132
plugins/module_utils/network/edgeos/edgeos.py
Normal file
132
plugins/module_utils/network/edgeos/edgeos.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2018 Red Hat Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
import json
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.module_utils.connection import Connection, ConnectionError
|
||||
|
||||
_DEVICE_CONFIGS = None
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
if hasattr(module, '_edgeos_connection'):
|
||||
return module._edgeos_connection
|
||||
|
||||
capabilities = get_capabilities(module)
|
||||
network_api = capabilities.get('network_api')
|
||||
if network_api == 'cliconf':
|
||||
module._edgeos_connection = Connection(module._socket_path)
|
||||
else:
|
||||
module.fail_json(msg='Invalid connection type %s' % network_api)
|
||||
|
||||
return module._edgeos_connection
|
||||
|
||||
|
||||
def get_capabilities(module):
|
||||
if hasattr(module, '_edgeos_capabilities'):
|
||||
return module._edgeos_capabilities
|
||||
|
||||
capabilities = Connection(module._socket_path).get_capabilities()
|
||||
module._edgeos_capabilities = json.loads(capabilities)
|
||||
return module._edgeos_capabilities
|
||||
|
||||
|
||||
def get_config(module):
|
||||
global _DEVICE_CONFIGS
|
||||
|
||||
if _DEVICE_CONFIGS is not None:
|
||||
return _DEVICE_CONFIGS
|
||||
else:
|
||||
connection = get_connection(module)
|
||||
out = connection.get_config()
|
||||
cfg = to_text(out, errors='surrogate_then_replace').strip()
|
||||
_DEVICE_CONFIGS = cfg
|
||||
return cfg
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
responses = list()
|
||||
connection = get_connection(module)
|
||||
|
||||
for cmd in to_list(commands):
|
||||
if isinstance(cmd, dict):
|
||||
command = cmd['command']
|
||||
prompt = cmd['prompt']
|
||||
answer = cmd['answer']
|
||||
else:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
|
||||
try:
|
||||
out = connection.get(command, prompt, answer)
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
|
||||
try:
|
||||
out = to_text(out, errors='surrogate_or_strict')
|
||||
except UnicodeError:
|
||||
module.fail_json(msg=u'Failed to decode output from %s: %s' %
|
||||
(cmd, to_text(out)))
|
||||
|
||||
responses.append(out)
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
def load_config(module, commands, commit=False, comment=None):
|
||||
connection = get_connection(module)
|
||||
|
||||
try:
|
||||
out = connection.edit_config(commands)
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
|
||||
diff = None
|
||||
if module._diff:
|
||||
out = connection.get('compare')
|
||||
out = to_text(out, errors='surrogate_or_strict')
|
||||
|
||||
if not out.startswith('No changes'):
|
||||
out = connection.get('show')
|
||||
diff = to_text(out, errors='surrogate_or_strict').strip()
|
||||
|
||||
if commit:
|
||||
try:
|
||||
out = connection.commit(comment)
|
||||
except ConnectionError:
|
||||
connection.discard_changes()
|
||||
module.fail_json(msg='commit failed: %s' % out)
|
||||
|
||||
if not commit:
|
||||
connection.discard_changes()
|
||||
else:
|
||||
connection.get('exit')
|
||||
|
||||
if diff:
|
||||
return diff
|
||||
0
plugins/module_utils/network/edgeswitch/__init__.py
Normal file
0
plugins/module_utils/network/edgeswitch/__init__.py
Normal file
168
plugins/module_utils/network/edgeswitch/edgeswitch.py
Normal file
168
plugins/module_utils/network/edgeswitch/edgeswitch.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2018 Red Hat Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
import json
|
||||
import re
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.connection import Connection, ConnectionError
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
|
||||
|
||||
def build_aggregate_spec(element_spec, required, *extra_spec):
|
||||
aggregate_spec = deepcopy(element_spec)
|
||||
for elt in required:
|
||||
aggregate_spec[elt] = dict(required=True)
|
||||
remove_default_spec(aggregate_spec)
|
||||
argument_spec = dict(
|
||||
aggregate=dict(type='list', elements='dict', options=aggregate_spec)
|
||||
)
|
||||
argument_spec.update(element_spec)
|
||||
for elt in extra_spec:
|
||||
argument_spec.update(elt)
|
||||
return argument_spec
|
||||
|
||||
|
||||
def map_params_to_obj(module):
|
||||
obj = []
|
||||
aggregate = module.params.get('aggregate')
|
||||
if aggregate:
|
||||
for item in aggregate:
|
||||
for key in item:
|
||||
if item.get(key) is None:
|
||||
item[key] = module.params[key]
|
||||
|
||||
d = item.copy()
|
||||
obj.append(d)
|
||||
else:
|
||||
obj.append(module.params)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
if hasattr(module, '_edgeswitch_connection'):
|
||||
return module._edgeswitch_connection
|
||||
|
||||
capabilities = get_capabilities(module)
|
||||
network_api = capabilities.get('network_api')
|
||||
if network_api == 'cliconf':
|
||||
module._edgeswitch_connection = Connection(module._socket_path)
|
||||
else:
|
||||
module.fail_json(msg='Invalid connection type %s' % network_api)
|
||||
|
||||
return module._edgeswitch_connection
|
||||
|
||||
|
||||
def get_capabilities(module):
|
||||
if hasattr(module, '_edgeswitch_capabilities'):
|
||||
return module._edgeswitch_capabilities
|
||||
try:
|
||||
capabilities = Connection(module._socket_path).get_capabilities()
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
module._edgeswitch_capabilities = json.loads(capabilities)
|
||||
return module._edgeswitch_capabilities
|
||||
|
||||
|
||||
def get_defaults_flag(module):
|
||||
connection = get_connection(module)
|
||||
try:
|
||||
out = connection.get_defaults_flag()
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
return to_text(out, errors='surrogate_then_replace').strip()
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flag_str = ' '.join(to_list(flags))
|
||||
|
||||
try:
|
||||
return _DEVICE_CONFIGS[flag_str]
|
||||
except KeyError:
|
||||
connection = get_connection(module)
|
||||
try:
|
||||
out = connection.get_config(flags=flags)
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
cfg = to_text(out, errors='surrogate_then_replace').strip()
|
||||
_DEVICE_CONFIGS[flag_str] = cfg
|
||||
return cfg
|
||||
|
||||
|
||||
def get_interfaces_config(module):
|
||||
config = get_config(module)
|
||||
lines = config.split('\n')
|
||||
interfaces = {}
|
||||
interface = None
|
||||
for line in lines:
|
||||
if line == 'exit':
|
||||
if interface:
|
||||
interfaces[interface[0]] = interface
|
||||
interface = None
|
||||
elif interface:
|
||||
interface.append(line)
|
||||
else:
|
||||
match = re.match(r'^interface (.*)$', line)
|
||||
if match:
|
||||
interface = list()
|
||||
interface.append(line)
|
||||
|
||||
return interfaces
|
||||
|
||||
|
||||
def to_commands(module, commands):
|
||||
spec = {
|
||||
'command': dict(key=True),
|
||||
'prompt': dict(),
|
||||
'answer': dict()
|
||||
}
|
||||
transform = ComplexList(spec, module)
|
||||
return transform(commands)
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
connection = get_connection(module)
|
||||
try:
|
||||
return connection.run_commands(commands=commands, check_rc=check_rc)
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
|
||||
|
||||
def load_config(module, commands):
|
||||
connection = get_connection(module)
|
||||
|
||||
try:
|
||||
resp = connection.edit_config(commands)
|
||||
return resp.get('response')
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
@@ -0,0 +1,91 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2018 Red Hat Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class InterfaceConfiguration:
|
||||
def __init__(self):
|
||||
self.commands = []
|
||||
self.merged = False
|
||||
|
||||
def has_same_commands(self, interface):
|
||||
len1 = len(self.commands)
|
||||
len2 = len(interface.commands)
|
||||
return len1 == len2 and len1 == len(frozenset(self.commands).intersection(interface.commands))
|
||||
|
||||
|
||||
def merge_interfaces(interfaces):
|
||||
""" to reduce commands generated by an edgeswitch module
|
||||
we take interfaces one by one and we try to merge them with neighbors if everyone has same commands to run
|
||||
"""
|
||||
merged = {}
|
||||
|
||||
for i, interface in interfaces.items():
|
||||
if interface.merged:
|
||||
continue
|
||||
interface.merged = True
|
||||
|
||||
match = re.match(r'(\d+)\/(\d+)', i)
|
||||
group = int(match.group(1))
|
||||
start = int(match.group(2))
|
||||
end = start
|
||||
|
||||
while True:
|
||||
try:
|
||||
start = start - 1
|
||||
key = '{0}/{1}'.format(group, start)
|
||||
neighbor = interfaces[key]
|
||||
if not neighbor.merged and interface.has_same_commands(neighbor):
|
||||
neighbor.merged = True
|
||||
else:
|
||||
break
|
||||
except KeyError:
|
||||
break
|
||||
start = start + 1
|
||||
|
||||
while True:
|
||||
try:
|
||||
end = end + 1
|
||||
key = '{0}/{1}'.format(group, end)
|
||||
neighbor = interfaces[key]
|
||||
if not neighbor.merged and interface.has_same_commands(neighbor):
|
||||
neighbor.merged = True
|
||||
else:
|
||||
break
|
||||
except KeyError:
|
||||
break
|
||||
end = end - 1
|
||||
|
||||
if end == start:
|
||||
key = '{0}/{1}'.format(group, start)
|
||||
else:
|
||||
key = '{0}/{1}-{2}/{3}'.format(group, start, group, end)
|
||||
|
||||
merged[key] = interface
|
||||
return merged
|
||||
0
plugins/module_utils/network/enos/__init__.py
Normal file
0
plugins/module_utils/network/enos/__init__.py
Normal file
172
plugins/module_utils/network/enos/enos.py
Normal file
172
plugins/module_utils/network/enos/enos.py
Normal file
@@ -0,0 +1,172 @@
|
||||
# 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.
|
||||
#
|
||||
# Copyright (C) 2017 Lenovo.
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Contains utility methods
|
||||
# Lenovo Networking
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, EntityCollection
|
||||
from ansible.module_utils.connection import Connection, exec_command
|
||||
from ansible.module_utils.connection import ConnectionError
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
_CONNECTION = None
|
||||
|
||||
enos_provider_spec = {
|
||||
'host': dict(),
|
||||
'port': dict(type='int'),
|
||||
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
|
||||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
|
||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
|
||||
'authorize': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTHORIZE']), type='bool'),
|
||||
'auth_pass': dict(fallback=(env_fallback, ['ANSIBLE_NET_AUTH_PASS']), no_log=True),
|
||||
'timeout': dict(type='int'),
|
||||
'context': dict(),
|
||||
'passwords': dict()
|
||||
}
|
||||
|
||||
enos_argument_spec = {
|
||||
'provider': dict(type='dict', options=enos_provider_spec),
|
||||
}
|
||||
|
||||
command_spec = {
|
||||
'command': dict(key=True),
|
||||
'prompt': dict(),
|
||||
'answer': dict()
|
||||
}
|
||||
|
||||
|
||||
def get_provider_argspec():
|
||||
return enos_provider_spec
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
pass
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
global _CONNECTION
|
||||
if _CONNECTION:
|
||||
return _CONNECTION
|
||||
_CONNECTION = Connection(module._socket_path)
|
||||
|
||||
context = None
|
||||
try:
|
||||
context = module.params['context']
|
||||
except KeyError:
|
||||
context = None
|
||||
|
||||
if context:
|
||||
if context == 'system':
|
||||
command = 'changeto system'
|
||||
else:
|
||||
command = 'changeto context %s' % context
|
||||
_CONNECTION.get(command)
|
||||
|
||||
return _CONNECTION
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = [] if flags is None else flags
|
||||
|
||||
passwords = None
|
||||
try:
|
||||
passwords = module.params['passwords']
|
||||
except KeyError:
|
||||
passwords = None
|
||||
if passwords:
|
||||
cmd = 'more system:running-config'
|
||||
else:
|
||||
cmd = 'show running-config '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
try:
|
||||
return _DEVICE_CONFIGS[cmd]
|
||||
except KeyError:
|
||||
conn = get_connection(module)
|
||||
out = conn.get(cmd)
|
||||
cfg = to_text(out, errors='surrogate_then_replace').strip()
|
||||
_DEVICE_CONFIGS[cmd] = cfg
|
||||
return cfg
|
||||
|
||||
|
||||
def to_commands(module, commands):
|
||||
if not isinstance(commands, list):
|
||||
raise AssertionError('argument must be of type <list>')
|
||||
|
||||
transform = EntityCollection(module, command_spec)
|
||||
commands = transform(commands)
|
||||
|
||||
for index, item in enumerate(commands):
|
||||
if module.check_mode and not item['command'].startswith('show'):
|
||||
module.warn('only show commands are supported when using check '
|
||||
'mode, not executing `%s`' % item['command'])
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
connection = get_connection(module)
|
||||
|
||||
commands = to_commands(module, to_list(commands))
|
||||
|
||||
responses = list()
|
||||
|
||||
for cmd in commands:
|
||||
out = connection.get(**cmd)
|
||||
responses.append(to_text(out, errors='surrogate_then_replace'))
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
def load_config(module, config):
|
||||
try:
|
||||
conn = get_connection(module)
|
||||
conn.get('enable')
|
||||
conn.edit_config(config)
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
|
||||
|
||||
def get_defaults_flag(module):
|
||||
rc, out, err = exec_command(module, 'show running-config ?')
|
||||
out = to_text(out, errors='surrogate_then_replace')
|
||||
|
||||
commands = set()
|
||||
for line in out.splitlines():
|
||||
if line:
|
||||
commands.add(line.strip().split()[0])
|
||||
|
||||
if 'all' in commands:
|
||||
return 'all'
|
||||
else:
|
||||
return 'full'
|
||||
0
plugins/module_utils/network/eric_eccli/__init__.py
Normal file
0
plugins/module_utils/network/eric_eccli/__init__.py
Normal file
49
plugins/module_utils/network/eric_eccli/eric_eccli.py
Normal file
49
plugins/module_utils/network/eric_eccli/eric_eccli.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# Copyright (c) 2019 Ericsson AB.
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
#
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.connection import Connection, ConnectionError
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
if hasattr(module, '_eric_eccli_connection'):
|
||||
return module._eric_eccli_connection
|
||||
|
||||
capabilities = get_capabilities(module)
|
||||
network_api = capabilities.get('network_api')
|
||||
if network_api == 'cliconf':
|
||||
module._eric_eccli_connection = Connection(module._socket_path)
|
||||
else:
|
||||
module.fail_json(msg='Invalid connection type %s' % network_api)
|
||||
|
||||
return module._eric_eccli_connection
|
||||
|
||||
|
||||
def get_capabilities(module):
|
||||
if hasattr(module, '_eric_eccli_capabilities'):
|
||||
return module._eric_eccli_capabilities
|
||||
try:
|
||||
capabilities = Connection(module._socket_path).get_capabilities()
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
module._eric_eccli_capabilities = json.loads(capabilities)
|
||||
return module._eric_eccli_capabilities
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
connection = get_connection(module)
|
||||
try:
|
||||
return connection.run_commands(commands=commands, check_rc=check_rc)
|
||||
except ConnectionError as exc:
|
||||
module.fail_json(msg=to_text(exc))
|
||||
0
plugins/module_utils/network/exos/__init__.py
Normal file
0
plugins/module_utils/network/exos/__init__.py
Normal file
23
plugins/module_utils/network/exos/argspec/facts/facts.py
Normal file
23
plugins/module_utils/network/exos/argspec/facts/facts.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
"""
|
||||
The arg spec for the exos facts module.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class FactsArgs(object): # pylint: disable=R0903
|
||||
""" The arg spec for the exos facts module
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
argument_spec = {
|
||||
'gather_subset': dict(default=['!config'], type='list'),
|
||||
'gather_network_resources': dict(type='list'),
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#############################################
|
||||
# WARNING #
|
||||
#############################################
|
||||
#
|
||||
# This file is auto generated by the resource
|
||||
# module builder playbook.
|
||||
#
|
||||
# Do not edit this file manually.
|
||||
#
|
||||
# Changes to this file will be over written
|
||||
# by the resource module builder.
|
||||
#
|
||||
# Changes should be made in the model used to
|
||||
# generate this file or in the resource module
|
||||
# builder template.
|
||||
#
|
||||
#############################################
|
||||
"""
|
||||
The arg spec for the exos_l2_interfaces module
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class L2_interfacesArgs(object): # pylint: disable=R0903
|
||||
"""The arg spec for the exos_l2_interfaces module
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
argument_spec = {
|
||||
'config': {
|
||||
'elements': 'dict',
|
||||
'options': {
|
||||
'access': {'options': {'vlan': {'type': 'int'}},
|
||||
'type': 'dict'},
|
||||
'name': {'required': True, 'type': 'str'},
|
||||
'trunk': {'options': {'native_vlan': {'type': 'int'}, 'trunk_allowed_vlans': {'type': 'list'}},
|
||||
'type': 'dict'}},
|
||||
'type': 'list'},
|
||||
'state': {'choices': ['merged', 'replaced', 'overridden', 'deleted'], 'default': 'merged', 'type': 'str'}
|
||||
} # pylint: disable=C0301
|
||||
@@ -0,0 +1,57 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#############################################
|
||||
# WARNING #
|
||||
#############################################
|
||||
#
|
||||
# This file is auto generated by the resource
|
||||
# module builder playbook.
|
||||
#
|
||||
# Do not edit this file manually.
|
||||
#
|
||||
# Changes to this file will be over written
|
||||
# by the resource module builder.
|
||||
#
|
||||
# Changes should be made in the model used to
|
||||
# generate this file or in the resource module
|
||||
# builder template.
|
||||
#
|
||||
#############################################
|
||||
|
||||
"""
|
||||
The arg spec for the exos_lldp_global module
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class Lldp_globalArgs(object): # pylint: disable=R0903
|
||||
"""The arg spec for the exos_lldp_global module
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
argument_spec = {
|
||||
'config': {
|
||||
'options': {
|
||||
'interval': {'default': 30, 'type': 'int'},
|
||||
'tlv_select': {
|
||||
'options': {
|
||||
'management_address': {'type': 'bool'},
|
||||
'port_description': {'type': 'bool'},
|
||||
'system_capabilities': {'type': 'bool'},
|
||||
'system_description': {
|
||||
'default': True,
|
||||
'type': 'bool'},
|
||||
'system_name': {'default': True, 'type': 'bool'}},
|
||||
'type': 'dict'}},
|
||||
'type': 'dict'},
|
||||
'state': {
|
||||
'choices': ['merged', 'replaced', 'deleted'],
|
||||
'default': 'merged',
|
||||
'type': 'str'}} # pylint: disable=C0301
|
||||
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#############################################
|
||||
# WARNING #
|
||||
#############################################
|
||||
#
|
||||
# This file is auto generated by the resource
|
||||
# module builder playbook.
|
||||
#
|
||||
# Do not edit this file manually.
|
||||
#
|
||||
# Changes to this file will be over written
|
||||
# by the resource module builder.
|
||||
#
|
||||
# Changes should be made in the model used to
|
||||
# generate this file or in the resource module
|
||||
# builder template.
|
||||
#
|
||||
#############################################
|
||||
|
||||
"""
|
||||
The arg spec for the exos_lldp_interfaces module
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class Lldp_interfacesArgs(object): # pylint: disable=R0903
|
||||
"""The arg spec for the exos_lldp_interfaces module
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
argument_spec = {
|
||||
'config': {
|
||||
'elements': 'dict',
|
||||
'options': {
|
||||
'enabled': {'type': 'bool'},
|
||||
'name': {'required': True, 'type': 'str'}},
|
||||
'type': 'list'},
|
||||
'state': {
|
||||
'choices': ['merged', 'replaced', 'overridden', 'deleted'],
|
||||
'default': 'merged',
|
||||
'type': 'str'}} # pylint: disable=C0301
|
||||
53
plugins/module_utils/network/exos/argspec/vlans/vlans.py
Normal file
53
plugins/module_utils/network/exos/argspec/vlans/vlans.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#############################################
|
||||
# WARNING #
|
||||
#############################################
|
||||
#
|
||||
# This file is auto generated by the resource
|
||||
# module builder playbook.
|
||||
#
|
||||
# Do not edit this file manually.
|
||||
#
|
||||
# Changes to this file will be over written
|
||||
# by the resource module builder.
|
||||
#
|
||||
# Changes should be made in the model used to
|
||||
# generate this file or in the resource module
|
||||
# builder template.
|
||||
#
|
||||
#############################################
|
||||
|
||||
"""
|
||||
The arg spec for the exos_vlans module
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class VlansArgs(object): # pylint: disable=R0903
|
||||
"""The arg spec for the exos_vlans module
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
argument_spec = {
|
||||
'config': {
|
||||
'elements': 'dict',
|
||||
'options': {
|
||||
'name': {'type': 'str'},
|
||||
'state': {
|
||||
'choices': ['active', 'suspend'],
|
||||
'default': 'active',
|
||||
'type': 'str'},
|
||||
'vlan_id': {'required': True, 'type': 'int'}},
|
||||
'type': 'list'},
|
||||
'state': {
|
||||
'choices': ['merged', 'replaced', 'overridden', 'deleted'],
|
||||
'default': 'merged',
|
||||
'type': 'str'}} # pylint: disable=C0301
|
||||
@@ -0,0 +1,294 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
"""
|
||||
The exos_l2_interfaces class
|
||||
It is in this file where the current configuration (as dict)
|
||||
is compared to the provided configuration (as dict) and the command set
|
||||
necessary to bring the current configuration to it's desired end-state is
|
||||
created
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ConfigBase
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, dict_diff
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.facts import Facts
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.exos import send_requests
|
||||
|
||||
|
||||
class L2_interfaces(ConfigBase):
|
||||
"""
|
||||
The exos_l2_interfaces class
|
||||
"""
|
||||
|
||||
gather_subset = [
|
||||
'!all',
|
||||
'!min',
|
||||
]
|
||||
|
||||
gather_network_resources = [
|
||||
'l2_interfaces',
|
||||
]
|
||||
|
||||
L2_INTERFACE_NATIVE = {
|
||||
"data": {
|
||||
"openconfig-vlan:config": {
|
||||
"interface-mode": "TRUNK",
|
||||
"native-vlan": None,
|
||||
"trunk-vlans": []
|
||||
}
|
||||
},
|
||||
"method": "PATCH",
|
||||
"path": None
|
||||
}
|
||||
|
||||
L2_INTERFACE_TRUNK = {
|
||||
"data": {
|
||||
"openconfig-vlan:config": {
|
||||
"interface-mode": "TRUNK",
|
||||
"trunk-vlans": []
|
||||
}
|
||||
},
|
||||
"method": "PATCH",
|
||||
"path": None
|
||||
}
|
||||
|
||||
L2_INTERFACE_ACCESS = {
|
||||
"data": {
|
||||
"openconfig-vlan:config": {
|
||||
"interface-mode": "ACCESS",
|
||||
"access-vlan": None
|
||||
}
|
||||
},
|
||||
"method": "PATCH",
|
||||
"path": None
|
||||
}
|
||||
|
||||
L2_PATH = "/rest/restconf/data/openconfig-interfaces:interfaces/interface="
|
||||
|
||||
def __init__(self, module):
|
||||
super(L2_interfaces, self).__init__(module)
|
||||
|
||||
def get_l2_interfaces_facts(self):
|
||||
""" Get the 'facts' (the current configuration)
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The current configuration as a dictionary
|
||||
"""
|
||||
facts, _warnings = Facts(self._module).get_facts(
|
||||
self.gather_subset, self.gather_network_resources)
|
||||
l2_interfaces_facts = facts['ansible_network_resources'].get(
|
||||
'l2_interfaces')
|
||||
if not l2_interfaces_facts:
|
||||
return []
|
||||
return l2_interfaces_facts
|
||||
|
||||
def execute_module(self):
|
||||
""" Execute the module
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The result from module execution
|
||||
"""
|
||||
result = {'changed': False}
|
||||
warnings = list()
|
||||
requests = list()
|
||||
|
||||
existing_l2_interfaces_facts = self.get_l2_interfaces_facts()
|
||||
requests.extend(self.set_config(existing_l2_interfaces_facts))
|
||||
if requests:
|
||||
if not self._module.check_mode:
|
||||
send_requests(self._module, requests=requests)
|
||||
result['changed'] = True
|
||||
result['requests'] = requests
|
||||
|
||||
changed_l2_interfaces_facts = self.get_l2_interfaces_facts()
|
||||
|
||||
result['before'] = existing_l2_interfaces_facts
|
||||
if result['changed']:
|
||||
result['after'] = changed_l2_interfaces_facts
|
||||
|
||||
result['warnings'] = warnings
|
||||
return result
|
||||
|
||||
def set_config(self, existing_l2_interfaces_facts):
|
||||
""" Collect the configuration from the args passed to the module,
|
||||
collect the current configuration (as a dict from facts)
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
want = self._module.params['config']
|
||||
have = existing_l2_interfaces_facts
|
||||
resp = self.set_state(want, have)
|
||||
return to_list(resp)
|
||||
|
||||
def set_state(self, want, have):
|
||||
""" Select the appropriate function based on the state provided
|
||||
|
||||
:param want: the desired configuration as a dictionary
|
||||
:param have: the current configuration as a dictionary
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
state = self._module.params['state']
|
||||
if state == 'overridden':
|
||||
requests = self._state_overridden(want, have)
|
||||
elif state == 'deleted':
|
||||
requests = self._state_deleted(want, have)
|
||||
elif state == 'merged':
|
||||
requests = self._state_merged(want, have)
|
||||
elif state == 'replaced':
|
||||
requests = self._state_replaced(want, have)
|
||||
return requests
|
||||
|
||||
def _state_replaced(self, want, have):
|
||||
""" The request generator when state is replaced
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
requests = []
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w["name"] == h["name"]:
|
||||
if dict_diff(w, h):
|
||||
l2_request = self._update_patch_request(w, h)
|
||||
l2_request["data"] = json.dumps(l2_request["data"])
|
||||
requests.append(l2_request)
|
||||
break
|
||||
|
||||
return requests
|
||||
|
||||
def _state_overridden(self, want, have):
|
||||
""" The request generator when state is overridden
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
requests = []
|
||||
have_copy = []
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w["name"] == h["name"]:
|
||||
if dict_diff(w, h):
|
||||
l2_request = self._update_patch_request(w, h)
|
||||
l2_request["data"] = json.dumps(l2_request["data"])
|
||||
requests.append(l2_request)
|
||||
have_copy.append(h)
|
||||
break
|
||||
|
||||
for h in have:
|
||||
if h not in have_copy:
|
||||
l2_delete = self._update_delete_request(h)
|
||||
if l2_delete["path"]:
|
||||
l2_delete["data"] = json.dumps(l2_delete["data"])
|
||||
requests.append(l2_delete)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_merged(self, want, have):
|
||||
""" The request generator when state is merged
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to merge the provided into
|
||||
the current configuration
|
||||
"""
|
||||
requests = []
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w["name"] == h["name"]:
|
||||
if dict_diff(h, w):
|
||||
l2_request = self._update_patch_request(w, h)
|
||||
l2_request["data"] = json.dumps(l2_request["data"])
|
||||
requests.append(l2_request)
|
||||
break
|
||||
|
||||
return requests
|
||||
|
||||
def _state_deleted(self, want, have):
|
||||
""" The request generator when state is deleted
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to remove the current configuration
|
||||
of the provided objects
|
||||
"""
|
||||
requests = []
|
||||
if want:
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w["name"] == h["name"]:
|
||||
l2_delete = self._update_delete_request(h)
|
||||
if l2_delete["path"]:
|
||||
l2_delete["data"] = json.dumps(l2_delete["data"])
|
||||
requests.append(l2_delete)
|
||||
break
|
||||
|
||||
else:
|
||||
for h in have:
|
||||
l2_delete = self._update_delete_request(h)
|
||||
if l2_delete["path"]:
|
||||
l2_delete["data"] = json.dumps(l2_delete["data"])
|
||||
requests.append(l2_delete)
|
||||
|
||||
return requests
|
||||
|
||||
def _update_patch_request(self, want, have):
|
||||
|
||||
facts, _warnings = Facts(self._module).get_facts(
|
||||
self.gather_subset, ['vlans', ])
|
||||
vlans_facts = facts['ansible_network_resources'].get('vlans')
|
||||
|
||||
vlan_id = []
|
||||
|
||||
for vlan in vlans_facts:
|
||||
vlan_id.append(vlan['vlan_id'])
|
||||
|
||||
if want.get("access"):
|
||||
if want["access"]["vlan"] in vlan_id:
|
||||
l2_request = deepcopy(self.L2_INTERFACE_ACCESS)
|
||||
l2_request["data"]["openconfig-vlan:config"]["access-vlan"] = want["access"]["vlan"]
|
||||
l2_request["path"] = self.L2_PATH + str(want["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
|
||||
else:
|
||||
self._module.fail_json(msg="VLAN %s does not exist" % (want["access"]["vlan"]))
|
||||
|
||||
elif want.get("trunk"):
|
||||
if want["trunk"]["native_vlan"]:
|
||||
if want["trunk"]["native_vlan"] in vlan_id:
|
||||
l2_request = deepcopy(self.L2_INTERFACE_NATIVE)
|
||||
l2_request["data"]["openconfig-vlan:config"]["native-vlan"] = want["trunk"]["native_vlan"]
|
||||
l2_request["path"] = self.L2_PATH + str(want["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
|
||||
for vlan in want["trunk"]["trunk_allowed_vlans"]:
|
||||
if int(vlan) in vlan_id:
|
||||
l2_request["data"]["openconfig-vlan:config"]["trunk-vlans"].append(int(vlan))
|
||||
else:
|
||||
self._module.fail_json(msg="VLAN %s does not exist" % (vlan))
|
||||
else:
|
||||
self._module.fail_json(msg="VLAN %s does not exist" % (want["trunk"]["native_vlan"]))
|
||||
else:
|
||||
l2_request = deepcopy(self.L2_INTERFACE_TRUNK)
|
||||
l2_request["path"] = self.L2_PATH + str(want["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
|
||||
for vlan in want["trunk"]["trunk_allowed_vlans"]:
|
||||
if int(vlan) in vlan_id:
|
||||
l2_request["data"]["openconfig-vlan:config"]["trunk-vlans"].append(int(vlan))
|
||||
else:
|
||||
self._module.fail_json(msg="VLAN %s does not exist" % (vlan))
|
||||
return l2_request
|
||||
|
||||
def _update_delete_request(self, have):
|
||||
|
||||
l2_request = deepcopy(self.L2_INTERFACE_ACCESS)
|
||||
|
||||
if have["access"] and have["access"]["vlan"] != 1 or have["trunk"] or not have["access"]:
|
||||
l2_request["data"]["openconfig-vlan:config"]["access-vlan"] = 1
|
||||
l2_request["path"] = self.L2_PATH + str(have["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
|
||||
|
||||
return l2_request
|
||||
@@ -0,0 +1,199 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
"""
|
||||
The exos_lldp_global class
|
||||
It is in this file where the current configuration (as dict)
|
||||
is compared to the provided configuration (as dict) and the command set
|
||||
necessary to bring the current configuration to it's desired end-state is
|
||||
created
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ConfigBase
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.facts import Facts
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.exos import send_requests
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
class Lldp_global(ConfigBase):
|
||||
"""
|
||||
The exos_lldp_global class
|
||||
"""
|
||||
|
||||
gather_subset = [
|
||||
'!all',
|
||||
'!min',
|
||||
]
|
||||
|
||||
gather_network_resources = [
|
||||
'lldp_global',
|
||||
]
|
||||
|
||||
LLDP_DEFAULT_INTERVAL = 30
|
||||
LLDP_DEFAULT_TLV = {
|
||||
'system_name': True,
|
||||
'system_description': True,
|
||||
'system_capabilities': False,
|
||||
'port_description': False,
|
||||
'management_address': False
|
||||
}
|
||||
LLDP_REQUEST = {
|
||||
"data": {"openconfig-lldp:config": {}},
|
||||
"method": "PUT",
|
||||
"path": "/rest/restconf/data/openconfig-lldp:lldp/config"
|
||||
}
|
||||
|
||||
def __init__(self, module):
|
||||
super(Lldp_global, self).__init__(module)
|
||||
|
||||
def get_lldp_global_facts(self):
|
||||
""" Get the 'facts' (the current configuration)
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The current configuration as a dictionary
|
||||
"""
|
||||
facts, _warnings = Facts(self._module).get_facts(
|
||||
self.gather_subset, self.gather_network_resources)
|
||||
lldp_global_facts = facts['ansible_network_resources'].get('lldp_global')
|
||||
if not lldp_global_facts:
|
||||
return {}
|
||||
return lldp_global_facts
|
||||
|
||||
def execute_module(self):
|
||||
""" Execute the module
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The result from module execution
|
||||
"""
|
||||
result = {'changed': False}
|
||||
warnings = list()
|
||||
requests = list()
|
||||
|
||||
existing_lldp_global_facts = self.get_lldp_global_facts()
|
||||
requests.extend(self.set_config(existing_lldp_global_facts))
|
||||
if requests:
|
||||
if not self._module.check_mode:
|
||||
send_requests(self._module, requests)
|
||||
result['changed'] = True
|
||||
result['requests'] = requests
|
||||
|
||||
changed_lldp_global_facts = self.get_lldp_global_facts()
|
||||
|
||||
result['before'] = existing_lldp_global_facts
|
||||
if result['changed']:
|
||||
result['after'] = changed_lldp_global_facts
|
||||
|
||||
result['warnings'] = warnings
|
||||
return result
|
||||
|
||||
def set_config(self, existing_lldp_global_facts):
|
||||
""" Collect the configuration from the args passed to the module,
|
||||
collect the current configuration (as a dict from facts)
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
want = self._module.params['config']
|
||||
have = existing_lldp_global_facts
|
||||
resp = self.set_state(want, have)
|
||||
return to_list(resp)
|
||||
|
||||
def set_state(self, want, have):
|
||||
""" Select the appropriate function based on the state provided
|
||||
|
||||
:param want: the desired configuration as a dictionary
|
||||
:param have: the current configuration as a dictionary
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
state = self._module.params['state']
|
||||
|
||||
if state == 'deleted':
|
||||
requests = self._state_deleted(want, have)
|
||||
elif state == 'merged':
|
||||
requests = self._state_merged(want, have)
|
||||
elif state == 'replaced':
|
||||
requests = self._state_replaced(want, have)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_replaced(self, want, have):
|
||||
""" The request generator when state is replaced
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
requests = []
|
||||
requests.extend(self._state_deleted(want, have))
|
||||
requests.extend(self._state_merged(want, have))
|
||||
return requests
|
||||
|
||||
def _state_merged(self, want, have):
|
||||
""" The request generator when state is merged
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to merge the provided into
|
||||
the current configuration
|
||||
"""
|
||||
requests = []
|
||||
|
||||
request = deepcopy(self.LLDP_REQUEST)
|
||||
self._update_lldp_config_body_if_diff(want, have, request)
|
||||
|
||||
if len(request["data"]["openconfig-lldp:config"]):
|
||||
request["data"] = json.dumps(request["data"])
|
||||
requests.append(request)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_deleted(self, want, have):
|
||||
""" The request generator when state is deleted
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to remove the current configuration
|
||||
of the provided objects
|
||||
"""
|
||||
requests = []
|
||||
|
||||
request = deepcopy(self.LLDP_REQUEST)
|
||||
if want:
|
||||
self._update_lldp_config_body_if_diff(want, have, request)
|
||||
else:
|
||||
if self.LLDP_DEFAULT_INTERVAL != have['interval']:
|
||||
request["data"]["openconfig-lldp:config"].update(
|
||||
{"hello-timer": self.LLDP_DEFAULT_INTERVAL})
|
||||
|
||||
if have['tlv_select'] != self.LLDP_DEFAULT_TLV:
|
||||
request["data"]["openconfig-lldp:config"].update(
|
||||
{"suppress-tlv-advertisement": [key.upper() for key, value in self.LLDP_DEFAULT_TLV.items() if not value]})
|
||||
request["data"]["openconfig-lldp:config"]["suppress-tlv-advertisement"].sort()
|
||||
if len(request["data"]["openconfig-lldp:config"]):
|
||||
request["data"] = json.dumps(request["data"])
|
||||
requests.append(request)
|
||||
|
||||
return requests
|
||||
|
||||
def _update_lldp_config_body_if_diff(self, want, have, request):
|
||||
if want.get('interval'):
|
||||
if want['interval'] != have['interval']:
|
||||
request["data"]["openconfig-lldp:config"].update(
|
||||
{"hello-timer": want['interval']})
|
||||
if want.get('tlv_select'):
|
||||
# Create list of TLVs to be suppressed which aren't already
|
||||
want_suppress = [key.upper() for key, value in want["tlv_select"].items() if have["tlv_select"][key] != value and value is False]
|
||||
if want_suppress:
|
||||
# Add previously suppressed TLVs to the list as we are doing a PUT op
|
||||
want_suppress.extend([key.upper() for key, value in have["tlv_select"].items() if value is False])
|
||||
request["data"]["openconfig-lldp:config"].update(
|
||||
{"suppress-tlv-advertisement": want_suppress})
|
||||
request["data"]["openconfig-lldp:config"]["suppress-tlv-advertisement"].sort()
|
||||
@@ -0,0 +1,243 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
"""
|
||||
The exos_lldp_interfaces class
|
||||
It is in this file where the current configuration (as dict)
|
||||
is compared to the provided configuration (as dict) and the command set
|
||||
necessary to bring the current configuration to it's desired end-state is
|
||||
created
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ConfigBase
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, dict_diff
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.facts import Facts
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.exos import send_requests
|
||||
|
||||
|
||||
class Lldp_interfaces(ConfigBase):
|
||||
"""
|
||||
The exos_lldp_interfaces class
|
||||
"""
|
||||
|
||||
gather_subset = [
|
||||
'!all',
|
||||
'!min',
|
||||
]
|
||||
|
||||
gather_network_resources = [
|
||||
'lldp_interfaces',
|
||||
]
|
||||
|
||||
LLDP_INTERFACE = {
|
||||
"data": {
|
||||
"openconfig-lldp:config": {
|
||||
"name": None,
|
||||
"enabled": True
|
||||
}
|
||||
},
|
||||
"method": "PATCH",
|
||||
"path": None
|
||||
}
|
||||
|
||||
LLDP_PATH = "/rest/restconf/data/openconfig-lldp:lldp/interfaces/interface="
|
||||
|
||||
def __init__(self, module):
|
||||
super(Lldp_interfaces, self).__init__(module)
|
||||
|
||||
def get_lldp_interfaces_facts(self):
|
||||
""" Get the 'facts' (the current configuration)
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The current configuration as a dictionary
|
||||
"""
|
||||
facts, _warnings = Facts(self._module).get_facts(
|
||||
self.gather_subset, self.gather_network_resources)
|
||||
lldp_interfaces_facts = facts['ansible_network_resources'].get(
|
||||
'lldp_interfaces')
|
||||
if not lldp_interfaces_facts:
|
||||
return []
|
||||
return lldp_interfaces_facts
|
||||
|
||||
def execute_module(self):
|
||||
""" Execute the module
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The result from module execution
|
||||
"""
|
||||
result = {'changed': False}
|
||||
warnings = list()
|
||||
requests = list()
|
||||
|
||||
existing_lldp_interfaces_facts = self.get_lldp_interfaces_facts()
|
||||
requests.extend(self.set_config(existing_lldp_interfaces_facts))
|
||||
if requests:
|
||||
if not self._module.check_mode:
|
||||
send_requests(self._module, requests=requests)
|
||||
result['changed'] = True
|
||||
result['requests'] = requests
|
||||
|
||||
changed_lldp_interfaces_facts = self.get_lldp_interfaces_facts()
|
||||
|
||||
result['before'] = existing_lldp_interfaces_facts
|
||||
if result['changed']:
|
||||
result['after'] = changed_lldp_interfaces_facts
|
||||
|
||||
result['warnings'] = warnings
|
||||
return result
|
||||
|
||||
def set_config(self, existing_lldp_interfaces_facts):
|
||||
""" Collect the configuration from the args passed to the module,
|
||||
collect the current configuration (as a dict from facts)
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
want = self._module.params['config']
|
||||
have = existing_lldp_interfaces_facts
|
||||
resp = self.set_state(want, have)
|
||||
return to_list(resp)
|
||||
|
||||
def set_state(self, want, have):
|
||||
""" Select the appropriate function based on the state provided
|
||||
|
||||
:param want: the desired configuration as a dictionary
|
||||
:param have: the current configuration as a dictionary
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
state = self._module.params['state']
|
||||
if state == 'overridden':
|
||||
requests = self._state_overridden(want, have)
|
||||
elif state == 'deleted':
|
||||
requests = self._state_deleted(want, have)
|
||||
elif state == 'merged':
|
||||
requests = self._state_merged(want, have)
|
||||
elif state == 'replaced':
|
||||
requests = self._state_replaced(want, have)
|
||||
return requests
|
||||
|
||||
def _state_replaced(self, want, have):
|
||||
""" The request generator when state is replaced
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
requests = []
|
||||
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w['name'] == h['name']:
|
||||
lldp_request = self._update_patch_request(w, h)
|
||||
if lldp_request["path"]:
|
||||
lldp_request["data"] = json.dumps(lldp_request["data"])
|
||||
requests.append(lldp_request)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_overridden(self, want, have):
|
||||
""" The request generator when state is overridden
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
requests = []
|
||||
have_copy = []
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w['name'] == h['name']:
|
||||
lldp_request = self._update_patch_request(w, h)
|
||||
if lldp_request["path"]:
|
||||
lldp_request["data"] = json.dumps(lldp_request["data"])
|
||||
requests.append(lldp_request)
|
||||
have_copy.append(h)
|
||||
|
||||
for h in have:
|
||||
if h not in have_copy:
|
||||
if not h['enabled']:
|
||||
lldp_delete = self._update_delete_request(h)
|
||||
if lldp_delete["path"]:
|
||||
lldp_delete["data"] = json.dumps(lldp_delete["data"])
|
||||
requests.append(lldp_delete)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_merged(self, want, have):
|
||||
""" The request generator when state is merged
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to merge the provided into
|
||||
the current configuration
|
||||
"""
|
||||
requests = []
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w['name'] == h['name']:
|
||||
lldp_request = self._update_patch_request(w, h)
|
||||
if lldp_request["path"]:
|
||||
lldp_request["data"] = json.dumps(lldp_request["data"])
|
||||
requests.append(lldp_request)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_deleted(self, want, have):
|
||||
""" The request generator when state is deleted
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to remove the current configuration
|
||||
of the provided objects
|
||||
"""
|
||||
requests = []
|
||||
if want:
|
||||
for w in want:
|
||||
for h in have:
|
||||
if w['name'] == h['name']:
|
||||
if not h['enabled']:
|
||||
lldp_delete = self._update_delete_request(h)
|
||||
if lldp_delete["path"]:
|
||||
lldp_delete["data"] = json.dumps(
|
||||
lldp_delete["data"])
|
||||
requests.append(lldp_delete)
|
||||
else:
|
||||
for h in have:
|
||||
if not h['enabled']:
|
||||
lldp_delete = self._update_delete_request(h)
|
||||
if lldp_delete["path"]:
|
||||
lldp_delete["data"] = json.dumps(lldp_delete["data"])
|
||||
requests.append(lldp_delete)
|
||||
|
||||
return requests
|
||||
|
||||
def _update_patch_request(self, want, have):
|
||||
|
||||
lldp_request = deepcopy(self.LLDP_INTERFACE)
|
||||
|
||||
if have['enabled'] != want['enabled']:
|
||||
lldp_request["data"]["openconfig-lldp:config"]["name"] = want[
|
||||
'name']
|
||||
lldp_request["data"]["openconfig-lldp:config"]["enabled"] = want[
|
||||
'enabled']
|
||||
lldp_request["path"] = self.LLDP_PATH + str(
|
||||
want['name']) + "/config"
|
||||
|
||||
return lldp_request
|
||||
|
||||
def _update_delete_request(self, have):
|
||||
|
||||
lldp_delete = deepcopy(self.LLDP_INTERFACE)
|
||||
|
||||
lldp_delete["data"]["openconfig-lldp:config"]["name"] = have['name']
|
||||
lldp_delete["data"]["openconfig-lldp:config"]["enabled"] = True
|
||||
lldp_delete["path"] = self.LLDP_PATH + str(have['name']) + "/config"
|
||||
|
||||
return lldp_delete
|
||||
277
plugins/module_utils/network/exos/config/vlans/vlans.py
Normal file
277
plugins/module_utils/network/exos/config/vlans/vlans.py
Normal file
@@ -0,0 +1,277 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
"""
|
||||
The exos_vlans class
|
||||
It is in this file where the current configuration (as dict)
|
||||
is compared to the provided configuration (as dict) and the command set
|
||||
necessary to bring the current configuration to it's desired end-state is
|
||||
created
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ConfigBase
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, dict_diff
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.facts import Facts
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.exos import send_requests
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.utils.utils import search_obj_in_list
|
||||
|
||||
|
||||
class Vlans(ConfigBase):
|
||||
"""
|
||||
The exos_vlans class
|
||||
"""
|
||||
|
||||
gather_subset = [
|
||||
'!all',
|
||||
'!min',
|
||||
]
|
||||
|
||||
gather_network_resources = [
|
||||
'vlans',
|
||||
]
|
||||
|
||||
VLAN_POST = {
|
||||
"data": {"openconfig-vlan:vlans": []},
|
||||
"method": "POST",
|
||||
"path": "/rest/restconf/data/openconfig-vlan:vlans/"
|
||||
}
|
||||
|
||||
VLAN_PATCH = {
|
||||
"data": {"openconfig-vlan:vlans": {"vlan": []}},
|
||||
"method": "PATCH",
|
||||
"path": "/rest/restconf/data/openconfig-vlan:vlans/"
|
||||
}
|
||||
|
||||
VLAN_DELETE = {
|
||||
"method": "DELETE",
|
||||
"path": None
|
||||
}
|
||||
|
||||
DEL_PATH = "/rest/restconf/data/openconfig-vlan:vlans/vlan="
|
||||
|
||||
REQUEST_BODY = {
|
||||
"config": {"name": None, "status": "ACTIVE", "tpid": "oc-vlan-types:TPID_0x8100", "vlan-id": None}
|
||||
}
|
||||
|
||||
def __init__(self, module):
|
||||
super(Vlans, self).__init__(module)
|
||||
|
||||
def get_vlans_facts(self):
|
||||
""" Get the 'facts' (the current configuration)
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The current configuration as a dictionary
|
||||
"""
|
||||
facts, _warnings = Facts(self._module).get_facts(
|
||||
self.gather_subset, self.gather_network_resources)
|
||||
vlans_facts = facts['ansible_network_resources'].get('vlans')
|
||||
if not vlans_facts:
|
||||
return []
|
||||
return vlans_facts
|
||||
|
||||
def execute_module(self):
|
||||
""" Execute the module
|
||||
|
||||
:rtype: A dictionary
|
||||
:returns: The result from module execution
|
||||
"""
|
||||
result = {'changed': False}
|
||||
warnings = list()
|
||||
requests = list()
|
||||
|
||||
existing_vlans_facts = self.get_vlans_facts()
|
||||
requests.extend(self.set_config(existing_vlans_facts))
|
||||
if requests:
|
||||
if not self._module.check_mode:
|
||||
send_requests(self._module, requests=requests)
|
||||
result['changed'] = True
|
||||
result['requests'] = requests
|
||||
|
||||
changed_vlans_facts = self.get_vlans_facts()
|
||||
|
||||
result['before'] = existing_vlans_facts
|
||||
if result['changed']:
|
||||
result['after'] = changed_vlans_facts
|
||||
|
||||
result['warnings'] = warnings
|
||||
return result
|
||||
|
||||
def set_config(self, existing_vlans_facts):
|
||||
""" Collect the configuration from the args passed to the module,
|
||||
collect the current configuration (as a dict from facts)
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
want = self._module.params['config']
|
||||
have = existing_vlans_facts
|
||||
resp = self.set_state(want, have)
|
||||
return to_list(resp)
|
||||
|
||||
def set_state(self, want, have):
|
||||
""" Select the appropriate function based on the state provided
|
||||
|
||||
:param want: the desired configuration as a dictionary
|
||||
:param have: the current configuration as a dictionary
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
state = self._module.params['state']
|
||||
if state == 'overridden':
|
||||
requests = self._state_overridden(want, have)
|
||||
elif state == 'deleted':
|
||||
requests = self._state_deleted(want, have)
|
||||
elif state == 'merged':
|
||||
requests = self._state_merged(want, have)
|
||||
elif state == 'replaced':
|
||||
requests = self._state_replaced(want, have)
|
||||
return requests
|
||||
|
||||
def _state_replaced(self, want, have):
|
||||
""" The request generator when state is replaced
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
requests = []
|
||||
request_patch = deepcopy(self.VLAN_PATCH)
|
||||
|
||||
for w in want:
|
||||
if w.get('vlan_id'):
|
||||
h = search_obj_in_list(w['vlan_id'], have, 'vlan_id')
|
||||
if h:
|
||||
if dict_diff(w, h):
|
||||
request_body = self._update_patch_request(w)
|
||||
request_patch["data"]["openconfig-vlan:vlans"]["vlan"].append(request_body)
|
||||
else:
|
||||
request_post = self._update_post_request(w)
|
||||
requests.append(request_post)
|
||||
|
||||
if len(request_patch["data"]["openconfig-vlan:vlans"]["vlan"]):
|
||||
request_patch["data"] = json.dumps(request_patch["data"])
|
||||
requests.append(request_patch)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_overridden(self, want, have):
|
||||
""" The request generator when state is overridden
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to migrate the current configuration
|
||||
to the desired configuration
|
||||
"""
|
||||
requests = []
|
||||
request_patch = deepcopy(self.VLAN_PATCH)
|
||||
|
||||
have_copy = []
|
||||
for w in want:
|
||||
if w.get('vlan_id'):
|
||||
h = search_obj_in_list(w['vlan_id'], have, 'vlan_id')
|
||||
if h:
|
||||
if dict_diff(w, h):
|
||||
request_body = self._update_patch_request(w)
|
||||
request_patch["data"]["openconfig-vlan:vlans"]["vlan"].append(request_body)
|
||||
have_copy.append(h)
|
||||
else:
|
||||
request_post = self._update_post_request(w)
|
||||
requests.append(request_post)
|
||||
|
||||
for h in have:
|
||||
if h not in have_copy and h['vlan_id'] != 1:
|
||||
request_delete = self._update_delete_request(h)
|
||||
requests.append(request_delete)
|
||||
|
||||
if len(request_patch["data"]["openconfig-vlan:vlans"]["vlan"]):
|
||||
request_patch["data"] = json.dumps(request_patch["data"])
|
||||
requests.append(request_patch)
|
||||
|
||||
return requests
|
||||
|
||||
def _state_merged(self, want, have):
|
||||
""" The requests generator when state is merged
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to merge the provided into
|
||||
the current configuration
|
||||
"""
|
||||
requests = []
|
||||
|
||||
request_patch = deepcopy(self.VLAN_PATCH)
|
||||
|
||||
for w in want:
|
||||
if w.get('vlan_id'):
|
||||
h = search_obj_in_list(w['vlan_id'], have, 'vlan_id')
|
||||
if h:
|
||||
if dict_diff(w, h):
|
||||
request_body = self._update_patch_request(w)
|
||||
request_patch["data"]["openconfig-vlan:vlans"]["vlan"].append(request_body)
|
||||
else:
|
||||
request_post = self._update_post_request(w)
|
||||
requests.append(request_post)
|
||||
|
||||
if len(request_patch["data"]["openconfig-vlan:vlans"]["vlan"]):
|
||||
request_patch["data"] = json.dumps(request_patch["data"])
|
||||
requests.append(request_patch)
|
||||
return requests
|
||||
|
||||
def _state_deleted(self, want, have):
|
||||
""" The requests generator when state is deleted
|
||||
|
||||
:rtype: A list
|
||||
:returns: the requests necessary to remove the current configuration
|
||||
of the provided objects
|
||||
"""
|
||||
requests = []
|
||||
|
||||
if want:
|
||||
for w in want:
|
||||
if w.get('vlan_id'):
|
||||
h = search_obj_in_list(w['vlan_id'], have, 'vlan_id')
|
||||
if h:
|
||||
request_delete = self._update_delete_request(h)
|
||||
requests.append(request_delete)
|
||||
|
||||
else:
|
||||
if not have:
|
||||
return requests
|
||||
for h in have:
|
||||
if h['vlan_id'] == 1:
|
||||
continue
|
||||
else:
|
||||
request_delete = self._update_delete_request(h)
|
||||
requests.append(request_delete)
|
||||
|
||||
return requests
|
||||
|
||||
def _update_vlan_config_body(self, want, request):
|
||||
request["config"]["name"] = want["name"]
|
||||
request["config"]["status"] = "SUSPENDED" if want["state"] == "suspend" else want["state"].upper()
|
||||
request["config"]["vlan-id"] = want["vlan_id"]
|
||||
return request
|
||||
|
||||
def _update_patch_request(self, want):
|
||||
request_body = deepcopy(self.REQUEST_BODY)
|
||||
request_body = self._update_vlan_config_body(want, request_body)
|
||||
return request_body
|
||||
|
||||
def _update_post_request(self, want):
|
||||
request_post = deepcopy(self.VLAN_POST)
|
||||
request_body = deepcopy(self.REQUEST_BODY)
|
||||
request_body = self._update_vlan_config_body(want, request_body)
|
||||
request_post["data"]["openconfig-vlan:vlans"].append(request_body)
|
||||
request_post["data"] = json.dumps(request_post["data"])
|
||||
return request_post
|
||||
|
||||
def _update_delete_request(self, have):
|
||||
request_delete = deepcopy(self.VLAN_DELETE)
|
||||
request_delete["path"] = self.DEL_PATH + str(have['vlan_id'])
|
||||
return request_delete
|
||||
219
plugins/module_utils/network/exos/exos.py
Normal file
219
plugins/module_utils/network/exos/exos.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# 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.
|
||||
#
|
||||
# (c) 2016 Red Hat Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
import json
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
from ansible.module_utils.connection import Connection, ConnectionError
|
||||
|
||||
_DEVICE_CONNECTION = None
|
||||
|
||||
|
||||
class Cli:
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._device_configs = {}
|
||||
self._connection = None
|
||||
|
||||
def get_capabilities(self):
|
||||
"""Returns platform info of the remove device
|
||||
"""
|
||||
connection = self._get_connection()
|
||||
return json.loads(connection.get_capabilities())
|
||||
|
||||
def _get_connection(self):
|
||||
if not self._connection:
|
||||
self._connection = Connection(self._module._socket_path)
|
||||
return self._connection
|
||||
|
||||
def get_config(self, flags=None):
|
||||
"""Retrieves the current config from the device or cache
|
||||
"""
|
||||
flags = [] if flags is None else flags
|
||||
if self._device_configs == {}:
|
||||
connection = self._get_connection()
|
||||
try:
|
||||
out = connection.get_config(flags=flags)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
self._device_configs = to_text(out, errors='surrogate_then_replace').strip()
|
||||
return self._device_configs
|
||||
|
||||
def run_commands(self, commands, check_rc=True):
|
||||
"""Runs list of commands on remote device and returns results
|
||||
"""
|
||||
connection = self._get_connection()
|
||||
try:
|
||||
response = connection.run_commands(commands=commands, check_rc=check_rc)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
return response
|
||||
|
||||
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
diff = conn.get_diff(candidate=candidate, running=running, diff_match=diff_match,
|
||||
diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
return diff
|
||||
|
||||
|
||||
class HttpApi:
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._device_configs = {}
|
||||
self._connection_obj = None
|
||||
|
||||
def get_capabilities(self):
|
||||
"""Returns platform info of the remove device
|
||||
"""
|
||||
try:
|
||||
capabilities = self._connection.get_capabilities()
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
|
||||
return json.loads(capabilities)
|
||||
|
||||
@property
|
||||
def _connection(self):
|
||||
if not self._connection_obj:
|
||||
self._connection_obj = Connection(self._module._socket_path)
|
||||
return self._connection_obj
|
||||
|
||||
def get_config(self, flags=None):
|
||||
"""Retrieves the current config from the device or cache
|
||||
"""
|
||||
flags = [] if flags is None else flags
|
||||
if self._device_configs == {}:
|
||||
try:
|
||||
out = self._connection.get_config(flags=flags)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
self._device_configs = to_text(out, errors='surrogate_then_replace').strip()
|
||||
return self._device_configs
|
||||
|
||||
def run_commands(self, commands, check_rc=True):
|
||||
"""Runs list of commands on remote device and returns results
|
||||
"""
|
||||
try:
|
||||
response = self._connection.run_commands(commands=commands, check_rc=check_rc)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
return response
|
||||
|
||||
def send_requests(self, requests):
|
||||
"""Send a list of http requests to remote device and return results
|
||||
"""
|
||||
if requests is None:
|
||||
raise ValueError("'requests' value is required")
|
||||
|
||||
responses = list()
|
||||
for req in to_list(requests):
|
||||
try:
|
||||
response = self._connection.send_request(**req)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
try:
|
||||
diff = self._connection.get_diff(candidate=candidate, running=running, diff_match=diff_match,
|
||||
diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)
|
||||
except ConnectionError as exc:
|
||||
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
|
||||
return diff
|
||||
|
||||
|
||||
def get_capabilities(module):
|
||||
conn = get_connection(module)
|
||||
return conn.get_capabilities()
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
global _DEVICE_CONNECTION
|
||||
if not _DEVICE_CONNECTION:
|
||||
connection_proxy = Connection(module._socket_path)
|
||||
cap = json.loads(connection_proxy.get_capabilities())
|
||||
if cap['network_api'] == 'cliconf':
|
||||
conn = Cli(module)
|
||||
elif cap['network_api'] == 'exosapi':
|
||||
conn = HttpApi(module)
|
||||
else:
|
||||
module.fail_json(msg='Invalid connection type %s' % cap['network_api'])
|
||||
_DEVICE_CONNECTION = conn
|
||||
return _DEVICE_CONNECTION
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = None if flags is None else flags
|
||||
conn = get_connection(module)
|
||||
return conn.get_config(flags)
|
||||
|
||||
|
||||
def load_config(module, commands):
|
||||
conn = get_connection(module)
|
||||
return conn.run_commands(to_command(module, commands))
|
||||
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
conn = get_connection(module)
|
||||
return conn.run_commands(to_command(module, commands), check_rc=check_rc)
|
||||
|
||||
|
||||
def to_command(module, commands):
|
||||
transform = ComplexList(dict(
|
||||
command=dict(key=True),
|
||||
output=dict(default='text'),
|
||||
prompt=dict(type='list'),
|
||||
answer=dict(type='list'),
|
||||
sendonly=dict(type='bool', default=False),
|
||||
check_all=dict(type='bool', default=False),
|
||||
), module)
|
||||
return transform(to_list(commands))
|
||||
|
||||
|
||||
def send_requests(module, requests):
|
||||
conn = get_connection(module)
|
||||
return conn.send_requests(to_request(module, requests))
|
||||
|
||||
|
||||
def to_request(module, requests):
|
||||
transform = ComplexList(dict(
|
||||
path=dict(key=True),
|
||||
method=dict(),
|
||||
data=dict(type='dict'),
|
||||
), module)
|
||||
return transform(to_list(requests))
|
||||
|
||||
|
||||
def get_diff(module, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
conn = get_connection(module)
|
||||
return conn.get_diff(candidate=candidate, running=running, diff_match=diff_match, diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)
|
||||
0
plugins/module_utils/network/exos/facts/__init__.py
Normal file
0
plugins/module_utils/network/exos/facts/__init__.py
Normal file
61
plugins/module_utils/network/exos/facts/facts.py
Normal file
61
plugins/module_utils/network/exos/facts/facts.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
"""
|
||||
The facts class for exos
|
||||
this file validates each subset of facts and selectively
|
||||
calls the appropriate facts gathering function
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.argspec.facts.facts import FactsArgs
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts import FactsBase
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.lldp_global.lldp_global import Lldp_globalFacts
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.vlans.vlans import VlansFacts
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.legacy.base import Default, Hardware, Interfaces, Config
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.facts.l2_interfaces.l2_interfaces import L2_interfacesFacts
|
||||
|
||||
FACT_LEGACY_SUBSETS = dict(
|
||||
default=Default,
|
||||
hardware=Hardware,
|
||||
interfaces=Interfaces,
|
||||
config=Config)
|
||||
|
||||
FACT_RESOURCE_SUBSETS = dict(
|
||||
lldp_global=Lldp_globalFacts,
|
||||
vlans=VlansFacts,
|
||||
lldp_interfaces=Lldp_interfacesFacts,
|
||||
l2_interfaces=L2_interfacesFacts,
|
||||
)
|
||||
|
||||
|
||||
class Facts(FactsBase):
|
||||
""" The fact class for exos
|
||||
"""
|
||||
|
||||
VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys())
|
||||
VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys())
|
||||
|
||||
def __init__(self, module):
|
||||
super(Facts, self).__init__(module)
|
||||
|
||||
def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None):
|
||||
""" Collect the facts for exos
|
||||
|
||||
:param legacy_facts_type: List of legacy facts types
|
||||
:param resource_facts_type: List of resource fact types
|
||||
:param data: previously collected conf
|
||||
:rtype: dict
|
||||
:return: the facts gathered
|
||||
"""
|
||||
if self.VALID_RESOURCE_SUBSETS:
|
||||
self.get_network_resources_facts(FACT_RESOURCE_SUBSETS, resource_facts_type, data)
|
||||
|
||||
if self.VALID_LEGACY_GATHER_SUBSETS:
|
||||
self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type)
|
||||
|
||||
return self.ansible_facts, self._warnings
|
||||
@@ -0,0 +1,92 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
"""
|
||||
The exos l2_interfaces fact class
|
||||
It is in this file the configuration is collected from the device
|
||||
for a given resource, parsed, and the facts tree is populated
|
||||
based on the configuration.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.argspec.l2_interfaces.l2_interfaces import L2_interfacesArgs
|
||||
from ansible_collections.community.general.plugins.module_utils.network.exos.exos import send_requests
|
||||
|
||||
|
||||
class L2_interfacesFacts(object):
|
||||
""" The exos l2_interfaces fact class
|
||||
"""
|
||||
def __init__(self, module, subspec='config', options='options'):
|
||||
self._module = module
|
||||
self.argument_spec = L2_interfacesArgs.argument_spec
|
||||
spec = deepcopy(self.argument_spec)
|
||||
if subspec:
|
||||
if options:
|
||||
facts_argument_spec = spec[subspec][options]
|
||||
else:
|
||||
facts_argument_spec = spec[subspec]
|
||||
else:
|
||||
facts_argument_spec = spec
|
||||
|
||||
self.generated_spec = utils.generate_dict(facts_argument_spec)
|
||||
|
||||
def populate_facts(self, connection, ansible_facts, data=None):
|
||||
""" Populate the facts for l2_interfaces
|
||||
:param connection: the device connection
|
||||
:param ansible_facts: Facts dictionary
|
||||
:param data: previously collected conf
|
||||
:rtype: dictionary
|
||||
:returns: facts
|
||||
"""
|
||||
|
||||
if not data:
|
||||
request = [{
|
||||
"path": "/rest/restconf/data/openconfig-interfaces:interfaces",
|
||||
"method": "GET"
|
||||
}]
|
||||
data = send_requests(self._module, requests=request)
|
||||
|
||||
objs = []
|
||||
if data:
|
||||
for d in data[0]["openconfig-interfaces:interfaces"]["interface"]:
|
||||
obj = self.render_config(self.generated_spec, d)
|
||||
if obj:
|
||||
objs.append(obj)
|
||||
|
||||
ansible_facts['ansible_network_resources'].pop('l2_interfaces', None)
|
||||
facts = {}
|
||||
if objs:
|
||||
params = utils.validate_config(self.argument_spec, {'config': objs})
|
||||
facts['l2_interfaces'] = params['config']
|
||||
|
||||
ansible_facts['ansible_network_resources'].update(facts)
|
||||
return ansible_facts
|
||||
|
||||
def render_config(self, spec, conf):
|
||||
"""
|
||||
Render config as dictionary structure and delete keys
|
||||
from spec for null values
|
||||
|
||||
:param spec: The facts tree, generated from the argspec
|
||||
:param conf: The configuration
|
||||
:rtype: dictionary
|
||||
:returns: The generated config
|
||||
"""
|
||||
config = deepcopy(spec)
|
||||
if conf["config"]["type"] == "ethernetCsmacd":
|
||||
conf_dict = conf["openconfig-if-ethernet:ethernet"]["openconfig-vlan:switched-vlan"]["config"]
|
||||
config["name"] = conf["name"]
|
||||
if conf_dict["interface-mode"] == "ACCESS":
|
||||
config["access"]["vlan"] = conf_dict.get("access-vlan")
|
||||
else:
|
||||
if 'native-vlan' in conf_dict:
|
||||
config["trunk"]["native_vlan"] = conf_dict.get("native-vlan")
|
||||
config["trunk"]["trunk_allowed_vlans"] = conf_dict.get("trunk-vlans")
|
||||
return utils.remove_empties(config)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user