1#!/usr/bin/python
2#
3# Copyright: Ansible Team
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14DOCUMENTATION = """
15---
16module: aireos_config
17version_added: "2.4"
18author: "James Mighion (@jmighion)"
19short_description: Manage Cisco WLC configurations
20description:
21  - AireOS does not use a block indent file syntax, so there are no sections or parents.
22    This module provides an implementation for working with AireOS configurations in
23    a deterministic way.
24extends_documentation_fragment: aireos
25options:
26  lines:
27    description:
28      - The ordered set of commands that should be configured.
29        The commands must be the exact same commands as found
30        in the device run-config.  Be sure to note the configuration
31        command syntax as some commands are automatically modified by the
32        device config parser.
33    aliases: ['commands']
34  src:
35    description:
36      - Specifies the source path to the file that contains the configuration
37        or configuration template to load.  The path to the source file can
38        either be the full path on the Ansible control host or a relative
39        path from the playbook or role root directory.  This argument is mutually
40        exclusive with I(lines).
41  before:
42    description:
43      - The ordered set of commands to push on to the command stack if
44        a change needs to be made.  This allows the playbook designer
45        the opportunity to perform configuration commands prior to pushing
46        any changes without affecting how the set of commands are matched
47        against the system.
48  after:
49    description:
50      - The ordered set of commands to append to the end of the command
51        stack if a change needs to be made.  Just like with I(before) this
52        allows the playbook designer to append a set of commands to be
53        executed after the command set.
54  match:
55    description:
56      - Instructs the module on the way to perform the matching of
57        the set of commands against the current device config.  If
58        match is set to I(line), commands are matched line by line.
59        If match is set to I(none), the module will not attempt to
60        compare the source configuration with the running
61        configuration on the remote device.
62    default: line
63    choices: ['line', 'none']
64  backup:
65    description:
66      - This argument will cause the module to create a full backup of
67        the current C(running-config) from the remote device before any
68        changes are made. If the C(backup_options) value is not given,
69        the backup file is written to the C(backup) folder in the playbook
70        root directory.  If the directory does not exist, it is created.
71    type: bool
72    default: 'no'
73  running_config:
74    description:
75      - The module, by default, will connect to the remote device and
76        retrieve the current running-config to use as a base for comparing
77        against the contents of source.  There are times when it is not
78        desirable to have the task get the current running-config for
79        every task in a playbook.  The I(running_config) argument allows the
80        implementer to pass in the configuration to use as the base
81        config for comparison.
82    aliases: ['config']
83  save:
84    description:
85      - The C(save) argument instructs the module to save the
86        running-config to startup-config.  This operation is performed
87        after any changes are made to the current running config.  If
88        no changes are made, the configuration is still saved to the
89        startup config.  This option will always cause the module to
90        return changed. This argument is mutually exclusive with I(save_when).
91      - This option is deprecated as of Ansible 2.7, use C(save_when)
92    type: bool
93    default: 'no'
94  save_when:
95    description:
96      - When changes are made to the device running-configuration, the
97        changes are not copied to non-volatile storage by default.  Using
98        this argument will change that.  If the argument is set to
99        I(always), then the running-config will always be copied to the
100        startup-config and the module will always return as changed.
101        If the argument is set to I(never), the running-config will never
102        be copied to the startup-config.  If the argument is set to I(changed),
103        then the running-config will only be copied to the startup-config if
104        the task has made a change.
105    default: never
106    choices: ['always', 'never', 'changed']
107    version_added: "2.7"
108  diff_against:
109    description:
110      - When using the C(ansible-playbook --diff) command line argument
111        the module can generate diffs against different sources.
112      - When this option is configured as I(intended), the module will
113        return the diff of the running-config against the configuration
114        provided in the C(intended_config) argument.
115      - When this option is configured as I(running), the module will
116        return the before and after diff of the running-config with respect
117        to any changes made to the device configuration.
118    choices: ['intended', 'running']
119  diff_ignore_lines:
120    description:
121      - Use this argument to specify one or more lines that should be
122        ignored during the diff.  This is used for lines in the configuration
123        that are automatically updated by the system.  This argument takes
124        a list of regular expressions or exact line matches.
125  intended_config:
126    description:
127      - The C(intended_config) provides the master configuration that
128        the node should conform to and is used to check the final
129        running-config against.   This argument will not modify any settings
130        on the remote device and is strictly used to check the compliance
131        of the current device's configuration against.  When specifying this
132        argument, the task should also modify the C(diff_against) value and
133        set it to I(intended).
134  backup_options:
135    description:
136      - This is a dict object containing configurable options related to backup file path.
137        The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set
138        to I(no) this option will be silently ignored.
139    suboptions:
140      filename:
141        description:
142          - The filename to be used to store the backup configuration. If the the filename
143            is not given it will be generated based on the hostname, current time and date
144            in format defined by <hostname>_config.<current-date>@<current-time>
145      dir_path:
146        description:
147          - This option provides the path ending with directory name in which the backup
148            configuration file will be stored. If the directory does not exist it will be first
149            created and the filename is either the value of C(filename) or default filename
150            as described in C(filename) options description. If the path value is not given
151            in that case a I(backup) directory will be created in the current working directory
152            and backup configuration will be copied in C(filename) within I(backup) directory.
153        type: path
154    type: dict
155    version_added: "2.8"
156"""
157
158EXAMPLES = """
159- name: configure configuration
160  aireos_config:
161    lines: sysname testDevice
162
163- name: diff the running-config against a provided config
164  aireos_config:
165    diff_against: intended
166    intended: "{{ lookup('file', 'master.cfg') }}"
167
168- name: load new acl into device
169  aireos_config:
170    lines:
171      - acl create testACL
172      - acl rule protocol testACL 1 any
173      - acl rule direction testACL 3 in
174    before: acl delete testACL
175
176- name: configurable backup path
177  aireos_config:
178    backup: yes
179    lines: sysname testDevice
180    backup_options:
181      filename: backup.cfg
182      dir_path: /home/user
183"""
184
185RETURN = """
186commands:
187  description: The set of commands that will be pushed to the remote device
188  returned: always
189  type: list
190  sample: ['hostname foo', 'vlan 1', 'name default']
191updates:
192  description: The set of commands that will be pushed to the remote device
193  returned: always
194  type: list
195  sample: ['hostname foo', 'vlan 1', 'name default']
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/aireos_config.2016-07-16@22:28:34
201"""
202from ansible.module_utils.network.aireos.aireos import run_commands, get_config, load_config
203from ansible.module_utils.network.aireos.aireos import aireos_argument_spec
204from ansible.module_utils.network.aireos.aireos import check_args as aireos_check_args
205from ansible.module_utils.basic import AnsibleModule
206from ansible.module_utils.network.common.config import NetworkConfig, dumps
207
208
209def get_running_config(module, config=None):
210    contents = module.params['running_config']
211    if not contents:
212        if config:
213            contents = config
214        else:
215            contents = get_config(module)
216    return NetworkConfig(indent=1, contents=contents)
217
218
219def get_candidate(module):
220    candidate = NetworkConfig(indent=1)
221
222    if module.params['src']:
223        candidate.load(module.params['src'])
224    elif module.params['lines']:
225        candidate.add(module.params['lines'])
226    return candidate
227
228
229def save_config(module, result):
230    result['changed'] = True
231    if not module.check_mode:
232        command = {"command": "save config", "prompt": "Are you sure you want to save", "answer": "y"}
233        run_commands(module, command)
234    else:
235        module.warn('Skipping command `save config` due to check_mode.  Configuration not copied to '
236                    'non-volatile storage')
237
238
239def main():
240    """ main entry point for module execution
241    """
242    backup_spec = dict(
243        filename=dict(),
244        dir_path=dict(type='path')
245    )
246    argument_spec = dict(
247        src=dict(type='path'),
248
249        lines=dict(aliases=['commands'], type='list'),
250
251        before=dict(type='list'),
252        after=dict(type='list'),
253
254        match=dict(default='line', choices=['line', 'none']),
255
256        running_config=dict(aliases=['config']),
257        intended_config=dict(),
258
259        backup=dict(type='bool', default=False),
260        backup_options=dict(type='dict', options=backup_spec),
261
262        # save is deprecated as of 2.7, use save_when instead
263        save=dict(type='bool', default=False, removed_in_version='2.11'),
264        save_when=dict(choices=['always', 'never', 'changed'], default='never'),
265
266        diff_against=dict(choices=['running', 'intended']),
267        diff_ignore_lines=dict(type='list')
268    )
269
270    argument_spec.update(aireos_argument_spec)
271
272    mutually_exclusive = [('lines', 'src'),
273                          ('save', 'save_when')]
274
275    required_if = [('diff_against', 'intended', ['intended_config'])]
276
277    module = AnsibleModule(argument_spec=argument_spec,
278                           mutually_exclusive=mutually_exclusive,
279                           required_if=required_if,
280                           supports_check_mode=True)
281
282    warnings = list()
283    aireos_check_args(module, warnings)
284    result = {'changed': False, 'warnings': warnings}
285
286    config = None
287
288    if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'):
289        contents = get_config(module)
290        config = NetworkConfig(indent=1, contents=contents)
291        if module.params['backup']:
292            result['__backup__'] = contents
293
294    if any((module.params['src'], module.params['lines'])):
295        match = module.params['match']
296
297        candidate = get_candidate(module)
298
299        if match != 'none':
300            config = get_running_config(module, config)
301            configobjs = candidate.difference(config, match=match)
302        else:
303            configobjs = candidate.items
304
305        if configobjs:
306            commands = dumps(configobjs, 'commands').split('\n')
307
308            if module.params['before']:
309                commands[:0] = module.params['before']
310
311            if module.params['after']:
312                commands.extend(module.params['after'])
313
314            result['commands'] = commands
315            result['updates'] = commands
316
317            if not module.check_mode:
318                load_config(module, commands)
319
320            result['changed'] = True
321
322    diff_ignore_lines = module.params['diff_ignore_lines']
323
324    if module.params['save_when'] == 'always' or module.params['save']:
325        save_config(module, result)
326    elif module.params['save_when'] == 'changed' and result['changed']:
327        save_config(module, result)
328
329    if module._diff:
330        output = run_commands(module, 'show run-config commands')
331        contents = output[0]
332
333        # recreate the object in order to process diff_ignore_lines
334        running_config = NetworkConfig(indent=1, contents=contents, ignore_lines=diff_ignore_lines)
335
336        if module.params['diff_against'] == 'running':
337            if module.check_mode:
338                module.warn("unable to perform diff against running-config due to check mode")
339                contents = None
340            else:
341                contents = config.config_text
342        elif module.params['diff_against'] == 'intended':
343            contents = module.params['intended_config']
344
345        if contents is not None:
346            base_config = NetworkConfig(indent=1, contents=contents, ignore_lines=diff_ignore_lines)
347
348            if running_config.sha1 != base_config.sha1:
349                result.update({
350                    'changed': True,
351                    'diff': {'before': str(base_config), 'after': str(running_config)}
352                })
353
354    module.exit_json(**result)
355
356
357if __name__ == '__main__':
358    main()
359