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