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: ios_system
27version_added: "2.3"
28author: "Peter Sprygada (@privateip)"
29short_description: Manage the system attributes on Cisco IOS devices
30description:
31  - This module provides declarative management of node system attributes
32    on Cisco IOS devices.  It provides an option to configure host system
33    parameters or remove those parameters from the device active
34    configuration.
35extends_documentation_fragment: ios
36notes:
37  - Tested against IOS 15.6
38options:
39  hostname:
40    description:
41      - Configure the device hostname parameter. This option takes an ASCII string value.
42  domain_name:
43    description:
44      - Configure the IP domain name
45        on the remote device to the provided value. Value
46        should be in the dotted name form and will be
47        appended to the C(hostname) to create a fully-qualified
48        domain name.
49  domain_search:
50    description:
51      - Provides the list of domain suffixes to
52        append to the hostname for the purpose of doing name resolution.
53        This argument accepts a list of names and will be reconciled
54        with the current active configuration on the running node.
55  lookup_source:
56    description:
57      - Provides one or more source
58        interfaces to use for performing DNS lookups.  The interface
59        provided in C(lookup_source) must be a valid interface configured
60        on the device.
61  lookup_enabled:
62    description:
63      - Administrative control
64        for enabling or disabling DNS lookups.  When this argument is
65        set to True, lookups are performed and when it is set to False,
66        lookups are not performed.
67    type: bool
68  name_servers:
69    description:
70      - List of DNS name servers by IP address to use to perform name resolution
71        lookups.  This argument accepts either a list of DNS servers See
72        examples.
73  state:
74    description:
75      - State of the configuration
76        values in the device's current active configuration.  When set
77        to I(present), the values should be configured in the device active
78        configuration and when set to I(absent) the values should not be
79        in the device active configuration
80    default: present
81    choices: ['present', 'absent']
82"""
83
84EXAMPLES = """
85- name: configure hostname and domain name
86  ios_system:
87    hostname: ios01
88    domain_name: test.example.com
89    domain_search:
90      - ansible.com
91      - redhat.com
92      - cisco.com
93
94- name: remove configuration
95  ios_system:
96    state: absent
97
98- name: configure DNS lookup sources
99  ios_system:
100    lookup_source: MgmtEth0/0/CPU0/0
101    lookup_enabled: yes
102
103- name: configure name servers
104  ios_system:
105    name_servers:
106      - 8.8.8.8
107      - 8.8.4.4
108"""
109
110RETURN = """
111commands:
112  description: The list of configuration mode commands to send to the device
113  returned: always
114  type: list
115  sample:
116    - hostname ios01
117    - ip domain name test.example.com
118"""
119import re
120
121from ansible.module_utils.basic import AnsibleModule
122from ansible.module_utils.network.ios.ios import get_config, load_config
123from ansible.module_utils.network.ios.ios import ios_argument_spec, check_args
124from ansible.module_utils.network.common.utils import ComplexList
125
126_CONFIGURED_VRFS = None
127
128
129def has_vrf(module, vrf):
130    global _CONFIGURED_VRFS
131    if _CONFIGURED_VRFS is not None:
132        return vrf in _CONFIGURED_VRFS
133    config = get_config(module)
134    _CONFIGURED_VRFS = re.findall(r'vrf definition (\S+)', config)
135    return vrf in _CONFIGURED_VRFS
136
137
138def requires_vrf(module, vrf):
139    if not has_vrf(module, vrf):
140        module.fail_json(msg='vrf %s is not configured' % vrf)
141
142
143def diff_list(want, have):
144    adds = [w for w in want if w not in have]
145    removes = [h for h in have if h not in want]
146    return (adds, removes)
147
148
149def map_obj_to_commands(want, have, module):
150    commands = list()
151    state = module.params['state']
152
153    def needs_update(x):
154        return want.get(x) is not None and (want.get(x) != have.get(x))
155
156    if state == 'absent':
157        if have['hostname'] != 'Router':
158            commands.append('no hostname')
159
160        if have['lookup_source']:
161            commands.append('no ip domain lookup source-interface %s' % have['lookup_source'])
162
163        if have['lookup_enabled'] is False:
164            commands.append('ip domain lookup')
165
166        vrfs = set()
167        for item in have['domain_name']:
168            if item['vrf'] and item['vrf'] not in vrfs:
169                vrfs.add(item['vrf'])
170                commands.append('no ip domain name vrf %s' % item['vrf'])
171            elif None not in vrfs:
172                vrfs.add(None)
173                commands.append('no ip domain name')
174
175        vrfs = set()
176        for item in have['domain_search']:
177            if item['vrf'] and item['vrf'] not in vrfs:
178                vrfs.add(item['vrf'])
179                commands.append('no ip domain list vrf %s' % item['vrf'])
180            elif None not in vrfs:
181                vrfs.add(None)
182                commands.append('no ip domain list')
183
184        vrfs = set()
185        for item in have['name_servers']:
186            if item['vrf'] and item['vrf'] not in vrfs:
187                vrfs.add(item['vrf'])
188                commands.append('no ip name-server vrf %s' % item['vrf'])
189            elif None not in vrfs:
190                vrfs.add(None)
191                commands.append('no ip name-server')
192
193    elif state == 'present':
194        if needs_update('hostname'):
195            commands.append('hostname %s' % want['hostname'])
196
197        if needs_update('lookup_source'):
198            commands.append('ip domain lookup source-interface %s' % want['lookup_source'])
199
200        if needs_update('lookup_enabled'):
201            cmd = 'ip domain lookup'
202            if want['lookup_enabled'] is False:
203                cmd = 'no %s' % cmd
204            commands.append(cmd)
205
206        if want['domain_name']:
207            adds, removes = diff_list(want['domain_name'], have['domain_name'])
208            for item in removes:
209                if item['vrf']:
210                    commands.append('no ip domain name vrf %s %s' % (item['vrf'], item['name']))
211                else:
212                    commands.append('no ip domain name %s' % item['name'])
213            for item in adds:
214                if item['vrf']:
215                    requires_vrf(module, item['vrf'])
216                    commands.append('ip domain name vrf %s %s' % (item['vrf'], item['name']))
217                else:
218                    commands.append('ip domain name %s' % item['name'])
219
220        if want['domain_search']:
221            adds, removes = diff_list(want['domain_search'], have['domain_search'])
222            for item in removes:
223                if item['vrf']:
224                    commands.append('no ip domain list vrf %s %s' % (item['vrf'], item['name']))
225                else:
226                    commands.append('no ip domain list %s' % item['name'])
227            for item in adds:
228                if item['vrf']:
229                    requires_vrf(module, item['vrf'])
230                    commands.append('ip domain list vrf %s %s' % (item['vrf'], item['name']))
231                else:
232                    commands.append('ip domain list %s' % item['name'])
233
234        if want['name_servers']:
235            adds, removes = diff_list(want['name_servers'], have['name_servers'])
236            for item in removes:
237                if item['vrf']:
238                    commands.append('no ip name-server vrf %s %s' % (item['vrf'], item['server']))
239                else:
240                    commands.append('no ip name-server %s' % item['server'])
241            for item in adds:
242                if item['vrf']:
243                    requires_vrf(module, item['vrf'])
244                    commands.append('ip name-server vrf %s %s' % (item['vrf'], item['server']))
245                else:
246                    commands.append('ip name-server %s' % item['server'])
247
248    return commands
249
250
251def parse_hostname(config):
252    match = re.search(r'^hostname (\S+)', config, re.M)
253    return match.group(1)
254
255
256def parse_domain_name(config):
257    match = re.findall(r'^ip domain[- ]name (?:vrf (\S+) )*(\S+)', config, re.M)
258    matches = list()
259    for vrf, name in match:
260        if not vrf:
261            vrf = None
262        matches.append({'name': name, 'vrf': vrf})
263    return matches
264
265
266def parse_domain_search(config):
267    match = re.findall(r'^ip domain[- ]list (?:vrf (\S+) )*(\S+)', config, re.M)
268    matches = list()
269    for vrf, name in match:
270        if not vrf:
271            vrf = None
272        matches.append({'name': name, 'vrf': vrf})
273    return matches
274
275
276def parse_name_servers(config):
277    match = re.findall(r'^ip name-server (?:vrf (\S+) )*(.*)', config, re.M)
278    matches = list()
279    for vrf, servers in match:
280        if not vrf:
281            vrf = None
282        for server in servers.split():
283            matches.append({'server': server, 'vrf': vrf})
284    return matches
285
286
287def parse_lookup_source(config):
288    match = re.search(r'ip domain[- ]lookup source-interface (\S+)', config, re.M)
289    if match:
290        return match.group(1)
291
292
293def map_config_to_obj(module):
294    config = get_config(module)
295    return {
296        'hostname': parse_hostname(config),
297        'domain_name': parse_domain_name(config),
298        'domain_search': parse_domain_search(config),
299        'lookup_source': parse_lookup_source(config),
300        'lookup_enabled': 'no ip domain lookup' not in config and 'no ip domain-lookup' not in config,
301        'name_servers': parse_name_servers(config)
302    }
303
304
305def map_params_to_obj(module):
306    obj = {
307        'hostname': module.params['hostname'],
308        'lookup_source': module.params['lookup_source'],
309        'lookup_enabled': module.params['lookup_enabled'],
310    }
311
312    domain_name = ComplexList(dict(
313        name=dict(key=True),
314        vrf=dict()
315    ), module)
316
317    domain_search = ComplexList(dict(
318        name=dict(key=True),
319        vrf=dict()
320    ), module)
321
322    name_servers = ComplexList(dict(
323        server=dict(key=True),
324        vrf=dict()
325    ), module)
326
327    for arg, cast in [('domain_name', domain_name),
328                      ('domain_search', domain_search),
329                      ('name_servers', name_servers)]:
330
331        if module.params[arg]:
332            obj[arg] = cast(module.params[arg])
333        else:
334            obj[arg] = None
335
336    return obj
337
338
339def main():
340    """ Main entry point for Ansible module execution
341    """
342    argument_spec = dict(
343        hostname=dict(),
344
345        domain_name=dict(type='list'),
346        domain_search=dict(type='list'),
347        name_servers=dict(type='list'),
348
349        lookup_source=dict(),
350        lookup_enabled=dict(type='bool'),
351
352        state=dict(choices=['present', 'absent'], default='present')
353    )
354
355    argument_spec.update(ios_argument_spec)
356
357    module = AnsibleModule(argument_spec=argument_spec,
358                           supports_check_mode=True)
359
360    result = {'changed': False}
361
362    warnings = list()
363    check_args(module, warnings)
364    result['warnings'] = warnings
365
366    want = map_params_to_obj(module)
367    have = map_config_to_obj(module)
368
369    commands = map_obj_to_commands(want, have, module)
370    result['commands'] = commands
371
372    if commands:
373        if not module.check_mode:
374            load_config(module, commands)
375        result['changed'] = True
376
377    module.exit_json(**result)
378
379
380if __name__ == "__main__":
381    main()
382