1#!/usr/bin/python
2#
3# Copyright: Ansible Project
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
9ANSIBLE_METADATA = {'metadata_version': '1.1',
10                    'status': ['preview'],
11                    'supported_by': 'community'}
12
13DOCUMENTATION = """
14---
15module: onyx_config
16extends_documentation_fragment: onyx
17version_added: "2.5"
18author: "Alex Tabachnik (@atabachnik), Samer Deeb (@samerd)"
19short_description: Manage Mellanox ONYX configuration sections
20description:
21  - Mellanox ONYX configurations uses a simple block indent file syntax
22    for segmenting configuration into sections. This module provides
23    an implementation for working with ONYX configuration sections in
24    a deterministic way.
25options:
26  lines:
27    description:
28      - The ordered set of commands that should be configured in the
29        section.  The commands must be the exact same commands as found
30        in the device running-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  parents:
35    description:
36      - The ordered set of parents that uniquely identify the section
37        the commands should be checked against.  If the parents argument
38        is omitted, the commands are checked against the set of top
39        level or global commands.
40  src:
41    description:
42      - Specifies the source path to the file that contains the configuration
43        or configuration template to load.  The path to the source file can
44        either be the full path on the Ansible control host or a relative
45        path from the playbook or role root directory.  This argument is mutually
46        exclusive with I(lines), I(parents).
47  before:
48    description:
49      - The ordered set of commands to push on to the command stack if
50        a change needs to be made.  This allows the playbook designer
51        the opportunity to perform configuration commands prior to pushing
52        any changes without affecting how the set of commands are matched
53        against the system.
54  after:
55    description:
56      - The ordered set of commands to append to the end of the command
57        stack if a change needs to be made.  Just like with I(before) this
58        allows the playbook designer to append a set of commands to be
59        executed after the command set.
60  match:
61    description:
62      - Instructs the module on the way to perform the matching of
63        the set of commands against the current device config.  If
64        match is set to I(line), commands are matched line by line.  If
65        match is set to I(strict), command lines are matched with respect
66        to position.  If match is set to I(exact), command lines
67        must be an equal match.  Finally, if match is set to I(none), the
68        module will not attempt to compare the source configuration with
69        the running configuration on the remote device.
70    default: line
71    choices: ['line', 'strict', 'exact', 'none']
72  replace:
73    description:
74      - Instructs the module on the way to perform the configuration
75        on the device.  If the replace argument is set to I(line) then
76        the modified lines are pushed to the device in configuration
77        mode.  If the replace argument is set to I(block) then the entire
78        command block is pushed to the device in configuration mode if any
79        line is not correct
80    default: line
81    choices: ['line', 'block']
82  backup:
83    description:
84      - This argument will cause the module to create a full backup of
85        the current C(running-config) from the remote device before any
86        changes are made. If the C(backup_options) value is not given,
87        the backup file is written to the C(backup) folder in the playbook
88        root directory.  If the directory does not exist, it is created.
89    default: no
90    type: bool
91  config:
92    description:
93      - The C(config) argument allows the playbook designer to supply
94        the base configuration to be used to validate configuration
95        changes necessary.  If this argument is provided, the module
96        will not download the running-config from the remote node.
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    default: no
103    type: bool
104  backup_options:
105    description:
106      - This is a dict object containing configurable options related to backup file path.
107        The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set
108        to I(no) this option will be silently ignored.
109    suboptions:
110      filename:
111        description:
112          - The filename to be used to store the backup configuration. If the the filename
113            is not given it will be generated based on the hostname, current time and date
114            in format defined by <hostname>_config.<current-date>@<current-time>
115      dir_path:
116        description:
117          - This option provides the path ending with directory name in which the backup
118            configuration file will be stored. If the directory does not exist it will be first
119            created and the filename is either the value of C(filename) or default filename
120            as described in C(filename) options description. If the path value is not given
121            in that case a I(backup) directory will be created in the current working directory
122            and backup configuration will be copied in C(filename) within I(backup) directory.
123        type: path
124    type: dict
125    version_added: "2.8"
126"""
127
128EXAMPLES = """
129---
130- onyx_config:
131    lines:
132      - snmp-server community
133      - snmp-server host 10.2.2.2 traps version 2c
134"""
135
136RETURN = """
137updates:
138  description: The set of commands that will be pushed to the remote device
139  returned: always
140  type: list
141  sample: ['...', '...']
142backup_path:
143  description: The full path to the backup file
144  returned: when backup is yes
145  type: str
146  sample: /playbooks/ansible/backup/onyx_config.2016-07-16@22:28:34
147"""
148
149from ansible.module_utils.basic import AnsibleModule
150from ansible.module_utils.network.common.config import NetworkConfig, dumps
151
152from ansible.module_utils.network.onyx.onyx import get_config
153from ansible.module_utils.network.onyx.onyx import load_config
154from ansible.module_utils.network.onyx.onyx import run_commands
155
156
157def get_candidate(module):
158    candidate = NetworkConfig(indent=1)
159    if module.params['src']:
160        candidate.load(module.params['src'])
161    elif module.params['lines']:
162        parents = module.params['parents'] or list()
163        candidate.add(module.params['lines'], parents=parents)
164    return candidate
165
166
167def run(module, result):
168    match = module.params['match']
169    replace = module.params['replace']
170    path = module.params['parents']
171
172    candidate = get_candidate(module)
173    if match != 'none':
174        contents = module.params['config']
175        if not contents:
176            contents = get_config(module)
177        config = NetworkConfig(indent=1, contents=contents)
178        configobjs = candidate.difference(config, path=path, match=match,
179                                          replace=replace)
180
181    else:
182        configobjs = candidate.items
183
184    total_commands = []
185    if configobjs:
186        commands = dumps(configobjs, 'commands').split('\n')
187
188        if module.params['lines']:
189            if module.params['before']:
190                commands[:0] = module.params['before']
191
192            if module.params['after']:
193                commands.extend(module.params['after'])
194
195        total_commands.extend(commands)
196        result['updates'] = total_commands
197
198    if module.params['save']:
199        total_commands.append('configuration write')
200    if total_commands:
201        result['changed'] = True
202        if not module.check_mode:
203            load_config(module, total_commands)
204
205
206def main():
207    """ main entry point for module execution
208    """
209    backup_spec = dict(
210        filename=dict(),
211        dir_path=dict(type='path')
212    )
213    argument_spec = dict(
214        src=dict(type='path'),
215
216        lines=dict(aliases=['commands'], type='list'),
217        parents=dict(type='list'),
218
219        before=dict(type='list'),
220        after=dict(type='list'),
221
222        match=dict(default='line', choices=['line', 'strict', 'exact', 'none']),
223        replace=dict(default='line', choices=['line', 'block']),
224
225        config=dict(),
226
227        backup=dict(type='bool', default=False),
228        backup_options=dict(type='dict', options=backup_spec),
229        save=dict(type='bool', default=False),
230    )
231
232    mutually_exclusive = [('lines', 'src'),
233                          ('parents', 'src')]
234
235    required_if = [('match', 'strict', ['lines']),
236                   ('match', 'exact', ['lines']),
237                   ('replace', 'block', ['lines'])]
238
239    module = AnsibleModule(argument_spec=argument_spec,
240                           mutually_exclusive=mutually_exclusive,
241                           required_if=required_if,
242                           supports_check_mode=True)
243
244    result = {'changed': False}
245    if module.params['backup']:
246        result['__backup__'] = get_config(module)
247
248    run(module, result)
249
250    module.exit_json(**result)
251
252
253if __name__ == '__main__':
254    main()
255