Initial commit

This commit is contained in:
Ansible Core Team 2020-03-09 09:11:07 +00:00
commit aebc1b03fd
4861 changed files with 812621 additions and 0 deletions

View file

View 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

View 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)

View 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

View 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

View 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)

View 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

View 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

View file

File diff suppressed because it is too large Load diff

View 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

View 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

View 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

View 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)'
)

View 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)

View 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
View 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

View 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

View 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

View 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'
)

View 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)

View 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)

View 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)))

View 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']]

View 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
View 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),
)

View 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

View 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)

View 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

View 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
View 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

View 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)

View 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)

View 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']),
)

View 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])

View file

View 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)

View 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')

View 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)

View 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'

View 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')

View 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'))

View 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)

View 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

View 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)

View 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

View 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

File diff suppressed because it is too large Load diff

View 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

View 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

View 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))

View 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) 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

View 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'

View 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))

View 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'),
}

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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()

View file

@ -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

View 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

View 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)

View 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

View file

@ -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