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