junos_facts refactor to work with resources modules (#58566)

* junos_facts refactor to work with resources modules

*  Refactor junos_facts module to work with
   network resource module.

* Fix unit test failures

* Fix review comments
This commit is contained in:
Ganesh Nalawade 2019-07-03 10:08:45 +05:30 committed by GitHub
parent 6c5de9e6eb
commit 1e3034b96d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 396 additions and 307 deletions

View file

@ -23,7 +23,7 @@ class FactsBase(object):
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_network_resources'] = list()
self.ansible_facts['ansible_net_gather_subset'] = list()
if not self._gather_subset:
@ -65,7 +65,8 @@ class FactsBase(object):
exclude = False
if subset not in valid_subsets:
self._module.fail_json(msg='Bad subset')
self._module.fail_json(msg='Subset must be one of [%s], got %s' %
(', '.join(sorted([item for item in valid_subsets])), subset))
if exclude:
exclude_subsets.add(subset)
@ -75,7 +76,6 @@ class FactsBase(object):
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):
@ -95,7 +95,7 @@ class FactsBase(object):
restorun_subsets = self.gen_runable(resource_facts_type, frozenset(net_res_choices))
if restorun_subsets:
self.ansible_facts['gather_network_resources'] = list(restorun_subsets)
self.ansible_facts['ansible_net_gather_network_resources'] = list(restorun_subsets)
instances = list()
for key in restorun_subsets:
fact_cls_obj = facts_resource_obj_map.get(key)
@ -112,9 +112,10 @@ class FactsBase(object):
legacy_facts_type = self._gather_subset
runable_subsets = self.gen_runable(legacy_facts_type, frozenset(fact_legacy_obj_map.keys()))
runable_subsets.add('default')
if runable_subsets:
facts = dict()
facts['gather_subset'] = list(runable_subsets)
facts['ansible_net_gather_subset'] = list(runable_subsets)
instances = list()
for key in runable_subsets:

View file

@ -0,0 +1,25 @@
#
# -*- 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 junos facts module.
"""
CHOICES = [
'all',
]
class FactsArgs(object):
""" The arg spec for the junos facts module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'gather_subset': dict(default=['!config'], type='list'),
'config_format': dict(default='text', choices=['xml', 'text', 'set', 'json']),
'gather_network_resources': dict(choices=CHOICES, type='list'),
}

View file

@ -0,0 +1,64 @@
#
# -*- 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 junos
this file validates each subset of facts and selectively
calls the appropriate facts gathering function
"""
from ansible.module_utils.network.junos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.junos.facts.legacy.base import Default, Hardware, Config, Interfaces, OFacts, HAS_PYEZ
FACT_LEGACY_SUBSETS = dict(
default=Default,
hardware=Hardware,
config=Config,
interfaces=Interfaces,
)
FACT_RESOURCE_SUBSETS = dict(
)
class Facts(FactsBase):
""" The fact class for junos
"""
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 junos
: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
"""
netres_choices = FactsArgs.argument_spec['gather_network_resources'].get('choices', [])
if self.VALID_RESOURCE_SUBSETS:
self.get_network_resources_facts(netres_choices, FACT_RESOURCE_SUBSETS, resource_facts_type, data)
if not legacy_facts_type:
legacy_facts_type = self._gather_subset
# fetch old style facts only when explicitly mentioned in gather_subset option
if 'ofacts' in legacy_facts_type:
if HAS_PYEZ:
self.ansible_facts.update(OFacts(self._module).populate())
else:
self._warnings.extend([
'junos-eznc is required to gather old style facts but does not appear to be installed. '
'It can be installed using `pip install junos-eznc`'])
self.ansible_facts['ansible_net_gather_subset'].append('ofacts')
legacy_facts_type.remove('ofacts')
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,226 @@
# -*- 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 junos 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.
"""
import platform
from ansible.module_utils.network.common.netconf import exec_rpc
from ansible.module_utils.network.junos.junos import get_param, tostring
from ansible.module_utils.network.junos.junos import get_configuration, get_capabilities
from ansible.module_utils._text import to_native
try:
from lxml.etree import Element, SubElement
except ImportError:
from xml.etree.ElementTree import Element, SubElement
try:
from jnpr.junos import Device
from jnpr.junos.exception import ConnectError
HAS_PYEZ = True
except ImportError:
HAS_PYEZ = False
class FactsBase(object):
def __init__(self, module):
self.module = module
self.facts = dict()
self.warnings = []
def populate(self):
raise NotImplementedError
def cli(self, command):
reply = command(self.module, command)
output = reply.find('.//output')
if not output:
self.module.fail_json(msg='failed to retrieve facts for command %s' % command)
return str(output.text).strip()
def rpc(self, rpc):
return exec_rpc(self.module, tostring(Element(rpc)))
def get_text(self, ele, tag):
try:
return str(ele.find(tag).text).strip()
except AttributeError:
pass
class Default(FactsBase):
def populate(self):
self.facts.update(self.platform_facts())
reply = self.rpc('get-chassis-inventory')
data = reply.find('.//chassis-inventory/chassis')
self.facts['serialnum'] = self.get_text(data, 'serial-number')
def platform_facts(self):
platform_facts = {}
resp = get_capabilities(self.module)
device_info = resp['device_info']
platform_facts['system'] = device_info['network_os']
for item in ('model', 'image', 'version', 'platform', 'hostname'):
val = device_info.get('network_os_%s' % item)
if val:
platform_facts[item] = val
platform_facts['api'] = resp['network_api']
platform_facts['python_version'] = platform.python_version()
return platform_facts
class Config(FactsBase):
def populate(self):
config_format = self.module.params['config_format']
reply = get_configuration(self.module, format=config_format)
if config_format == 'xml':
config = tostring(reply.find('configuration')).strip()
elif config_format == 'text':
config = self.get_text(reply, 'configuration-text')
elif config_format == 'json':
config = self.module.from_json(reply.text.strip())
elif config_format == 'set':
config = self.get_text(reply, 'configuration-set')
self.facts['config'] = config
class Hardware(FactsBase):
def populate(self):
reply = self.rpc('get-system-memory-information')
data = reply.find('.//system-memory-information/system-memory-summary-information')
self.facts.update({
'memfree_mb': int(self.get_text(data, 'system-memory-free')),
'memtotal_mb': int(self.get_text(data, 'system-memory-total'))
})
reply = self.rpc('get-system-storage')
data = reply.find('.//system-storage-information')
filesystems = list()
for obj in data:
filesystems.append(self.get_text(obj, 'filesystem-name'))
self.facts['filesystems'] = filesystems
reply = self.rpc('get-route-engine-information')
data = reply.find('.//route-engine-information')
routing_engines = dict()
for obj in data:
slot = self.get_text(obj, 'slot')
routing_engines.update({slot: {}})
routing_engines[slot].update({'slot': slot})
for child in obj:
if child.text != "\n":
routing_engines[slot].update({child.tag.replace("-", "_"): child.text})
self.facts['routing_engines'] = routing_engines
if len(data) > 1:
self.facts['has_2RE'] = True
else:
self.facts['has_2RE'] = False
reply = self.rpc('get-chassis-inventory')
data = reply.findall('.//chassis-module')
modules = list()
for obj in data:
mod = dict()
for child in obj:
if child.text != "\n":
mod.update({child.tag.replace("-", "_"): child.text})
modules.append(mod)
self.facts['modules'] = modules
class Interfaces(FactsBase):
def populate(self):
ele = Element('get-interface-information')
SubElement(ele, 'detail')
reply = exec_rpc(self.module, tostring(ele))
interfaces = {}
for item in reply[0]:
name = self.get_text(item, 'name')
obj = {
'oper-status': self.get_text(item, 'oper-status'),
'admin-status': self.get_text(item, 'admin-status'),
'speed': self.get_text(item, 'speed'),
'macaddress': self.get_text(item, 'hardware-physical-address'),
'mtu': self.get_text(item, 'mtu'),
'type': self.get_text(item, 'if-type'),
}
interfaces[name] = obj
self.facts['interfaces'] = interfaces
class OFacts(FactsBase):
def _connect(self, module):
host = get_param(module, 'host')
kwargs = {
'port': get_param(module, 'port') or 830,
'user': get_param(module, 'username')
}
if get_param(module, 'password'):
kwargs['passwd'] = get_param(module, 'password')
if get_param(module, 'ssh_keyfile'):
kwargs['ssh_private_key_file'] = get_param(module, 'ssh_keyfile')
kwargs['gather_facts'] = False
try:
device = Device(host, **kwargs)
device.open()
device.timeout = get_param(module, 'timeout') or 10
except ConnectError as exc:
module.fail_json('unable to connect to %s: %s' % (host, to_native(exc)))
return device
def populate(self):
device = self._connect(self.module)
facts = dict(device.facts)
if '2RE' in facts:
facts['has_2RE'] = facts['2RE']
del facts['2RE']
facts['version_info'] = dict(facts['version_info'])
if 'junos_info' in facts:
for key, value in facts['junos_info'].items():
if 'object' in value:
value['object'] = dict(value['object'])
return facts

View file

@ -0,0 +1,41 @@
#
# -*- 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)
# utils
from ansible.module_utils._text import to_text
try:
from ncclient.xml_ import to_ele, to_xml, new_ele, sub_ele
HAS_NCCLIENT = True
except (ImportError, AttributeError):
HAS_NCCLIENT = False
def build_root_xml_node(tag):
return new_ele(tag)
def build_child_xml_node(parent, tag, text=None, attrib=None):
element = sub_ele(parent, tag)
if text:
element.text = to_text(text)
if attrib:
element.attrib.update(attrib)
return element
def _handle_field_replace(root, field, have, want, tag=None):
tag = field if not tag else tag
want_value = want.get(field) if want else None
have_value = have.get(field) if have else None
if have_value:
if want_value:
if want_value != have_value:
build_child_xml_node(root, tag, want_value)
else:
build_child_xml_node(root, tag, None, {'delete': 'delete'})
elif want_value:
build_child_xml_node(root, tag, want_value)

View file

@ -42,7 +42,7 @@ options:
junos-eznc library to be installed on control node and the device login credentials
must be given in C(provider) option.
required: false
default: ['!config', '!ofacts']
default: ['!config']
version_added: "2.3"
config_format:
description:
@ -51,11 +51,21 @@ options:
only when C(config) value is present in I(gather_subset).
The I(config_format) should be supported by the junos version running on
device. This value is not applicable while fetching old style facts that is
when C(ofacts) value is present in value if I(gather_subset) value.
when C(ofacts) value is present in value if I(gather_subset) value. This option
is valid only for C(gather_subset) values.
required: false
default: 'text'
choices: ['xml', 'text', 'set', 'json']
version_added: "2.3"
gather_network_resources:
description:
- When supplied, this argument will restrict the facts collected
to a given subset. Possible values for this argument include
all and the resources like interfaces, vlans etc.
Can specify a list of values to include a larger subset.
choices: ['all']
required: false
version_added: "2.9"
requirements:
- ncclient (>=v0.5.2)
notes:
@ -78,6 +88,11 @@ EXAMPLES = """
- name: collect default set of facts and configuration
junos_facts:
gather_subset: config
- name: Gather legacy and resource facts
junos_facts:
gather_subset: all
gather_network_resources: all
"""
RETURN = """
@ -86,316 +101,28 @@ ansible_facts:
returned: always
type: dict
"""
import platform
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.netconf import exec_rpc
from ansible.module_utils.network.junos.junos import junos_argument_spec, get_param, tostring
from ansible.module_utils.network.junos.junos import get_configuration, get_capabilities
from ansible.module_utils._text import to_native
from ansible.module_utils.six import iteritems
try:
from lxml.etree import Element, SubElement
except ImportError:
from xml.etree.ElementTree import Element, SubElement
try:
from jnpr.junos import Device
from jnpr.junos.exception import ConnectError
HAS_PYEZ = True
except ImportError:
HAS_PYEZ = False
USE_PERSISTENT_CONNECTION = True
class FactsBase(object):
def __init__(self, module):
self.module = module
self.facts = dict()
def populate(self):
raise NotImplementedError
def cli(self, command):
reply = command(self.module, command)
output = reply.find('.//output')
if not output:
self.module.fail_json(msg='failed to retrieve facts for command %s' % command)
return str(output.text).strip()
def rpc(self, rpc):
return exec_rpc(self.module, tostring(Element(rpc)))
def get_text(self, ele, tag):
try:
return str(ele.find(tag).text).strip()
except AttributeError:
pass
class Default(FactsBase):
def populate(self):
self.facts.update(self.platform_facts())
reply = self.rpc('get-chassis-inventory')
data = reply.find('.//chassis-inventory/chassis')
self.facts['serialnum'] = self.get_text(data, 'serial-number')
def platform_facts(self):
platform_facts = {}
resp = get_capabilities(self.module)
device_info = resp['device_info']
platform_facts['system'] = device_info['network_os']
for item in ('model', 'image', 'version', 'platform', 'hostname'):
val = device_info.get('network_os_%s' % item)
if val:
platform_facts[item] = val
platform_facts['api'] = resp['network_api']
platform_facts['python_version'] = platform.python_version()
return platform_facts
class Config(FactsBase):
def populate(self):
config_format = self.module.params['config_format']
reply = get_configuration(self.module, format=config_format)
if config_format == 'xml':
config = tostring(reply.find('configuration')).strip()
elif config_format == 'text':
config = self.get_text(reply, 'configuration-text')
elif config_format == 'json':
config = self.module.from_json(reply.text.strip())
elif config_format == 'set':
config = self.get_text(reply, 'configuration-set')
self.facts['config'] = config
class Hardware(FactsBase):
def populate(self):
reply = self.rpc('get-system-memory-information')
data = reply.find('.//system-memory-information/system-memory-summary-information')
self.facts.update({
'memfree_mb': int(self.get_text(data, 'system-memory-free')),
'memtotal_mb': int(self.get_text(data, 'system-memory-total'))
})
reply = self.rpc('get-system-storage')
data = reply.find('.//system-storage-information')
filesystems = list()
for obj in data:
filesystems.append(self.get_text(obj, 'filesystem-name'))
self.facts['filesystems'] = filesystems
reply = self.rpc('get-route-engine-information')
data = reply.find('.//route-engine-information')
routing_engines = dict()
for obj in data:
slot = self.get_text(obj, 'slot')
routing_engines.update({slot: {}})
routing_engines[slot].update({'slot': slot})
for child in obj:
if child.text != "\n":
routing_engines[slot].update({child.tag.replace("-", "_"): child.text})
self.facts['routing_engines'] = routing_engines
if len(data) > 1:
self.facts['has_2RE'] = True
else:
self.facts['has_2RE'] = False
reply = self.rpc('get-chassis-inventory')
data = reply.findall('.//chassis-module')
modules = list()
for obj in data:
mod = dict()
for child in obj:
if child.text != "\n":
mod.update({child.tag.replace("-", "_"): child.text})
modules.append(mod)
self.facts['modules'] = modules
class Interfaces(FactsBase):
def populate(self):
ele = Element('get-interface-information')
SubElement(ele, 'detail')
reply = exec_rpc(self.module, tostring(ele))
interfaces = {}
for item in reply[0]:
name = self.get_text(item, 'name')
obj = {
'oper-status': self.get_text(item, 'oper-status'),
'admin-status': self.get_text(item, 'admin-status'),
'speed': self.get_text(item, 'speed'),
'macaddress': self.get_text(item, 'hardware-physical-address'),
'mtu': self.get_text(item, 'mtu'),
'type': self.get_text(item, 'if-type'),
}
interfaces[name] = obj
self.facts['interfaces'] = interfaces
class OFacts(FactsBase):
def _connect(self, module):
host = get_param(module, 'host')
kwargs = {
'port': get_param(module, 'port') or 830,
'user': get_param(module, 'username')
}
if get_param(module, 'password'):
kwargs['passwd'] = get_param(module, 'password')
if get_param(module, 'ssh_keyfile'):
kwargs['ssh_private_key_file'] = get_param(module, 'ssh_keyfile')
kwargs['gather_facts'] = False
try:
device = Device(host, **kwargs)
device.open()
device.timeout = get_param(module, 'timeout') or 10
except ConnectError as exc:
module.fail_json('unable to connect to %s: %s' % (host, to_native(exc)))
return device
def populate(self):
device = self._connect(self.module)
facts = dict(device.facts)
if '2RE' in facts:
facts['has_2RE'] = facts['2RE']
del facts['2RE']
facts['version_info'] = dict(facts['version_info'])
if 'junos_info' in facts:
for key, value in facts['junos_info'].items():
if 'object' in value:
value['object'] = dict(value['object'])
return facts
FACT_SUBSETS = dict(
default=Default,
hardware=Hardware,
config=Config,
interfaces=Interfaces,
ofacts=OFacts
)
VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
from ansible.module_utils.network.junos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.junos.facts.facts import Facts
from ansible.module_utils.network.junos.junos import junos_argument_spec
def main():
""" Main entry point for AnsibleModule
"""
argument_spec = dict(
gather_subset=dict(default=['!config', '!ofacts'], type='list'),
config_format=dict(default='text', choices=['xml', 'text', 'set', 'json']),
)
argument_spec = FactsArgs.argument_spec
argument_spec.update(junos_argument_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
warnings = list()
gather_subset = module.params['gather_subset']
warnings = ['default value for `gather_subset` '
'will be changed to `min` from `!config` v2.11 onwards']
runable_subsets = set()
exclude_subsets = set()
result = Facts(module).get_facts()
for subset in gather_subset:
if subset == 'all':
runable_subsets.update(VALID_SUBSETS)
continue
if subset.startswith('!'):
subset = subset[1:]
if subset == 'all':
exclude_subsets.update(VALID_SUBSETS)
continue
exclude = True
else:
exclude = False
if subset not in VALID_SUBSETS:
module.fail_json(msg='Subset must be one of [%s], got %s' %
(', '.join(sorted([subset for subset in
VALID_SUBSETS])), 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)
runable_subsets.add('default')
# handle fetching old style facts separately
runable_subsets.discard('ofacts')
facts = dict()
facts['gather_subset'] = list(runable_subsets)
instances = list()
ansible_facts = dict()
# fetch old style facts only when explicitly mentioned in gather_subset option
if 'ofacts' in gather_subset:
if HAS_PYEZ:
ansible_facts.update(OFacts(module).populate())
else:
warnings += ['junos-eznc is required to gather old style facts but does not appear to be installed. '
'It can be installed using `pip install junos-eznc`']
facts['gather_subset'].append('ofacts')
for key in runable_subsets:
instances.append(FACT_SUBSETS[key](module))
for inst in instances:
inst.populate()
facts.update(inst.facts)
for key, value in iteritems(facts):
key = 'ansible_net_%s' % key
ansible_facts[key] = value
ansible_facts, additional_warnings = result
warnings.extend(additional_warnings)
module.exit_json(ansible_facts=ansible_facts, warnings=warnings)

View file

@ -46,20 +46,24 @@ class TestJunosCommandModule(TestJunosModule):
def setUp(self):
super(TestJunosCommandModule, self).setUp()
self.mock_get_config = patch('ansible.modules.network.junos.junos_facts.get_configuration')
self.mock_get_config = patch('ansible.module_utils.network.junos.facts.legacy.base.get_configuration')
self.get_config = self.mock_get_config.start()
self.mock_netconf = patch('ansible.module_utils.network.junos.junos.NetconfConnection')
self.netconf_conn = self.mock_netconf.start()
self.mock_exec_rpc = patch('ansible.modules.network.junos.junos_facts.exec_rpc')
self.mock_exec_rpc = patch('ansible.module_utils.network.junos.facts.legacy.base.exec_rpc')
self.exec_rpc = self.mock_exec_rpc.start()
self.mock_netconf_rpc = patch('ansible.module_utils.network.common.netconf.NetconfConnection')
self.netconf_rpc = self.mock_netconf_rpc.start()
self.mock_get_capabilities = patch('ansible.modules.network.junos.junos_facts.get_capabilities')
self.mock_get_resource_connection = patch('ansible.module_utils.network.common.facts.facts.get_resource_connection')
self.get_resource_connection = self.mock_get_resource_connection.start()
self.mock_get_capabilities = patch('ansible.module_utils.network.junos.facts.legacy.base.get_capabilities')
self.get_capabilities = self.mock_get_capabilities.start()
self.get_capabilities.return_value = {
'device_info': {
'network_os': 'junos',
@ -76,6 +80,7 @@ class TestJunosCommandModule(TestJunosModule):
self.mock_exec_rpc.stop()
self.mock_netconf_rpc.stop()
self.mock_get_capabilities.stop()
self.mock_get_resource_connection.stop()
def load_fixtures(self, commands=None, format='text', changed=False):
def load_from_file(*args, **kwargs):