Platform agnostic net_system module (#24953)

* Platform agnostic net_system module

Also refactor the action network plugins for better code re-use

Still more refactoring to do once the connection plugin work is complete

* Replace importlib for imp

importlib is not available on 2.6, so we need to stick to imp

* Load action plugin via module metadata

* Better error message if no implementation is found

Now the plugin will show the module name and the network OS in the
error message

* Fix typo on documentation author line

* Fix pep8 issues

* Add missing options key on doc string and stringify version

* Return None in case module has no metadata

* Read module metadata only if it's a python module

Check for module suffix, if it's .py then read metadata.
Otherwise this fails on non-python modules, like Windows PS for example.

* Read metadata variable only if it's a python module

Fix referencing a variable before assignment

* Add action_handler to validate_modules metadata schema

* Pull metadata with plugin_docs get_docstring

Using load_source from PluginLoader is troublesome, it is not guaranteed
a module may be importable at the controller, e.g. if a module depends
on module_utils functions it won't work, because module_utils is not
in the sys path.
Rather than putting that module dependencies introspection, just
use plain parsing like plugin_docs get_docstring does as we only care
about reading ANSIBLE_METADATA.

* Add platform agnostic group of groups for integration tests

This will be the target for platform agnostic integration tests.

* Add integration tests for net_system

* Switch to action plugin inheritance from metadata driven action handler

As the metadata action driven action handler work is being worked on
on its standalone proposal+PR, let's just go back to have one
action handler per platform agnostic module.
Those action plugins will inherit from net_base.

* Add blank line to fix pep8

* Add aliases file to net_system integration test

This will avoid CI failure

* Fix integration tests for net_system

* Give more precedence to task network_os over inventory network_os
This commit is contained in:
Ricardo Carrillo Cruz 2017-06-02 14:06:38 +02:00 committed by GitHub
parent 2ee2c8c1ab
commit 64add28657
20 changed files with 674 additions and 6 deletions

View file

@ -66,6 +66,10 @@ ARGS_DEFAULT_VALUE = {
} }
def get_argspec():
return eos_argument_spec
def check_args(module, warnings): def check_args(module, warnings):
provider = module.params['provider'] or {} provider = module.params['provider'] or {}
for key in eos_argument_spec: for key in eos_argument_spec:

View file

@ -45,6 +45,10 @@ ios_argument_spec = {
} }
def get_argspec():
return ios_argument_spec
def check_args(module, warnings): def check_args(module, warnings):
provider = module.params['provider'] or {} provider = module.params['provider'] or {}
for key in ios_argument_spec: for key in ios_argument_spec:

View file

@ -44,6 +44,10 @@ iosxr_argument_spec = {
} }
def get_argspec():
return iosxr_argument_spec
def check_args(module, warnings): def check_args(module, warnings):
provider = module.params['provider'] or {} provider = module.params['provider'] or {}
for key in iosxr_argument_spec: for key in iosxr_argument_spec:

View file

@ -48,6 +48,10 @@ ARGS_DEFAULT_VALUE = {
} }
def get_argspec():
return junos_argument_spec
def check_args(module, warnings): def check_args(module, warnings):
provider = module.params['provider'] or {} provider = module.params['provider'] or {}
for key in junos_argument_spec: for key in junos_argument_spec:

View file

@ -62,6 +62,10 @@ ARGS_DEFAULT_VALUE = {
} }
def get_argspec():
return nxos_argument_spec
def check_args(module, warnings): def check_args(module, warnings):
provider = module.params['provider'] or {} provider = module.params['provider'] or {}
for key in nxos_argument_spec: for key in nxos_argument_spec:

View file

@ -45,6 +45,10 @@ vyos_argument_spec = {
} }
def get_argspec():
return vyos_argument_spec
def check_args(module, warnings): def check_args(module, warnings):
provider = module.params['provider'] or {} provider = module.params['provider'] or {}
for key in vyos_argument_spec: for key in vyos_argument_spec:

View file

@ -0,0 +1,110 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2017, Ansible by Red Hat, inc
#
# This file is part of Ansible by Red Hat
#
# 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/>.
#
ANSIBLE_METADATA = {'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'core'}
DOCUMENTATION = """
---
module: net_system
version_added: "2.4"
author: "Ricardo Carrillo Cruz (@rcarrillocruz)"
short_description: Manage the system attributes on network devices
description:
- This module provides declarative management of node system attributes
on network devices. It provides an option to configure host system
parameters or remove those parameters from the device active
configuration.
options:
hostname:
description:
- Configure the device hostname parameter. This option takes an ASCII string value.
domain_name:
description:
- Configure the IP domain name
on the remote device to the provided value. Value
should be in the dotted name form and will be
appended to the C(hostname) to create a fully-qualified
domain name.
domain_search:
description:
- Provides the list of domain suffixes to
append to the hostname for the purpose of doing name resolution.
This argument accepts a list of names and will be reconciled
with the current active configuration on the running node.
lookup_source:
description:
- Provides one or more source
interfaces to use for performing DNS lookups. The interface
provided in C(lookup_source) must be a valid interface configured
on the device.
name_servers:
description:
- List of DNS name servers by IP address to use to perform name resolution
lookups. This argument accepts either a list of DNS servers See
examples.
state:
description:
- State of the configuration
values in the device's current active configuration. When set
to I(present), the values should be configured in the device active
configuration and when set to I(absent) the values should not be
in the device active configuration
default: present
choices: ['present', 'absent']
"""
EXAMPLES = """
- name: configure hostname and domain name
net_system:
hostname: ios01
domain_name: test.example.com
domain-search:
- ansible.com
- redhat.com
- cisco.com
- name: remove configuration
net_system:
state: absent
- name: configure DNS lookup sources
net_system:
lookup_source: MgmtEth0/0/CPU0/0
- name: configure name servers
net_system:
name_servers:
- 8.8.8.8
- 8.8.4.4
"""
RETURN = """
commands:
description: The list of configuration mode commands to send to the device
returned: always
type: list
sample:
- hostname ios01
- ip domain name test.example.com
"""

View file

@ -0,0 +1,195 @@
# (c) 2015, Ansible 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/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
import copy
from ansible.plugins.action import ActionBase
from ansible.utils.path import unfrackpath
from ansible.plugins import connection_loader
from ansible.module_utils.basic import AnsibleFallbackNotFound
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_bytes
from imp import find_module, load_module
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
if self._play_context.connection != 'local':
return dict(
failed=True,
msg='invalid connection specified, expected connection=local, '
'got %s' % self._play_context.connection
)
play_context = copy.deepcopy(self._play_context)
play_context.network_os = self._get_network_os(task_vars)
# TODO this can be netconf
play_context.connection = 'network_cli'
self.provider = self._load_provider(play_context.network_os)
play_context.remote_addr = self.provider['host'] or self._play_context.remote_addr
play_context.port = self.provider['port'] or self._play_context.port or 22
play_context.remote_user = self.provider['username'] or self._play_context.connection_user
play_context.password = self.provider['password'] or self._play_context.password
play_context.private_key_file = self.provider['ssh_keyfile'] or self._play_context.private_key_file
play_context.timeout = self.provider['timeout'] or self._play_context.timeout
if 'authorize' in self.provider.keys():
play_context.become = self.provider['authorize'] or False
play_context.become_pass = self.provider['auth_pass']
socket_path = self._start_connection(play_context)
task_vars['ansible_socket'] = socket_path
result = super(ActionModule, self).run(tmp, task_vars)
module = self._get_implementation_module(play_context.network_os, self._task.action)
if not module:
result['failed'] = True
result['msg'] = ('Could not find implementation module %s for %s' %
(self._task.action, play_context.network_os))
else:
new_module_args = self._task.args.copy()
# perhaps delete the provider argument here as well since the
# module code doesn't need the information, the connection is
# already started
if 'network_os' in new_module_args:
del new_module_args['network_os']
display.vvvv('Running implementation module %s' % module)
result.update(self._execute_module(module_name=module,
module_args=new_module_args, task_vars=task_vars,
wrap_async=self._task.async))
display.vvvv('Caching network OS %s in facts' % play_context.network_os)
result['ansible_facts'] = {'network_os': play_context.network_os}
return result
def _start_connection(self, play_context):
display.vvv('using connection plugin %s' % play_context.connection, play_context.remote_addr)
connection = self._shared_loader_obj.connection_loader.get('persistent',
play_context, sys.stdin)
socket_path = self._get_socket_path(play_context)
display.vvvv('socket_path: %s' % socket_path, play_context.remote_addr)
if not os.path.exists(socket_path):
# start the connection if it isn't started
rc, out, err = connection.exec_command('open_shell()')
display.vvvv('open_shell() returned %s %s %s' % (rc, out, err))
if not rc == 0:
return {'failed': True,
'msg': 'unable to open shell. Please see: ' +
'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell',
'rc': rc}
else:
# make sure we are in the right cli context which should be
# enable mode and not config module
rc, out, err = connection.exec_command('prompt()')
if str(out).strip().endswith(')#'):
display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr)
connection.exec_command('exit')
if self._play_context.become_method == 'enable':
self._play_context.become = False
self._play_context.become_method = None
return socket_path
def _get_network_os(self, task_vars):
if ('network_os' in self._task.args and self._task.args['network_os']):
display.vvvv('Getting network OS from task argument')
network_os = self._task.args['network_os']
elif (self._play_context.network_os):
display.vvvv('Getting network OS from inventory')
network_os = self._play_context.network_os
elif ('network_os' in task_vars['ansible_facts'] and
task_vars['ansible_facts']['network_os']):
display.vvvv('Getting network OS from fact')
network_os = task_vars['ansible_facts']['network_os']
else:
# this will be replaced by the call to get_capabilities() on the
# connection
display.vvvv('Getting network OS from net discovery')
network_os = None
return network_os
def _get_implementation_module(self, network_os, platform_agnostic_module):
implementation_module = network_os + '_' + platform_agnostic_module.partition('_')[2]
if implementation_module not in self._shared_loader_obj.module_loader:
implementation_module = None
return implementation_module
# this will be removed once the new connection work is done
def _get_socket_path(self, play_context):
ssh = connection_loader.get('ssh', class_only=True)
cp = ssh._create_control_path(play_context.remote_addr, play_context.port, play_context.remote_user)
path = unfrackpath("$HOME/.ansible/pc")
return cp % dict(directory=path)
def _load_provider(self, network_os):
# we should be able to stream line this a bit by creating a common
# provider argument spec in module_utils/network_common.py or another
# option is that there isn't a need to push provider into the module
# since the connection is started in the action handler.
f, p, d = find_module('ansible')
f2, p2, d2 = find_module('module_utils', [p])
f3, p3, d3 = find_module(network_os, [p2])
module = load_module('ansible.module_utils.' + network_os, f3, p3, d3)
provider = self._task.args.get('provider', {})
for key, value in iteritems(module.get_argspec()):
if key != 'provider' and key not in provider:
if key in self._task.args:
provider[key] = self._task.args[key]
elif 'fallback' in value:
provider[key] = self._fallback(value['fallback'])
elif key not in provider:
provider[key] = None
return provider
def _fallback(self, fallback):
strategy = fallback[0]
args = []
kwargs = {}
for item in fallback[1:]:
if isinstance(item, dict):
kwargs = item
else:
args = item
try:
return strategy(*args, **kwargs)
except AnsibleFallbackNotFound:
pass

View file

@ -0,0 +1,26 @@
# (c) 2017, Ansible 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/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.plugins.action.net_base import ActionModule as _ActionModule
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None):
result = super(ActionModule, self).run(tmp, task_vars)
return result

View file

@ -5,27 +5,27 @@ ansible_python_interpreter=python
[eos] [eos]
#veos-dut-01 #veos-dut-01
veos01 veos01 ansible_network_os=eos
[nxos] [nxos]
nxos01 nxos01 ansible_network_os=nxos
[iosxr] [iosxr]
iosxr01 iosxr01 ansible_network_os=iosxr
[ios] [ios]
ios01 ios01 ansible_network_os=ios
#csr01 #csr01
[junos] [junos]
vsrx01 vsrx01 ansible_network_os=junos
[cumulus] [cumulus]
clvx01 clvx01
[vyos] [vyos]
vyos-dut-01 vyos01 ansible_network_os=vyos
[ops] [ops]
ops01 ops01
@ -33,4 +33,12 @@ ops01
[asa] [asa]
asa01 asa01
[platform_agnostic:children]
ios
iosxr
eos
junos
vyos
nxos
# vim: nospell filetype=dosini # vim: nospell filetype=dosini

View file

@ -0,0 +1,11 @@
---
- hosts: platform_agnostic
gather_facts: no
connection: local
vars:
limit_to: "*"
debug: false
roles:
- { role: net_system, when: "limit_to in ['*', 'net_system']" }

View file

@ -0,0 +1,2 @@
network/ci
skip/python3

View file

@ -0,0 +1,2 @@
---
testcase: "*"

View file

@ -0,0 +1,16 @@
---
- name: collect all cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
register: test_cases
delegate_to: localhost
- name: set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
- name: run test case
include: "{{ test_case_to_run }}"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -0,0 +1,16 @@
---
- debug: msg="START cli/set_name_servers.yaml"
- include: "{{ role_path }}/tests/ios/set_name_servers.yaml"
when: hostvars[inventory_hostname]['ansible_network_os'] == 'ios'
- include: "{{ role_path }}/tests/iosxr/set_name_servers.yaml"
when: hostvars[inventory_hostname]['ansible_network_os'] == 'iosxr'
- include: "{{ role_path }}/tests/nxos/set_name_servers.yaml"
when: hostvars[inventory_hostname]['ansible_network_os'] == 'nxos'
- include: "{{ role_path }}/tests/eos/set_name_servers.yaml"
when: hostvars[inventory_hostname]['ansible_network_os'] == 'eos'
- debug: msg="END cli/set_name_servers.yaml"

View file

@ -0,0 +1,64 @@
---
- debug: msg="START eos/set_name_servers.yaml"
- name: setup
eos_config:
lines:
- no ip name-server
- vrf definition ansible
match: none
provider: "{{ cli }}"
- name: configure name_servers
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 3
- "'ip name-server 1.1.1.1' in result.commands"
- "'ip name-server 2.2.2.2' in result.commands"
- "'ip name-server 3.3.3.3' in result.commands"
- name: verify name_servers
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == false
- name: remove one
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 1
- "'no ip name-server 3.3.3.3' in result.commands"
- name: teardown
eos_config:
lines:
- no ip domain lookup source-interface
- no vrf definition ansible
match: none
provider: "{{ cli }}"
- debug: msg="END eos/set_name_servers.yaml"

View file

@ -0,0 +1,67 @@
---
- debug: msg="START ios/set_name_servers.yaml"
- name: setup
ios_config:
lines:
- no ip name-server
match: none
authorize: yes
provider: "{{ cli }}"
- name: configure name_servers
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
authorize: yes
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 3
- "'ip name-server 1.1.1.1' in result.commands"
- "'ip name-server 2.2.2.2' in result.commands"
- "'ip name-server 3.3.3.3' in result.commands"
- name: verify name_servers
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
authorize: yes
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == false
- name: remove one
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
authorize: yes
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 1
- "'no ip name-server 3.3.3.3' in result.commands"
- name: teardown
ios_config:
lines:
- no ip domain lookup source-interface
match: none
authorize: yes
provider: "{{ cli }}"
- debug: msg="END ios/set_name_servers.yaml"

View file

@ -0,0 +1,59 @@
---
- debug: msg="START iosxr/set_name_servers.yaml"
- name: setup
iosxr_config:
lines:
- no ip name-server 1.1.1.1
- no ip name-server 2.2.2.2
- no ip name-server 3.3.3.3
match: none
provider: "{{ cli }}"
- name: configure name_servers
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 3
- "'domain name-server 1.1.1.1' in result.commands"
- "'domain name-server 2.2.2.2' in result.commands"
- "'domain name-server 3.3.3.3' in result.commands"
- name: verify name_servers
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == false
- name: remove one
net_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 1
- "'no domain name-server 3.3.3.3' in result.commands"
# FIXME: No teardown
#
- debug: msg="END iosxr/set_name_servers.yaml"

View file

@ -0,0 +1,66 @@
---
- debug: msg="START nxos/set_name_servers.yaml"
- name: setup
nxos_config:
lines:
- no ip name-server 1.1.1.1
- no ip name-server 2.2.2.2
- no ip name-server 3.3.3.3
match: none
provider: "{{ cli }}"
- name: configure name_servers
nxos_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 3
- "'ip name-server 1.1.1.1' in result.commands"
- "'ip name-server 2.2.2.2' in result.commands"
- "'ip name-server 3.3.3.3' in result.commands"
- name: verify name_servers
nxos_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == false
- name: remove one
nxos_system:
name_servers:
- 1.1.1.1
- 2.2.2.2
provider: "{{ cli }}"
register: result
- assert:
that:
- result.changed == true
- result.commands|length == 1
- "'no ip name-server 3.3.3.3' in result.commands"
- name: teardown
nxos_config:
lines:
- no ip lookup source-interface
match: none
provider: "{{ cli }}"
ignore_errors: yes
# FIXME Copied from iosxr, not sure what we need here
- debug: msg="END nxos/set_name_servers.yaml"