Add network resource modules utils function (#58273)
* Add network resource modules utils function * `FactsBase` parent class to handle facts gathering * `ConfigBase` parent class for resource module config handling * utils funtions for resource modules * Fix review comments * Fix CI issues and review comments * Fix review comments * Fix CI issues and minor updates
This commit is contained in:
parent
78c8ee9261
commit
80d6a2f344
6 changed files with 316 additions and 5 deletions
0
lib/ansible/module_utils/network/common/cfg/__init__.py
Normal file
0
lib/ansible/module_utils/network/common/cfg/__init__.py
Normal file
18
lib/ansible/module_utils/network/common/cfg/base.py
Normal file
18
lib/ansible/module_utils/network/common/cfg/base.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# -*- 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 base class for all resource modules
|
||||
"""
|
||||
|
||||
from ansible.module_utils.network.common.network import get_resource_connection
|
||||
|
||||
|
||||
class ConfigBase(object):
|
||||
""" The base class for all resource modules
|
||||
"""
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._connection = get_resource_connection(module)
|
130
lib/ansible/module_utils/network/common/facts/facts.py
Normal file
130
lib/ansible/module_utils/network/common/facts/facts.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
#
|
||||
# -*- 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 base class
|
||||
this contains methods common to all facts subsets
|
||||
"""
|
||||
from ansible.module_utils.network.common.network import get_resource_connection
|
||||
from ansible.module_utils.six import iteritems
|
||||
|
||||
|
||||
class FactsBase(object):
|
||||
"""
|
||||
The facts base class
|
||||
"""
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self._warnings = []
|
||||
self._gather_subset = module.params.get('gather_subset')
|
||||
self._gather_network_resources = module.params.get('gather_network_resources')
|
||||
self._connection = get_resource_connection(module)
|
||||
|
||||
self.ansible_facts = {'ansible_network_resources': {}}
|
||||
self.ansible_facts['ansible_gather_network_resources'] = list()
|
||||
self.ansible_facts['ansible_net_gather_subset'] = list()
|
||||
|
||||
if not self._gather_subset:
|
||||
self._gather_subset = ['!config']
|
||||
if not self._gather_network_resources:
|
||||
self._gather_network_resources = ['all']
|
||||
|
||||
def gen_runable(self, subsets, valid_subsets):
|
||||
""" Generate the runable subset
|
||||
|
||||
:param module: The module instance
|
||||
:param subsets: The provided subsets
|
||||
:param valid_subsets: The valid subsets
|
||||
:rtype: list
|
||||
:returns: The runable subsets
|
||||
"""
|
||||
runable_subsets = set()
|
||||
exclude_subsets = set()
|
||||
minimal_gather_subset = frozenset(['default'])
|
||||
|
||||
for subset in subsets:
|
||||
if subset == 'all':
|
||||
runable_subsets.update(valid_subsets)
|
||||
continue
|
||||
if subset == 'min' and minimal_gather_subset:
|
||||
runable_subsets.update(minimal_gather_subset)
|
||||
continue
|
||||
if subset.startswith('!'):
|
||||
subset = subset[1:]
|
||||
if subset == 'min':
|
||||
exclude_subsets.update(minimal_gather_subset)
|
||||
continue
|
||||
if subset == 'all':
|
||||
exclude_subsets.update(
|
||||
valid_subsets - minimal_gather_subset)
|
||||
continue
|
||||
exclude = True
|
||||
else:
|
||||
exclude = False
|
||||
|
||||
if subset not in valid_subsets:
|
||||
self._module.fail_json(msg='Bad subset')
|
||||
|
||||
if exclude:
|
||||
exclude_subsets.add(subset)
|
||||
else:
|
||||
runable_subsets.add(subset)
|
||||
|
||||
if not runable_subsets:
|
||||
runable_subsets.update(valid_subsets)
|
||||
runable_subsets.difference_update(exclude_subsets)
|
||||
|
||||
return runable_subsets
|
||||
|
||||
def get_network_resources_facts(self, net_res_choices, facts_resource_obj_map, resource_facts_type=None, data=None):
|
||||
"""
|
||||
:param net_res_choices:
|
||||
:param fact_resource_subsets:
|
||||
:param data: previously collected configuration
|
||||
:return:
|
||||
"""
|
||||
if net_res_choices:
|
||||
if 'all' in net_res_choices:
|
||||
net_res_choices.remove('all')
|
||||
|
||||
if net_res_choices:
|
||||
if not resource_facts_type:
|
||||
resource_facts_type = self._gather_network_resources
|
||||
|
||||
restorun_subsets = self.gen_runable(resource_facts_type, frozenset(net_res_choices))
|
||||
if restorun_subsets:
|
||||
self.ansible_facts['gather_network_resources'] = list(restorun_subsets)
|
||||
instances = list()
|
||||
for key in restorun_subsets:
|
||||
fact_cls_obj = facts_resource_obj_map.get(key)
|
||||
if fact_cls_obj:
|
||||
instances.append(fact_cls_obj(self._module))
|
||||
else:
|
||||
self._warnings.extend(["network resource fact gathering for '%s' is not supported" % key])
|
||||
|
||||
for inst in instances:
|
||||
inst.populate_facts(self._connection, self.ansible_facts, data)
|
||||
|
||||
def get_network_legacy_facts(self, fact_legacy_obj_map, legacy_facts_type=None):
|
||||
if not legacy_facts_type:
|
||||
legacy_facts_type = self._gather_subset
|
||||
|
||||
runable_subsets = self.gen_runable(legacy_facts_type, frozenset(fact_legacy_obj_map.keys()))
|
||||
if runable_subsets:
|
||||
facts = dict()
|
||||
facts['gather_subset'] = list(runable_subsets)
|
||||
|
||||
instances = list()
|
||||
for key in runable_subsets:
|
||||
instances.append(fact_legacy_obj_map[key](self._module))
|
||||
|
||||
for inst in instances:
|
||||
inst.populate()
|
||||
facts.update(inst.facts)
|
||||
self._warnings.extend(inst.warnings)
|
||||
|
||||
for key, value in iteritems(facts):
|
||||
key = 'ansible_net_%s' % key
|
||||
self.ansible_facts[key] = value
|
|
@ -26,11 +26,14 @@
|
|||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import traceback
|
||||
import json
|
||||
|
||||
from ansible.module_utils._text import to_text, to_native
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible.module_utils.connection import Connection, ConnectionError
|
||||
from ansible.module_utils.network.common.netconf import NetconfConnection
|
||||
from ansible.module_utils.network.common.parsing import Cli
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.six import iteritems
|
||||
|
||||
|
||||
|
@ -201,3 +204,31 @@ def register_transport(transport, default=False):
|
|||
|
||||
def add_argument(key, value):
|
||||
NET_CONNECTION_ARGS[key] = value
|
||||
|
||||
|
||||
def get_resource_connection(module):
|
||||
if hasattr(module, '_connection'):
|
||||
return module._connection
|
||||
|
||||
capabilities = get_capabilities(module)
|
||||
network_api = capabilities.get('network_api')
|
||||
if network_api == 'cliconf':
|
||||
module._connection = Connection(module._socket_path)
|
||||
elif network_api == 'netconf':
|
||||
module._connection = NetconfConnection(module._socket_path)
|
||||
else:
|
||||
module.fail_json(msg='Invalid connection type {0!s}'.format(network_api))
|
||||
|
||||
return module._connection
|
||||
|
||||
|
||||
def get_capabilities(module):
|
||||
if hasattr(module, 'capabilities'):
|
||||
return module._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._capabilities = json.loads(capabilities)
|
||||
|
||||
return module._capabilities
|
||||
|
|
|
@ -32,14 +32,16 @@ import re
|
|||
import ast
|
||||
import operator
|
||||
import socket
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
from socket import inet_aton
|
||||
from json import dumps
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils._text import to_text, to_bytes
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
from ansible.module_utils.six import iteritems, string_types
|
||||
from ansible.module_utils.basic import AnsibleFallbackNotFound
|
||||
from ansible.module_utils import basic
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
|
||||
# Backwards compatibility for 3rd party modules
|
||||
|
@ -195,7 +197,7 @@ class Entity(object):
|
|||
fallback_args = item
|
||||
try:
|
||||
value[name] = fallback_strategy(*fallback_args, **fallback_kwargs)
|
||||
except AnsibleFallbackNotFound:
|
||||
except basic.AnsibleFallbackNotFound:
|
||||
continue
|
||||
|
||||
if attr.get('required') and value.get(name) is None:
|
||||
|
@ -438,10 +440,140 @@ def _fallback(fallback):
|
|||
args = item
|
||||
try:
|
||||
return strategy(*args, **kwargs)
|
||||
except AnsibleFallbackNotFound:
|
||||
except basic.AnsibleFallbackNotFound:
|
||||
pass
|
||||
|
||||
|
||||
def generate_dict(spec):
|
||||
"""
|
||||
Generate dictionary which is in sync with argspec
|
||||
|
||||
:param spec: A dictionary that is the argspec of the module
|
||||
:rtype: A dictionary
|
||||
:returns: A dictionary in sync with argspec with default value
|
||||
"""
|
||||
obj = {}
|
||||
if not spec:
|
||||
return obj
|
||||
|
||||
for key, val in iteritems(spec):
|
||||
if 'default' in val:
|
||||
dct = {key: val['default']}
|
||||
elif 'type' in val and val['type'] == 'dict':
|
||||
dct = {key: generate_dict(val['options'])}
|
||||
else:
|
||||
dct = {key: None}
|
||||
obj.update(dct)
|
||||
return obj
|
||||
|
||||
|
||||
def parse_conf_arg(cfg, arg):
|
||||
"""
|
||||
Parse config based on argument
|
||||
|
||||
:param cfg: A text string which is a line of configuration.
|
||||
:param arg: A text string which is to be matched.
|
||||
:rtype: A text string
|
||||
:returns: A text string if match is found
|
||||
"""
|
||||
match = re.search(r'%s (.+)(\n|$)' % arg, cfg, re.M)
|
||||
if match:
|
||||
result = match.group(1).strip()
|
||||
else:
|
||||
result = None
|
||||
return result
|
||||
|
||||
|
||||
def parse_conf_cmd_arg(cfg, cmd, res1, res2=None, delete_str='no'):
|
||||
"""
|
||||
Parse config based on command
|
||||
|
||||
:param cfg: A text string which is a line of configuration.
|
||||
:param cmd: A text string which is the command to be matched
|
||||
:param res1: A text string to be returned if the command is present
|
||||
:param res2: A text string to be returned if the negate command
|
||||
is present
|
||||
:param delete_str: A text string to identify the start of the
|
||||
negate command
|
||||
:rtype: A text string
|
||||
:returns: A text string if match is found
|
||||
"""
|
||||
match = re.search(r'\n\s+%s(\n|$)' % cmd, cfg)
|
||||
if match:
|
||||
return res1
|
||||
if res2 is not None:
|
||||
match = re.search(r'\n\s+%s %s(\n|$)' % (delete_str, cmd), cfg)
|
||||
if match:
|
||||
return res2
|
||||
return None
|
||||
|
||||
|
||||
def get_xml_conf_arg(cfg, path, data='text'):
|
||||
"""
|
||||
:param cfg: The top level configuration lxml Element tree object
|
||||
:param path: The relative xpath w.r.t to top level element (cfg)
|
||||
to be searched in the xml hierarchy
|
||||
:param data: The type of data to be returned for the matched xml node.
|
||||
Valid values are text, tag, attrib, with default as text.
|
||||
:return: Returns the required type for the matched xml node or else None
|
||||
"""
|
||||
match = cfg.xpath(path)
|
||||
if len(match):
|
||||
if data == 'tag':
|
||||
result = getattr(match[0], 'tag')
|
||||
elif data == 'attrib':
|
||||
result = getattr(match[0], 'attrib')
|
||||
else:
|
||||
result = getattr(match[0], 'text')
|
||||
else:
|
||||
result = None
|
||||
return result
|
||||
|
||||
|
||||
def remove_empties(cfg_dict):
|
||||
"""
|
||||
Generate final config dictionary
|
||||
|
||||
:param cfg_dict: A dictionary parsed in the facts system
|
||||
:rtype: A dictionary
|
||||
:returns: A dictionary by eliminating keys that have null values
|
||||
"""
|
||||
final_cfg = {}
|
||||
if not cfg_dict:
|
||||
return final_cfg
|
||||
|
||||
for key, val in iteritems(cfg_dict):
|
||||
dct = None
|
||||
if isinstance(val, dict):
|
||||
child_val = remove_empties(val)
|
||||
if child_val:
|
||||
dct = {key: child_val}
|
||||
elif (isinstance(val, list) and val
|
||||
and all([isinstance(x, dict) for x in val])):
|
||||
child_val = [remove_empties(x) for x in val]
|
||||
if child_val:
|
||||
dct = {key: child_val}
|
||||
elif val not in [None, [], {}, (), '']:
|
||||
dct = {key: val}
|
||||
if dct:
|
||||
final_cfg.update(dct)
|
||||
return final_cfg
|
||||
|
||||
|
||||
def validate_config(spec, data):
|
||||
"""
|
||||
Validate if the input data against the AnsibleModule spec format
|
||||
:param spec: Ansible argument spec
|
||||
:param data: Data to be validated
|
||||
:return:
|
||||
"""
|
||||
params = basic._ANSIBLE_ARGS
|
||||
basic._ANSIBLE_ARGS = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': data}))
|
||||
validated_data = basic.AnsibleModule(spec).params
|
||||
basic._ANSIBLE_ARGS = params
|
||||
return validated_data
|
||||
|
||||
|
||||
class Template:
|
||||
|
||||
def __init__(self):
|
||||
|
|
Loading…
Reference in a new issue