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_l2_interface
16version_added: "2.5"
17author: "Samer Deeb (@samerd)"
18short_description: Manage Layer-2 interface on Mellanox ONYX network devices
19description:
20  - This module provides declarative management of Layer-2 interface
21    on Mellanox ONYX network devices.
22options:
23  name:
24    description:
25      - Name of the interface.
26  aggregate:
27    description:
28      - List of Layer-2 interface definitions.
29  mode:
30    description:
31      - Mode in which interface needs to be configured.
32    default: access
33    choices: ['access', 'trunk', 'hybrid']
34  access_vlan:
35    description:
36      - Configure given VLAN in access port.
37  trunk_allowed_vlans:
38    description:
39      - List of allowed VLANs in a given trunk port.
40  state:
41    description:
42      - State of the Layer-2 Interface configuration.
43    default: present
44    choices: ['present', 'absent']
45"""
46
47EXAMPLES = """
48- name: configure Layer-2 interface
49  onyx_l2_interface:
50    name: Eth1/1
51    mode: access
52    access_vlan: 30
53- name: remove Layer-2 interface configuration
54  onyx_l2_interface:
55    name: Eth1/1
56    state: absent
57"""
58
59RETURN = """
60commands:
61  description: The list of configuration mode commands to send to the device
62  returned: always.
63  type: list
64  sample:
65    - interface ethernet 1/1
66    - switchport mode access
67    - switchport access vlan 30
68"""
69from copy import deepcopy
70import re
71
72from ansible.module_utils.basic import AnsibleModule
73from ansible.module_utils.six import iteritems
74from ansible.module_utils.network.common.utils import remove_default_spec
75
76from ansible.module_utils.network.onyx.onyx import BaseOnyxModule
77from ansible.module_utils.network.onyx.onyx import get_interfaces_config
78
79
80class OnyxL2InterfaceModule(BaseOnyxModule):
81    IFNAME_REGEX = re.compile(r"^.*(Eth\d+\/\d+|Mpo\d+|Po\d+)")
82
83    @classmethod
84    def _get_element_spec(cls):
85        return dict(
86            name=dict(),
87            access_vlan=dict(type='int'),
88            trunk_allowed_vlans=dict(type='list', elements='int'),
89            state=dict(default='present',
90                       choices=['present', 'absent']),
91            mode=dict(default='access',
92                      choices=['access', 'hybrid', 'trunk']),
93        )
94
95    @classmethod
96    def _get_aggregate_spec(cls, element_spec):
97        aggregate_spec = deepcopy(element_spec)
98        aggregate_spec['name'] = dict(required=True)
99
100        # remove default in aggregate spec, to handle common arguments
101        remove_default_spec(aggregate_spec)
102        return aggregate_spec
103
104    def init_module(self):
105        """ module initialization
106        """
107        element_spec = self._get_element_spec()
108        aggregate_spec = self._get_aggregate_spec(element_spec)
109        argument_spec = dict(
110            aggregate=dict(type='list', elements='dict',
111                           options=aggregate_spec),
112        )
113        argument_spec.update(element_spec)
114        required_one_of = [['name', 'aggregate']]
115        mutually_exclusive = [['name', 'aggregate']]
116        self._module = AnsibleModule(
117            argument_spec=argument_spec,
118            required_one_of=required_one_of,
119            mutually_exclusive=mutually_exclusive,
120            supports_check_mode=True)
121
122    def get_required_config(self):
123        self._required_config = list()
124        module_params = self._module.params
125        aggregate = module_params.get('aggregate')
126        if aggregate:
127            for item in aggregate:
128                for key in item:
129                    if item.get(key) is None:
130                        item[key] = module_params[key]
131                self.validate_param_values(item, item)
132                req_item = item.copy()
133                self._required_config.append(req_item)
134        else:
135            params = {
136                'name': module_params['name'],
137                'access_vlan': module_params['access_vlan'],
138                'trunk_allowed_vlans': module_params['trunk_allowed_vlans'],
139                'mode': module_params['mode'],
140                'state': module_params['state'],
141            }
142            self.validate_param_values(params)
143            self._required_config.append(params)
144
145    def validate_access_vlan(self, value):
146        if value and not 1 <= int(value) <= 4094:
147            self._module.fail_json(msg='vlan id must be between 1 and 4094')
148
149    @classmethod
150    def get_allowed_vlans(cls, if_data):
151        allowed_vlans = cls.get_config_attr(if_data, 'Allowed vlans')
152        interface_allwoed_vlans = []
153        if allowed_vlans:
154            vlans = [x.strip() for x in allowed_vlans.split(',')]
155            for vlan in vlans:
156                if '-' not in vlan:
157                    interface_allwoed_vlans.append(int(vlan))
158                else:
159                    vlan_range = vlan.split("-")
160                    min_number = int(vlan_range[0].strip())
161                    max_number = int(vlan_range[1].strip())
162                    vlan_list = range(min_number, max_number + 1)
163                    interface_allwoed_vlans.extend(vlan_list)
164        return interface_allwoed_vlans
165
166    @classmethod
167    def get_access_vlan(cls, if_data):
168        access_vlan = cls.get_config_attr(if_data, 'Access vlan')
169        if access_vlan:
170            try:
171                return int(access_vlan)
172            except ValueError:
173                return None
174
175    def _create_switchport_data(self, if_name, if_data):
176        if self._os_version >= self.ONYX_API_VERSION:
177            if_data = if_data[0]
178
179        return {
180            'name': if_name,
181            'mode': self.get_config_attr(if_data, 'Mode'),
182            'access_vlan': self.get_access_vlan(if_data),
183            'trunk_allowed_vlans': self.get_allowed_vlans(if_data)
184        }
185
186    def _get_switchport_config(self):
187        return get_interfaces_config(self._module, 'switchport')
188
189    def load_current_config(self):
190        # called in base class in run function
191        self._os_version = self._get_os_version()
192        self._current_config = dict()
193        switchports_config = self._get_switchport_config()
194        if not switchports_config:
195            return
196        for if_name, if_data in iteritems(switchports_config):
197            self._current_config[if_name] = \
198                self._create_switchport_data(if_name, if_data)
199
200    def _get_switchport_command_name(self, if_name):
201        if if_name.startswith('Eth'):
202            return if_name.replace("Eth", "ethernet ")
203        if if_name.startswith('Po'):
204            return if_name.replace("Po", "port-channel ")
205        if if_name.startswith('Mpo'):
206            return if_name.replace("Mpo", "mlag-port-channel ")
207        self._module.fail_json(
208            msg='invalid interface name: %s' % if_name)
209
210    def _add_interface_commands(self, if_name, commands):
211        if_cmd_name = self._get_switchport_command_name(if_name)
212        self._commands.append("interface %s" % if_cmd_name)
213        self._commands.extend(commands)
214        self._commands.append('exit')
215
216    def _generate_no_switchport_commands(self, if_name):
217        commands = ['no switchport force']
218        self._add_interface_commands(if_name, commands)
219
220    def _generate_switchport_commands(self, if_name, req_conf):
221        commands = []
222        curr_conf = self._current_config.get(if_name, {})
223        curr_mode = curr_conf.get('mode')
224        req_mode = req_conf.get('mode')
225        if req_mode != curr_mode:
226            commands.append('switchport mode %s' % req_mode)
227        curr_access_vlan = curr_conf.get('access_vlan')
228        req_access_vlan = req_conf.get('access_vlan')
229        if curr_access_vlan != req_access_vlan and req_access_vlan:
230            commands.append('switchport access vlan %s' % req_access_vlan)
231        curr_trunk_vlans = curr_conf.get('trunk_allowed_vlans') or set()
232        if curr_trunk_vlans:
233            curr_trunk_vlans = set(curr_trunk_vlans)
234        req_trunk_vlans = req_conf.get('trunk_allowed_vlans') or set()
235        if req_trunk_vlans:
236            req_trunk_vlans = set(req_trunk_vlans)
237        if req_mode != 'access' and curr_trunk_vlans != req_trunk_vlans:
238            added_vlans = req_trunk_vlans - curr_trunk_vlans
239            for vlan_id in added_vlans:
240                commands.append('switchport %s allowed-vlan add %s' %
241                                (req_mode, vlan_id))
242            removed_vlans = curr_trunk_vlans - req_trunk_vlans
243            for vlan_id in removed_vlans:
244                commands.append('switchport %s allowed-vlan remove %s' %
245                                (req_mode, vlan_id))
246
247        if commands:
248            self._add_interface_commands(if_name, commands)
249
250    def generate_commands(self):
251        for req_conf in self._required_config:
252            state = req_conf['state']
253            if_name = req_conf['name']
254            if state == 'absent':
255                if if_name in self._current_config:
256                    self._generate_no_switchport_commands(if_name)
257            else:
258                self._generate_switchport_commands(if_name, req_conf)
259
260    def _generate_vlan_commands(self, vlan_id, req_conf):
261        curr_vlan = self._current_config.get(vlan_id, {})
262        if not curr_vlan:
263            cmd = "vlan " + vlan_id
264            self._commands.append("vlan %s" % vlan_id)
265            self._commands.append("exit")
266        vlan_name = req_conf['vlan_name']
267        if vlan_name:
268            if vlan_name != curr_vlan.get('vlan_name'):
269                self._commands.append("vlan %s name %s" % (vlan_id, vlan_name))
270        curr_members = set(curr_vlan.get('interfaces', []))
271        req_members = req_conf['interfaces']
272        mode = req_conf['mode']
273        for member in req_members:
274            if member in curr_members:
275                continue
276            if_name = self.get_switchport_command_name(member)
277            cmd = "interface %s switchport mode %s" % (if_name, mode)
278            self._commands.append(cmd)
279            cmd = "interface %s switchport %s allowed-vlan add %s" % (
280                if_name, mode, vlan_id)
281            self._commands.append(cmd)
282        req_members = set(req_members)
283        for member in curr_members:
284            if member in req_members:
285                continue
286            if_name = self.get_switchport_command_name(member)
287            cmd = "interface %s switchport %s allowed-vlan remove %s" % (
288                if_name, mode, vlan_id)
289            self._commands.append(cmd)
290
291
292def main():
293    """ main entry point for module execution
294    """
295    OnyxL2InterfaceModule.main()
296
297
298if __name__ == '__main__':
299    main()
300