1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# (c) 2017, Ansible by Red Hat, 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': 'network'}
14
15
16DOCUMENTATION = """
17---
18module: junos_netconf
19version_added: "2.1"
20author: "Peter Sprygada (@privateip)"
21short_description: Configures the Junos Netconf system service
22description:
23  - This module provides an abstraction that enables and configures
24    the netconf system service running on Junos devices.  This module
25    can be used to easily enable the Netconf API. Netconf provides
26    a programmatic interface for working with configuration and state
27    resources as defined in RFC 6242. If the C(netconf_port) is not
28    mentioned in the task by default netconf will be enabled on port 830
29    only.
30extends_documentation_fragment: junos
31options:
32  netconf_port:
33    description:
34      - This argument specifies the port the netconf service should
35        listen on for SSH connections.  The default port as defined
36        in RFC 6242 is 830.
37    required: false
38    default: 830
39    aliases: ['listens_on']
40    version_added: "2.2"
41  state:
42    description:
43      - Specifies the state of the C(junos_netconf) resource on
44        the remote device.  If the I(state) argument is set to
45        I(present) the netconf service will be configured.  If the
46        I(state) argument is set to I(absent) the netconf service
47        will be removed from the configuration.
48    required: false
49    default: present
50    choices: ['present', 'absent']
51notes:
52  - Tested against vSRX JUNOS version 15.1X49-D15.4, vqfx-10000 JUNOS Version 15.1X53-D60.4.
53  - Recommended connection is C(network_cli). See L(the Junos OS Platform Options,../network/user_guide/platform_junos.html).
54  - This module also works with C(local) connections for legacy playbooks.
55  - If C(netconf_port) value is not mentioned in task by default it will be enabled on port 830 only.
56    Although C(netconf_port) value can be from 1 through 65535, avoid configuring access on a port
57    that is normally assigned for another service. This practice avoids potential resource conflicts.
58"""
59
60EXAMPLES = """
61- name: enable netconf service on port 830
62  junos_netconf:
63    listens_on: 830
64    state: present
65
66- name: disable netconf service
67  junos_netconf:
68    state: absent
69"""
70
71RETURN = """
72commands:
73  description: Returns the command sent to the remote device
74  returned: when changed is True
75  type: str
76  sample: 'set system services netconf ssh port 830'
77"""
78import re
79
80from ansible.module_utils._text import to_text
81from ansible.module_utils.connection import ConnectionError
82from ansible.module_utils.basic import AnsibleModule
83from ansible.module_utils.network.junos.junos import junos_argument_spec, get_connection
84from ansible.module_utils.network.common.utils import to_list
85from ansible.module_utils.six import iteritems
86
87USE_PERSISTENT_CONNECTION = True
88
89
90def map_obj_to_commands(updates, module):
91    want, have = updates
92    commands = list()
93
94    if want['state'] == 'absent':
95        if have['state'] == 'present':
96            commands.append('delete system services netconf')
97    else:
98        if have['state'] == 'absent' or want['netconf_port'] != have.get('netconf_port'):
99            commands.append(
100                'set system services netconf ssh port %s' % want['netconf_port']
101            )
102
103    return commands
104
105
106def parse_port(config):
107    match = re.search(r'port (\d+)', config)
108    if match:
109        return int(match.group(1))
110
111
112def map_config_to_obj(module):
113    conn = get_connection(module)
114    out = conn.get(command='show configuration system services netconf')
115    if out is None:
116        module.fail_json(msg='unable to retrieve current config')
117    config = str(out).strip()
118
119    obj = {'state': 'absent'}
120    if 'ssh' in config:
121        obj.update({
122            'state': 'present',
123            'netconf_port': parse_port(config)
124        })
125    return obj
126
127
128def validate_netconf_port(value, module):
129    if not 1 <= value <= 65535:
130        module.fail_json(msg='netconf_port must be between 1 and 65535')
131
132
133def map_params_to_obj(module):
134    obj = {
135        'netconf_port': module.params['netconf_port'],
136        'state': module.params['state']
137    }
138
139    for key, value in iteritems(obj):
140        # validate the param value (if validator func exists)
141        validator = globals().get('validate_%s' % key)
142        if callable(validator):
143            validator(value, module)
144
145    return obj
146
147
148def load_config(module, config, commit=False):
149    conn = get_connection(module)
150    try:
151        resp = conn.edit_config(to_list(config) + ['top'], commit)
152    except ConnectionError as exc:
153        module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
154
155    diff = resp.get('diff', '')
156    return to_text(diff, errors='surrogate_then_replace').strip()
157
158
159def main():
160    """main entry point for module execution
161    """
162    argument_spec = dict(
163        netconf_port=dict(type='int', default=830, aliases=['listens_on']),
164        state=dict(default='present', choices=['present', 'absent']),
165    )
166
167    argument_spec.update(junos_argument_spec)
168
169    module = AnsibleModule(argument_spec=argument_spec,
170                           supports_check_mode=True)
171
172    warnings = list()
173    result = {'changed': False, 'warnings': warnings}
174
175    want = map_params_to_obj(module)
176    have = map_config_to_obj(module)
177
178    commands = map_obj_to_commands((want, have), module)
179    result['commands'] = commands
180
181    if commands:
182        commit = not module.check_mode
183        diff = load_config(module, commands, commit=commit)
184        if diff:
185            if module._diff:
186                result['diff'] = {'prepared': diff}
187            result['changed'] = True
188
189    module.exit_json(**result)
190
191
192if __name__ == '__main__':
193    main()
194