diff options
Diffstat (limited to 'plugins/module_utils')
14 files changed, 2643 insertions, 1 deletions
diff --git a/plugins/module_utils/network/vyos/argspec/firewall_rules/__init__.py b/plugins/module_utils/network/vyos/argspec/firewall_rules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/firewall_rules/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py new file mode 100644 index 00000000..a018cc0b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py @@ -0,0 +1,263 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The arg spec for the vyos_firewall_rules module +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class Firewall_rulesArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_firewall_rules module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "afi": { + "choices": ["ipv4", "ipv6"], + "required": True, + "type": "str", + }, + "rule_sets": { + "elements": "dict", + "options": { + "default_action": { + "choices": ["drop", "reject", "accept"], + "type": "str", + }, + "description": {"type": "str"}, + "enable_default_log": {"type": "bool"}, + "name": {"type": "str"}, + "rules": { + "elements": "dict", + "options": { + "action": { + "choices": [ + "drop", + "reject", + "accept", + "inspect", + ], + "type": "str", + }, + "description": {"type": "str"}, + "destination": { + "options": { + "address": {"type": "str"}, + "group": { + "options": { + "address_group": { + "type": "str" + }, + "network_group": { + "type": "str" + }, + "port_group": {"type": "str"}, + }, + "type": "dict", + }, + "port": {"type": "str"}, + }, + "type": "dict", + }, + "disabled": {"type": "bool"}, + "fragment": { + "choices": [ + "match-frag", + "match-non-frag", + ], + "type": "str", + }, + "icmp": { + "options": { + "code": {"type": "int"}, + "type": {"type": "int"}, + "type_name": { + "choices": [ + "any", + "echo-reply", + "destination-unreachable", + "network-unreachable", + "host-unreachable", + "protocol-unreachable", + "port-unreachable", + "fragmentation-needed", + "source-route-failed", + "network-unknown", + "host-unknown", + "network-prohibited", + "host-prohibited", + "TOS-network-unreachable", + "TOS-host-unreachable", + "communication-prohibited", + "host-precedence-violation", + "precedence-cutoff", + "source-quench", + "redirect", + "network-redirect", + "host-redirect", + "TOS-network-redirect", + "TOS-host-redirect", + "echo-request", + "router-advertisement", + "router-solicitation", + "time-exceeded", + "ttl-zero-during-transit", + "ttl-zero-during-reassembly", + "parameter-problem", + "ip-header-bad", + "required-option-missing", + "timestamp-request", + "timestamp-reply", + "address-mask-request", + "address-mask-reply", + "ping", + "pong", + "ttl-exceeded", + ], + "type": "str", + }, + }, + "type": "dict", + }, + "ipsec": { + "choices": ["match-ipsec", "match-none"], + "type": "str", + }, + "limit": { + "options": { + "burst": {"type": "int"}, + "rate": { + "options": { + "number": {"type": "int"}, + "unit": {"type": "str"}, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + "number": {"required": True, "type": "int"}, + "p2p": { + "elements": "dict", + "options": { + "application": { + "choices": [ + "all", + "applejuice", + "bittorrent", + "directconnect", + "edonkey", + "gnutella", + "kazaa", + ], + "type": "str", + } + }, + "type": "list", + }, + "protocol": {"type": "str"}, + "recent": { + "options": { + "count": {"type": "int"}, + "time": {"type": "int"}, + }, + "type": "dict", + }, + "source": { + "options": { + "address": {"type": "str"}, + "group": { + "options": { + "address_group": { + "type": "str" + }, + "network_group": { + "type": "str" + }, + "port_group": {"type": "str"}, + }, + "type": "dict", + }, + "mac_address": {"type": "str"}, + "port": {"type": "str"}, + }, + "type": "dict", + }, + "state": { + "options": { + "established": {"type": "bool"}, + "invalid": {"type": "bool"}, + "new": {"type": "bool"}, + "related": {"type": "bool"}, + }, + "type": "dict", + }, + "tcp": { + "options": {"flags": {"type": "str"}}, + "type": "dict", + }, + "time": { + "options": { + "monthdays": {"type": "str"}, + "startdate": {"type": "str"}, + "starttime": {"type": "str"}, + "stopdate": {"type": "str"}, + "stoptime": {"type": "str"}, + "utc": {"type": "bool"}, + "weekdays": {"type": "str"}, + }, + "type": "dict", + }, + }, + "type": "list", + }, + }, + "type": "list", + }, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/static_routes/__init__.py b/plugins/module_utils/network/vyos/argspec/static_routes/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/static_routes/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py new file mode 100644 index 00000000..8ecd955a --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py @@ -0,0 +1,99 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The arg spec for the vyos_static_routes module +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class Static_routesArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_static_routes module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "address_families": { + "elements": "dict", + "options": { + "afi": { + "choices": ["ipv4", "ipv6"], + "required": True, + "type": "str", + }, + "routes": { + "elements": "dict", + "options": { + "blackhole_config": { + "options": { + "distance": {"type": "int"}, + "type": {"type": "str"}, + }, + "type": "dict", + }, + "dest": {"required": True, "type": "str"}, + "next_hops": { + "elements": "dict", + "options": { + "admin_distance": {"type": "int"}, + "enabled": {"type": "bool"}, + "forward_router_address": { + "required": True, + "type": "str", + }, + "interface": {"type": "str"}, + }, + "type": "list", + }, + }, + "type": "list", + }, + }, + "type": "list", + } + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/config/firewall_rules/__init__.py b/plugins/module_utils/network/vyos/config/firewall_rules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/firewall_rules/__init__.py diff --git a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py new file mode 100644 index 00000000..e58593f4 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py @@ -0,0 +1,1034 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos_firewall_rules class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from copy import deepcopy +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( + ConfigBase, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, + remove_empties, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( + Facts, +) +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( + list_diff_want_only, +) + + +class Firewall_rules(ConfigBase): + """ + The vyos_firewall_rules class + """ + + gather_subset = [ + "!all", + "!min", + ] + + gather_network_resources = [ + "firewall_rules", + ] + + def __init__(self, module): + super(Firewall_rules, self).__init__(module) + + def get_firewall_rules_facts(self, data=None): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts( + self.gather_subset, self.gather_network_resources, data=data + ) + firewall_rules_facts = facts["ansible_network_resources"].get( + "firewall_rules" + ) + if not firewall_rules_facts: + return [] + return firewall_rules_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + warnings = list() + commands = list() + + if self.state in self.ACTION_STATES: + existing_firewall_rules_facts = self.get_firewall_rules_facts() + else: + existing_firewall_rules_facts = [] + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_firewall_rules_facts)) + + if commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + self._connection.edit_config(commands) + result["changed"] = True + + if self.state in self.ACTION_STATES: + result["commands"] = commands + + if self.state in self.ACTION_STATES or self.state == "gathered": + changed_firewall_rules_facts = self.get_firewall_rules_facts() + elif self.state == "rendered": + result["rendered"] = commands + elif self.state == "parsed": + running_config = self._module.params["running_config"] + if not running_config: + self._module.fail_json( + msg="value of running_config parameter must not be empty for state parsed" + ) + result["parsed"] = self.get_firewall_rules_facts( + data=running_config + ) + else: + changed_firewall_rules_facts = [] + + if self.state in self.ACTION_STATES: + result["before"] = existing_firewall_rules_facts + if result["changed"]: + result["after"] = changed_firewall_rules_facts + elif self.state == "gathered": + result["gathered"] = changed_firewall_rules_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_firewall_rules_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + want = self._module.params["config"] + have = existing_firewall_rules_facts + resp = self.set_state(want, have) + return to_list(resp) + + def set_state(self, w, h): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if ( + self.state in ("merged", "replaced", "overridden", "rendered") + and not w + ): + self._module.fail_json( + msg="value of config parameter must not be empty for state {0}".format( + self.state + ) + ) + if self.state == "overridden": + commands.extend(self._state_overridden(w, h)) + elif self.state == "deleted": + commands.extend(self._state_deleted(w, h)) + elif w: + if self.state == "merged" or self.state == "rendered": + commands.extend(self._state_merged(w, h)) + elif self.state == "replaced": + commands.extend(self._state_replaced(w, h)) + return commands + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if have: + for h in have: + r_sets = self._get_r_sets(h) + for rs in r_sets: + w = self.search_r_sets_in_have(want, rs["name"], "r_list") + commands.extend( + self._add_r_sets(h["afi"], rs, w, opr=False) + ) + commands.extend(self._state_merged(want, have)) + return commands + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if have: + for h in have: + r_sets = self._get_r_sets(h) + for rs in r_sets: + w = self.search_r_sets_in_have(want, rs["name"], "r_list") + if not w: + commands.append( + self._compute_command( + h["afi"], rs["name"], remove=True + ) + ) + else: + commands.extend( + self._add_r_sets(h["afi"], rs, w, opr=False) + ) + commands.extend(self._state_merged(want, have)) + return commands + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + for w in want: + r_sets = self._get_r_sets(w) + for rs in r_sets: + h = self.search_r_sets_in_have(have, rs["name"], "r_list") + commands.extend(self._add_r_sets(w["afi"], rs, h)) + return commands + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + if want: + for w in want: + r_sets = self._get_r_sets(w) + if r_sets: + for rs in r_sets: + h = self.search_r_sets_in_have( + have, rs["name"], "r_list" + ) + if h: + w_rules = rs.get("rules") or [] + h_rules = h.get("rules") or [] + if w_rules and h_rules: + for rule in w_rules: + if self.search_r_sets_in_have( + h_rules, rule["number"], "rules" + ): + commands.append( + self._add_r_base_attrib( + w["afi"], + rs["name"], + "number", + rule, + opr=False, + ) + ) + else: + commands.append( + self._compute_command( + w["afi"], h["name"], remove=True + ) + ) + elif have: + for h in have: + if h["afi"] == w["afi"]: + commands.append( + self._compute_command(w["afi"], remove=True) + ) + elif have: + for h in have: + r_sets = self._get_r_sets(h) + if r_sets: + commands.append( + self._compute_command(afi=h["afi"], remove=True) + ) + return commands + + def _add_r_sets(self, afi, want, have, opr=True): + """ + This function forms the set/delete commands based on the 'opr' type + for rule-sets attributes. + :param afi: address type. + :param want: desired config. + :param have: target config. + :param opr: True/False. + :return: generated commands list. + """ + commands = [] + l_set = ("description", "default_action", "enable_default_log") + h_rs = {} + h_rules = {} + w_rs = deepcopy(remove_empties(want)) + w_rules = w_rs.pop("rules", None) + if have: + h_rs = deepcopy(remove_empties(have)) + h_rules = h_rs.pop("rules", None) + if w_rs: + for key, val in iteritems(w_rs): + if ( + opr + and key in l_set + and not (h_rs and self._is_w_same(w_rs, h_rs, key)) + ): + if key == "enable_default_log": + if val and ( + not h_rs or key not in h_rs or not h_rs[key] + ): + commands.append( + self._add_rs_base_attrib( + afi, want["name"], key, w_rs + ) + ) + else: + commands.append( + self._add_rs_base_attrib( + afi, want["name"], key, w_rs + ) + ) + elif not opr and key in l_set: + if ( + key == "enable_default_log" + and val + and h_rs + and (key not in h_rs or not h_rs[key]) + ): + commands.append( + self._add_rs_base_attrib( + afi, want["name"], key, w_rs, opr + ) + ) + elif not (h_rs and self._in_target(h_rs, key)): + commands.append( + self._add_rs_base_attrib( + afi, want["name"], key, w_rs, opr + ) + ) + commands.extend( + self._add_rules(afi, want["name"], w_rules, h_rules, opr) + ) + if h_rules: + have["rules"] = h_rules + if w_rules: + want["rules"] = w_rules + return commands + + def _add_rules(self, afi, name, w_rules, h_rules, opr=True): + """ + This function forms the set/delete commands based on the 'opr' type + for rules attributes. + :param want: desired config. + :param have: target config. + :return: generated commands list. + """ + commands = [] + l_set = ( + "ipsec", + "action", + "number", + "protocol", + "fragment", + "disabled", + "description", + ) + if w_rules: + for w in w_rules: + cmd = self._compute_command(afi, name, w["number"], opr=opr) + h = self.search_r_sets_in_have( + h_rules, w["number"], type="rules" + ) + for key, val in iteritems(w): + if val: + if ( + opr + and key in l_set + and not (h and self._is_w_same(w, h, key)) + ): + if key == "disabled": + if not ( + not val + and (not h or key not in h or not h[key]) + ): + commands.append( + self._add_r_base_attrib( + afi, name, key, w + ) + ) + else: + commands.append( + self._add_r_base_attrib(afi, name, key, w) + ) + elif not opr: + if key == "number" and self._is_del(l_set, h): + commands.append( + self._add_r_base_attrib( + afi, name, key, w, opr=opr + ) + ) + continue + elif ( + key == "disabled" + and val + and h + and (key not in h or not h[key]) + ): + commands.append( + self._add_r_base_attrib( + afi, name, key, w, opr=opr + ) + ) + elif ( + key in l_set + and not (h and self._in_target(h, key)) + and not self._is_del(l_set, h) + ): + commands.append( + self._add_r_base_attrib( + afi, name, key, w, opr=opr + ) + ) + elif key == "p2p": + commands.extend(self._add_p2p(key, w, h, cmd, opr)) + elif key == "tcp": + commands.extend(self._add_tcp(key, w, h, cmd, opr)) + elif key == "time": + commands.extend( + self._add_time(key, w, h, cmd, opr) + ) + elif key == "icmp": + commands.extend( + self._add_icmp(key, w, h, cmd, opr) + ) + elif key == "state": + commands.extend( + self._add_state(key, w, h, cmd, opr) + ) + elif key == "limit": + commands.extend( + self._add_limit(key, w, h, cmd, opr) + ) + elif key == "recent": + commands.extend( + self._add_recent(key, w, h, cmd, opr) + ) + elif key == "destination" or key == "source": + commands.extend( + self._add_src_or_dest(key, w, h, cmd, opr) + ) + return commands + + def _add_p2p(self, attr, w, h, cmd, opr): + """ + This function forms the set/delete commands based on the 'opr' type + for p2p applications attributes. + :param want: desired config. + :param have: target config. + :return: generated commands list. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + if want: + if opr: + applications = list_diff_want_only(want, have) + for app in applications: + commands.append( + cmd + (" " + attr + " " + app["application"]) + ) + elif not opr and have: + applications = list_diff_want_only(want, have) + for app in applications: + commands.append( + cmd + (" " + attr + " " + app["application"]) + ) + return commands + + def _add_state(self, attr, w, h, cmd, opr): + """ + This function forms the command for 'state' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + h_state = {} + commands = [] + l_set = ("new", "invalid", "related", "established") + if w[attr]: + if h and attr in h.keys(): + h_state = h.get(attr) or {} + for item, val in iteritems(w[attr]): + if ( + opr + and item in l_set + and not ( + h_state and self._is_w_same(w[attr], h_state, item) + ) + ): + commands.append( + cmd + + ( + " " + + attr + + " " + + item + + " " + + self._bool_to_str(val) + ) + ) + elif ( + not opr + and item in l_set + and not (h_state and self._in_target(h_state, item)) + ): + commands.append(cmd + (" " + attr + " " + item)) + return commands + + def _add_recent(self, attr, w, h, cmd, opr): + """ + This function forms the command for 'recent' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + commands = [] + h_recent = {} + l_set = ("count", "time") + if w[attr]: + if h and attr in h.keys(): + h_recent = h.get(attr) or {} + for item, val in iteritems(w[attr]): + if ( + opr + and item in l_set + and not ( + h_recent and self._is_w_same(w[attr], h_recent, item) + ) + ): + commands.append( + cmd + (" " + attr + " " + item + " " + str(val)) + ) + elif ( + not opr + and item in l_set + and not (h_recent and self._in_target(h_recent, item)) + ): + commands.append(cmd + (" " + attr + " " + item)) + return commands + + def _add_icmp(self, attr, w, h, cmd, opr): + """ + This function forms the commands for 'icmp' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + commands = [] + h_icmp = {} + l_set = ("code", "type", "type_name") + if w[attr]: + if h and attr in h.keys(): + h_icmp = h.get(attr) or {} + for item, val in iteritems(w[attr]): + if ( + opr + and item in l_set + and not (h_icmp and self._is_w_same(w[attr], h_icmp, item)) + ): + if item == "type_name": + if "ipv6-name" in cmd: + commands.append( + cmd + + (" " + "icmpv6" + " " + "type" + " " + val) + ) + else: + commands.append( + cmd + + ( + " " + + attr + + " " + + item.replace("_", "-") + + " " + + val + ) + ) + else: + commands.append( + cmd + (" " + attr + " " + item + " " + str(val)) + ) + elif ( + not opr + and item in l_set + and not (h_icmp and self._in_target(h_icmp, item)) + ): + commands.append(cmd + (" " + attr + " " + item)) + return commands + + def _add_time(self, attr, w, h, cmd, opr): + """ + This function forms the commands for 'time' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + commands = [] + h_time = {} + l_set = ( + "utc", + "stopdate", + "stoptime", + "weekdays", + "monthdays", + "startdate", + "starttime", + ) + if w[attr]: + if h and attr in h.keys(): + h_time = h.get(attr) or {} + for item, val in iteritems(w[attr]): + if ( + opr + and item in l_set + and not (h_time and self._is_w_same(w[attr], h_time, item)) + ): + if item == "utc": + if not ( + not val and (not h_time or item not in h_time) + ): + commands.append(cmd + (" " + attr + " " + item)) + else: + commands.append( + cmd + (" " + attr + " " + item + " " + val) + ) + elif ( + not opr + and item in l_set + and not (h_time and self._is_w_same(w[attr], h_time, item)) + ): + commands.append(cmd + (" " + attr + " " + item)) + return commands + + def _add_tcp(self, attr, w, h, cmd, opr): + """ + This function forms the commands for 'tcp' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + h_tcp = {} + commands = [] + if w[attr]: + key = "flags" + flags = w[attr].get(key) or {} + if flags: + if h and key in h[attr].keys(): + h_tcp = h[attr].get(key) or {} + if flags: + if opr and not ( + h_tcp and self._is_w_same(w[attr], h[attr], key) + ): + commands.append( + cmd + (" " + attr + " " + key + " " + flags) + ) + if not opr and not ( + h_tcp and self._is_w_same(w[attr], h[attr], key) + ): + commands.append( + cmd + (" " + attr + " " + key + " " + flags) + ) + return commands + + def _add_limit(self, attr, w, h, cmd, opr): + """ + This function forms the commands for 'limit' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + h_limit = {} + commands = [] + if w[attr]: + key = "burst" + if ( + opr + and key in w[attr].keys() + and not ( + h + and attr in h.keys() + and self._is_w_same(w[attr], h[attr], key) + ) + ): + commands.append( + cmd + + (" " + attr + " " + key + " " + str(w[attr].get(key))) + ) + elif ( + not opr + and key in w[attr].keys() + and not ( + h and attr in h.keys() and self._in_target(h[attr], key) + ) + ): + commands.append( + cmd + + (" " + attr + " " + key + " " + str(w[attr].get(key))) + ) + key = "rate" + rate = w[attr].get(key) or {} + if rate: + if h and key in h[attr].keys(): + h_limit = h[attr].get(key) or {} + if "unit" in rate and "number" in rate: + if opr and not ( + h_limit + and self._is_w_same(rate, h_limit, "unit") + and self.is_w_same(rate, h_limit, "number") + ): + commands.append( + cmd + + ( + " " + + attr + + " " + + key + + " " + + str(rate["number"]) + + "/" + + rate["unit"] + ) + ) + if not opr and not ( + h_limit + and self._is_w_same(rate, h_limit, "unit") + and self._is_w_same(rate, h_limit, "number") + ): + commands.append(cmd + (" " + attr + " " + key)) + return commands + + def _add_src_or_dest(self, attr, w, h, cmd, opr=True): + """ + This function forms the commands for 'src/dest' attributes based on the 'opr'. + :param attr: attribute name. + :param w: base config. + :param h: target config. + :param cmd: commands to be prepend. + :return: generated list of commands. + """ + commands = [] + h_group = {} + g_set = ("port_group", "address_group", "network_group") + if w[attr]: + keys = ("address", "mac_address", "port") + for key in keys: + if ( + opr + and key in w[attr].keys() + and not ( + h + and attr in h.keys() + and self._is_w_same(w[attr], h[attr], key) + ) + ): + commands.append( + cmd + + ( + " " + + attr + + " " + + key.replace("_", "-") + + " " + + w[attr].get(key) + ) + ) + elif ( + not opr + and key in w[attr].keys() + and not ( + h + and attr in h.keys() + and self._in_target(h[attr], key) + ) + ): + commands.append(cmd + (" " + attr + " " + key)) + + key = "group" + group = w[attr].get(key) or {} + if group: + if h and key in h[attr].keys(): + h_group = h[attr].get(key) or {} + for item, val in iteritems(group): + if val: + if ( + opr + and item in g_set + and not ( + h_group + and self._is_w_same(group, h_group, item) + ) + ): + commands.append( + cmd + + ( + " " + + attr + + " " + + key + + " " + + item.replace("_", "-") + + " " + + val + ) + ) + elif ( + not opr + and item in g_set + and not ( + h_group and self._in_target(h_group, item) + ) + ): + commands.append( + cmd + + ( + " " + + attr + + " " + + key + + " " + + item.replace("_", "-") + ) + ) + return commands + + def search_r_sets_in_have(self, have, w_name, type="rule_sets"): + """ + This function returns the rule-set/rule if it is present in target config. + :param have: target config. + :param w_name: rule-set name. + :param type: rule_sets/rule/r_list. + :return: rule-set/rule. + """ + if have: + key = "name" + if type == "rules": + key = "number" + for r in have: + if r[key] == w_name: + return r + elif type == "r_list": + for h in have: + r_sets = self._get_r_sets(h) + for rs in r_sets: + if rs[key] == w_name: + return rs + else: + for rs in have: + if rs[key] == w_name: + return rs + return None + + def _get_r_sets(self, item, type="rule_sets"): + """ + This function returns the list of rule-sets/rules. + :param item: config dictionary. + :param type: rule_sets/rule/r_list. + :return: list of rule-sets/rules. + """ + rs_list = [] + r_sets = item[type] + if r_sets: + for rs in r_sets: + rs_list.append(rs) + return rs_list + + def _compute_command( + self, + afi, + name=None, + number=None, + attrib=None, + value=None, + remove=False, + opr=True, + ): + """ + This function construct the add/delete command based on passed attributes. + :param afi: address type. + :param name: rule-set name. + :param number: rule-number. + :param attrib: attribute name. + :param value: value. + :param remove: True if delete command needed to be construct. + :param opr: opeeration flag. + :return: generated command. + """ + if remove or not opr: + cmd = "delete firewall " + self._get_fw_type(afi) + else: + cmd = "set firewall " + self._get_fw_type(afi) + if name: + cmd += " " + name + if number: + cmd += " rule " + str(number) + if attrib: + cmd += " " + attrib.replace("_", "-") + if ( + value + and opr + and attrib != "enable_default_log" + and attrib != "disabled" + ): + cmd += " '" + str(value) + "'" + return cmd + + def _add_r_base_attrib(self, afi, name, attr, rule, opr=True): + """ + This function forms the command for 'rules' attributes which doesn't + have further sub attributes. + :param afi: address type. + :param name: rule-set name + :param attrib: attribute name + :param rule: rule config dictionary. + :param opr: True/False. + :return: generated command. + """ + if attr == "number": + command = self._compute_command( + afi=afi, name=name, number=rule["number"], opr=opr + ) + else: + command = self._compute_command( + afi=afi, + name=name, + number=rule["number"], + attrib=attr, + value=rule[attr], + opr=opr, + ) + return command + + def _add_rs_base_attrib(self, afi, name, attrib, rule, opr=True): + """ + + This function forms the command for 'rule-sets' attributes which doesn't + have further sub attributes. + :param afi: address type. + :param name: rule-set name + :param attrib: attribute name + :param rule: rule config dictionary. + :param opr: True/False. + :return: generated command. + """ + command = self._compute_command( + afi=afi, name=name, attrib=attrib, value=rule[attrib], opr=opr + ) + return command + + def _bool_to_str(self, val): + """ + This function converts the bool value into string. + :param val: bool value. + :return: enable/disable. + """ + return "enable" if val else "disable" + + def _get_fw_type(self, afi): + """ + This function returns the firewall rule-set type based on IP address. + :param afi: address type + :return: rule-set type. + """ + return "ipv6-name" if afi == "ipv6" else "name" + + def _is_del(self, l_set, h, key="number"): + """ + This function checks whether rule needs to be deleted based on + the rule number. + :param l_set: attribute set. + :param h: target config. + :param key: number. + :return: True/False. + """ + return key in l_set and not (h and self._in_target(h, key)) + + def _is_w_same(self, w, h, key): + """ + This function checks whether the key value is same in base and + target config dictionary. + :param w: base config. + :param h: target config. + :param key:attribute name. + :return: True/False. + """ + return True if h and key in h and h[key] == w[key] else False + + def _in_target(self, h, key): + """ + This function checks whether the target nexist and key present in target config. + :param h: target config. + :param key: attribute name. + :return: True/False. + """ + return True if h and key in h else False + + def _is_base_attrib(self, key): + """ + This function checks whether key is present in predefined + based attribute set. + :param key: + :return: True/False. + """ + r_set = ( + "p2p", + "ipsec", + "action", + "fragment", + "protocol", + "disabled", + "description", + "mac_address", + "default_action", + "enable_default_log", + ) + return True if key in r_set else False diff --git a/plugins/module_utils/network/vyos/config/static_routes/__init__.py b/plugins/module_utils/network/vyos/config/static_routes/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/static_routes/__init__.py diff --git a/plugins/module_utils/network/vyos/config/static_routes/static_routes.py b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py new file mode 100644 index 00000000..e93d4ee3 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py @@ -0,0 +1,627 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos_static_routes class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +from copy import deepcopy +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( + ConfigBase, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, + dict_diff, + remove_empties, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( + Facts, +) +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( + get_route_type, + get_lst_diff_for_dicts, + get_lst_same_for_dicts, + dict_delete, +) + + +class Static_routes(ConfigBase): + """ + The vyos_static_routes class + """ + + gather_subset = [ + "!all", + "!min", + ] + + gather_network_resources = [ + "static_routes", + ] + + def __init__(self, module): + super(Static_routes, self).__init__(module) + + def get_static_routes_facts(self, data=None): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts( + self.gather_subset, self.gather_network_resources, data=data + ) + static_routes_facts = facts["ansible_network_resources"].get( + "static_routes" + ) + if not static_routes_facts: + return [] + return static_routes_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + warnings = list() + commands = list() + + if self.state in self.ACTION_STATES: + existing_static_routes_facts = self.get_static_routes_facts() + else: + existing_static_routes_facts = [] + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_static_routes_facts)) + + if commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + self._connection.edit_config(commands) + result["changed"] = True + + if self.state in self.ACTION_STATES: + result["commands"] = commands + + if self.state in self.ACTION_STATES or self.state == "gathered": + changed_static_routes_facts = self.get_static_routes_facts() + elif self.state == "rendered": + result["rendered"] = commands + elif self.state == "parsed": + running_config = self._module.params["running_config"] + if not running_config: + self._module.fail_json( + msg="value of running_config parameter must not be empty for state parsed" + ) + result["parsed"] = self.get_static_routes_facts( + data=running_config + ) + else: + changed_static_routes_facts = [] + + if self.state in self.ACTION_STATES: + result["before"] = existing_static_routes_facts + if result["changed"]: + result["after"] = changed_static_routes_facts + elif self.state == "gathered": + result["gathered"] = changed_static_routes_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_static_routes_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + want = self._module.params["config"] + have = existing_static_routes_facts + resp = self.set_state(want, have) + return to_list(resp) + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if ( + self.state in ("merged", "replaced", "overridden", "rendered") + and not want + ): + self._module.fail_json( + msg="value of config parameter must not be empty for state {0}".format( + self.state + ) + ) + if self.state == "overridden": + commands.extend(self._state_overridden(want=want, have=have)) + elif self.state == "deleted": + commands.extend(self._state_deleted(want=want, have=have)) + elif want: + routes = self._get_routes(want) + for r in routes: + h_item = self.search_route_in_have(have, r["dest"]) + if self.state == "merged" or self.state == "rendered": + commands.extend(self._state_merged(want=r, have=h_item)) + elif self.state == "replaced": + commands.extend(self._state_replaced(want=r, have=h_item)) + return commands + + def search_route_in_have(self, have, want_dest): + """ + This function returns the route if its found in + have config. + :param have: + :param dest: + :return: the matched route + """ + routes = self._get_routes(have) + for r in routes: + if r["dest"] == want_dest: + return r + return None + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if have: + for key, value in iteritems(want): + if value: + if key == "next_hops": + commands.extend(self._update_next_hop(want, have)) + elif key == "blackhole_config": + commands.extend( + self._update_blackhole(key, want, have) + ) + commands.extend(self._state_merged(want, have)) + return commands + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + routes = self._get_routes(have) + for r in routes: + route_in_want = self.search_route_in_have(want, r["dest"]) + if not route_in_want: + commands.append(self._compute_command(r["dest"], remove=True)) + routes = self._get_routes(want) + for r in routes: + route_in_have = self.search_route_in_have(have, r["dest"]) + commands.extend(self._state_replaced(r, route_in_have)) + return commands + + def _state_merged(self, want, have, opr=True): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + if have: + commands.extend(self._render_updates(want, have)) + else: + commands.extend(self._render_set_commands(want)) + return commands + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + if want: + routes = self._get_routes(want) + if not routes: + for w in want: + af = w["address_families"] + for item in af: + if self.afi_in_have(have, item): + commands.append( + self._compute_command( + afi=item["afi"], remove=True + ) + ) + for r in routes: + h_route = self.search_route_in_have(have, r["dest"]) + if h_route: + commands.extend( + self._render_updates(r, h_route, opr=False) + ) + else: + routes = self._get_routes(have) + if self._is_ip_route_exist(routes): + commands.append(self._compute_command(afi="ipv4", remove=True)) + if self._is_ip_route_exist(routes, "route6"): + commands.append(self._compute_command(afi="ipv6", remove=True)) + return commands + + def _render_set_commands(self, want): + """ + This function returns the list of commands to add attributes which are + present in want + :param want: + :return: list of commands. + """ + commands = [] + have = {} + for key, value in iteritems(want): + if value: + if key == "dest": + commands.append(self._compute_command(dest=want["dest"])) + elif key == "blackhole_config": + commands.extend(self._add_blackhole(key, want, have)) + + elif key == "next_hops": + commands.extend(self._add_next_hop(want, have)) + + return commands + + def _add_blackhole(self, key, want, have): + """ + This function gets the diff for blackhole config specific attributes + and form the commands for attributes which are present in want but not in have. + :param key: + :param want: + :param have: + :return: list of commands + """ + commands = [] + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(remove_empties(have)) + + want_blackhole = want_copy.get(key) or {} + have_blackhole = have_copy.get(key) or {} + + updates = dict_delete(want_blackhole, have_blackhole) + if updates: + for attrib, value in iteritems(updates): + if value: + if attrib == "distance": + commands.append( + self._compute_command( + dest=want["dest"], + key="blackhole", + attrib=attrib, + remove=False, + value=str(value), + ) + ) + elif attrib == "type": + commands.append( + self._compute_command( + dest=want["dest"], key="blackhole" + ) + ) + return commands + + def _add_next_hop(self, want, have, opr=True): + """ + This function gets the diff for next hop specific attributes + and form the commands to add attributes which are present in want but not in have. + :param want: + :param have: + :return: list of commands. + """ + commands = [] + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(remove_empties(have)) + if not opr: + diff_next_hops = get_lst_same_for_dicts( + want_copy, have_copy, "next_hops" + ) + else: + diff_next_hops = get_lst_diff_for_dicts( + want_copy, have_copy, "next_hops" + ) + if diff_next_hops: + for hop in diff_next_hops: + for element in hop: + if element == "forward_router_address": + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + value=hop[element], + opr=opr, + ) + ) + elif element == "enabled" and not hop[element]: + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + attrib=hop["forward_router_address"], + value="disable", + opr=opr, + ) + ) + elif element == "admin_distance": + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + attrib=hop["forward_router_address"] + + " " + + element, + value=str(hop[element]), + opr=opr, + ) + ) + elif element == "interface": + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + attrib=hop["forward_router_address"] + + " " + + element, + value=hop[element], + opr=opr, + ) + ) + return commands + + def _update_blackhole(self, key, want, have): + """ + This function gets the difference for blackhole dict and + form the commands to delete the attributes which are present in have but not in want. + :param want: + :param have: + :return: list of commands + :param key: + :param want: + :param have: + :return: list of commands + """ + commands = [] + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(remove_empties(have)) + + want_blackhole = want_copy.get(key) or {} + have_blackhole = have_copy.get(key) or {} + updates = dict_delete(have_blackhole, want_blackhole) + if updates: + for attrib, value in iteritems(updates): + if value: + if attrib == "distance": + commands.append( + self._compute_command( + dest=want["dest"], + key="blackhole", + attrib=attrib, + remove=True, + value=str(value), + ) + ) + elif ( + attrib == "type" + and "distance" not in want_blackhole.keys() + ): + commands.append( + self._compute_command( + dest=want["dest"], key="blackhole", remove=True + ) + ) + return commands + + def _update_next_hop(self, want, have, opr=True): + """ + This function gets the difference for next_hops list and + form the commands to delete the attributes which are present in have but not in want. + :param want: + :param have: + :return: list of commands + """ + commands = [] + + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(remove_empties(have)) + + diff_next_hops = get_lst_diff_for_dicts( + have_copy, want_copy, "next_hops" + ) + if diff_next_hops: + for hop in diff_next_hops: + for element in hop: + if element == "forward_router_address": + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + value=hop[element], + remove=True, + ) + ) + elif element == "enabled": + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + attrib=hop["forward_router_address"], + value="disable", + remove=True, + ) + ) + elif element == "admin_distance": + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + attrib=hop["forward_router_address"] + + " " + + element, + value=str(hop[element]), + remove=True, + ) + ) + elif element == "interface": + commands.append( + self._compute_command( + dest=want["dest"], + key="next-hop", + attrib=hop["forward_router_address"] + + " " + + element, + value=hop[element], + remove=True, + ) + ) + return commands + + def _render_updates(self, want, have, opr=True): + """ + This function takes the diff between want and have and + invokes the appropriate functions to create the commands + to update the attributes. + :param want: + :param have: + :return: list of commands + """ + commands = [] + want_nh = want.get("next_hops") or [] + # delete static route operation per destination + if not opr and not want_nh: + commands.append( + self._compute_command(dest=want["dest"], remove=True) + ) + + else: + temp_have_next_hops = have.pop("next_hops", None) + temp_want_next_hops = want.pop("next_hops", None) + updates = dict_diff(have, want) + if temp_have_next_hops: + have["next_hops"] = temp_have_next_hops + if temp_want_next_hops: + want["next_hops"] = temp_want_next_hops + commands.extend(self._add_next_hop(want, have, opr=opr)) + + if opr and updates: + for key, value in iteritems(updates): + if value: + if key == "blackhole_config": + commands.extend( + self._add_blackhole(key, want, have) + ) + return commands + + def _compute_command( + self, + dest=None, + key=None, + attrib=None, + value=None, + remove=False, + afi=None, + opr=True, + ): + """ + This functions construct the required command based on the passed arguments. + :param dest: + :param key: + :param attrib: + :param value: + :param remove: + :return: constructed command + """ + if remove or not opr: + cmd = "delete protocols static " + self.get_route_type(dest, afi) + else: + cmd = "set protocols static " + self.get_route_type(dest, afi) + if dest: + cmd += " " + dest + if key: + cmd += " " + key + if attrib: + cmd += " " + attrib + if value: + cmd += " '" + value + "'" + return cmd + + def afi_in_have(self, have, w_item): + """ + This functions checks for the afi + list in have + :param have: + :param w_item: + :return: + """ + if have: + for h in have: + af = h.get("address_families") or [] + for item in af: + if w_item["afi"] == item["afi"]: + return True + return False + + def get_route_type(self, dest=None, afi=None): + """ + This function returns the route type based on + destination ip address or afi + :param address: + :return: + """ + if dest: + return get_route_type(dest) + elif afi == "ipv4": + return "route" + elif afi == "ipv6": + return "route6" + + def _is_ip_route_exist(self, routes, type="route"): + """ + This functions checks for the type of route. + :param routes: + :param type: + :return: True/False + """ + for r in routes: + if type == self.get_route_type(r["dest"]): + return True + return False + + def _get_routes(self, lst): + """ + This function returns the list of routes + :param lst: list of address families + :return: list of routes + """ + r_list = [] + for item in lst: + af = item["address_families"] + for element in af: + routes = element.get("routes") or [] + for r in routes: + r_list.append(r) + return r_list diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index b5816c20..8f0a3bb6 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -27,6 +27,12 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_interfaces.lldp_interfaces import ( Lldp_interfacesFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_rules.firewall_rules import ( + Firewall_rulesFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import ( + Static_routesFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.legacy.base import ( Default, Neighbors, @@ -41,6 +47,8 @@ FACT_RESOURCE_SUBSETS = dict( lag_interfaces=Lag_interfacesFacts, lldp_global=Lldp_globalFacts, lldp_interfaces=Lldp_interfacesFacts, + static_routes=Static_routesFacts, + firewall_rules=Firewall_rulesFacts, ) @@ -72,5 +80,4 @@ class Facts(FactsBase): self.get_network_legacy_facts( FACT_LEGACY_SUBSETS, legacy_facts_type ) - return self.ansible_facts, self._warnings diff --git a/plugins/module_utils/network/vyos/facts/firewall_rules/__init__.py b/plugins/module_utils/network/vyos/facts/firewall_rules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/firewall_rules/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py new file mode 100644 index 00000000..971ea6fe --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py @@ -0,0 +1,380 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos firewall_rules fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from re import findall, search, M +from copy import deepcopy +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.firewall_rules.firewall_rules import ( + Firewall_rulesArgs, +) + + +class Firewall_rulesFacts(object): + """ The vyos firewall_rules fact class + """ + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Firewall_rulesArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def get_device_data(self, connection): + return connection.get_config() + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for firewall_rules + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_device_data(connection) + # split the config into instances of the resource + objs = [] + v6_rules = findall( + r"^set firewall ipv6-name (?:\'*)(\S+)(?:\'*)", data, M + ) + v4_rules = findall(r"^set firewall name (?:\'*)(\S+)(?:\'*)", data, M) + if v6_rules: + config = self.get_rules(data, v6_rules, type="ipv6") + if config: + config = utils.remove_empties(config) + objs.append(config) + if v4_rules: + config = self.get_rules(data, v4_rules, type="ipv4") + if config: + config = utils.remove_empties(config) + objs.append(config) + + ansible_facts["ansible_network_resources"].pop("firewall_rules", None) + facts = {} + if objs: + facts["firewall_rules"] = [] + params = utils.validate_config( + self.argument_spec, {"config": objs} + ) + for cfg in params["config"]: + facts["firewall_rules"].append(utils.remove_empties(cfg)) + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def get_rules(self, data, rules, type): + """ + This function performs following: + - Form regex to fetch 'rule-sets' specific config from data. + - Form the rule-set list based on ip address. + :param data: configuration. + :param rules: list of rule-sets. + :param type: ip address type. + :return: generated rule-sets configuration. + """ + r_v4 = [] + r_v6 = [] + for r in set(rules): + rule_regex = r" %s .+$" % r.strip("'") + cfg = findall(rule_regex, data, M) + fr = self.render_config(cfg, r.strip("'")) + fr["name"] = r.strip("'") + if type == "ipv6": + r_v6.append(fr) + else: + r_v4.append(fr) + if r_v4: + config = {"afi": "ipv4", "rule_sets": r_v4} + if r_v6: + config = {"afi": "ipv6", "rule_sets": r_v6} + return config + + def render_config(self, conf, match): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + conf = "\n".join(filter(lambda x: x, conf)) + a_lst = ["description", "default_action", "enable_default_log"] + config = self.parse_attr(conf, a_lst, match) + if not config: + config = {} + config["rules"] = self.parse_rules_lst(conf) + return config + + def parse_rules_lst(self, conf): + """ + This function forms the regex to fetch the 'rules' with in + 'rule-sets' + :param conf: configuration data. + :return: generated rule list configuration. + """ + r_lst = [] + rules = findall(r"rule (?:\'*)(\d+)(?:\'*)", conf, M) + if rules: + rules_lst = [] + for r in set(rules): + r_regex = r" %s .+$" % r + cfg = "\n".join(findall(r_regex, conf, M)) + obj = self.parse_rules(cfg) + obj["number"] = int(r) + if obj: + rules_lst.append(obj) + r_lst = sorted(rules_lst, key=lambda i: i["number"]) + return r_lst + + def parse_rules(self, conf): + """ + This function triggers the parsing of 'rule' attributes. + a_lst is a list having rule attributes which doesn't + have further sub attributes. + :param conf: configuration + :return: generated rule configuration dictionary. + """ + a_lst = [ + "ipsec", + "action", + "protocol", + "fragment", + "disabled", + "description", + ] + rule = self.parse_attr(conf, a_lst) + r_sub = { + "p2p": self.parse_p2p(conf), + "tcp": self.parse_tcp(conf, "tcp"), + "icmp": self.parse_icmp(conf, "icmp"), + "time": self.parse_time(conf, "time"), + "limit": self.parse_limit(conf, "limit"), + "state": self.parse_state(conf, "state"), + "recent": self.parse_recent(conf, "recent"), + "source": self.parse_src_or_dest(conf, "source"), + "destination": self.parse_src_or_dest(conf, "destination"), + } + rule.update(r_sub) + return rule + + def parse_p2p(self, conf): + """ + This function forms the regex to fetch the 'p2p' with in + 'rules' + :param conf: configuration data. + :return: generated rule list configuration. + """ + a_lst = [] + applications = findall(r"p2p (?:\'*)(\d+)(?:\'*)", conf, M) + if applications: + app_lst = [] + for r in set(applications): + obj = {"application": r.strip("'")} + app_lst.append(obj) + a_lst = sorted(app_lst, key=lambda i: i["application"]) + return a_lst + + def parse_src_or_dest(self, conf, attrib=None): + """ + This function triggers the parsing of 'source or + destination' attributes. + :param conf: configuration. + :param attrib:'source/destination'. + :return:generated source/destination configuration dictionary. + """ + a_lst = ["port", "address", "mac_address"] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + cfg_dict["group"] = self.parse_group(conf, attrib + " group") + return cfg_dict + + def parse_recent(self, conf, attrib=None): + """ + This function triggers the parsing of 'recent' attributes + :param conf: configuration. + :param attrib: 'recent'. + :return: generated config dictionary. + """ + a_lst = ["time", "count"] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + return cfg_dict + + def parse_tcp(self, conf, attrib=None): + """ + This function triggers the parsing of 'tcp' attributes. + :param conf: configuration. + :param attrib: 'tcp'. + :return: generated config dictionary. + """ + cfg_dict = self.parse_attr(conf, ["flags"], match=attrib) + return cfg_dict + + def parse_time(self, conf, attrib=None): + """ + This function triggers the parsing of 'time' attributes. + :param conf: configuration. + :param attrib: 'time'. + :return: generated config dictionary. + """ + a_lst = [ + "stopdate", + "stoptime", + "weekdays", + "monthdays", + "startdate", + "starttime", + ] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + return cfg_dict + + def parse_state(self, conf, attrib=None): + """ + This function triggers the parsing of 'state' attributes. + :param conf: configuration + :param attrib: 'state'. + :return: generated config dictionary. + """ + a_lst = ["new", "invalid", "related", "established"] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + return cfg_dict + + def parse_group(self, conf, attrib=None): + """ + This function triggers the parsing of 'group' attributes. + :param conf: configuration. + :param attrib: 'group'. + :return: generated config dictionary. + """ + a_lst = ["port_group", "address_group", "network_group"] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + return cfg_dict + + def parse_icmp(self, conf, attrib=None): + """ + This function triggers the parsing of 'icmp' attributes. + :param conf: configuration to be parsed. + :param attrib: 'icmp'. + :return: generated config dictionary. + """ + a_lst = ["code", "type", "type_name"] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + return cfg_dict + + def parse_limit(self, conf, attrib=None): + """ + This function triggers the parsing of 'limit' attributes. + :param conf: configuration to be parsed. + :param attrib: 'limit' + :return: generated config dictionary. + """ + cfg_dict = self.parse_attr(conf, ["burst"], match=attrib) + cfg_dict["rate"] = self.parse_rate(conf, "rate") + return cfg_dict + + def parse_rate(self, conf, attrib=None): + """ + This function triggers the parsing of 'rate' attributes. + :param conf: configuration. + :param attrib: 'rate' + :return: generated config dictionary. + """ + a_lst = ["unit", "number"] + cfg_dict = self.parse_attr(conf, a_lst, match=attrib) + return cfg_dict + + def parse_attr(self, conf, attr_list, match=None): + """ + This function peforms the following: + - Form the regex to fetch the required attribute config. + - Type cast the output in desired format. + :param conf: configuration. + :param attr_list: list of attributes. + :param match: parent node/attribute name. + :return: generated config dictionary. + """ + config = {} + for attrib in attr_list: + regex = self.map_regex(attrib) + if match: + regex = match + " " + regex + if conf: + if self.is_bool(attrib): + out = conf.find(attrib.replace("_", "-")) + + dis = conf.find(attrib.replace("_", "-") + " 'disable'") + if out >= 1: + if dis >= 1: + config[attrib] = False + else: + config[attrib] = True + else: + out = search(r"^.*" + regex + " (.+)", conf, M) + if out: + val = out.group(1).strip("'") + if self.is_num(attrib): + val = int(val) + config[attrib] = val + return config + + def map_regex(self, attrib): + """ + - This function construct the regex string. + - replace the underscore with hyphen. + :param attrib: attribute + :return: regex string + """ + regex = attrib.replace("_", "-") + if attrib == "disabled": + regex = "disable" + return regex + + def is_bool(self, attrib): + """ + This function looks for the attribute in predefined bool type set. + :param attrib: attribute. + :return: True/False + """ + bool_set = ( + "new", + "invalid", + "related", + "disabled", + "established", + "enable_default_log", + ) + return True if attrib in bool_set else False + + def is_num(self, attrib): + """ + This function looks for the attribute in predefined integer type set. + :param attrib: attribute. + :return: True/false. + """ + num_set = ("time", "code", "type", "count", "burst", "number") + return True if attrib in num_set else False diff --git a/plugins/module_utils/network/vyos/facts/static_routes/__init__.py b/plugins/module_utils/network/vyos/facts/static_routes/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/static_routes/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py new file mode 100644 index 00000000..00049475 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py @@ -0,0 +1,181 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos static_routes fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +from re import findall, search, M +from copy import deepcopy +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.static_routes.static_routes import ( + Static_routesArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( + get_route_type, +) + + +class Static_routesFacts(object): + """ The vyos static_routes fact class + """ + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Static_routesArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def get_device_data(self, connection): + return connection.get_config() + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for static_routes + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + objs = [] + r_v4 = [] + r_v6 = [] + af = [] + static_routes = findall( + r"set protocols static route(6)? (\S+)", data, M + ) + if static_routes: + for route in set(static_routes): + route_regex = r" %s .+$" % route[1] + cfg = findall(route_regex, data, M) + sr = self.render_config(cfg) + sr["dest"] = route[1].strip("'") + afi = self.get_afi(sr["dest"]) + if afi == "ipv4": + r_v4.append(sr) + else: + r_v6.append(sr) + if r_v4: + afi_v4 = {"afi": "ipv4", "routes": r_v4} + af.append(afi_v4) + if r_v6: + afi_v6 = {"afi": "ipv6", "routes": r_v6} + af.append(afi_v6) + config = {"address_families": af} + if config: + objs.append(config) + + ansible_facts["ansible_network_resources"].pop("static_routes", None) + facts = {} + if objs: + facts["static_routes"] = [] + params = utils.validate_config( + self.argument_spec, {"config": objs} + ) + for cfg in params["config"]: + facts["static_routes"].append(utils.remove_empties(cfg)) + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + next_hops_conf = "\n".join(filter(lambda x: ("next-hop" in x), conf)) + blackhole_conf = "\n".join(filter(lambda x: ("blackhole" in x), conf)) + routes_dict = { + "blackhole_config": self.parse_blackhole(blackhole_conf), + "next_hops": self.parse_next_hop(next_hops_conf), + } + return routes_dict + + def parse_blackhole(self, conf): + blackhole = None + if conf: + distance = search(r"^.*blackhole distance (.\S+)", conf, M) + bh = conf.find("blackhole") + if distance is not None: + blackhole = {} + value = distance.group(1).strip("'") + blackhole["distance"] = int(value) + elif bh: + blackhole = {} + blackhole["type"] = "blackhole" + return blackhole + + def get_afi(self, address): + route_type = get_route_type(address) + if route_type == "route": + return "ipv4" + elif route_type == "route6": + return "ipv6" + + def parse_next_hop(self, conf): + nh_list = None + if conf: + nh_list = [] + hop_list = findall(r"^.*next-hop (.+)", conf, M) + if hop_list: + for hop in hop_list: + distance = search(r"^.*distance (.\S+)", hop, M) + interface = search(r"^.*interface (.\S+)", hop, M) + + dis = hop.find("disable") + hop_info = hop.split(" ") + nh_info = { + "forward_router_address": hop_info[0].strip("'") + } + if interface: + nh_info["interface"] = interface.group(1).strip("'") + if distance: + value = distance.group(1).strip("'") + nh_info["admin_distance"] = int(value) + elif dis >= 1: + nh_info["enabled"] = False + for element in nh_list: + if ( + element["forward_router_address"] + == nh_info["forward_router_address"] + ): + if "interface" in nh_info.keys(): + element["interface"] = nh_info["interface"] + if "admin_distance" in nh_info.keys(): + element["admin_distance"] = nh_info[ + "admin_distance" + ] + if "enabled" in nh_info.keys(): + element["enabled"] = nh_info["enabled"] + nh_info = None + if nh_info is not None: + nh_list.append(nh_info) + return nh_list diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py index 6504bcd9..402adfc9 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -8,6 +8,9 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ( + ipaddress, +) def search_obj_in_list(name, lst, key="name"): @@ -87,6 +90,27 @@ def get_lst_diff_for_dicts(want, have, lst): return diff +def get_lst_same_for_dicts(want, have, lst): + """ + This function generates a list containing values + that are common for list in want and list in have dict + :param want: dict object to want + :param have: dict object to have + :param lst: list the comparison on + :return: new list object with values which are common in want and have. + """ + diff = None + if want and have: + want_list = want.get(lst) or {} + have_list = have.get(lst) or {} + diff = [ + i + for i in want_list and have_list + if i in have_list and i in want_list + ] + return diff + + def list_diff_have_only(want_list, have_list): """ This function generated the list containing values @@ -178,3 +202,30 @@ def is_dict_element_present(dict, key): if item == key: return True return False + + +def get_ip_address_version(address): + """ + This function returns the version of IP address + :param address: IP address + :return: + """ + try: + address = unicode(address) + except NameError: + address = str(address) + version = ipaddress.ip_address(address.split("/")[0]).version + return version + + +def get_route_type(address): + """ + This function returns the route type based on IP address + :param address: + :return: + """ + version = get_ip_address_version(address) + if version == 6: + return "route6" + elif version == 4: + return "route" |