1#!/usr/bin/python
2#
3# (c) 2015 Peter Sprygada, <psprygada@ansible.com>
4# Copyright (c) 2017 Dell Inc.
5#
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11
12ANSIBLE_METADATA = {'metadata_version': '1.1',
13                    'status': ['preview'],
14                    'supported_by': 'community'}
15
16
17DOCUMENTATION = """
18---
19module: dellos10_config
20version_added: "2.2"
21author: "Senthil Kumar Ganesan (@skg-net)"
22short_description: Manage Dell EMC Networking OS10 configuration sections
23description:
24  - OS10 configurations use a simple block indent file syntax
25    for segmenting configuration into sections.  This module provides
26    an implementation for working with OS10 configuration sections in
27    a deterministic way.
28extends_documentation_fragment: dellos10
29options:
30  lines:
31    description:
32      - The ordered set of commands that should be configured in the
33        section.  The commands must be the exact same commands as found
34        in the device running-config. Be sure to note the configuration
35        command syntax as some commands are automatically modified by the
36        device config parser. This argument is mutually exclusive with I(src).
37    aliases: ['commands']
38  parents:
39    description:
40      - The ordered set of parents that uniquely identify the section or hierarchy
41        the commands should be checked against.  If the parents argument
42        is omitted, the commands are checked against the set of top
43        level or global commands.
44  src:
45    description:
46      - Specifies the source path to the file that contains the configuration
47        or configuration template to load.  The path to the source file can
48        either be the full path on the Ansible control host or a relative
49        path from the playbook or role root directory. This argument is
50        mutually exclusive with I(lines).
51  before:
52    description:
53      - The ordered set of commands to push on to the command stack if
54        a change needs to be made.  This allows the playbook designer
55        the opportunity to perform configuration commands prior to pushing
56        any changes without affecting how the set of commands are matched
57        against the system.
58  after:
59    description:
60      - The ordered set of commands to append to the end of the command
61        stack if a change needs to be made.  Just like with I(before) this
62        allows the playbook designer to append a set of commands to be
63        executed after the command set.
64  match:
65    description:
66      - Instructs the module on the way to perform the matching of
67        the set of commands against the current device config.  If
68        match is set to I(line), commands are matched line by line.  If
69        match is set to I(strict), command lines are matched with respect
70        to position.  If match is set to I(exact), command lines
71        must be an equal match.  Finally, if match is set to I(none), the
72        module will not attempt to compare the source configuration with
73        the running configuration on the remote device.
74    default: line
75    choices: ['line', 'strict', 'exact', 'none']
76  replace:
77    description:
78      - Instructs the module on the way to perform the configuration
79        on the device.  If the replace argument is set to I(line) then
80        the modified lines are pushed to the device in configuration
81        mode.  If the replace argument is set to I(block) then the entire
82        command block is pushed to the device in configuration mode if any
83        line is not correct.
84    default: line
85    choices: ['line', 'block']
86  update:
87    description:
88      - The I(update) argument controls how the configuration statements
89        are processed on the remote device.  Valid choices for the I(update)
90        argument are I(merge) and I(check).  When you set this argument to
91        I(merge), the configuration changes merge with the current
92        device running configuration.  When you set this argument to I(check)
93        the configuration updates are determined but not actually configured
94        on the remote device.
95    default: merge
96    choices: ['merge', 'check']
97  save:
98    description:
99      - The C(save) argument instructs the module to save the running-
100        config to the startup-config at the conclusion of the module
101        running.  If check mode is specified, this argument is ignored.
102    type: bool
103    default: 'no'
104  config:
105    description:
106      - The module, by default, will connect to the remote device and
107        retrieve the current running-config to use as a base for comparing
108        against the contents of source.  There are times when it is not
109        desirable to have the task get the current running-config for
110        every task in a playbook.  The I(config) argument allows the
111        implementer to pass in the configuration to use as the base
112        config for comparison.
113  backup:
114    description:
115      - This argument will cause the module to create a full backup of
116        the current C(running-config) from the remote device before any
117        changes are made. If the C(backup_options) value is not given,
118        the backup file is written to the C(backup) folder in the playbook
119        root directory. If the directory does not exist, it is created.
120    type: bool
121    default: 'no'
122  backup_options:
123    description:
124      - This is a dict object containing configurable options related to backup file path.
125        The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set
126        to I(no) this option will be silently ignored.
127    suboptions:
128      filename:
129        description:
130          - The filename to be used to store the backup configuration. If the the filename
131            is not given it will be generated based on the hostname, current time and date
132            in format defined by <hostname>_config.<current-date>@<current-time>
133      dir_path:
134        description:
135          - This option provides the path ending with directory name in which the backup
136            configuration file will be stored. If the directory does not exist it will be first
137            created and the filename is either the value of C(filename) or default filename
138            as described in C(filename) options description. If the path value is not given
139            in that case a I(backup) directory will be created in the current working directory
140            and backup configuration will be copied in C(filename) within I(backup) directory.
141        type: path
142    type: dict
143    version_added: "2.8"
144"""
145
146EXAMPLES = """
147- dellos10_config:
148    lines: ['hostname {{ inventory_hostname }}']
149
150- dellos10_config:
151    lines:
152      - 10 permit ip host 1.1.1.1 any log
153      - 20 permit ip host 2.2.2.2 any log
154      - 30 permit ip host 3.3.3.3 any log
155      - 40 permit ip host 4.4.4.4 any log
156      - 50 permit ip host 5.5.5.5 any log
157    parents: ['ip access-list test']
158    before: ['no ip access-list test']
159    match: exact
160
161- dellos10_config:
162    lines:
163      - 10 permit ip host 1.1.1.1 any log
164      - 20 permit ip host 2.2.2.2 any log
165      - 30 permit ip host 3.3.3.3 any log
166      - 40 permit ip host 4.4.4.4 any log
167    parents: ['ip access-list test']
168    before: ['no ip access-list test']
169    replace: block
170
171- dellos10_config:
172    lines: ['hostname {{ inventory_hostname }}']
173    backup: yes
174    backup_options:
175      filename: backup.cfg
176      dir_path: /home/user
177"""
178
179RETURN = """
180updates:
181  description: The set of commands that will be pushed to the remote device.
182  returned: always
183  type: list
184  sample: ['hostname foo', 'router bgp 1', 'router-id 1.1.1.1']
185commands:
186  description: The set of commands that will be pushed to the remote device
187  returned: always
188  type: list
189  sample: ['hostname foo', 'router bgp 1', 'router-id 1.1.1.1']
190saved:
191  description: Returns whether the configuration is saved to the startup
192               configuration or not.
193  returned: When not check_mode.
194  type: bool
195  sample: True
196backup_path:
197  description: The full path to the backup file
198  returned: when backup is yes
199  type: str
200  sample: /playbooks/ansible/backup/dellos10_config.2016-07-16@22:28:34
201"""
202from ansible.module_utils.basic import AnsibleModule
203from ansible.module_utils.network.dellos10.dellos10 import get_config, get_sublevel_config
204from ansible.module_utils.network.dellos10.dellos10 import dellos10_argument_spec, check_args
205from ansible.module_utils.network.dellos10.dellos10 import load_config, run_commands
206from ansible.module_utils.network.dellos10.dellos10 import WARNING_PROMPTS_RE
207from ansible.module_utils.network.common.config import NetworkConfig, dumps
208
209
210def get_candidate(module):
211    candidate = NetworkConfig(indent=1)
212    if module.params['src']:
213        candidate.load(module.params['src'])
214    elif module.params['lines']:
215        parents = module.params['parents'] or list()
216        commands = module.params['lines'][0]
217        if (isinstance(commands, dict)) and (isinstance((commands['command']), list)):
218            candidate.add(commands['command'], parents=parents)
219        elif (isinstance(commands, dict)) and (isinstance((commands['command']), str)):
220            candidate.add([commands['command']], parents=parents)
221        else:
222            candidate.add(module.params['lines'], parents=parents)
223    return candidate
224
225
226def get_running_config(module):
227    contents = module.params['config']
228    if not contents:
229        contents = get_config(module)
230    return contents
231
232
233def main():
234
235    backup_spec = dict(
236        filename=dict(),
237        dir_path=dict(type='path')
238    )
239    argument_spec = dict(
240        lines=dict(aliases=['commands'], type='list'),
241        parents=dict(type='list'),
242
243        src=dict(type='path'),
244
245        before=dict(type='list'),
246        after=dict(type='list'),
247
248        match=dict(default='line',
249                   choices=['line', 'strict', 'exact', 'none']),
250        replace=dict(default='line', choices=['line', 'block']),
251
252        update=dict(choices=['merge', 'check'], default='merge'),
253        save=dict(type='bool', default=False),
254        config=dict(),
255        backup=dict(type='bool', default=False),
256        backup_options=dict(type='dict', options=backup_spec)
257    )
258
259    argument_spec.update(dellos10_argument_spec)
260
261    mutually_exclusive = [('lines', 'src')]
262
263    module = AnsibleModule(argument_spec=argument_spec,
264                           mutually_exclusive=mutually_exclusive,
265                           supports_check_mode=True)
266
267    parents = module.params['parents'] or list()
268
269    match = module.params['match']
270    replace = module.params['replace']
271
272    warnings = list()
273    check_args(module, warnings)
274
275    result = dict(changed=False, saved=False, warnings=warnings)
276
277    if module.params['backup']:
278        if not module.check_mode:
279            result['__backup__'] = get_config(module)
280
281    commands = list()
282    candidate = get_candidate(module)
283
284    if any((module.params['lines'], module.params['src'])):
285        if match != 'none':
286            config = get_running_config(module)
287            if parents:
288                contents = get_sublevel_config(config, module)
289                config = NetworkConfig(contents=contents, indent=1)
290            else:
291                config = NetworkConfig(contents=config, indent=1)
292            configobjs = candidate.difference(config, match=match, replace=replace)
293        else:
294            configobjs = candidate.items
295
296        if configobjs:
297            commands = dumps(configobjs, 'commands')
298            if ((isinstance((module.params['lines']), list)) and
299                    (isinstance((module.params['lines'][0]), dict)) and
300                    (set(['prompt', 'answer']).issubset(module.params['lines'][0]))):
301
302                cmd = {'command': commands,
303                       'prompt': module.params['lines'][0]['prompt'],
304                       'answer': module.params['lines'][0]['answer']}
305                commands = [module.jsonify(cmd)]
306            else:
307                commands = commands.split('\n')
308
309            if module.params['before']:
310                commands[:0] = module.params['before']
311
312            if module.params['after']:
313                commands.extend(module.params['after'])
314
315            if not module.check_mode and module.params['update'] == 'merge':
316                load_config(module, commands)
317
318            result['changed'] = True
319            result['commands'] = commands
320            result['updates'] = commands
321
322    if module.params['save']:
323        result['changed'] = True
324        if not module.check_mode:
325            cmd = {r'command': 'copy running-config startup-config',
326                   r'prompt': r'\[confirm yes/no\]:\s?$', 'answer': 'yes'}
327            run_commands(module, [cmd])
328            result['saved'] = True
329        else:
330            module.warn('Skipping command `copy running-config startup-config`'
331                        'due to check_mode.  Configuration not copied to '
332                        'non-volatile storage')
333
334    module.exit_json(**result)
335
336
337if __name__ == '__main__':
338    main()
339