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 vyos_static_routes 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"""
13
14from __future__ import absolute_import, division, print_function
15
16__metaclass__ = type
17from copy import deepcopy
18from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import (
19    ConfigBase,
20)
21from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
22    to_list,
23    dict_diff,
24    remove_empties,
25)
26from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import (
27    Facts,
28)
29from ansible.module_utils.six import iteritems
30from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import (
31    get_route_type,
32    get_lst_diff_for_dicts,
33    get_lst_same_for_dicts,
34    dict_delete,
35)
36
37
38class Static_routes(ConfigBase):
39    """
40    The vyos_static_routes class
41    """
42
43    gather_subset = ["!all", "!min"]
44
45    gather_network_resources = ["static_routes"]
46
47    def __init__(self, module):
48        super(Static_routes, self).__init__(module)
49
50    def get_static_routes_facts(self, data=None):
51        """Get the 'facts' (the current configuration)
52
53        :rtype: A dictionary
54        :returns: The current configuration as a dictionary
55        """
56        facts, _warnings = Facts(self._module).get_facts(
57            self.gather_subset, self.gather_network_resources, data=data
58        )
59        static_routes_facts = facts["ansible_network_resources"].get(
60            "static_routes"
61        )
62        if not static_routes_facts:
63            return []
64        return static_routes_facts
65
66    def execute_module(self):
67        """Execute the module
68
69        :rtype: A dictionary
70        :returns: The result from module execution
71        """
72        result = {"changed": False}
73        warnings = list()
74        commands = list()
75
76        if self.state in self.ACTION_STATES:
77            existing_static_routes_facts = self.get_static_routes_facts()
78        else:
79            existing_static_routes_facts = []
80
81        if self.state in self.ACTION_STATES or self.state == "rendered":
82            commands.extend(self.set_config(existing_static_routes_facts))
83
84        if commands and self.state in self.ACTION_STATES:
85            if not self._module.check_mode:
86                self._connection.edit_config(commands)
87            result["changed"] = True
88
89        if self.state in self.ACTION_STATES:
90            result["commands"] = commands
91
92        if self.state in self.ACTION_STATES or self.state == "gathered":
93            changed_static_routes_facts = self.get_static_routes_facts()
94        elif self.state == "rendered":
95            result["rendered"] = commands
96        elif self.state == "parsed":
97            running_config = self._module.params["running_config"]
98            if not running_config:
99                self._module.fail_json(
100                    msg="value of running_config parameter must not be empty for state parsed"
101                )
102            result["parsed"] = self.get_static_routes_facts(
103                data=running_config
104            )
105        else:
106            changed_static_routes_facts = []
107
108        if self.state in self.ACTION_STATES:
109            result["before"] = existing_static_routes_facts
110            if result["changed"]:
111                result["after"] = changed_static_routes_facts
112        elif self.state == "gathered":
113            result["gathered"] = changed_static_routes_facts
114
115        result["warnings"] = warnings
116        return result
117
118    def set_config(self, existing_static_routes_facts):
119        """Collect the configuration from the args passed to the module,
120            collect the current configuration (as a dict from facts)
121
122        :rtype: A list
123        :returns: the commands necessary to migrate the current configuration
124                  to the desired configuration
125        """
126        want = self._module.params["config"]
127        have = existing_static_routes_facts
128        resp = self.set_state(want, have)
129        return to_list(resp)
130
131    def set_state(self, want, have):
132        """Select the appropriate function based on the state provided
133
134        :param want: the desired configuration as a dictionary
135        :param have: the current configuration as a dictionary
136        :rtype: A list
137        :returns: the commands necessary to migrate the current configuration
138                  to the desired configuration
139        """
140        commands = []
141        if (
142            self.state in ("merged", "replaced", "overridden", "rendered")
143            and not want
144        ):
145            self._module.fail_json(
146                msg="value of config parameter must not be empty for state {0}".format(
147                    self.state
148                )
149            )
150        if self.state == "overridden":
151            commands.extend(self._state_overridden(want=want, have=have))
152        elif self.state == "deleted":
153            commands.extend(self._state_deleted(want=want, have=have))
154        elif want:
155            routes = self._get_routes(want)
156            for r in routes:
157                h_item = self.search_route_in_have(have, r["dest"])
158                if self.state in ("merged", "rendered"):
159                    commands.extend(self._state_merged(want=r, have=h_item))
160                elif self.state == "replaced":
161                    commands.extend(self._state_replaced(want=r, have=h_item))
162        return commands
163
164    def search_route_in_have(self, have, want_dest):
165        """
166        This function  returns the route if its found in
167        have config.
168        :param have:
169        :param dest:
170        :return: the matched route
171        """
172        routes = self._get_routes(have)
173        for r in routes:
174            if r["dest"] == want_dest:
175                return r
176        return None
177
178    def _state_replaced(self, want, have):
179        """The command generator when state is replaced
180
181        :rtype: A list
182        :returns: the commands necessary to migrate the current configuration
183                  to the desired configuration
184        """
185        commands = []
186        if have:
187            for key, value in iteritems(want):
188                if value:
189                    if key == "next_hops":
190                        commands.extend(self._update_next_hop(want, have))
191                    elif key == "blackhole_config":
192                        commands.extend(
193                            self._update_blackhole(key, want, have)
194                        )
195        commands.extend(self._state_merged(want, have))
196        return commands
197
198    def _state_overridden(self, want, have):
199        """The command generator when state is overridden
200
201        :rtype: A list
202        :returns: the commands necessary to migrate the current configuration
203                  to the desired configuration
204        """
205        commands = []
206        routes = self._get_routes(have)
207        for r in routes:
208            route_in_want = self.search_route_in_have(want, r["dest"])
209            if not route_in_want:
210                commands.append(self._compute_command(r["dest"], remove=True))
211        routes = self._get_routes(want)
212        for r in routes:
213            route_in_have = self.search_route_in_have(have, r["dest"])
214            commands.extend(self._state_replaced(r, route_in_have))
215        return commands
216
217    def _state_merged(self, want, have, opr=True):
218        """The command generator when state is merged
219
220        :rtype: A list
221        :returns: the commands necessary to merge the provided into
222                  the current configuration
223        """
224        commands = []
225        if have:
226            commands.extend(self._render_updates(want, have))
227        else:
228            commands.extend(self._render_set_commands(want))
229        return commands
230
231    def _state_deleted(self, want, have):
232        """The command generator when state is deleted
233
234        :rtype: A list
235        :returns: the commands necessary to remove the current configuration
236                  of the provided objects
237        """
238        commands = []
239        if want:
240            routes = self._get_routes(want)
241            if not routes:
242                for w in want:
243                    af = w["address_families"]
244                    for item in af:
245                        if self.afi_in_have(have, item):
246                            commands.append(
247                                self._compute_command(
248                                    afi=item["afi"], remove=True
249                                )
250                            )
251        else:
252            routes = self._get_routes(have)
253            if self._is_ip_route_exist(routes):
254                commands.append(self._compute_command(afi="ipv4", remove=True))
255            if self._is_ip_route_exist(routes, "route6"):
256                commands.append(self._compute_command(afi="ipv6", remove=True))
257        return commands
258
259    def _render_set_commands(self, want):
260        """
261        This function returns the list of commands to add attributes which are
262        present in want
263        :param want:
264        :return: list of commands.
265        """
266        commands = []
267        have = {}
268        for key, value in iteritems(want):
269            if value:
270                if key == "dest":
271                    commands.append(self._compute_command(dest=want["dest"]))
272                elif key == "blackhole_config":
273                    commands.extend(self._add_blackhole(key, want, have))
274
275                elif key == "next_hops":
276                    commands.extend(self._add_next_hop(want, have))
277
278        return commands
279
280    def _add_blackhole(self, key, want, have):
281        """
282        This function gets the diff for blackhole config specific attributes
283        and form the commands for attributes which are present in want but not in have.
284        :param key:
285        :param want:
286        :param have:
287        :return: list of commands
288        """
289        commands = []
290        want_copy = deepcopy(remove_empties(want))
291        have_copy = deepcopy(remove_empties(have))
292
293        want_blackhole = want_copy.get(key) or {}
294        have_blackhole = have_copy.get(key) or {}
295
296        updates = dict_delete(want_blackhole, have_blackhole)
297        if updates:
298            for attrib, value in iteritems(updates):
299                if value:
300                    if attrib == "distance":
301                        commands.append(
302                            self._compute_command(
303                                dest=want["dest"],
304                                key="blackhole",
305                                attrib=attrib,
306                                remove=False,
307                                value=str(value),
308                            )
309                        )
310                    elif attrib == "type":
311                        commands.append(
312                            self._compute_command(
313                                dest=want["dest"], key="blackhole"
314                            )
315                        )
316        return commands
317
318    def _add_next_hop(self, want, have, opr=True):
319        """
320        This function gets the diff for next hop specific attributes
321        and form the commands to add attributes which are present in want but not in have.
322        :param want:
323        :param have:
324        :return: list of commands.
325        """
326        commands = []
327        want_copy = deepcopy(remove_empties(want))
328        have_copy = deepcopy(remove_empties(have))
329        if not opr:
330            diff_next_hops = get_lst_same_for_dicts(
331                want_copy, have_copy, "next_hops"
332            )
333        else:
334            diff_next_hops = get_lst_diff_for_dicts(
335                want_copy, have_copy, "next_hops"
336            )
337        if diff_next_hops:
338            for hop in diff_next_hops:
339                for element in hop:
340                    if element == "forward_router_address":
341                        commands.append(
342                            self._compute_command(
343                                dest=want["dest"],
344                                key="next-hop",
345                                value=hop[element],
346                                opr=opr,
347                            )
348                        )
349                    elif element == "enabled" and not hop[element]:
350                        commands.append(
351                            self._compute_command(
352                                dest=want["dest"],
353                                key="next-hop",
354                                attrib=hop["forward_router_address"],
355                                value="disable",
356                                opr=opr,
357                            )
358                        )
359                    elif element == "admin_distance":
360                        commands.append(
361                            self._compute_command(
362                                dest=want["dest"],
363                                key="next-hop",
364                                attrib=hop["forward_router_address"]
365                                + " "
366                                + "distance",
367                                value=str(hop[element]),
368                                opr=opr,
369                            )
370                        )
371                    elif element == "interface":
372                        commands.append(
373                            self._compute_command(
374                                dest=want["dest"],
375                                key="next-hop",
376                                attrib=hop["forward_router_address"]
377                                + " "
378                                + "next-hop-interface",
379                                value=hop[element],
380                                opr=opr,
381                            )
382                        )
383        return commands
384
385    def _update_blackhole(self, key, want, have):
386        """
387        This function gets the difference for blackhole dict and
388        form the commands to delete the attributes which are present in have but not in want.
389        :param want:
390        :param have:
391        :return: list of commands
392        :param key:
393        :param want:
394        :param have:
395        :return: list of commands
396        """
397        commands = []
398        want_copy = deepcopy(remove_empties(want))
399        have_copy = deepcopy(remove_empties(have))
400
401        want_blackhole = want_copy.get(key) or {}
402        have_blackhole = have_copy.get(key) or {}
403        updates = dict_delete(have_blackhole, want_blackhole)
404        if updates:
405            for attrib, value in iteritems(updates):
406                if value:
407                    if attrib == "distance":
408                        commands.append(
409                            self._compute_command(
410                                dest=want["dest"],
411                                key="blackhole",
412                                attrib=attrib,
413                                remove=True,
414                                value=str(value),
415                            )
416                        )
417                    elif (
418                        attrib == "type"
419                        and "distance" not in want_blackhole.keys()
420                    ):
421                        commands.append(
422                            self._compute_command(
423                                dest=want["dest"], key="blackhole", remove=True
424                            )
425                        )
426        return commands
427
428    def _update_next_hop(self, want, have, opr=True):
429        """
430        This function gets the difference for next_hops list and
431        form the commands to delete the attributes which are present in have but not in want.
432        :param want:
433        :param have:
434        :return: list of commands
435        """
436        commands = []
437
438        want_copy = deepcopy(remove_empties(want))
439        have_copy = deepcopy(remove_empties(have))
440
441        diff_next_hops = get_lst_diff_for_dicts(
442            have_copy, want_copy, "next_hops"
443        )
444        if diff_next_hops:
445            for hop in diff_next_hops:
446                for element in hop:
447                    if element == "forward_router_address":
448                        commands.append(
449                            self._compute_command(
450                                dest=want["dest"],
451                                key="next-hop",
452                                value=hop[element],
453                                remove=True,
454                            )
455                        )
456                    elif element == "enabled":
457                        commands.append(
458                            self._compute_command(
459                                dest=want["dest"],
460                                key="next-hop",
461                                attrib=hop["forward_router_address"],
462                                value="disable",
463                                remove=True,
464                            )
465                        )
466                    elif element == "admin_distance":
467                        commands.append(
468                            self._compute_command(
469                                dest=want["dest"],
470                                key="next-hop",
471                                attrib=hop["forward_router_address"]
472                                + " "
473                                + "distance",
474                                value=str(hop[element]),
475                                remove=True,
476                            )
477                        )
478                    elif element == "interface":
479                        commands.append(
480                            self._compute_command(
481                                dest=want["dest"],
482                                key="next-hop",
483                                attrib=hop["forward_router_address"]
484                                + " "
485                                + "next-hop-interface",
486                                value=hop[element],
487                                remove=True,
488                            )
489                        )
490        return commands
491
492    def _render_updates(self, want, have, opr=True):
493        """
494        This function takes the diff between want and have and
495        invokes the appropriate functions to create the commands
496        to update the attributes.
497        :param want:
498        :param have:
499        :return: list of commands
500        """
501        commands = []
502        want_nh = want.get("next_hops") or []
503        # delete static route operation per destination
504        if not opr and not want_nh:
505            commands.append(
506                self._compute_command(dest=want["dest"], remove=True)
507            )
508
509        else:
510            temp_have_next_hops = have.pop("next_hops", None)
511            temp_want_next_hops = want.pop("next_hops", None)
512            updates = dict_diff(have, want)
513            if temp_have_next_hops:
514                have["next_hops"] = temp_have_next_hops
515            if temp_want_next_hops:
516                want["next_hops"] = temp_want_next_hops
517            commands.extend(self._add_next_hop(want, have, opr=opr))
518
519            if opr and updates:
520                for key, value in iteritems(updates):
521                    if value:
522                        if key == "blackhole_config":
523                            commands.extend(
524                                self._add_blackhole(key, want, have)
525                            )
526        return commands
527
528    def _compute_command(
529        self,
530        dest=None,
531        key=None,
532        attrib=None,
533        value=None,
534        remove=False,
535        afi=None,
536        opr=True,
537    ):
538        """
539        This functions construct the required command based on the passed arguments.
540        :param dest:
541        :param key:
542        :param attrib:
543        :param value:
544        :param remove:
545        :return:  constructed command
546        """
547        if remove or not opr:
548            cmd = "delete protocols static " + self.get_route_type(dest, afi)
549        else:
550            cmd = "set protocols static " + self.get_route_type(dest, afi)
551        if dest:
552            cmd += " " + dest
553        if key:
554            cmd += " " + key
555        if attrib:
556            cmd += " " + attrib
557        if value:
558            cmd += " '" + value + "'"
559        return cmd
560
561    def afi_in_have(self, have, w_item):
562        """
563        This functions checks for the afi
564        list in have
565        :param have:
566        :param w_item:
567        :return:
568        """
569        if have:
570            for h in have:
571                af = h.get("address_families") or []
572            for item in af:
573                if w_item["afi"] == item["afi"]:
574                    return True
575        return False
576
577    def get_route_type(self, dest=None, afi=None):
578        """
579        This function returns the route type based on
580        destination ip address or afi
581        :param address:
582        :return:
583        """
584        if dest:
585            return get_route_type(dest)
586        elif afi == "ipv4":
587            return "route"
588        elif afi == "ipv6":
589            return "route6"
590
591    def _is_ip_route_exist(self, routes, type="route"):
592        """
593        This functions checks for the type of route.
594        :param routes:
595        :param type:
596        :return: True/False
597        """
598        for r in routes:
599            if type == self.get_route_type(r["dest"]):
600                return True
601        return False
602
603    def _get_routes(self, lst):
604        """
605        This function returns the list of routes
606        :param lst: list of address families
607        :return: list of routes
608        """
609        r_list = []
610        for item in lst:
611            af = item["address_families"]
612            for element in af:
613                routes = element.get("routes") or []
614                for r in routes:
615                    r_list.append(r)
616        return r_list
617