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