1#!/usr/bin/python
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17#
18
19ANSIBLE_METADATA = {'metadata_version': '1.1',
20                    'status': ['preview'],
21                    'supported_by': 'network'}
22
23
24DOCUMENTATION = """
25---
26module: nxos_system
27extends_documentation_fragment: nxos
28version_added: "2.3"
29author: "Peter Sprygada (@privateip)"
30short_description: Manage the system attributes on Cisco NXOS devices
31description:
32  - This module provides declarative management of node system attributes
33    on Cisco NXOS devices.  It provides an option to configure host system
34    parameters or remove those parameters from the device active
35    configuration.
36options:
37  hostname:
38    description:
39      - Configure the device hostname parameter. This option takes an ASCII string value
40        or keyword 'default'
41  domain_name:
42    description:
43      - Configures the default domain
44        name suffix to be used when referencing this node by its
45        FQDN.  This argument accepts either a list of domain names or
46        a list of dicts that configure the domain name and VRF name or
47        keyword 'default'. See examples.
48  domain_lookup:
49    description:
50      - Enables or disables the DNS
51        lookup feature in Cisco NXOS.  This argument accepts boolean
52        values.  When enabled, the system will try to resolve hostnames
53        using DNS and when disabled, hostnames will not be resolved.
54    type: bool
55  domain_search:
56    description:
57      - Configures a list of domain
58        name suffixes to search when performing DNS name resolution.
59        This argument accepts either a list of domain names or
60        a list of dicts that configure the domain name and VRF name or
61        keyword 'default'. See examples.
62  name_servers:
63    description:
64      - List of DNS name servers by IP address to use to perform name resolution
65        lookups.  This argument accepts either a list of DNS servers or
66        a list of hashes that configure the name server and VRF name or
67        keyword 'default'. See examples.
68  system_mtu:
69    description:
70      - Specifies the mtu, must be an integer or keyword 'default'.
71  state:
72    description:
73      - State of the configuration
74        values in the device's current active configuration.  When set
75        to I(present), the values should be configured in the device active
76        configuration and when set to I(absent) the values should not be
77        in the device active configuration
78    default: present
79    choices: ['present', 'absent']
80"""
81
82EXAMPLES = """
83- name: configure hostname and domain-name
84  nxos_system:
85    hostname: nxos01
86    domain_name: test.example.com
87
88- name: remove configuration
89  nxos_system:
90    state: absent
91
92- name: configure name servers
93  nxos_system:
94    name_servers:
95      - 8.8.8.8
96      - 8.8.4.4
97
98- name: configure name servers with VRF support
99  nxos_system:
100    name_servers:
101      - { server: 8.8.8.8, vrf: mgmt }
102      - { server: 8.8.4.4, vrf: mgmt }
103"""
104
105RETURN = """
106commands:
107  description: The list of configuration mode commands to send to the device
108  returned: always
109  type: list
110  sample:
111    - hostname nxos01
112    - ip domain-name test.example.com
113"""
114import re
115
116from ansible.module_utils.network.nxos.nxos import get_config, load_config
117from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args
118from ansible.module_utils.basic import AnsibleModule
119from ansible.module_utils.six import iteritems
120from ansible.module_utils.network.common.config import NetworkConfig
121from ansible.module_utils.network.common.utils import ComplexList
122
123_CONFIGURED_VRFS = None
124
125
126def has_vrf(module, vrf):
127    global _CONFIGURED_VRFS
128    if _CONFIGURED_VRFS is not None:
129        return vrf in _CONFIGURED_VRFS
130    config = get_config(module)
131    _CONFIGURED_VRFS = re.findall(r'vrf context (\S+)', config)
132    return vrf in _CONFIGURED_VRFS
133
134
135def map_obj_to_commands(want, have, module):
136    commands = list()
137    state = module.params['state']
138
139    def needs_update(x):
140        return want.get(x) and (want.get(x) != have.get(x))
141
142    def difference(x, y, z):
143        return [item for item in x[z] if item not in y[z]]
144
145    def remove(cmd, commands, vrf=None):
146        if vrf:
147            commands.append('vrf context %s' % vrf)
148        commands.append(cmd)
149        if vrf:
150            commands.append('exit')
151
152    def add(cmd, commands, vrf=None):
153        if vrf:
154            if not has_vrf(module, vrf):
155                module.fail_json(msg='invalid vrf name %s' % vrf)
156        return remove(cmd, commands, vrf)
157
158    if state == 'absent':
159        if have['hostname']:
160            commands.append('no hostname')
161
162        for item in have['domain_name']:
163            cmd = 'no ip domain-name %s' % item['name']
164            remove(cmd, commands, item['vrf'])
165
166        for item in have['domain_search']:
167            cmd = 'no ip domain-list %s' % item['name']
168            remove(cmd, commands, item['vrf'])
169
170        for item in have['name_servers']:
171            cmd = 'no ip name-server %s' % item['server']
172            remove(cmd, commands, item['vrf'])
173
174        if have['system_mtu']:
175            commands.append('no system jumbomtu')
176
177    if state == 'present':
178        if needs_update('hostname'):
179            if want['hostname'] == 'default':
180                if have['hostname']:
181                    commands.append('no hostname')
182            else:
183                commands.append('hostname %s' % want['hostname'])
184
185        if want.get('domain_lookup') is not None:
186            if have.get('domain_lookup') != want.get('domain_lookup'):
187                cmd = 'ip domain-lookup'
188                if want['domain_lookup'] is False:
189                    cmd = 'no %s' % cmd
190                commands.append(cmd)
191
192        if want['domain_name']:
193            if want.get('domain_name')[0]['name'] == 'default':
194                if have['domain_name']:
195                    for item in have['domain_name']:
196                        cmd = 'no ip domain-name %s' % item['name']
197                        remove(cmd, commands, item['vrf'])
198            else:
199                for item in difference(have, want, 'domain_name'):
200                    cmd = 'no ip domain-name %s' % item['name']
201                    remove(cmd, commands, item['vrf'])
202                for item in difference(want, have, 'domain_name'):
203                    cmd = 'ip domain-name %s' % item['name']
204                    add(cmd, commands, item['vrf'])
205
206        if want['domain_search']:
207            if want.get('domain_search')[0]['name'] == 'default':
208                if have['domain_search']:
209                    for item in have['domain_search']:
210                        cmd = 'no ip domain-list %s' % item['name']
211                        remove(cmd, commands, item['vrf'])
212            else:
213                for item in difference(have, want, 'domain_search'):
214                    cmd = 'no ip domain-list %s' % item['name']
215                    remove(cmd, commands, item['vrf'])
216                for item in difference(want, have, 'domain_search'):
217                    cmd = 'ip domain-list %s' % item['name']
218                    add(cmd, commands, item['vrf'])
219
220        if want['name_servers']:
221            if want.get('name_servers')[0]['server'] == 'default':
222                if have['name_servers']:
223                    for item in have['name_servers']:
224                        cmd = 'no ip name-server %s' % item['server']
225                        remove(cmd, commands, item['vrf'])
226            else:
227                for item in difference(have, want, 'name_servers'):
228                    cmd = 'no ip name-server %s' % item['server']
229                    remove(cmd, commands, item['vrf'])
230                for item in difference(want, have, 'name_servers'):
231                    cmd = 'ip name-server %s' % item['server']
232                    add(cmd, commands, item['vrf'])
233
234        if needs_update('system_mtu'):
235            if want['system_mtu'] == 'default':
236                if have['system_mtu']:
237                    commands.append('no system jumbomtu')
238            else:
239                commands.append('system jumbomtu %s' % want['system_mtu'])
240
241    return commands
242
243
244def parse_hostname(config):
245    match = re.search(r'^hostname (\S+)', config, re.M)
246    if match:
247        return match.group(1)
248
249
250def parse_domain_name(config, vrf_config):
251    objects = list()
252    regex = re.compile(r'ip domain-name (\S+)')
253
254    match = regex.search(config, re.M)
255    if match:
256        objects.append({'name': match.group(1), 'vrf': None})
257
258    for vrf, cfg in iteritems(vrf_config):
259        match = regex.search(cfg, re.M)
260        if match:
261            objects.append({'name': match.group(1), 'vrf': vrf})
262
263    return objects
264
265
266def parse_domain_search(config, vrf_config):
267    objects = list()
268
269    for item in re.findall(r'^ip domain-list (\S+)', config, re.M):
270        objects.append({'name': item, 'vrf': None})
271
272    for vrf, cfg in iteritems(vrf_config):
273        for item in re.findall(r'ip domain-list (\S+)', cfg, re.M):
274            objects.append({'name': item, 'vrf': vrf})
275
276    return objects
277
278
279def parse_name_servers(config, vrf_config, vrfs):
280    objects = list()
281
282    match = re.search('^ip name-server (.+)$', config, re.M)
283    if match and 'use-vrf' not in match.group(1):
284        for addr in match.group(1).split(' '):
285            objects.append({'server': addr, 'vrf': None})
286
287    for vrf, cfg in iteritems(vrf_config):
288        vrf_match = re.search('ip name-server (.+)', cfg, re.M)
289        if vrf_match:
290            for addr in vrf_match.group(1).split(' '):
291                objects.append({'server': addr, 'vrf': vrf})
292
293    return objects
294
295
296def parse_system_mtu(config):
297    match = re.search(r'^system jumbomtu (\d+)', config, re.M)
298    if match:
299        return match.group(1)
300
301
302def map_config_to_obj(module):
303    config = get_config(module)
304    configobj = NetworkConfig(indent=2, contents=config)
305
306    vrf_config = {}
307
308    vrfs = re.findall(r'^vrf context (\S+)$', config, re.M)
309    for vrf in vrfs:
310        config_data = configobj.get_block_config(path=['vrf context %s' % vrf])
311        vrf_config[vrf] = config_data
312
313    return {
314        'hostname': parse_hostname(config),
315        'domain_lookup': 'no ip domain-lookup' not in config,
316        'domain_name': parse_domain_name(config, vrf_config),
317        'domain_search': parse_domain_search(config, vrf_config),
318        'name_servers': parse_name_servers(config, vrf_config, vrfs),
319        'system_mtu': parse_system_mtu(config)
320    }
321
322
323def map_params_to_obj(module):
324    obj = {
325        'hostname': module.params['hostname'],
326        'domain_lookup': module.params['domain_lookup'],
327        'system_mtu': module.params['system_mtu']
328    }
329
330    domain_name = ComplexList(dict(
331        name=dict(key=True),
332        vrf=dict()
333    ), module)
334
335    domain_search = ComplexList(dict(
336        name=dict(key=True),
337        vrf=dict()
338    ), module)
339
340    name_servers = ComplexList(dict(
341        server=dict(key=True),
342        vrf=dict()
343    ), module)
344
345    for arg, cast in [('domain_name', domain_name), ('domain_search', domain_search),
346                      ('name_servers', name_servers)]:
347        if module.params[arg] is not None:
348            obj[arg] = cast(module.params[arg])
349        else:
350            obj[arg] = None
351
352    return obj
353
354
355def main():
356    """ main entry point for module execution
357    """
358    argument_spec = dict(
359        hostname=dict(),
360        domain_lookup=dict(type='bool'),
361
362        # { name: <str>, vrf: <str> }
363        domain_name=dict(type='list'),
364
365        # {name: <str>, vrf: <str> }
366        domain_search=dict(type='list'),
367
368        # { server: <str>; vrf: <str> }
369        name_servers=dict(type='list'),
370
371        system_mtu=dict(type='str'),
372        state=dict(default='present', choices=['present', 'absent'])
373    )
374
375    argument_spec.update(nxos_argument_spec)
376
377    module = AnsibleModule(argument_spec=argument_spec,
378                           supports_check_mode=True)
379
380    warnings = list()
381    check_args(module, warnings)
382
383    result = {'changed': False}
384    if warnings:
385        result['warnings'] = warnings
386
387    want = map_params_to_obj(module)
388    have = map_config_to_obj(module)
389
390    commands = map_obj_to_commands(want, have, module)
391    result['commands'] = commands
392
393    if commands:
394        if not module.check_mode:
395            load_config(module, commands)
396        result['changed'] = True
397
398    module.exit_json(**result)
399
400
401if __name__ == '__main__':
402    main()
403