new module nxos_system (#21627)

* provides declarative config support for nxos system attributes
* adds unit test cases for new module
pull/21631/head
Peter Sprygada 8 years ago committed by GitHub
parent 2f10bdf0c7
commit 76c9ad9dfc

@ -0,0 +1,359 @@
#!/usr/bin/python
#
# 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/>.
#
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'core',
'version': '1.0'
}
DOCUMENTATION = """
---
module: nxos_system
version_added: "2.3"
author: "Peter Sprygada (@privateip)"
short_description: Manage the system attributes on Cisco NXOS devices
description:
- This module provides declarative management of node system attributes
on Cisco NXOS devices. It provides an option to configure host system
parameters or remove those parameters from the device active
configuration.
options:
hostname:
description:
- The C(hostname) argument will configure the device hostname
parameter on Cisco NXOS devices. The C(hostname) value is an
ASCII string value.
required: false
default: null
domain_lookup:
description:
- The C(domain_lookup) argument enables or disables the DNS
lookup feature in Cisco NXOS. This argument accepts boolean
values. When enabled, the system will try to resolve hostnames
using DNS and when disabled, hostnames will not be resolved.
required: false
default: null
domain_search:
description:
- The C(domain_search) argument configures a list of domain
name suffixes to search when performing DNS name resolution.
This argument accepts either a list of domain names or
a list of dicts that configure the domain name and VRF name. See
examples.
required: false
default: null
domain_name:
description:
- The C(domain_name) argument configures the default domain
name suffix to be used when referencing this node by its
FQDN. This argument accepts either a list of domain names or
a list of dicts that configure the domain name and VRF name. See
examples.
required: false
default: null
name_servers:
description:
- The C(name_servers) argument accepts a list of DNS name servers by
way of either FQDN or IP address to use to perform name resolution
lookups. This argument accepts wither a list of DNS servers or
a list of hashes that configure the name server and VRF name. See
examples.
required: false
default: null
state:
description:
- The C(state) argument configures the 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
required: false
default: present
choices: ['present', 'absent']
"""
EXAMPLES = """
- name: configure hostname and domain-name
nxos_system:
hostname: nxos01
domain_name: eng.ansible.com
- name: remove configuration
nxos_system:
state: absent
- name: configure DNS lookup sources
nxos_system:
lookup_source: Management1
- name: configure name servers
nxos_system:
name_servers:
- 8.8.8.8
- 8.8.4.4
- name: configure name servers with VRF support
nxos_system:
name_servers:
- { server: 8.8.8.8, vrf: mgmt }
- { server: 8.8.4.4, vrf: mgmt }
"""
RETURN = """
commands:
description: The list of configuration mode commands to send to the device
returned: always
type: list
sample:
- hostname nxos01
- ip domain-name eng.ansible.com
"""
import re
from ansible.module_utils.nxos import get_config, load_config
from ansible.module_utils.nxos import nxos_argument_spec, check_args
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible.module_utils.netcfg import NetworkConfig
from ansible.module_utils.network_common import ComplexList
_CONFIGURED_VRFS = None
def has_vrf(module, vrf):
global _CONFIGURED_VRFS
if _CONFIGURED_VRFS is not None:
return vrf in _CONFIGURED_VRFS
config = get_config(module)
_CONFIGURED_VRFS = re.findall('vrf context (\S+)', config)
return vrf in _CONFIGURED_VRFS
def map_obj_to_commands(want, have, module):
commands = list()
state = module.params['state']
needs_update = lambda x: want.get(x) and (want.get(x) != have.get(x))
difference = lambda x,y,z: [item for item in x[z] if item not in y[z]]
def remove(cmd, commands, vrf=None):
if vrf:
commands.append('vrf context %s' % vrf)
commands.append(cmd)
if vrf:
commands.append('exit')
def add(cmd, commands, vrf=None):
if vrf:
if not has_vrf(module, vrf):
module.fail_json(msg='invalid vrf name %s' % vrf)
return remove(cmd, commands, vrf)
if state == 'absent':
if have['hostname']:
commands.append('no hostname')
for item in have['domain_name']:
cmd = 'no ip domain-name %s' % item['name']
remove(cmd, commands, item['vrf'])
for item in have['domain_search']:
cmd = 'no ip domain-list %s' % item['name']
remove(cmd, commands, item['vrf'])
for item in have['name_servers']:
cmd = 'no ip name-server %s' % item['server']
remove(cmd, commands, item['vrf'])
if state == 'present':
if needs_update('hostname'):
commands.append('hostname %s' % want['hostname'])
if needs_update('domain_lookup'):
cmd = 'ip domain-lookup'
if want['domain_lookup'] is False:
cmd = 'no %s' % cmd
commands.append(cmd)
if want['domain_name']:
for item in difference(have, want, 'domain_name'):
cmd = 'no ip domain-name %s' % item['name']
remove(cmd, commands, item['vrf'])
for item in difference(want, have, 'domain_name'):
cmd = 'ip domain-name %s' % item['name']
add(cmd, commands, item['vrf'])
if want['domain_search']:
for item in difference(have, want, 'domain_search'):
cmd = 'no ip domain-list %s' % item['name']
remove(cmd, commands, item['vrf'])
for item in difference(want, have, 'domain_search'):
cmd = 'ip domain-list %s' % item['name']
add(cmd, commands, item['vrf'])
if want['name_servers']:
for item in difference(have, want, 'name_servers'):
cmd = 'no ip name-server %s' % item['server']
remove(cmd, commands, item['vrf'])
for item in difference(want, have, 'name_servers'):
cmd = 'ip name-server %s' % item['server']
add(cmd, commands, item['vrf'])
return commands
def parse_hostname(config):
match = re.search('^hostname (\S+)', config, re.M)
if match:
return match.group(1)
def parse_domain_name(config, vrf_config):
objects = list()
regex = re.compile('ip domain-name (\S+)')
match = regex.search(config, re.M)
if match:
objects.append({'name': match.group(1), 'vrf': None})
for vrf, cfg in iteritems(vrf_config):
match = regex.search(cfg, re.M)
if match:
objects.append({'name': match.group(1), 'vrf': vrf})
return objects
def parse_domain_search(config, vrf_config):
objects = list()
for item in re.findall('^ip domain-list (\S+)', config, re.M):
objects.append({'name': item, 'vrf': None})
for vrf, cfg in iteritems(vrf_config):
for item in re.findall('ip domain-list (\S+)', cfg, re.M):
objects.append({'name': item, 'vrf': vrf})
return objects
def parse_name_servers(config, vrf_config):
objects = list()
match = re.search('^ip name-server (.+)$', config, re.M)
if match:
for addr in match.group(1).split(' '):
objects.append({'server': addr, 'vrf': None})
for vrf, cfg in iteritems(vrf_config):
for item in re.findall('ip name-server (\S+)', cfg, re.M):
for addr in match.group(1).split(' '):
objects.append({'server': addr, 'vrf': vrf})
return objects
def map_config_to_obj(module):
config = get_config(module)
configobj = NetworkConfig(indent=2, contents=config)
vrf_config = {}
vrfs = re.findall('^vrf context (\S+)$', config, re.M)
for vrf in vrfs:
config_data = configobj.get_block_config(path=['vrf context %s' % vrf])
vrf_config[vrf] = config_data
return {
'hostname': parse_hostname(config),
'domain_lookup': 'no ip domain-lookup' not in config,
'domain_name': parse_domain_name(config, vrf_config),
'domain_search': parse_domain_search(config, vrf_config),
'name_servers': parse_name_servers(config, vrf_config)
}
def map_params_to_obj(module):
obj = {
'hostname': module.params['hostname'],
'domain_lookup': module.params['domain_lookup'],
}
domain_name = ComplexList(dict(
name=dict(key=True),
vrf=dict()
), module)
domain_search = ComplexList(dict(
name=dict(key=True),
vrf=dict()
), module)
name_servers = ComplexList(dict(
server=dict(key=True),
vrf=dict()
), module)
for arg, cast in [('domain_name', domain_name), ('domain_search', domain_search),
('name_servers', name_servers)]:
if module.params[arg] is not None:
obj[arg] = cast(module.params[arg])
else:
obj[arg] = None
return obj
def main():
""" main entry point for module execution
"""
argument_spec = dict(
hostname=dict(),
domain_lookup=dict(type='bool'),
# { name: <str>, vrf: <str> }
domain_name=dict(type='list'),
# {name: <str>, vrf: <str> }
domain_search=dict(type='list'),
# { server: <str>; vrf: <str> }
name_servers=dict(type='list'),
state=dict(default='present', choices=['present', 'absent'])
)
argument_spec.update(nxos_argument_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
warnings = list()
check_args(module, warnings)
result = {'changed': False}
if warnings:
result['warnings'] = warnings
want = map_params_to_obj(module)
have = map_config_to_obj(module)
commands = map_obj_to_commands(want, have, module)
result['commands'] = commands
if commands:
if not module.check_mode:
load_config(module, commands)
result['changed'] = True
module.exit_json(**result)
if __name__ == '__main__':
main()

@ -0,0 +1,15 @@
hostname nxos01
!
no ip domain-lookup
ip domain-name ansible.com
ip domain-list ansible.com
ip domain-list redhat.com
ip name-server 8.8.8.8 172.26.1.1
!
vrf context management
ip domain-name eng.ansible.com
ip domain-list ansible.com
ip domain-list redhat.com
ip name-server 172.26.1.1 8.8.8.8
ip route 172.26.0.0/16 172.26.4.1

@ -0,0 +1,113 @@
# (c) 2016 Red Hat 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/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch
from ansible.module_utils import basic
from ansible.module_utils._text import to_bytes
def set_module_args(args):
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except:
pass
fixture_data[path] = data
return data
class AnsibleExitJson(Exception):
pass
class AnsibleFailJson(Exception):
pass
class TestNxosModule(unittest.TestCase):
def execute_module(self, failed=False, changed=False, commands=None,
sort=True, defaults=False):
self.load_fixtures(commands)
if failed:
result = self.failed()
self.assertTrue(result['failed'], result)
else:
result = self.changed(changed)
self.assertEqual(result['changed'], changed, result)
if commands:
if sort:
self.assertEqual(sorted(commands), sorted(result['commands']), result['commands'])
else:
self.assertEqual(commands, result['commands'], result['commands'])
return result
def failed(self):
def fail_json(*args, **kwargs):
kwargs['failed'] = True
raise AnsibleFailJson(kwargs)
with patch.object(basic.AnsibleModule, 'fail_json', fail_json):
with self.assertRaises(AnsibleFailJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertTrue(result['failed'], result)
return result
def changed(self, changed=False):
def exit_json(*args, **kwargs):
if 'changed' not in kwargs:
kwargs['changed'] = False
raise AnsibleExitJson(kwargs)
with patch.object(basic.AnsibleModule, 'exit_json', exit_json):
with self.assertRaises(AnsibleExitJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertEqual(result['changed'], changed, result)
return result
def load_fixtures(self, commands=None):
pass

@ -0,0 +1,124 @@
#
# (c) 2016 Red Hat 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/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
from ansible.compat.tests.mock import patch
from ansible.modules.network.nxos import nxos_system
from .nxos_module import TestNxosModule, load_fixture, set_module_args
class TestNxosSystemModule(TestNxosModule):
module = nxos_system
def setUp(self):
self.mock_get_config = patch('ansible.modules.network.nxos.nxos_system.get_config')
self.get_config = self.mock_get_config.start()
self.mock_load_config = patch('ansible.modules.network.nxos.nxos_system.load_config')
self.load_config = self.mock_load_config.start()
def tearDown(self):
self.mock_get_config.stop()
self.mock_load_config.stop()
def load_fixtures(self, commands=None):
self.get_config.return_value = load_fixture('nxos_system_config.cfg')
self.load_config.return_value = None
def test_nxos_system_hostname_changed(self):
set_module_args(dict(hostname='foo'))
commands = ['hostname foo']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_domain_lookup(self):
set_module_args(dict(domain_lookup=True))
commands = ['ip domain-lookup']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_missing_vrf(self):
domain_name = dict(name='example.com', vrf='example')
set_module_args(dict(domain_name=domain_name))
self.execute_module(failed=True)
def test_nxos_system_domain_name(self):
set_module_args(dict(domain_name=['example.net']))
commands = ['no ip domain-name ansible.com',
'vrf context management', 'no ip domain-name eng.ansible.com', 'exit',
'ip domain-name example.net']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_domain_name_complex(self):
domain_name = dict(name='example.net', vrf='management')
set_module_args(dict(domain_name=[domain_name]))
commands = ['no ip domain-name ansible.com',
'vrf context management', 'no ip domain-name eng.ansible.com', 'exit',
'vrf context management', 'ip domain-name example.net', 'exit']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_domain_search(self):
set_module_args(dict(domain_search=['example.net']))
commands = ['vrf context management', 'no ip domain-list ansible.com', 'exit',
'vrf context management', 'no ip domain-list redhat.com', 'exit',
'no ip domain-list ansible.com', 'no ip domain-list redhat.com',
'ip domain-list example.net']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_domain_search_complex(self):
domain_search = dict(name='example.net', vrf='management')
set_module_args(dict(domain_search=[domain_search]))
commands = ['vrf context management', 'no ip domain-list ansible.com', 'exit',
'vrf context management', 'no ip domain-list redhat.com', 'exit',
'no ip domain-list ansible.com', 'no ip domain-list redhat.com',
'vrf context management', 'ip domain-list example.net', 'exit']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_name_servers(self):
set_module_args(dict(name_servers=['1.2.3.4', '8.8.8.8']))
commands = ['no ip name-server 172.26.1.1',
'vrf context management', 'no ip name-server 8.8.8.8', 'exit',
'vrf context management', 'no ip name-server 172.26.1.1', 'exit',
'ip name-server 1.2.3.4']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_name_servers_complex(self):
name_servers = dict(server='1.2.3.4', vrf='management')
set_module_args(dict(name_servers=[name_servers]))
commands = ['no ip name-server 8.8.8.8', 'no ip name-server 172.26.1.1',
'vrf context management', 'no ip name-server 8.8.8.8', 'exit',
'vrf context management', 'no ip name-server 172.26.1.1', 'exit',
'vrf context management', 'ip name-server 1.2.3.4', 'exit']
self.execute_module(changed=True, commands=commands)
def test_nxos_system_state_absent(self):
set_module_args(dict(state='absent'))
commands = ['no hostname', 'no ip domain-name ansible.com',
'vrf context management', 'no ip domain-name eng.ansible.com', 'exit',
'no ip domain-list ansible.com', 'no ip domain-list redhat.com',
'vrf context management', 'no ip domain-list ansible.com', 'exit',
'vrf context management', 'no ip domain-list redhat.com', 'exit',
'no ip name-server 8.8.8.8', 'no ip name-server 172.26.1.1',
'vrf context management', 'no ip name-server 8.8.8.8', 'exit',
'vrf context management', 'no ip name-server 172.26.1.1', 'exit']
self.execute_module(changed=True, commands=commands)
Loading…
Cancel
Save