1# 2# -*- coding: utf-8 -*- 3# © Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved 4# GNU General Public License v3.0+ 5# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6""" 7The sonic_interfaces class 8It is in this file where the current configuration (as dict) 9is compared to the provided configuration (as dict) and the command set 10necessary to bring the current configuration to it's desired end-state is 11created 12""" 13from __future__ import absolute_import, division, print_function 14__metaclass__ = type 15 16try: 17 from urllib import quote 18except ImportError: 19 from urllib.parse import quote 20 21from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( 22 ConfigBase, 23) 24from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( 25 to_list, 26) 27from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import ( 28 Facts, 29) 30from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( 31 to_request, 32 edit_config 33) 34from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.interfaces_util import ( 35 build_interfaces_create_request, 36) 37from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( 38 get_diff, 39 update_states, 40 normalize_interface_name 41) 42from ansible.module_utils._text import to_native 43from ansible.module_utils.connection import ConnectionError 44import traceback 45 46LIB_IMP_ERR = None 47ERR_MSG = None 48try: 49 import requests 50 HAS_LIB = True 51except Exception as e: 52 HAS_LIB = False 53 ERR_MSG = to_native(e) 54 LIB_IMP_ERR = traceback.format_exc() 55 56PATCH = 'patch' 57DELETE = 'delete' 58 59 60class Interfaces(ConfigBase): 61 """ 62 The sonic_interfaces class 63 """ 64 65 gather_subset = [ 66 '!all', 67 '!min', 68 ] 69 70 gather_network_resources = [ 71 'interfaces', 72 ] 73 74 params = ('description', 'mtu', 'enabled') 75 delete_flag = False 76 77 def __init__(self, module): 78 super(Interfaces, self).__init__(module) 79 80 def get_interfaces_facts(self): 81 """ Get the 'facts' (the current configuration) 82 83 :rtype: A dictionary 84 :returns: The current configuration as a dictionary 85 """ 86 facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources) 87 interfaces_facts = facts['ansible_network_resources'].get('interfaces') 88 if not interfaces_facts: 89 return [] 90 91 return interfaces_facts 92 93 def execute_module(self): 94 """ Execute the module 95 96 :rtype: A dictionary 97 :returns: The result from module execution 98 """ 99 result = {'changed': False} 100 warnings = list() 101 102 existing_interfaces_facts = self.get_interfaces_facts() 103 commands, requests = self.set_config(existing_interfaces_facts) 104 if commands and len(requests) > 0: 105 if not self._module.check_mode: 106 try: 107 edit_config(self._module, to_request(self._module, requests)) 108 except ConnectionError as exc: 109 self._module.fail_json(msg=str(exc), code=exc.code) 110 result['changed'] = True 111 result['commands'] = commands 112 113 changed_interfaces_facts = self.get_interfaces_facts() 114 115 result['before'] = existing_interfaces_facts 116 if result['changed']: 117 result['after'] = changed_interfaces_facts 118 119 result['warnings'] = warnings 120 return result 121 122 def set_config(self, existing_interfaces_facts): 123 """ Collect the configuration from the args passed to the module, 124 collect the current configuration (as a dict from facts) 125 126 :rtype: A list 127 :returns: the commands necessary to migrate the current configuration 128 to the desired configuration 129 """ 130 want = self._module.params['config'] 131 normalize_interface_name(want, self._module) 132 have = existing_interfaces_facts 133 134 resp = self.set_state(want, have) 135 return to_list(resp) 136 137 def set_state(self, want, have): 138 """ Select the appropriate function based on the state provided 139 140 :param want: the desired configuration as a dictionary 141 :param have: the current configuration as a dictionary 142 :rtype: A list 143 :returns: the commands necessary to migrate the current configuration 144 to the desired configuration 145 """ 146 state = self._module.params['state'] 147 # diff method works on dict, so creating temp dict 148 diff = get_diff(want, have) 149 # removing the dict in case diff found 150 151 if state == 'overridden': 152 have = [each_intf for each_intf in have if each_intf['name'].startswith('Ethernet')] 153 commands, requests = self._state_overridden(want, have, diff) 154 elif state == 'deleted': 155 commands, requests = self._state_deleted(want, have, diff) 156 elif state == 'merged': 157 commands, requests = self._state_merged(want, have, diff) 158 elif state == 'replaced': 159 commands, requests = self._state_replaced(want, have, diff) 160 161 return commands, requests 162 163 def _state_replaced(self, want, have, diff): 164 """ The command generator when state is replaced 165 166 :param want: the desired configuration as a dictionary 167 :param have: the current configuration as a dictionary 168 :param interface_type: interface type 169 :rtype: A list 170 :returns: the commands necessary to migrate the current configuration 171 to the desired configuration 172 """ 173 commands = self.filter_comands_to_change(diff, have) 174 requests = self.get_delete_interface_requests(commands, have) 175 requests.extend(self.get_modify_interface_requests(commands, have)) 176 if commands and len(requests) > 0: 177 commands = update_states(commands, "replaced") 178 else: 179 commands = [] 180 181 return commands, requests 182 183 def _state_overridden(self, want, have, diff): 184 """ The command generator when state is overridden 185 186 :param want: the desired configuration as a dictionary 187 :param obj_in_have: the current configuration as a dictionary 188 :rtype: A list 189 :returns: the commands necessary to migrate the current configuration 190 to the desired configuration 191 """ 192 commands = [] 193 commands_del = self.filter_comands_to_change(want, have) 194 requests = self.get_delete_interface_requests(commands_del, have) 195 del_req_count = len(requests) 196 if commands_del and del_req_count > 0: 197 commands_del = update_states(commands_del, "deleted") 198 commands.extend(commands_del) 199 200 commands_over = diff 201 requests.extend(self.get_modify_interface_requests(commands_over, have)) 202 if commands_over and len(requests) > del_req_count: 203 commands_over = update_states(commands_over, "overridden") 204 commands.extend(commands_over) 205 206 return commands, requests 207 208 def _state_merged(self, want, have, diff): 209 """ The command generator when state is merged 210 211 :param want: the additive configuration as a dictionary 212 :param obj_in_have: the current configuration as a dictionary 213 :rtype: A list 214 :returns: the commands necessary to merge the provided into 215 the current configuration 216 """ 217 commands = diff 218 requests = self.get_modify_interface_requests(commands, have) 219 if commands and len(requests) > 0: 220 commands = update_states(commands, "merged") 221 else: 222 commands = [] 223 224 return commands, requests 225 226 def _state_deleted(self, want, have, diff): 227 """ The command generator when state is deleted 228 229 :param want: the objects from which the configuration should be removed 230 :param obj_in_have: the current configuration as a dictionary 231 :param interface_type: interface type 232 :rtype: A list 233 :returns: the commands necessary to remove the current configuration 234 of the provided objects 235 """ 236 # if want is none, then delete all the interfaces 237 if not want: 238 commands = have 239 else: 240 commands = want 241 242 requests = self.get_delete_interface_requests(commands, have) 243 244 if commands and len(requests) > 0: 245 commands = update_states(commands, "deleted") 246 else: 247 commands = [] 248 249 return commands, requests 250 251 def filter_comands_to_delete(self, configs, have): 252 commands = [] 253 254 for conf in configs: 255 if self.is_this_delete_required(conf, have): 256 temp_conf = dict() 257 temp_conf['name'] = conf['name'] 258 temp_conf['description'] = '' 259 temp_conf['mtu'] = 9100 260 temp_conf['enabled'] = True 261 commands.append(temp_conf) 262 return commands 263 264 def filter_comands_to_change(self, configs, have): 265 commands = [] 266 if configs: 267 for conf in configs: 268 if self.is_this_change_required(conf, have): 269 commands.append(conf) 270 return commands 271 272 def get_modify_interface_requests(self, configs, have): 273 self.delete_flag = False 274 commands = self.filter_comands_to_change(configs, have) 275 276 return self.get_interface_requests(commands, have) 277 278 def get_delete_interface_requests(self, configs, have): 279 self.delete_flag = True 280 commands = self.filter_comands_to_delete(configs, have) 281 282 return self.get_interface_requests(commands, have) 283 284 def get_interface_requests(self, configs, have): 285 requests = [] 286 if not configs: 287 return requests 288 289 # Create URL and payload 290 for conf in configs: 291 name = conf["name"] 292 if self.delete_flag and name.startswith('Loopback'): 293 method = DELETE 294 url = 'data/openconfig-interfaces:interfaces/interface=%s' % quote(name, safe='') 295 request = {"path": url, "method": method} 296 else: 297 # Create Loopback in case not availble in have 298 if name.startswith('Loopback'): 299 have_conf = next((cfg for cfg in have if cfg['name'] == name), None) 300 if not have_conf: 301 loopback_create_request = build_interfaces_create_request(name) 302 requests.append(loopback_create_request) 303 method = PATCH 304 url = 'data/openconfig-interfaces:interfaces/interface=%s/config' % quote(name, safe='') 305 payload = self.build_create_payload(conf) 306 request = {"path": url, "method": method, "data": payload} 307 requests.append(request) 308 309 return requests 310 311 def is_this_delete_required(self, conf, have): 312 if conf['name'] == "eth0": 313 return False 314 intf = next((e_intf for e_intf in have if conf['name'] == e_intf['name']), None) 315 if intf: 316 if (intf['name'].startswith('Loopback') or not ((intf.get('description') is None or intf.get('description') == '') and 317 (intf.get('enabled') is None or intf.get('enabled') is True) and (intf.get('mtu') is None or intf.get('mtu') == 9100))): 318 return True 319 return False 320 321 def is_this_change_required(self, conf, have): 322 if conf['name'] == "eth0": 323 return False 324 ret_flag = False 325 intf = next((e_intf for e_intf in have if conf['name'] == e_intf['name']), None) 326 if intf: 327 # Check all parameter if any one is differen from existing 328 for param in self.params: 329 if conf.get(param) is not None and conf.get(param) != intf.get(param): 330 ret_flag = True 331 break 332 # if given interface is not present 333 else: 334 ret_flag = True 335 336 return ret_flag 337 338 def build_create_payload(self, conf): 339 temp_conf = dict() 340 temp_conf['name'] = conf['name'] 341 342 if not temp_conf['name'].startswith('Loopback'): 343 if conf.get('enabled') is not None: 344 if conf.get('enabled'): 345 temp_conf['enabled'] = True 346 else: 347 temp_conf['enabled'] = False 348 if conf.get('description') is not None: 349 temp_conf['description'] = conf['description'] 350 if conf.get('mtu') is not None: 351 temp_conf['mtu'] = conf['mtu'] 352 353 payload = {'openconfig-interfaces:config': temp_conf} 354 return payload 355