1# 2# -*- coding: utf-8 -*- 3# Copyright 2019 Red Hat 4# GNU General Public License v3.0+ 5# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6""" 7The sonic_bgp_neighbors_af 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 Facts 28from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( 29 to_request, 30 edit_config 31) 32from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( 33 update_states, 34 get_diff, 35) 36from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( 37 validate_bgps, 38 normalize_neighbors_interface_name, 39) 40from ansible.module_utils.connection import ConnectionError 41 42PATCH = 'patch' 43DELETE = 'delete' 44TEST_KEYS = [ 45 {'config': {'vrf_name': '', 'bgp_as': ''}}, 46 {'neighbors': {'neighbor': ''}}, 47 {'address_family': {'afi': '', 'safi': ''}}, 48 {'route_map': {'name': '', 'direction': ''}}, 49] 50 51 52class Bgp_neighbors_af(ConfigBase): 53 """ 54 The sonic_bgp_neighbors_af class 55 """ 56 57 gather_subset = [ 58 '!all', 59 '!min', 60 ] 61 62 gather_network_resources = [ 63 'bgp_neighbors_af', 64 ] 65 66 network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' 67 protocol_bgp_path = 'protocols/protocol=BGP,bgp/bgp' 68 neighbor_path = 'neighbors/neighbor' 69 afi_safi_path = 'afi-safis/afi-safi' 70 activate_path = "/config/enabled" 71 ref_client_path = "/config/openconfig-bgp-ext:route-reflector-client" 72 serv_client_path = "/config/openconfig-bgp-ext:route-server-client" 73 allowas_origin_path = "/openconfig-bgp-ext:allow-own-as/config/origin" 74 allowas_value_path = "/openconfig-bgp-ext:allow-own-as/config/as-count" 75 allowas_enabled_path = "/openconfig-bgp-ext:allow-own-as/config/enabled" 76 77 def __init__(self, module): 78 super(Bgp_neighbors_af, self).__init__(module) 79 80 def get_bgp_neighbors_af_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 bgp_neighbors_af_facts = facts['ansible_network_resources'].get('bgp_neighbors_af') 88 if not bgp_neighbors_af_facts: 89 bgp_neighbors_af_facts = [] 90 return bgp_neighbors_af_facts 91 92 def execute_module(self): 93 """ Execute the module 94 95 :rtype: A dictionary 96 :returns: The result from module execution 97 """ 98 result = {'changed': False} 99 warnings = list() 100 existing_bgp_neighbors_af_facts = self.get_bgp_neighbors_af_facts() 101 commands, requests = self.set_config(existing_bgp_neighbors_af_facts) 102 if commands and len(requests) > 0: 103 if not self._module.check_mode: 104 try: 105 edit_config(self._module, to_request(self._module, requests)) 106 except ConnectionError as exc: 107 self._module.fail_json(msg=str(exc), code=exc.code) 108 result['changed'] = True 109 result['commands'] = commands 110 111 changed_bgp_neighbors_af_facts = self.get_bgp_neighbors_af_facts() 112 113 result['before'] = existing_bgp_neighbors_af_facts 114 if result['changed']: 115 result['after'] = changed_bgp_neighbors_af_facts 116 117 result['warnings'] = warnings 118 return result 119 120 def set_config(self, existing_bgp_neighbors_af_facts): 121 """ Collect the configuration from the args passed to the module, 122 collect the current configuration (as a dict from facts) 123 124 :rtype: A list 125 :returns: the commands necessary to migrate the current configuration 126 to the desired configuration 127 """ 128 want = self._module.params['config'] 129 normalize_neighbors_interface_name(want, self._module) 130 have = existing_bgp_neighbors_af_facts 131 resp = self.set_state(want, have) 132 return to_list(resp) 133 134 def set_state(self, want, have): 135 """ Select the appropriate function based on the state provided 136 137 :param want: the desired configuration as a dictionary 138 :param have: the current configuration as a dictionary 139 :rtype: A list 140 :returns: the commands necessary to migrate the current configuration 141 to the desired configuration 142 """ 143 commands = [] 144 requests = [] 145 state = self._module.params['state'] 146 147 diff = get_diff(want, have, TEST_KEYS) 148 149 if state == 'overridden': 150 commands, requests = self._state_overridden(want, have, diff) 151 elif state == 'deleted': 152 commands, requests = self._state_deleted(want, have, diff) 153 elif state == 'merged': 154 commands, requests = self._state_merged(want, have, diff) 155 elif state == 'replaced': 156 commands, requests = self._state_replaced(want, have, diff) 157 return commands, requests 158 159 def _state_merged(self, want, have, diff): 160 """ The command generator when state is merged 161 162 :param want: the additive configuration as a dictionary 163 :param obj_in_have: the current configuration as a dictionary 164 :rtype: A list 165 :returns: the commands necessary to merge the provided into 166 the current configuration 167 """ 168 commands = diff 169 validate_bgps(self._module, want, have) 170 requests = self.get_modify_bgp_neighbors_af_requests(commands, have) 171 if commands and len(requests) > 0: 172 commands = update_states(commands, "merged") 173 else: 174 commands = [] 175 176 return commands, requests 177 178 def _state_deleted(self, want, have, diff): 179 """ The command generator when state is deleted 180 181 :param want: the objects from which the configuration should be removed 182 :param obj_in_have: the current configuration as a dictionary 183 :rtype: A list 184 :returns: the commands necessary to remove the current configuration 185 of the provided objects 186 """ 187 # if want is none, then delete all the bgp_neighbors_afs 188 is_delete_all = False 189 if not want: 190 commands = have 191 is_delete_all = True 192 else: 193 commands = want 194 195 requests = self.get_delete_bgp_neighbors_af_requests(commands, have, is_delete_all) 196 197 if commands and len(requests) > 0: 198 commands = update_states(commands, "deleted") 199 else: 200 commands = [] 201 202 return commands, requests 203 204 def set_val(self, cfg, var, src_key, des_key): 205 value = var.get(src_key, None) 206 if value is not None: 207 cfg[des_key] = value 208 209 def get_allowas_in(self, match, conf_neighbor_val, conf_afi, conf_safi): 210 mat_allowas_in = None 211 if match: 212 mat_neighbors = match.get('neighbors', None) 213 if mat_neighbors: 214 mat_neighbor = next((nei for nei in mat_neighbors if nei['neighbor'] == conf_neighbor_val), None) 215 if mat_neighbor: 216 mat_nei_addr_fams = mat_neighbor.get('address_family', []) 217 if mat_nei_addr_fams: 218 mat_nei_addr_fam = next((af for af in mat_nei_addr_fams if (af['afi'] == conf_afi and af['safi'] == conf_safi)), None) 219 if mat_nei_addr_fam: 220 mat_allowas_in = mat_nei_addr_fam.get('allowas_in', None) 221 return mat_allowas_in 222 223 def get_single_neighbors_af_modify_request(self, match, vrf_name, conf_neighbor_val, conf_neighbor): 224 requests = [] 225 conf_nei_addr_fams = conf_neighbor.get('address_family', []) 226 url = '%s=%s/%s/%s=%s/afi-safis' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, conf_neighbor_val) 227 payload = {} 228 afi_safis = [] 229 if not conf_nei_addr_fams: 230 return requests 231 232 for conf_nei_addr_fam in conf_nei_addr_fams: 233 afi_safi = {} 234 conf_afi = conf_nei_addr_fam.get('afi', None) 235 conf_safi = conf_nei_addr_fam.get('safi', None) 236 afi_safi_val = ("%s_%s" % (conf_afi, conf_safi)).upper() 237 del_url = '%s=%s/%s/%s=%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, conf_neighbor_val) 238 del_url += '%s=openconfig-bgp-types:%s' % (self.afi_safi_path, afi_safi_val) 239 240 afi_safi_cfg = {} 241 if conf_afi and conf_safi: 242 afi_safi_name = ("%s_%s" % (conf_afi, conf_safi)).upper() 243 afi_safi['afi-safi-name'] = afi_safi_name 244 afi_safi_cfg['afi-safi-name'] = afi_safi_name 245 246 self.set_val(afi_safi_cfg, conf_nei_addr_fam, 'activate', 'enabled') 247 self.set_val(afi_safi_cfg, conf_nei_addr_fam, 'route_reflector_client', 'openconfig-bgp-ext:route-reflector-client') 248 self.set_val(afi_safi_cfg, conf_nei_addr_fam, 'route_server_client', 'openconfig-bgp-ext:route-server-client') 249 250 if afi_safi_cfg: 251 afi_safi['config'] = afi_safi_cfg 252 253 policy_cfg = {} 254 conf_route_map = conf_nei_addr_fam.get('route_map', None) 255 if conf_route_map: 256 for route in conf_route_map: 257 policy_key = "import-policy" if "in" == route['direction'] else "export-policy" 258 route_name = route['name'] 259 policy_cfg[policy_key] = [route_name] 260 if policy_cfg: 261 afi_safi['apply-policy'] = {'config': policy_cfg} 262 263 allowas_in_cfg = {} 264 conf_allowas_in = conf_nei_addr_fam.get('allowas_in', None) 265 if conf_allowas_in: 266 mat_allowas_in = self.get_allowas_in(match, conf_neighbor_val, conf_afi, conf_safi) 267 origin = conf_allowas_in.get('origin', None) 268 if origin is not None: 269 if mat_allowas_in: 270 mat_value = mat_allowas_in.get('value', None) 271 if mat_value: 272 self.append_delete_request(requests, mat_value, mat_allowas_in, 'value', del_url, self.allowas_value_path) 273 allowas_in_cfg['origin'] = origin 274 else: 275 value = conf_allowas_in.get('value', None) 276 if value is not None: 277 if mat_allowas_in: 278 mat_origin = mat_allowas_in.get('origin', None) 279 if mat_origin: 280 self.append_delete_request(requests, mat_origin, mat_allowas_in, 'origin', del_url, self.allowas_origin_path) 281 allowas_in_cfg['as-count'] = value 282 if allowas_in_cfg: 283 allowas_in_cfg['enabled'] = True 284 afi_safi['openconfig-bgp-ext:allow-own-as'] = {'config': allowas_in_cfg} 285 286 if afi_safi: 287 afi_safis.append(afi_safi) 288 289 if afi_safis: 290 payload = {"openconfig-network-instance:afi-safis": {"afi-safi": afi_safis}} 291 requests.append({'path': url, 'method': PATCH, 'data': payload}) 292 293 return requests 294 295 def get_delete_neighbor_af_routemaps_requests(self, vrf_name, conf_neighbor_val, afi, safi, routes): 296 requests = [] 297 for route in routes: 298 afi_safi_name = ("%s_%s" % (afi, safi)).upper() 299 policy_type = "import-policy" if "in" == route['direction'] else "export-policy" 300 url = '%s=%s/%s/%s=%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, conf_neighbor_val) 301 url += ('%s=%s/apply-policy/config/%s' % (self.afi_safi_path, afi_safi_name, policy_type)) 302 requests.append({'path': url, 'method': DELETE}) 303 return requests 304 305 def get_all_neighbors_af_modify_requests(self, match, conf_neighbors, vrf_name): 306 requests = [] 307 for conf_neighbor in conf_neighbors: 308 conf_neighbor_val = conf_neighbor.get('neighbor', None) 309 if conf_neighbor_val: 310 requests.extend(self.get_single_neighbors_af_modify_request(match, vrf_name, conf_neighbor_val, conf_neighbor)) 311 return requests 312 313 def get_modify_requests(self, conf, match, vrf_name): 314 requests = [] 315 conf_neighbors = conf.get('neighbors', []) 316 mat_neighbors = [] 317 if match and match.get('neighbors', None): 318 mat_neighbors = match.get('neighbors') 319 320 if conf_neighbors: 321 for conf_neighbor in conf_neighbors: 322 conf_neighbor_val = conf_neighbor.get('neighbor', None) 323 if conf_neighbor_val is None: 324 continue 325 326 mat_neighbor = next((e_neighbor for e_neighbor in mat_neighbors if e_neighbor['neighbor'] == conf_neighbor_val), None) 327 if mat_neighbor is None: 328 continue 329 330 conf_nei_addr_fams = conf_neighbor.get('address_family', None) 331 mat_nei_addr_fams = mat_neighbor.get('address_family', None) 332 if conf_nei_addr_fams is None or mat_nei_addr_fams is None: 333 continue 334 335 for conf_nei_addr_fam in conf_nei_addr_fams: 336 afi = conf_nei_addr_fam.get('afi', None) 337 safi = conf_nei_addr_fam.get('safi', None) 338 if afi is None or safi is None: 339 continue 340 341 mat_nei_addr_fam = next((addr_fam for addr_fam in mat_nei_addr_fams if (addr_fam['afi'] == afi and addr_fam['safi'] == safi)), None) 342 if mat_nei_addr_fam is None: 343 continue 344 345 conf_route_map = conf_nei_addr_fam.get('route_map', None) 346 mat_route_map = mat_nei_addr_fam.get('route_map', None) 347 if conf_route_map is None or mat_route_map is None: 348 continue 349 350 del_routes = [] 351 for route in conf_route_map: 352 exist_route = next((e_route for e_route in mat_route_map if e_route['direction'] == route['direction']), None) 353 if exist_route: 354 del_routes.append(exist_route) 355 if del_routes: 356 requests.extend(self.get_delete_neighbor_af_routemaps_requests(vrf_name, conf_neighbor_val, afi, safi, del_routes)) 357 358 requests.extend(self.get_all_neighbors_af_modify_requests(match, conf_neighbors, vrf_name)) 359 return requests 360 361 def get_modify_bgp_neighbors_af_requests(self, commands, have): 362 requests = [] 363 if not commands: 364 return requests 365 366 # Create URL and payload 367 for conf in commands: 368 vrf_name = conf['vrf_name'] 369 as_val = conf['bgp_as'] 370 371 match = next((cfg for cfg in have if (cfg['vrf_name'] == vrf_name and (cfg['bgp_as'] == as_val))), None) 372 modify_reqs = self.get_modify_requests(conf, match, vrf_name) 373 if modify_reqs: 374 requests.extend(modify_reqs) 375 376 return requests 377 378 def append_delete_request(self, requests, cur_var, mat_var, key, url, path): 379 ret_value = False 380 request = None 381 if cur_var is not None and mat_var.get(key, None): 382 requests.append({'path': url + path, 'method': DELETE}) 383 ret_value = True 384 return ret_value 385 386 def process_delete_specific_params(self, vrf_name, conf_neighbor_val, conf_nei_addr_fam, conf_afi, conf_safi, matched_nei_addr_fams, url): 387 requests = [] 388 389 mat_nei_addr_fam = None 390 if matched_nei_addr_fams: 391 mat_nei_addr_fam = next((e_af for e_af in matched_nei_addr_fams if (e_af['afi'] == conf_afi and e_af['safi'] == conf_safi)), None) 392 393 if mat_nei_addr_fam: 394 conf_alllowas_in = conf_nei_addr_fam.get('allowas_in', None) 395 conf_activate = conf_nei_addr_fam.get('activate', None) 396 conf_route_map = conf_nei_addr_fam.get('route_map', None) 397 conf_route_reflector_client = conf_nei_addr_fam.get('route_reflector_client', None) 398 conf_route_server_client = conf_nei_addr_fam.get('route_server_client', None) 399 400 var_list = [conf_alllowas_in, conf_activate, conf_route_map, conf_route_reflector_client, conf_route_server_client] 401 if len(list(filter(lambda var: (var is None), var_list))) == len(var_list): 402 requests.append({'path': url, 'method': DELETE}) 403 else: 404 mat_route_map = mat_nei_addr_fam.get('route_map', None) 405 if conf_route_map and mat_route_map: 406 del_routes = [] 407 for route in conf_route_map: 408 if any([e_route for e_route in mat_route_map if route['direction'] == e_route['direction']]): 409 del_routes.append(route) 410 if del_routes: 411 requests.extend(self.get_delete_neighbor_af_routemaps_requests(vrf_name, conf_neighbor_val, conf_afi, conf_safi, del_routes)) 412 413 self.append_delete_request(requests, conf_activate, mat_nei_addr_fam, 'activate', url, self.activate_path) 414 self.append_delete_request(requests, conf_route_reflector_client, mat_nei_addr_fam, 'route_reflector_client', url, self.ref_client_path) 415 self.append_delete_request(requests, conf_route_server_client, mat_nei_addr_fam, 'route_server_client', url, self.serv_client_path) 416 417 mat_alllowas_in = mat_nei_addr_fam.get('allowas_in', None) 418 if conf_alllowas_in is not None and mat_alllowas_in: 419 origin = conf_alllowas_in.get('origin', None) 420 if origin is not None: 421 if self.append_delete_request(requests, origin, mat_alllowas_in, 'origin', url, self.allowas_origin_path): 422 self.append_delete_request(requests, True, {'enabled': True}, 'enabled', url, self.allowas_enabled_path) 423 else: 424 value = conf_alllowas_in.get('value', None) 425 if value is not None: 426 if self.append_delete_request(requests, value, mat_alllowas_in, 'value', url, self.allowas_value_path): 427 self.append_delete_request(requests, True, {'enabled': True}, 'enabled', url, self.allowas_enabled_path) 428 return requests 429 430 def process_neighbor_delete_address_families(self, vrf_name, conf_nei_addr_fams, matched_nei_addr_fams, neighbor_val, is_delete_all): 431 requests = [] 432 433 for conf_nei_addr_fam in conf_nei_addr_fams: 434 conf_afi = conf_nei_addr_fam.get('afi', None) 435 conf_safi = conf_nei_addr_fam.get('safi', None) 436 if not conf_afi or not conf_safi: 437 continue 438 afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() 439 url = '%s=%s/%s/%s=%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, neighbor_val) 440 url += '%s=openconfig-bgp-types:%s' % (self.afi_safi_path, afi_safi) 441 if is_delete_all: 442 requests.append({'path': url, 'method': DELETE}) 443 else: 444 requests.extend(self.process_delete_specific_params(vrf_name, neighbor_val, conf_nei_addr_fam, conf_afi, conf_safi, matched_nei_addr_fams, url)) 445 446 return requests 447 448 def get_delete_single_bgp_neighbors_af_request(self, conf, is_delete_all, match=None): 449 requests = [] 450 vrf_name = conf['vrf_name'] 451 conf_neighbors = conf.get('neighbors', []) 452 453 if match and not conf_neighbors: 454 conf_neighbors = match.get('neighbors', []) 455 if conf_neighbors: 456 conf_neighbors = [{'neighbor': nei['neighbor']} for nei in conf_neighbors] 457 458 if not conf_neighbors: 459 return requests 460 mat_neighbors = None 461 if match: 462 mat_neighbors = match.get('neighbors', []) 463 464 for conf_neighbor in conf_neighbors: 465 conf_neighbor_val = conf_neighbor.get('neighbor', None) 466 if not conf_neighbor_val: 467 continue 468 469 mat_neighbor = None 470 if mat_neighbors: 471 mat_neighbor = next((e_nei for e_nei in mat_neighbors if e_nei['neighbor'] == conf_neighbor_val), None) 472 473 conf_nei_addr_fams = conf_neighbor.get('address_family', None) 474 if mat_neighbor and not conf_nei_addr_fams: 475 conf_nei_addr_fams = mat_neighbor.get('address_family', None) 476 if conf_nei_addr_fams: 477 conf_nei_addr_fams = [{'afi': af['afi'], 'safi': af['safi']} for af in conf_nei_addr_fams] 478 479 if not conf_nei_addr_fams: 480 continue 481 482 mat_nei_addr_fams = None 483 if mat_neighbor: 484 mat_nei_addr_fams = mat_neighbor.get('address_family', None) 485 486 requests.extend(self.process_neighbor_delete_address_families(vrf_name, conf_nei_addr_fams, mat_nei_addr_fams, conf_neighbor_val, is_delete_all)) 487 488 return requests 489 490 def get_delete_bgp_neighbors_af_requests(self, commands, have, is_delete_all): 491 requests = [] 492 for cmd in commands: 493 vrf_name = cmd['vrf_name'] 494 as_val = cmd['bgp_as'] 495 match = None 496 if not is_delete_all: 497 match = next((have_cfg for have_cfg in have if have_cfg['vrf_name'] == vrf_name and have_cfg['bgp_as'] == as_val), None) 498 requests.extend(self.get_delete_single_bgp_neighbors_af_request(cmd, is_delete_all, match)) 499 return requests 500