diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/arista/eos/plugins | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/arista/eos/plugins')
257 files changed, 54619 insertions, 0 deletions
diff --git a/ansible_collections/arista/eos/plugins/action/__init__.py b/ansible_collections/arista/eos/plugins/action/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/__init__.py diff --git a/ansible_collections/arista/eos/plugins/action/acl_interfaces.py b/ansible_collections/arista/eos/plugins/action/acl_interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/acl_interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/acls.py b/ansible_collections/arista/eos/plugins/action/acls.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/acls.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/banner.py b/ansible_collections/arista/eos/plugins/action/banner.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/banner.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/bgp.py b/ansible_collections/arista/eos/plugins/action/bgp.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/bgp.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/bgp_address_family.py b/ansible_collections/arista/eos/plugins/action/bgp_address_family.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/bgp_address_family.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/bgp_global.py b/ansible_collections/arista/eos/plugins/action/bgp_global.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/bgp_global.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/command.py b/ansible_collections/arista/eos/plugins/action/command.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/command.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/config.py b/ansible_collections/arista/eos/plugins/action/config.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/config.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/eapi.py b/ansible_collections/arista/eos/plugins/action/eapi.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/eapi.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/eos.py b/ansible_collections/arista/eos/plugins/action/eos.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/eos.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/facts.py b/ansible_collections/arista/eos/plugins/action/facts.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/facts.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/hostname.py b/ansible_collections/arista/eos/plugins/action/hostname.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/hostname.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/interface.py b/ansible_collections/arista/eos/plugins/action/interface.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/interface.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/interfaces.py b/ansible_collections/arista/eos/plugins/action/interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/l2_interface.py b/ansible_collections/arista/eos/plugins/action/l2_interface.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/l2_interface.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/l2_interfaces.py b/ansible_collections/arista/eos/plugins/action/l2_interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/l2_interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/l3_interface.py b/ansible_collections/arista/eos/plugins/action/l3_interface.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/l3_interface.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/l3_interfaces.py b/ansible_collections/arista/eos/plugins/action/l3_interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/l3_interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/lacp.py b/ansible_collections/arista/eos/plugins/action/lacp.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/lacp.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/lacp_interfaces.py b/ansible_collections/arista/eos/plugins/action/lacp_interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/lacp_interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/lag_interfaces.py b/ansible_collections/arista/eos/plugins/action/lag_interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/lag_interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/linkagg.py b/ansible_collections/arista/eos/plugins/action/linkagg.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/linkagg.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/lldp.py b/ansible_collections/arista/eos/plugins/action/lldp.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/lldp.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/lldp_global.py b/ansible_collections/arista/eos/plugins/action/lldp_global.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/lldp_global.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/lldp_interfaces.py b/ansible_collections/arista/eos/plugins/action/lldp_interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/lldp_interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/logging.py b/ansible_collections/arista/eos/plugins/action/logging.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/logging.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/logging_global.py b/ansible_collections/arista/eos/plugins/action/logging_global.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/logging_global.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/ntp_global.py b/ansible_collections/arista/eos/plugins/action/ntp_global.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/ntp_global.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/ospf_interfaces.py b/ansible_collections/arista/eos/plugins/action/ospf_interfaces.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/ospf_interfaces.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/ospfv2.py b/ansible_collections/arista/eos/plugins/action/ospfv2.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/ospfv2.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/ospfv3.py b/ansible_collections/arista/eos/plugins/action/ospfv3.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/ospfv3.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/prefix_lists.py b/ansible_collections/arista/eos/plugins/action/prefix_lists.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/prefix_lists.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/route_maps.py b/ansible_collections/arista/eos/plugins/action/route_maps.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/route_maps.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/snmp_server.py b/ansible_collections/arista/eos/plugins/action/snmp_server.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/snmp_server.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/static_route.py b/ansible_collections/arista/eos/plugins/action/static_route.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/static_route.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/static_routes.py b/ansible_collections/arista/eos/plugins/action/static_routes.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/static_routes.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/system.py b/ansible_collections/arista/eos/plugins/action/system.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/system.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/user.py b/ansible_collections/arista/eos/plugins/action/user.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/user.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/vlan.py b/ansible_collections/arista/eos/plugins/action/vlan.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/vlan.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/vlans.py b/ansible_collections/arista/eos/plugins/action/vlans.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/vlans.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/action/vrf.py b/ansible_collections/arista/eos/plugins/action/vrf.py new file mode 100644 index 000000000..0afb7e88f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/action/vrf.py @@ -0,0 +1,58 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.utils.display import Display +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + module_name = self._task.action.split(".")[-1] + self._config_module = ( + True if module_name in ["eos_config", "config"] else False + ) + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection not in ("network_cli", "httpapi"): + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/ansible_collections/arista/eos/plugins/cliconf/__init__.py b/ansible_collections/arista/eos/plugins/cliconf/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/cliconf/__init__.py diff --git a/ansible_collections/arista/eos/plugins/cliconf/eos.py b/ansible_collections/arista/eos/plugins/cliconf/eos.py new file mode 100644 index 000000000..d5347307d --- /dev/null +++ b/ansible_collections/arista/eos/plugins/cliconf/eos.py @@ -0,0 +1,494 @@ +# +# (c) 2017 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +author: Ansible Networking Team (@ansible-network) +name: eos +short_description: Use eos cliconf to run command on Arista EOS platform +description: +- This eos plugin provides low level abstraction apis for sending and receiving CLI + commands from Arista EOS network devices. +version_added: 1.0.0 +options: + eos_use_sessions: + type: boolean + default: true + description: + - Specifies if sessions should be used on remote host or not + env: + - name: ANSIBLE_EOS_USE_SESSIONS + vars: + - name: ansible_eos_use_sessions + config_commands: + description: + - Specifies a list of commands that can make configuration changes + to the target device. + - When `ansible_network_single_user_mode` is enabled, if a command sent + to the device is present in this list, the existing cache is invalidated. + version_added: 2.0.0 + type: list + elements: str + default: [] + vars: + - name: ansible_eos_config_commands +""" + +import json +import re + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_text +from ansible.module_utils.common._collections_compat import Mapping +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, + dumps, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.cliconf_base import ( + CliconfBase, + enable_mode, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + session_name, +) + + +class Cliconf(CliconfBase): + __rpc__ = CliconfBase.__rpc__ + [ + "commit", + "discard_changes", + "get_diff", + "run_commands", + "supports_sessions", + ] + + def __init__(self, *args, **kwargs): + super(Cliconf, self).__init__(*args, **kwargs) + self._device_info = {} + self._session_support = None + + @enable_mode + def get_config(self, source="running", flags=None, format="text"): + options_values = self.get_option_values() + if format not in options_values["format"]: + raise ValueError( + "'format' value %s is invalid. Valid values are %s" + % (format, ",".join(options_values["format"])), + ) + + lookup = {"running": "running-config", "startup": "startup-config"} + if source not in lookup: + raise ValueError( + "fetching configuration from %s is not supported" % source, + ) + + cmd = "show %s " % lookup[source] + if format and format != "text": + cmd += "| %s " % format + + cmd += " ".join(to_list(flags)) + cmd = cmd.strip() + return self.send_command(cmd) + + @enable_mode + def get_session_config( + self, + candidate=None, + commit=True, + replace=None, + comment=None, + ): + operations = self.get_device_operations() + self.check_edit_config_capability( + operations, + candidate, + commit, + replace, + comment, + ) + + if (commit is False) and (not self.supports_sessions()): + raise ValueError( + "check mode is not supported without configuration session", + ) + + resp = {} + session = None + if self.supports_sessions(): + session = session_name() + resp.update({"session": session}) + self.send_command("configure session %s" % session) + if replace: + self.send_command("rollback clean-config") + else: + self.send_command("configure") + + results = [] + requests = [] + multiline = False + for line in to_list(candidate): + if not isinstance(line, Mapping): + line = {"command": line} + + cmd = line["command"] + if cmd == "end": + continue + if cmd.startswith("banner") or multiline: + multiline = True + elif cmd == "EOF" and multiline: + multiline = False + + if multiline: + line["sendonly"] = True + + if cmd != "end" and not cmd.startswith("!"): + try: + results.append(self.send_command(**line)) + requests.append(cmd) + except AnsibleConnectionFailure as e: + self.discard_changes(session) + raise AnsibleConnectionFailure(e.message) + + resp["request"] = requests + resp["response"] = results + if self.supports_sessions(): + out = self.send_command("show session-config") + if out: + resp["diff"] = out.strip() + + if commit: + self.commit() + else: + self.discard_changes(session) + else: + self.send_command("end") + if resp.get("diff"): + return resp["diff"] + return resp + + @enable_mode + def edit_config( + self, + candidate=None, + commit=True, + replace=None, + comment=None, + ): + operations = self.get_device_operations() + self.check_edit_config_capability( + operations, + candidate, + commit, + replace, + comment, + ) + + if (commit is False) and (not self.supports_sessions()): + raise ValueError( + "check mode is not supported without configuration session", + ) + + resp = {} + session = None + if self.supports_sessions(): + session = session_name() + resp.update({"session": session}) + self.send_command("configure session %s" % session) + if replace: + self.send_command("rollback clean-config") + else: + self.send_command("configure") + + results = [] + requests = [] + multiline = False + for line in to_list(candidate): + if not isinstance(line, Mapping): + line = {"command": line} + + cmd = line["command"] + if cmd == "end": + continue + if cmd.startswith("banner") or multiline: + multiline = True + elif cmd == "EOF" and multiline: + multiline = False + + if multiline: + line["sendonly"] = True + + if cmd != "end" and not cmd.startswith("!"): + try: + results.append(self.send_command(**line)) + requests.append(cmd) + except AnsibleConnectionFailure as e: + self.discard_changes(session) + raise AnsibleConnectionFailure(e.message) + + resp["request"] = requests + resp["response"] = results + if self.supports_sessions(): + out = self.send_command("show session-config diffs") + if out: + resp["diff"] = out.strip() + + if commit: + self.commit() + else: + self.discard_changes(session) + else: + self.send_command("end") + return resp + + def get( + self, + command, + prompt=None, + answer=None, + sendonly=False, + newline=True, + output=None, + check_all=False, + version=None, + ): + if output: + command = self._get_command_with_output(command, output, version) + return self.send_command( + command=command, + prompt=prompt, + answer=answer, + sendonly=sendonly, + newline=newline, + check_all=check_all, + ) + + def commit(self): + self.send_command("commit") + + def discard_changes(self, session=None): + commands = ["end"] + if self.supports_sessions(): + # to close session gracefully execute abort in top level session prompt. + commands.extend(["configure session %s" % session, "abort"]) + + for cmd in commands: + self.send_command(cmd) + + def run_commands(self, commands=None, check_rc=True): + if commands is None: + raise ValueError("'commands' value is required") + responses = list() + for cmd in to_list(commands): + if not isinstance(cmd, Mapping): + cmd = {"command": cmd} + + output = cmd.pop("output", None) + version = cmd.pop("version", None) + if output: + cmd["command"] = self._get_command_with_output( + cmd["command"], + output, + version, + ) + + try: + out = self.send_command(**cmd) + except AnsibleConnectionFailure as e: + if check_rc: + raise + out = getattr(e, "err", e) + out = to_text(out, errors="surrogate_or_strict") + + if out is not None: + try: + out = json.loads(out) + except ValueError: + out = out.strip() + + responses.append(out) + return responses + + def get_diff( + self, + candidate=None, + running=None, + diff_match="line", + diff_ignore_lines=None, + path=None, + diff_replace="line", + ): + diff = {} + device_operations = self.get_device_operations() + option_values = self.get_option_values() + + if candidate is None and device_operations["supports_generate_diff"]: + raise ValueError( + "candidate configuration is required to generate diff", + ) + + if diff_match not in option_values["diff_match"]: + raise ValueError( + "'match' value %s in invalid, valid values are %s" + % (diff_match, ", ".join(option_values["diff_match"])), + ) + + if diff_replace not in option_values["diff_replace"]: + raise ValueError( + "'replace' value %s in invalid, valid values are %s" + % (diff_replace, ", ".join(option_values["diff_replace"])), + ) + + # prepare candidate configuration + candidate_obj = NetworkConfig(indent=3) + candidate_obj.load(candidate) + + if running and diff_match != "none" and diff_replace != "config": + # running configuration + running_obj = NetworkConfig( + indent=3, + contents=running, + ignore_lines=diff_ignore_lines, + ) + configdiffobjs = candidate_obj.difference( + running_obj, + path=path, + match=diff_match, + replace=diff_replace, + ) + + else: + configdiffobjs = candidate_obj.items + + diff["config_diff"] = ( + dumps(configdiffobjs, "commands") if configdiffobjs else "" + ) + return diff + + def supports_sessions(self): + if not self.get_option("eos_use_sessions"): + self._session_support = False + else: + if self._session_support: + return self._session_support + + try: + self.get("show configuration sessions") + self._session_support = True + except AnsibleConnectionFailure: + self._session_support = False + + return self._session_support + + def get_device_info(self): + if not self._device_info: + device_info = {} + + device_info["network_os"] = "eos" + reply = self.get("show version | json") + data = json.loads(reply) + + device_info["network_os_version"] = data["version"] + device_info["network_os_model"] = data["modelName"] + + reply = self.get("show hostname | json") + data = json.loads(reply) + + device_info["network_os_hostname"] = data["hostname"] + + try: + reply = self.get("bash timeout 5 cat /mnt/flash/boot-config") + + match = re.search(r"SWI=(.+)$", reply, re.M) + if match: + device_info["network_os_image"] = match.group(1) + except AnsibleConnectionFailure: + # This requires enable mode to run + self._connection.queue_message( + "vvv", + "Unable to gather network_os_image without enable mode", + ) + + self._device_info = device_info + + return self._device_info + + def get_device_operations(self): + return { + "supports_diff_replace": True, + "supports_commit": bool(self.supports_sessions()), + "supports_rollback": False, + "supports_defaults": False, + "supports_onbox_diff": bool(self.supports_sessions()), + "supports_commit_comment": False, + "supports_multiline_delimiter": False, + "supports_diff_match": True, + "supports_diff_ignore_lines": True, + "supports_generate_diff": not bool(self.supports_sessions()), + "supports_replace": bool(self.supports_sessions()), + } + + def get_option_values(self): + return { + "format": ["text", "json"], + "diff_match": ["line", "strict", "exact", "none"], + "diff_replace": ["line", "block", "config"], + "output": ["text", "json"], + } + + def get_capabilities(self): + result = super(Cliconf, self).get_capabilities() + result["device_operations"] = self.get_device_operations() + result.update(self.get_option_values()) + + return json.dumps(result) + + def set_cli_prompt_context(self): + """ + Make sure we are in the operational cli mode + :return: None + """ + if self._connection.connected: + self._update_cli_prompt_context( + config_context="(config", + exit_command="abort", + ) + + def _get_command_with_output(self, command, output, version): + options_values = self.get_option_values() + if output not in options_values["output"]: + raise ValueError( + "'output' value %s is invalid. Valid values are %s" + % (output, ",".join(options_values["output"])), + ) + + if output == "json" and not command.endswith("| json"): + cmd = "%s | json" % command + else: + cmd = command + if version != "latest" and "| json" in cmd: + cmd = "%s version %s" % (cmd, version) + return cmd diff --git a/ansible_collections/arista/eos/plugins/filter/__init__.py b/ansible_collections/arista/eos/plugins/filter/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/filter/__init__.py diff --git a/ansible_collections/arista/eos/plugins/httpapi/__init__.py b/ansible_collections/arista/eos/plugins/httpapi/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/httpapi/__init__.py diff --git a/ansible_collections/arista/eos/plugins/httpapi/eos.py b/ansible_collections/arista/eos/plugins/httpapi/eos.py new file mode 100644 index 000000000..9fa9dfd3e --- /dev/null +++ b/ansible_collections/arista/eos/plugins/httpapi/eos.py @@ -0,0 +1,217 @@ +# (c) 2018 Red Hat Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +author: Ansible Networking Team (@ansible-network) +name: eos +short_description: Use eAPI to run command on eos platform +description: +- This eos plugin provides low level abstraction api's for sending and receiving CLI + commands with eos network devices. +version_added: 1.0.0 +options: + eos_use_sessions: + type: bool + default: yes + description: + - Specifies if sessions should be used on remote host or not + env: + - name: ANSIBLE_EOS_USE_SESSIONS + vars: + - name: ansible_eos_use_sessions +""" + +import json + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_text +from ansible.module_utils.connection import ConnectionError +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.httpapi_base import ( + HttpApiBase, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + session_name, +) + + +OPTIONS = { + "format": ["text", "json"], + "diff_match": ["line", "strict", "exact", "none"], + "diff_replace": ["line", "block", "config"], + "output": ["text", "json"], +} + + +class HttpApi(HttpApiBase): + def __init__(self, *args, **kwargs): + super(HttpApi, self).__init__(*args, **kwargs) + self._device_info = None + self._session_support = None + + def supports_sessions(self): + if not self.get_option("eos_use_sessions"): + self._session_support = False + else: + if self._session_support: + return self._session_support + + try: + response = self.send_request("show configuration sessions") + self._session_support = "error" not in response + except AnsibleConnectionFailure: + self._session_support = False + + return self._session_support + + def send_request(self, data, **message_kwargs): + data = to_list(data) + become = self._become + if become: + self.connection.queue_message("vvvv", "firing event: on_become") + data.insert(0, {"cmd": "enable", "input": self._become_pass}) + + output = message_kwargs.get("output") or "text" + version = message_kwargs.get("version") or "latest" + request = request_builder(data, output, version) + headers = {"Content-Type": "application/json-rpc"} + + _response, response_data = self.connection.send( + "/command-api", + request, + headers=headers, + method="POST", + ) + + try: + response_data = json.loads(to_text(response_data.getvalue())) + except ValueError: + raise ConnectionError( + "Response was not valid JSON, got {0}".format( + to_text(response_data.getvalue()), + ), + ) + + results = handle_response(response_data) + + if become: + results = results[1:] + if len(results) == 1: + results = results[0] + + return results + + def get_device_info(self): + if self._device_info: + return self._device_info + + device_info = {} + + device_info["network_os"] = "eos" + reply = self.send_request("show version", output="json") + data = json.loads(reply) + + device_info["network_os_version"] = data["version"] + device_info["network_os_model"] = data["modelName"] + + reply = self.send_request("show hostname", output="json") + data = json.loads(reply) + + device_info["network_os_hostname"] = data["hostname"] + + self._device_info = device_info + return self._device_info + + def get_device_operations(self): + return { + "supports_diff_replace": True, + "supports_commit": bool(self.supports_sessions()), + "supports_rollback": False, + "supports_defaults": False, + "supports_onbox_diff": bool(self.supports_sessions()), + "supports_commit_comment": False, + "supports_multiline_delimiter": False, + "supports_diff_match": True, + "supports_diff_ignore_lines": True, + "supports_generate_diff": not bool(self.supports_sessions()), + "supports_replace": bool(self.supports_sessions()), + } + + def get_capabilities(self): + result = {} + result["rpc"] = [] + result["device_info"] = self.get_device_info() + result["device_operations"] = self.get_device_operations() + result.update(OPTIONS) + result["network_api"] = "eapi" + + return json.dumps(result) + + # Shims for resource module support + def get(self, command, output=None): + # This method is ONLY here to support resource modules. Therefore most + # arguments are unsupported and not present. + + return self.send_request(data=command, output=output) + + def edit_config(self, candidate): + # This method is ONLY here to support resource modules. Therefore most + # arguments are unsupported and not present. + + session = None + if self.supports_sessions(): + session = session_name() + candidate = ["configure session %s" % session] + candidate + else: + candidate = ["configure"] + candidate + candidate.append("commit") + + try: + responses = self.send_request(candidate) + except ConnectionError: + if session: + self.send_request(["configure session %s" % session, "abort"]) + raise + + return [resp for resp in to_list(responses) if resp != "{}"] + + +def handle_response(response): + if "error" in response: + error = response["error"] + + error_text = [] + for data in error.get("data", []): + error_text.extend(data.get("errors", [])) + error_text = "\n".join(error_text) or error["message"] + + raise ConnectionError(error_text, code=error["code"]) + + results = [] + + for result in response["result"]: + if "messages" in result: + results.append(result["messages"][0]) + elif "output" in result: + results.append(result["output"].strip()) + else: + results.append(json.dumps(result)) + + return results + + +def request_builder(commands, output, version, reqid=None): + if version != "latest": + version = int(version) + params = dict(version=version, cmds=commands, format=output) + return json.dumps( + dict(jsonrpc="2.0", id=reqid, method="runCmds", params=params), + ) diff --git a/ansible_collections/arista/eos/plugins/inventory/__init__.py b/ansible_collections/arista/eos/plugins/inventory/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/inventory/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acl_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acl_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acl_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acl_interfaces/acl_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acl_interfaces/acl_interfaces.py new file mode 100644 index 000000000..f76152800 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acl_interfaces/acl_interfaces.py @@ -0,0 +1,85 @@ +# +# -*- 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 eos_acl_interfaces module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class Acl_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_acl_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "access_groups": { + "elements": "dict", + "options": { + "acls": { + "elements": "dict", + "options": { + "direction": { + "required": True, + "choices": ["in", "out"], + "type": "str", + }, + "name": {"required": True, "type": "str"}, + }, + "type": "list", + }, + "afi": { + "required": True, + "choices": ["ipv4", "ipv6"], + "type": "str", + }, + }, + "type": "list", + }, + "name": {"required": True, "type": "str"}, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "parsed", + "rendered", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acls/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acls/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acls/acls.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acls/acls.py new file mode 100644 index 000000000..6f010fe9f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/acls/acls.py @@ -0,0 +1,416 @@ +# +# -*- 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 eos_acls module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class AclsArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_acls module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "acls": { + "elements": "dict", + "options": { + "aces": { + "elements": "dict", + "options": { + "destination": { + "mutually_exclusive": [ + [ + "address", + "subnet_address", + "any", + "host", + ], + [ + "wildcard_bits", + "subnet_address", + "any", + "host", + ], + ], + "options": { + "address": {"type": "str"}, + "any": {"type": "bool"}, + "host": {"type": "str"}, + "port_protocol": {"type": "dict"}, + "subnet_address": {"type": "str"}, + "wildcard_bits": {"type": "str"}, + }, + "required_together": [ + ["address", "wildcard_bits"], + ], + "type": "dict", + }, + "fragment_rules": {"type": "bool"}, + "fragments": {"type": "bool"}, + "grant": { + "choices": ["permit", "deny"], + "type": "str", + }, + "line": {"type": "str", "aliases": ["ace"]}, + "hop_limit": {"type": "dict"}, + "log": {"type": "bool"}, + "protocol": {"type": "str"}, + "protocol_options": { + "options": { + "icmp": { + "options": { + "administratively_prohibited": { + "type": "bool", + }, + "alternate_address": { + "type": "bool", + }, + "conversion_error": { + "type": "bool", + }, + "dod_host_prohibited": { + "type": "bool", + }, + "dod_net_prohibited": { + "type": "bool", + }, + "echo": {"type": "bool"}, + "echo_reply": {"type": "bool"}, + "general_parameter_problem": { + "type": "bool", + }, + "host_isolated": { + "type": "bool", + }, + "host_precedence_unreachable": { + "type": "bool", + }, + "host_redirect": { + "type": "bool", + }, + "host_tos_redirect": { + "type": "bool", + }, + "host_tos_unreachable": { + "type": "bool", + }, + "host_unknown": { + "type": "bool", + }, + "host_unreachable": { + "type": "bool", + }, + "information_reply": { + "type": "bool", + }, + "information_request": { + "type": "bool", + }, + "mask_reply": {"type": "bool"}, + "mask_request": { + "type": "bool", + }, + "message_code": { + "type": "int", + }, + "message_num": {"type": "int"}, + "message_type": { + "type": "int", + }, + "mobile_redirect": { + "type": "bool", + }, + "net_redirect": { + "type": "bool", + }, + "net_tos_redirect": { + "type": "bool", + }, + "net_tos_unreachable": { + "type": "bool", + }, + "net_unreachable": { + "type": "bool", + }, + "network_unknown": { + "type": "bool", + }, + "no_room_for_option": { + "type": "bool", + }, + "option_missing": { + "type": "bool", + }, + "packet_too_big": { + "type": "bool", + }, + "parameter_problem": { + "type": "bool", + }, + "port_unreachable": { + "type": "bool", + }, + "precedence_unreachable": { + "type": "bool", + }, + "protocol_unreachable": { + "type": "bool", + }, + "reassembly_timeout": { + "type": "bool", + }, + "redirect": {"type": "bool"}, + "router_advertisement": { + "type": "bool", + }, + "router_solicitation": { + "type": "bool", + }, + "source_quench": { + "type": "bool", + }, + "source_route_failed": { + "type": "bool", + }, + "time_exceeded": { + "type": "bool", + }, + "timestamp_reply": { + "type": "bool", + }, + "timestamp_request": { + "type": "bool", + }, + "traceroute": {"type": "bool"}, + "ttl_exceeded": { + "type": "bool", + }, + "unreachable": { + "type": "bool", + }, + }, + "type": "dict", + }, + "icmpv6": { + "options": { + "address_unreachable": { + "type": "bool", + }, + "beyond_scope": { + "type": "bool", + }, + "echo_reply": {"type": "bool"}, + "echo_request": { + "type": "bool", + }, + "erroneous_header": { + "type": "bool", + }, + "fragment_reassembly_exceeded": { + "type": "bool", + }, + "hop_limit_exceeded": { + "type": "bool", + }, + "neighbor_advertisement": { + "type": "bool", + }, + "neighbor_solicitation": { + "type": "bool", + }, + "no_admin": {"type": "bool"}, + "no_route": {"type": "bool"}, + "packet_too_big": { + "type": "bool", + }, + "parameter_problem": { + "type": "bool", + }, + "port_unreachable": { + "type": "bool", + }, + "redirect_message": { + "type": "bool", + }, + "reject_route": { + "type": "bool", + }, + "router_advertisement": { + "type": "bool", + }, + "router_solicitation": { + "type": "bool", + }, + "source_address_failed": { + "type": "bool", + }, + "source_routing_error": { + "type": "bool", + }, + "time_exceeded": { + "type": "bool", + }, + "unreachable": { + "type": "bool", + }, + "unrecognized_ipv6_option": { + "type": "bool", + }, + "unrecognized_next_header": { + "type": "bool", + }, + }, + "type": "dict", + }, + "ip": { + "options": { + "nexthop_group": { + "type": "str", + }, + }, + "type": "dict", + }, + "ipv6": { + "options": { + "nexthop_group": { + "type": "str", + }, + }, + "type": "dict", + }, + "tcp": { + "options": { + "flags": { + "options": { + "ack": { + "type": "bool", + }, + "established": { + "type": "bool", + }, + "fin": { + "type": "bool", + }, + "psh": { + "type": "bool", + }, + "rst": { + "type": "bool", + }, + "syn": { + "type": "bool", + }, + "urg": { + "type": "bool", + }, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + "remark": {"type": "str"}, + "sequence": {"type": "int"}, + "source": { + "mutually_exclusive": [ + [ + "address", + "subnet_address", + "any", + "host", + ], + [ + "wildcard_bits", + "subnet_address", + "any", + "host", + ], + ], + "options": { + "address": {"type": "str"}, + "any": {"type": "bool"}, + "host": {"type": "str"}, + "port_protocol": {"type": "dict"}, + "subnet_address": {"type": "str"}, + "wildcard_bits": {"type": "str"}, + }, + "required_together": [ + ["address", "wildcard_bits"], + ], + "type": "dict", + }, + "tracked": {"type": "bool"}, + "ttl": { + "options": { + "eq": {"type": "int"}, + "gt": {"type": "int"}, + "lt": {"type": "int"}, + "neq": {"type": "int"}, + }, + "type": "dict", + }, + "vlan": {"type": "str"}, + }, + "type": "list", + }, + "name": {"required": True, "type": "str"}, + "standard": {"type": "bool"}, + }, + "type": "list", + }, + "afi": { + "choices": ["ipv4", "ipv6"], + "required": True, + "type": "str", + }, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_address_family/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_address_family/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_address_family/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_address_family/bgp_address_family.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_address_family/bgp_address_family.py new file mode 100644 index 000000000..c706afe18 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_address_family/bgp_address_family.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# 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 eos_bgp_address_family module +""" + + +class Bgp_afArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_bgp_address_family module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + }, + "config": { + "type": "dict", + "options": { + "as_number": {"type": "str"}, + "address_family": { + "elements": "dict", + "type": "list", + "options": { + "network": { + "type": "list", + "elements": "dict", + "options": { + "route_map": {"type": "str"}, + "address": {"type": "str"}, + }, + }, + "redistribute": { + "elements": "dict", + "type": "list", + "options": { + "ospf_route": { + "type": "str", + "choices": [ + "internal", + "external", + "nssa_external_1", + "nssa_external_2", + ], + }, + "route_map": {"type": "str"}, + "protocol": { + "type": "str", + "choices": ["isis", "ospfv3", "dhcp"], + }, + "isis_level": { + "type": "str", + "choices": [ + "level-1", + "level-2", + "level-1-2", + ], + }, + }, + }, + "route_target": { + "type": "dict", + "options": { + "action": { + "type": "str", + "choices": ["both", "import", "export"], + "aliases": ["mode"], + }, + "type": { + "type": "str", + "choices": [ + "evpn", + "vpn-ipv4", + "vpn-ipv6", + ], + }, + "route_map": {"type": "str"}, + "target": {"type": "str"}, + "imported_route": {"type": "bool"}, + }, + }, + "graceful_restart": {"type": "bool"}, + "bgp_params": { + "type": "dict", + "options": { + "next_hop_address_family": { + "type": "str", + "choices": ["ipv6"], + }, + "redistribute_internal": {"type": "bool"}, + "route": {"type": "str"}, + "additional_paths": { + "type": "str", + "choices": ["install", "send", "receive"], + }, + "next_hop_unchanged": {"type": "bool"}, + }, + }, + "safi": { + "type": "str", + "choices": ["labeled-unicast", "multicast"], + }, + "neighbor": { + "elements": "dict", + "type": "list", + "options": { + "activate": {"type": "bool"}, + "graceful_restart": {"type": "bool"}, + "weight": {"type": "int"}, + "default_originate": { + "type": "dict", + "options": { + "route_map": {"type": "str"}, + "always": {"type": "bool"}, + }, + }, + "route_map": { + "type": "dict", + "options": { + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + "name": {"type": "str"}, + }, + }, + "next_hop_address_family": { + "type": "str", + "choices": ["ipv6"], + }, + "additional_paths": { + "type": "str", + "choices": ["send", "receive"], + }, + "peer": {"type": "str"}, + "encapsulation": { + "type": "dict", + "options": { + "transport": { + "type": "str", + "choices": ["mpls", "vxlan"], + }, + "source_interface": {"type": "str"}, + }, + }, + "prefix_list": { + "type": "dict", + "options": { + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + "name": {"type": "str"}, + }, + }, + "next_hop_unchanged": {"type": "bool"}, + }, + }, + "afi": { + "type": "str", + "choices": ["ipv4", "ipv6", "evpn"], + }, + "vrf": {"type": "str"}, + }, + }, + }, + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_global/bgp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_global/bgp_global.py new file mode 100644 index 000000000..96a055e43 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/bgp_global/bgp_global.py @@ -0,0 +1,1033 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# 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 eos_bgp_global module +""" + + +class Bgp_globalArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_bgp_global module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "type": "str", + "choices": [ + "deleted", + "purged", + "merged", + "replaced", + "gathered", + "rendered", + "parsed", + ], + }, + "config": { + "type": "dict", + "options": { + "router_id": {"type": "str"}, + "as_number": {"type": "str"}, + "redistribute": { + "elements": "dict", + "type": "list", + "options": { + "ospf_route": { + "type": "str", + "choices": [ + "internal", + "external", + "nssa_external_1", + "nssa_external_2", + ], + }, + "route_map": {"type": "str"}, + "protocol": { + "type": "str", + "choices": [ + "isis", + "ospfv3", + "ospf", + "attached-host", + "connected", + "rip", + "static", + ], + }, + "isis_level": { + "type": "str", + "choices": ["level-1", "level-2", "level-1-2"], + }, + }, + }, + "monitoring": { + "type": "dict", + "options": { + "received": { + "type": "str", + "choices": ["post_policy", "pre_policy"], + }, + "station": {"type": "str"}, + "port": {"type": "int"}, + "timestamp": { + "type": "str", + "choices": ["none", "send_time"], + }, + }, + }, + "default_metric": {"type": "int"}, + "bgp_params": { + "type": "dict", + "options": { + "labeled_unicast": { + "type": "str", + "choices": ["ip", "tunnel"], + }, + "host_routes": {"type": "bool"}, + "transport": {"type": "int"}, + "next_hop_unchanged": {"type": "bool"}, + "missing_policy": { + "type": "dict", + "options": { + "action": { + "type": "str", + "choices": [ + "deny", + "permit", + "deny-in-out", + ], + }, + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + }, + }, + "monitoring": {"type": "bool"}, + "allowas_in": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + "additional_paths": { + "type": "str", + "choices": ["install", "send", "receive"], + }, + "advertise_inactive": {"type": "bool"}, + "listen": { + "type": "dict", + "options": { + "range": { + "type": "dict", + "options": { + "peer_group": { + "type": "dict", + "options": { + "peer_filter": {"type": "str"}, + "remote_as": {"type": "str"}, + "name": {"type": "str"}, + }, + }, + "address": {"type": "str"}, + }, + }, + "limit": {"type": "int"}, + }, + }, + "route_reflector": { + "type": "dict", + "options": { + "preserve": {"type": "bool"}, + "set": {"type": "bool"}, + }, + }, + "always_compare_med": {"type": "bool"}, + "client_to_client": {"type": "bool"}, + "bestpath": { + "type": "dict", + "options": { + "ecmp_fast": {"type": "bool"}, + "tie_break": { + "type": "str", + "choices": [ + "cluster_list_length", + "router_id", + ], + }, + "skip": {"type": "bool"}, + "as_path": { + "type": "str", + "choices": ["ignore", "multipath_relax"], + }, + "med": { + "type": "dict", + "options": { + "confed": {"type": "bool"}, + "missing_as_worst": {"type": "bool"}, + }, + }, + }, + }, + "convergence": { + "type": "dict", + "options": { + "slow_peer": {"type": "bool"}, + "time": {"type": "int"}, + }, + }, + "log_neighbor_changes": {"type": "bool"}, + "asn": { + "type": "str", + "choices": ["asdot", "asplain"], + }, + "default": { + "type": "str", + "choices": ["ipv4_unicast", "ipv6_unicast"], + }, + "route": {"type": "str"}, + "enforce_first_as": {"type": "bool"}, + "auto_local_addr": {"type": "bool"}, + "redistribute_internal": {"type": "bool"}, + "cluster_id": {"type": "str"}, + "control_plane_filter": {"type": "bool"}, + "confederation": { + "type": "dict", + "options": { + "peers": {"type": "str"}, + "identifier": {"type": "str"}, + }, + }, + }, + }, + "vlan": {"type": "int"}, + "update": { + "type": "dict", + "options": { + "wait_for": { + "type": "str", + "choices": [ + "wait_for_convergence", + "wait_install", + ], + }, + "batch_size": {"type": "int"}, + }, + }, + "vlan_aware_bundle": {"type": "str"}, + "aggregate_address": { + "elements": "dict", + "type": "list", + "options": { + "advertise_only": {"type": "bool"}, + "match_map": {"type": "str"}, + "attribute_map": {"type": "str"}, + "as_set": {"type": "bool"}, + "summary_only": {"type": "bool"}, + "address": {"type": "str"}, + }, + }, + "neighbor": { + "elements": "dict", + "type": "list", + "aliases": ["neighbors"], + "options": { + "bfd": {"type": "str", "choices": ["c_bit", "enable"]}, + "weight": {"type": "int"}, + "default_originate": { + "type": "dict", + "options": { + "route_map": {"type": "str"}, + "always": {"type": "bool"}, + }, + }, + "next_hop_v6_address": {"type": "str"}, + "route_reflector_client": {"type": "bool"}, + "ttl": {"type": "int"}, + "remove_private_as": { + "type": "dict", + "options": { + "all": {"type": "bool"}, + "set": {"type": "bool"}, + "replace_as": {"type": "bool"}, + }, + }, + "local_v6_addr": {"type": "str"}, + "transport": { + "type": "dict", + "options": { + "connection_mode": {"type": "str"}, + "remote_port": {"type": "int"}, + }, + }, + "next_hop_unchanged": {"type": "bool"}, + "monitoring": {"type": "bool"}, + "ebgp_multihop": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "ttl": {"type": "int"}, + }, + }, + "shutdown": {"type": "bool"}, + "fall_over": {"type": "bool"}, + "idle_restart_timer": {"type": "int"}, + "allowas_in": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + "additional_paths": { + "type": "str", + "choices": ["send", "receive"], + }, + "peer_group": {"type": "str"}, + "out_delay": {"type": "int"}, + "import_localpref": {"type": "int"}, + "prefix_list": { + "type": "dict", + "options": { + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + "name": {"type": "str"}, + }, + }, + "dont_capability_negotiate": {"type": "bool"}, + "update_source": {"type": "str"}, + "export_localpref": {"type": "int"}, + "local_as": { + "type": "dict", + "options": { + "as_number": {"type": "str"}, + "fallback": {"type": "bool"}, + }, + }, + "maximum_received_routes": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "warning_limit": { + "type": "dict", + "options": { + "limit_count": {"type": "int"}, + "limit_percent": {"type": "int"}, + }, + }, + "warning_only": {"type": "bool"}, + }, + }, + "encryption_password": { + "type": "dict", + "no_log": True, + "options": { + "password": {"type": "str", "no_log": True}, + "type": {"type": "int", "choices": [0, 7]}, + }, + }, + "link_bandwidth": { + "type": "dict", + "options": { + "default": {"type": "str"}, + "auto": {"type": "bool"}, + "set": {"type": "bool"}, + "update_delay": {"type": "int"}, + }, + }, + "graceful_restart_helper": {"type": "bool"}, + "neighbor_address": { + "type": "str", + "aliases": ["peer"], + }, + "next_hop_self": {"type": "bool"}, + "route_to_peer": {"type": "bool"}, + "soft_recognition": { + "type": "str", + "choices": ["all", "None"], + }, + "graceful_restart": {"type": "bool"}, + "enforce_first_as": {"type": "bool"}, + "send_community": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "community_attribute": {"type": "str"}, + "sub_attribute": { + "type": "str", + "choices": [ + "extended", + "link-bandwidth", + "standard", + ], + }, + "speed": {"type": "str"}, + "divide": { + "type": "str", + "choices": ["equal", "ratio"], + }, + "link_bandwidth_attribute": { + "type": "str", + "choices": ["aggregate", "divide"], + }, + }, + }, + "description": {"type": "str"}, + "maximum_accepted_routes": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "warning_limit": {"type": "int"}, + }, + }, + "auto_local_addr": {"type": "bool"}, + "metric_out": {"type": "int"}, + "timers": { + "type": "dict", + "options": { + "holdtime": {"type": "int"}, + "keepalive": {"type": "int"}, + }, + }, + "route_map": { + "type": "dict", + "options": { + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + "name": {"type": "str"}, + }, + }, + "remote_as": {"type": "str"}, + }, + }, + "graceful_restart": { + "type": "dict", + "options": { + "stalepath_time": {"type": "int"}, + "restart_time": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + "timers": { + "type": "dict", + "options": { + "holdtime": {"type": "int"}, + "keepalive": {"type": "int"}, + }, + }, + "distance": { + "type": "dict", + "options": { + "internal": {"type": "int"}, + "local": {"type": "int"}, + "external": {"type": "int"}, + }, + }, + "route_target": { + "type": "dict", + "options": { + "action": { + "type": "str", + "choices": ["both", "import", "export"], + }, + "target": {"type": "str"}, + }, + }, + "vrfs": { + "elements": "dict", + "type": "list", + "options": { + "access_group": { + "elements": "dict", + "type": "list", + "options": { + "direction": {"type": "str"}, + "afi": { + "type": "str", + "choices": ["ipv4", "ipv6"], + }, + "acl_name": {"type": "str"}, + }, + }, + "router_id": {"type": "str"}, + "vrf": {"type": "str"}, + "route_target": { + "type": "dict", + "options": { + "action": { + "type": "str", + "choices": ["both", "import", "export"], + }, + "type": { + "type": "str", + "choices": [ + "evpn", + "vpn-ipv4", + "vpn-ipv6", + ], + }, + "route_map": {"type": "str"}, + "target": {"type": "str"}, + "imported_route": {"type": "bool"}, + }, + }, + "redistribute": { + "elements": "dict", + "type": "list", + "options": { + "ospf_route": { + "type": "str", + "choices": [ + "internal", + "external", + "nssa_external_1", + "nssa_external_2", + ], + }, + "route_map": {"type": "str"}, + "protocol": { + "type": "str", + "choices": [ + "isis", + "ospfv3", + "ospf", + "attached-host", + "connected", + "rip", + "static", + ], + }, + "isis_level": { + "type": "str", + "choices": [ + "level-1", + "level-2", + "level-1-2", + ], + }, + }, + }, + "distance": { + "type": "dict", + "options": { + "internal": {"type": "int"}, + "local": {"type": "int"}, + "external": {"type": "int"}, + }, + }, + "default_metric": {"type": "int"}, + "bgp_params": { + "type": "dict", + "options": { + "control_plane_filter": {"type": "bool"}, + "convergence": { + "type": "dict", + "options": { + "slow_peer": {"type": "bool"}, + "time": {"type": "int"}, + }, + }, + "host_routes": {"type": "bool"}, + "transport": {"type": "int"}, + "next_hop_unchanged": {"type": "bool"}, + "missing_policy": { + "type": "dict", + "options": { + "action": { + "type": "str", + "choices": [ + "deny", + "permit", + "deny-in-out", + ], + }, + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + }, + }, + "monitoring": {"type": "bool"}, + "allowas_in": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + "additional_paths": { + "type": "str", + "choices": ["install", "send", "receive"], + }, + "advertise_inactive": {"type": "bool"}, + "listen": { + "type": "dict", + "options": { + "range": { + "type": "dict", + "options": { + "peer_group": { + "type": "dict", + "options": { + "peer_filter": { + "type": "str", + }, + "remote_as": { + "type": "str", + }, + "name": { + "type": "str", + }, + }, + }, + "address": {"type": "str"}, + }, + }, + "limit": {"type": "int"}, + }, + }, + "route_reflector": { + "type": "dict", + "options": { + "preserve": {"type": "bool"}, + "set": {"type": "bool"}, + }, + }, + "always_compare_med": {"type": "bool"}, + "client_to_client": {"type": "bool"}, + "bestpath": { + "type": "dict", + "options": { + "ecmp_fast": {"type": "bool"}, + "tie_break": { + "type": "str", + "choices": [ + "cluster_list_length", + "router_id", + ], + }, + "skip": {"type": "bool"}, + "as_path": { + "type": "str", + "choices": [ + "ignore", + "multipath_relax", + ], + }, + "med": { + "type": "dict", + "options": { + "confed": {"type": "bool"}, + "missing_as_worst": { + "type": "bool", + }, + }, + }, + }, + }, + "labeled_unicast": { + "type": "str", + "choices": ["ip", "tunnel"], + }, + "log_neighbor_changes": {"type": "bool"}, + "asn": { + "type": "str", + "choices": ["asdot", "asplain"], + }, + "default": { + "type": "str", + "choices": [ + "ipv4_unicast", + "ipv6_unicast", + ], + }, + "route": {"type": "str"}, + "enforce_first_as": {"type": "bool"}, + "auto_local_addr": {"type": "bool"}, + "redistribute_internal": {"type": "bool"}, + "cluster_id": {"type": "str"}, + "confederation": { + "type": "dict", + "options": { + "peers": {"type": "str"}, + "identifier": {"type": "str"}, + }, + }, + }, + }, + "update": { + "type": "dict", + "options": { + "wait_for": { + "type": "str", + "choices": [ + "wait_for_convergence", + "wait_install", + ], + }, + "batch_size": {"type": "int"}, + }, + }, + "aggregate_address": { + "elements": "dict", + "type": "list", + "options": { + "advertise_only": {"type": "bool"}, + "match_map": {"type": "str"}, + "attribute_map": {"type": "str"}, + "as_set": {"type": "bool"}, + "summary_only": {"type": "bool"}, + "address": {"type": "str"}, + }, + }, + "neighbor": { + "elements": "dict", + "aliases": ["neighbors"], + "type": "list", + "options": { + "bfd": { + "type": "str", + "choices": ["c_bit", "enable"], + }, + "weight": {"type": "int"}, + "default_originate": { + "type": "dict", + "options": { + "route_map": {"type": "str"}, + "always": {"type": "bool"}, + }, + }, + "next_hop_v6_address": {"type": "str"}, + "route_reflector_client": {"type": "bool"}, + "ttl": {"type": "int"}, + "remove_private_as": { + "type": "dict", + "options": { + "all": {"type": "bool"}, + "set": {"type": "bool"}, + "replace_as": {"type": "bool"}, + }, + }, + "local_v6_addr": {"type": "str"}, + "transport": { + "type": "dict", + "options": { + "connection_mode": {"type": "str"}, + "remote_port": {"type": "int"}, + }, + }, + "next_hop_unchanged": {"type": "bool"}, + "monitoring": {"type": "bool"}, + "ebgp_multihop": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "ttl": {"type": "int"}, + }, + }, + "shutdown": {"type": "bool"}, + "fall_over": {"type": "bool"}, + "idle_restart_timer": {"type": "int"}, + "allowas_in": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + "additional_paths": { + "type": "str", + "choices": ["send", "receive"], + }, + "peer_group": {"type": "str"}, + "out_delay": {"type": "int"}, + "import_localpref": {"type": "int"}, + "prefix_list": { + "type": "dict", + "options": { + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + "name": {"type": "str"}, + }, + }, + "dont_capability_negotiate": {"type": "bool"}, + "update_source": {"type": "str"}, + "export_localpref": {"type": "int"}, + "local_as": { + "type": "dict", + "options": { + "as_number": {"type": "str"}, + "fallback": {"type": "bool"}, + }, + }, + "maximum_received_routes": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "warning_limit": { + "type": "dict", + "options": { + "limit_count": {"type": "int"}, + "limit_percent": { + "type": "int", + }, + }, + }, + "warning_only": {"type": "bool"}, + }, + }, + "encryption_password": { + "type": "dict", + "no_log": True, + "options": { + "password": { + "type": "str", + "no_log": True, + }, + "type": { + "type": "int", + "choices": [0, 7], + }, + }, + }, + "link_bandwidth": { + "type": "dict", + "options": { + "default": {"type": "str"}, + "auto": {"type": "bool"}, + "set": {"type": "bool"}, + "update_delay": {"type": "int"}, + }, + }, + "graceful_restart_helper": {"type": "bool"}, + "neighbor_address": { + "type": "str", + "aliases": ["peer"], + }, + "next_hop_self": {"type": "bool"}, + "route_to_peer": {"type": "bool"}, + "soft_recognition": { + "type": "str", + "choices": ["all", "None"], + }, + "graceful_restart": {"type": "bool"}, + "enforce_first_as": {"type": "bool"}, + "send_community": { + "type": "dict", + "options": { + "community_attribute": {"type": "str"}, + "sub_attribute": { + "type": "str", + "choices": [ + "extended", + "link-bandwidth", + "standard", + ], + }, + "speed": {"type": "str"}, + "divide": { + "type": "str", + "choices": ["equal", "ratio"], + }, + "link_bandwidth_attribute": { + "type": "str", + "choices": ["aggregate", "divide"], + }, + }, + }, + "description": {"type": "str"}, + "maximum_accepted_routes": { + "type": "dict", + "options": { + "count": {"type": "int"}, + "warning_limit": {"type": "int"}, + }, + }, + "auto_local_addr": {"type": "bool"}, + "metric_out": {"type": "int"}, + "timers": { + "type": "dict", + "options": { + "holdtime": {"type": "int"}, + "keepalive": {"type": "int"}, + }, + }, + "route_map": { + "type": "dict", + "options": { + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + "name": {"type": "str"}, + }, + }, + "remote_as": {"type": "str"}, + }, + }, + "graceful_restart": { + "type": "dict", + "options": { + "stalepath_time": {"type": "int"}, + "restart_time": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + "timers": { + "type": "dict", + "options": { + "holdtime": {"type": "int"}, + "keepalive": {"type": "int"}, + }, + }, + "shutdown": {"type": "bool"}, + "graceful_restart_helper": {"type": "bool"}, + "ucmp": { + "type": "dict", + "options": { + "link_bandwidth": { + "type": "dict", + "options": { + "update_delay": {"type": "int"}, + "mode": { + "type": "str", + "choices": [ + "encoding_weighted", + "recursive", + "update_delay", + ], + }, + }, + }, + "fec": { + "type": "dict", + "options": { + "clear": {"type": "int"}, + "trigger": {"type": "int"}, + }, + }, + "mode": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "nexthops": {"type": "int"}, + }, + }, + }, + }, + "maximum_paths": { + "type": "dict", + "options": { + "max_equal_cost_paths": {"type": "int"}, + "max_installed_ecmp_paths": {"type": "int"}, + }, + }, + "network": { + "type": "list", + "aliases": ["networks"], + "elements": "dict", + "options": { + "route_map": {"type": "str"}, + "address": {"type": "str"}, + }, + }, + }, + }, + "access_group": { + "elements": "dict", + "type": "list", + "options": { + "direction": {"type": "str"}, + "afi": {"type": "str", "choices": ["ipv4", "ipv6"]}, + "acl_name": {"type": "str"}, + }, + }, + "graceful_restart_helper": {"type": "bool"}, + "ucmp": { + "type": "dict", + "options": { + "link_bandwidth": { + "type": "dict", + "options": { + "update_delay": {"type": "int"}, + "mode": { + "type": "str", + "choices": [ + "encoding_weighted", + "recursive", + ], + }, + }, + }, + "fec": { + "type": "dict", + "options": { + "clear": {"type": "int"}, + "trigger": {"type": "int"}, + }, + }, + "mode": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "nexthops": {"type": "int"}, + }, + }, + }, + }, + "shutdown": {"type": "bool"}, + "maximum_paths": { + "type": "dict", + "options": { + "max_equal_cost_paths": {"type": "int"}, + "max_installed_ecmp_paths": {"type": "int"}, + }, + }, + "network": { + "type": "list", + "aliases": ["networks"], + "elements": "dict", + "options": { + "route_map": {"type": "str"}, + "address": {"type": "str"}, + }, + }, + }, + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/facts/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/facts/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/facts/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/facts/facts.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/facts/facts.py new file mode 100644 index 000000000..beef509a8 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/facts/facts.py @@ -0,0 +1,24 @@ +# -*- 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 arg spec for the eos facts module. +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class FactsArgs(object): + """The arg spec for the eos facts module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "gather_subset": dict(default=["min"], type="list", elements="str"), + "gather_network_resources": dict(type="list", elements="str"), + "available_network_resources": {"type": "bool", "default": False}, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/hostname/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/hostname/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/hostname/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/hostname/hostname.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/hostname/hostname.py new file mode 100644 index 000000000..8e866d4dc --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/hostname/hostname.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the eos_hostname module +""" + + +class HostnameArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_hostname module""" + + argument_spec = { + "config": {"type": "dict", "options": {"hostname": {"type": "str"}}}, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/interfaces/interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/interfaces/interfaces.py new file mode 100644 index 000000000..bd861c6b5 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/interfaces/interfaces.py @@ -0,0 +1,72 @@ +# -*- 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 eos_interfaces module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class InterfacesArgs(object): + """The arg spec for the eos_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "name": {"required": True, "type": "str"}, + "description": {"required": False, "type": "str"}, + "enabled": { + "default": True, + "required": False, + "type": "bool", + }, + "mtu": {"required": False, "type": "int"}, + "speed": {"required": False, "type": "str"}, + "duplex": {"required": False, "type": "str"}, + "mode": {"choices": ["layer2", "layer3"], "type": "str"}, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "required": False, + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l2_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l2_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l2_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l2_interfaces/l2_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l2_interfaces/l2_interfaces.py new file mode 100644 index 000000000..bdf7e7451 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l2_interfaces/l2_interfaces.py @@ -0,0 +1,77 @@ +# -*- 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 eos_l2_interfaces module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class L2_interfacesArgs(object): + """The arg spec for the eos_l2_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "access": { + "options": {"vlan": {"type": "int"}}, + "type": "dict", + }, + "mode": {"type": "str", "choices": ["access", "trunk"]}, + "name": {"required": True, "type": "str"}, + "trunk": { + "options": { + "native_vlan": {"type": "int"}, + "trunk_allowed_vlans": { + "type": "list", + "elements": "str", + }, + }, + "type": "dict", + }, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "required": False, + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l3_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l3_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l3_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l3_interfaces/l3_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l3_interfaces/l3_interfaces.py new file mode 100644 index 000000000..a4ac42f63 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/l3_interfaces/l3_interfaces.py @@ -0,0 +1,76 @@ +# -*- 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 eos_l3_interfaces module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class L3_interfacesArgs(object): + """The arg spec for the eos_l3_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "name": {"required": True, "type": "str"}, + "ipv4": { + "elements": "dict", + "options": { + "address": {"type": "str"}, + "secondary": {"type": "bool"}, + "virtual": {"type": "bool"}, + }, + "type": "list", + }, + "ipv6": { + "elements": "dict", + "options": {"address": {"type": "str"}}, + "type": "list", + }, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp/lacp.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp/lacp.py new file mode 100644 index 000000000..bbb9df8db --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp/lacp.py @@ -0,0 +1,63 @@ +# -*- 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 eos_lacp module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class LacpArgs(object): + """The arg spec for the eos_lacp module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "options": { + "system": { + "options": {"priority": {"type": "int"}}, + "type": "dict", + }, + }, + "type": "dict", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp_interfaces/lacp_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp_interfaces/lacp_interfaces.py new file mode 100644 index 000000000..bea5a062b --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lacp_interfaces/lacp_interfaces.py @@ -0,0 +1,69 @@ +# +# -*- 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 eos_lacp_interfaces module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class Lacp_interfacesArgs(object): + """The arg spec for the eos_lacp_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "name": {"type": "str"}, + "port_priority": {"type": "int"}, + "timer": { + "choices": ["fast", "normal"], + "type": "str", + "aliases": ["rate"], + }, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lag_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lag_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lag_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lag_interfaces/lag_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lag_interfaces/lag_interfaces.py new file mode 100644 index 000000000..0f2aa73dd --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lag_interfaces/lag_interfaces.py @@ -0,0 +1,73 @@ +# -*- 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 eos_lag_interfaces module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class Lag_interfacesArgs(object): + """The arg spec for the eos_lag_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "name": {"required": True, "type": "str"}, + "members": { + "elements": "dict", + "options": { + "member": {"type": "str"}, + "mode": { + "choices": ["active", "on", "passive"], + "type": "str", + }, + }, + "type": "list", + }, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_global/lldp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_global/lldp_global.py new file mode 100644 index 000000000..cc0ea3190 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_global/lldp_global.py @@ -0,0 +1,75 @@ +# +# -*- 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 eos_lldp_global module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class Lldp_globalArgs(object): + """The arg spec for the eos_lldp_global module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "options": { + "holdtime": {"type": "int"}, + "reinit": {"type": "int"}, + "timer": {"type": "int"}, + "tlv_select": { + "options": { + "link_aggregation": {"type": "bool"}, + "management_address": {"type": "bool"}, + "max_frame_size": {"type": "bool"}, + "port_description": {"type": "bool"}, + "system_capabilities": {"type": "bool"}, + "system_description": {"type": "bool"}, + "system_name": {"type": "bool"}, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_interfaces/lldp_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_interfaces/lldp_interfaces.py new file mode 100644 index 000000000..fa0d409f0 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/lldp_interfaces/lldp_interfaces.py @@ -0,0 +1,65 @@ +# +# -*- 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 eos_lldp_interfaces module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class Lldp_interfacesArgs(object): + """The arg spec for the eos_lldp_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "name": {"type": "str"}, + "receive": {"type": "bool"}, + "transmit": {"type": "bool"}, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/logging_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/logging_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/logging_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/logging_global/logging_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/logging_global/logging_global.py new file mode 100644 index 000000000..7899a3933 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/logging_global/logging_global.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the eos_logging_global module +""" + + +class Logging_globalArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_logging_global module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "type": "dict", + "options": { + "buffered": { + "type": "dict", + "options": { + "severity": { + "type": "str", + "choices": [ + "alerts", + "critical", + "debugging", + "emergencies", + "errors", + "informational", + "notifications", + "warnings", + ], + }, + "buffer_size": {"type": "int"}, + }, + }, + "console": { + "type": "dict", + "options": { + "severity": { + "type": "str", + "choices": [ + "alerts", + "critical", + "debugging", + "emergencies", + "errors", + "informational", + "notifications", + "warnings", + ], + }, + }, + }, + "event": { + "type": "str", + "choices": [ + "link-status", + "port-channel", + "spanning-tree", + ], + }, + "facility": { + "type": "str", + "choices": [ + "auth", + "cron", + "daemon", + "kern", + "local0", + "local1", + "local2", + "local3", + "local4", + "local5", + "local6", + "local7", + "lpr", + "mail", + "news", + "sys10", + "sys11", + "sys12", + "sys13", + "sys14", + "sys9", + "syslog", + "user", + "uucp", + ], + }, + "format": { + "type": "dict", + "options": { + "hostname": {"type": "str"}, + "timestamp": { + "type": "dict", + "options": { + "high_resolution": {"type": "bool"}, + "traditional": { + "type": "dict", + "options": { + "state": { + "type": "str", + "choices": ["enabled", "disabled"], + }, + "timezone": {"type": "bool"}, + "year": {"type": "bool"}, + }, + }, + }, + }, + "sequence_numbers": {"type": "bool"}, + }, + }, + "hosts": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str"}, + "add": {"type": "bool"}, + "remove": {"type": "bool"}, + "protocol": {"type": "str", "choices": ["tcp", "udp"]}, + "port": {"type": "int"}, + }, + }, + "level": { + "type": "dict", + "options": { + "facility": {"type": "str"}, + "severity": { + "type": "str", + "choices": [ + "alerts", + "critical", + "debugging", + "emergencies", + "errors", + "informational", + "notifications", + "warnings", + ], + }, + }, + }, + "monitor": {"type": "str"}, + "turn_on": {"type": "bool"}, + "persistent": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "size": {"type": "int"}, + }, + }, + "policy": { + "type": "dict", + "options": { + "invert_result": {"type": "bool"}, + "match_list": {"type": "str"}, + }, + }, + "qos": {"type": "int"}, + "relogging_interval": {"type": "int"}, + "repeat_messages": {"type": "bool"}, + "source_interface": {"type": "str"}, + "synchronous": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "level": {"type": "str"}, + }, + }, + "trap": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "severity": { + "type": "str", + "choices": [ + "alerts", + "critical", + "debugging", + "emergencies", + "errors", + "informational", + "notifications", + "warnings", + ], + }, + }, + }, + "vrfs": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str"}, + "hosts": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str"}, + "add": {"type": "bool"}, + "remove": {"type": "bool"}, + "protocol": { + "type": "str", + "choices": ["tcp", "udp"], + }, + "port": {"type": "int"}, + }, + }, + "source_interface": {"type": "str"}, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ntp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ntp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ntp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ntp_global/ntp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ntp_global/ntp_global.py new file mode 100644 index 000000000..ad3a231dd --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ntp_global/ntp_global.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the eos_ntp_global module +""" + + +class Ntp_globalArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_ntp_global module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "type": "dict", + "options": { + "authenticate": { + "type": "dict", + "options": { + "enable": {"type": "bool"}, + "servers": {"type": "bool"}, + }, + }, + "authentication_keys": { + "type": "list", + "elements": "dict", + "no_log": False, + "options": { + "id": {"type": "int"}, + "algorithm": { + "type": "str", + "choices": ["md5", "sha1"], + }, + "encryption": {"type": "int", "choices": [0, 7]}, + "key": {"type": "str", "no_log": True}, + }, + }, + "local_interface": {"type": "str"}, + "qos_dscp": {"type": "int"}, + "serve": { + "type": "dict", + "options": { + "all": {"type": "bool"}, + "access_lists": { + "type": "list", + "elements": "dict", + "options": { + "afi": {"type": "str"}, + "acls": { + "type": "list", + "elements": "dict", + "options": { + "acl_name": {"type": "str"}, + "direction": { + "type": "str", + "choices": ["in", "out"], + }, + "vrf": {"type": "str"}, + }, + }, + }, + }, + }, + }, + "servers": { + "type": "list", + "elements": "dict", + "options": { + "vrf": {"type": "str"}, + "server": {"type": "str", "required": True}, + "burst": {"type": "bool"}, + "iburst": {"type": "bool"}, + "key_id": {"type": "int"}, + "local_interface": {"type": "str"}, + "source": {"type": "str"}, + "maxpoll": {"type": "int"}, + "minpoll": {"type": "int"}, + "prefer": {"type": "bool"}, + "version": {"type": "int"}, + }, + }, + "trusted_key": {"type": "str", "no_log": False}, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospf_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospf_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospf_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospf_interfaces/ospf_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospf_interfaces/ospf_interfaces.py new file mode 100644 index 000000000..3e8dcbc9f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospf_interfaces/ospf_interfaces.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# 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 eos_ospf_interfaces module +""" + + +class Ospf_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_ospf_interfaces module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "state": { + "default": "merged", + "type": "str", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "parsed", + "rendered", + ], + }, + "running_config": {"type": "str"}, + "config": { + "elements": "dict", + "type": "list", + "options": { + "name": {"type": "str"}, + "address_family": { + "elements": "dict", + "type": "list", + "options": { + "ip_params": { + "elements": "dict", + "type": "list", + "options": { + "retransmit_interval": {"type": "int"}, + "cost": {"type": "int"}, + "afi": { + "required": True, + "type": "str", + "choices": ["ipv4", "ipv6"], + }, + "area": { + "type": "dict", + "options": { + "area_id": { + "required": True, + "type": "str", + }, + }, + }, + "bfd": {"type": "bool"}, + "mtu_ignore": {"type": "bool"}, + "priority": {"type": "int"}, + "dead_interval": {"type": "int"}, + "hello_interval": {"type": "int"}, + "passive_interface": {"type": "bool"}, + "transmit_delay": {"type": "int"}, + "network": {"type": "str"}, + }, + }, + "encryption_v3": { + "type": "dict", + "options": { + "key": {"type": "str", "no_log": True}, + "algorithm": { + "type": "str", + "choices": ["md5", "sha1"], + }, + "encryption": { + "type": "str", + "choices": [ + "3des-cbc", + "aes-128-cbc", + "aes-192-cbc", + "aes-256-cbc", + "null", + ], + }, + "keytype": {"type": "str", "no_log": False}, + "spi": {"type": "int"}, + "passphrase": {"type": "str", "no_log": True}, + }, + }, + "cost": {"type": "int"}, + "afi": { + "required": True, + "type": "str", + "choices": ["ipv4", "ipv6"], + }, + "authentication_v2": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "message_digest": {"type": "bool"}, + }, + }, + "bfd": {"type": "bool"}, + "authentication_v3": { + "type": "dict", + "options": { + "key": {"type": "str", "no_log": True}, + "spi": {"type": "int"}, + "keytype": {"type": "str", "no_log": False}, + "passphrase": {"type": "str", "no_log": True}, + "algorithm": { + "type": "str", + "choices": ["md5", "sha1"], + }, + }, + }, + "retransmit_interval": {"type": "int"}, + "message_digest_key": { + "no_log": False, + "type": "dict", + "options": { + "key_id": {"type": "int"}, + "key": {"type": "str", "no_log": True}, + "encryption": {"type": "str"}, + }, + }, + "mtu_ignore": {"type": "bool"}, + "priority": {"type": "int"}, + "area": { + "type": "dict", + "options": { + "area_id": {"required": True, "type": "str"}, + }, + }, + "dead_interval": {"type": "int"}, + "shutdown": {"type": "bool"}, + "passive_interface": {"type": "bool"}, + "authentication_key": { + "type": "dict", + "no_log": False, + "options": { + "encryption": {"type": "str"}, + "key": {"type": "str", "no_log": True}, + }, + }, + "hello_interval": {"type": "int"}, + "transmit_delay": {"type": "int"}, + "network": {"type": "str"}, + }, + }, + }, + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv2/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv2/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv2/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv2/ospfv2.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv2/ospfv2.py new file mode 100644 index 000000000..42388d8d5 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv2/ospfv2.py @@ -0,0 +1,340 @@ +# +# -*- 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) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# 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 eos_ospfv2 module +""" + + +class Ospfv2Args(object): # pylint: disable=R0903 + """The arg spec for the eos_ospfv2 module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "options": { + "processes": { + "elements": "dict", + "options": { + "process_id": {"type": "int"}, + "vrf": {"type": "str"}, + "traffic_engineering": {"type": "bool"}, + "adjacency": { + "options": { + "exchange_start": { + "options": {"threshold": {"type": "int"}}, + "type": "dict", + }, + }, + "type": "dict", + }, + "areas": { + "elements": "dict", + "options": { + "default_cost": {"type": "int"}, + "filter": { + "options": { + "address": {"type": "str"}, + "prefix_list": {"type": "str"}, + "subnet_address": {"type": "str"}, + "subnet_mask": {"type": "str"}, + }, + "type": "dict", + }, + "not_so_stubby": { + "options": { + "default_information_originate": { + "options": { + "metric": {"type": "int"}, + "metric_type": {"type": "int"}, + "nssa_only": {"type": "bool"}, + }, + "type": "dict", + }, + "no_summary": {"type": "bool"}, + "nssa_only": {"type": "bool"}, + "lsa": {"type": "bool"}, + "set": {"type": "bool"}, + }, + "type": "dict", + }, + "nssa": { + "options": { + "default_information_originate": { + "options": { + "metric": {"type": "int"}, + "metric_type": {"type": "int"}, + "nssa_only": {"type": "bool"}, + }, + "type": "dict", + }, + "no_summary": {"type": "bool"}, + "nssa_only": {"type": "bool"}, + "set": {"type": "bool"}, + }, + "type": "dict", + }, + "area_id": {"type": "str"}, + "range": { + "options": { + "address": {"type": "str"}, + "advertise": {"type": "bool"}, + "cost": {"type": "int"}, + "subnet_address": {"type": "str"}, + "subnet_mask": {"type": "str"}, + }, + "type": "dict", + }, + "stub": { + "options": { + "no_summary": {"type": "bool"}, + "set": {"type": "bool"}, + }, + "type": "dict", + }, + }, + "type": "list", + }, + "auto_cost": { + "options": { + "reference_bandwidth": {"type": "int"}, + }, + "type": "dict", + }, + "bfd": { + "options": {"all_interfaces": {"type": "bool"}}, + "type": "dict", + }, + "default_information": { + "options": { + "always": {"type": "bool"}, + "metric": {"type": "int"}, + "metric_type": {"type": "int"}, + "originate": {"type": "bool"}, + "route_map": {"type": "str"}, + }, + "type": "dict", + }, + "default_metric": {"type": "int"}, + "distance": { + "options": { + "external": {"type": "int"}, + "inter_area": {"type": "int"}, + "intra_area": {"type": "int"}, + }, + "type": "dict", + }, + "distribute_list": { + "options": { + "prefix_list": {"type": "str"}, + "route_map": {"type": "str"}, + }, + "type": "dict", + }, + "dn_bit_ignore": {"type": "bool"}, + "fips_restrictions": {"type": "str"}, + "graceful_restart": { + "options": { + "grace_period": {"type": "int"}, + "set": {"type": "bool"}, + }, + "type": "dict", + }, + "graceful_restart_helper": {"type": "bool"}, + "log_adjacency_changes": { + "options": {"detail": {"type": "bool"}}, + "type": "dict", + }, + "max_lsa": { + "options": { + "count": {"type": "int"}, + "ignore_count": {"type": "int"}, + "ignore_time": {"type": "int"}, + "reset_time": {"type": "int"}, + "threshold": {"type": "int"}, + "warning": {"type": "bool"}, + }, + "type": "dict", + }, + "max_metric": { + "options": { + "router_lsa": { + "options": { + "set": {"type": "bool"}, + "include_stub": {"type": "bool"}, + "on_startup": { + "options": { + "wait_period": {"type": "int"}, + }, + "type": "dict", + }, + "summary_lsa": { + "options": { + "max_metric_value": { + "type": "int", + }, + "set": {"type": "bool"}, + }, + "type": "dict", + }, + "external_lsa": { + "options": { + "max_metric_value": { + "type": "int", + }, + "set": {"type": "bool"}, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + "maximum_paths": {"type": "int"}, + "mpls_ldp": {"type": "bool"}, + "networks": { + "elements": "dict", + "options": { + "area": {"type": "str"}, + "mask": {"type": "str"}, + "network_address": {"type": "str"}, + "prefix": {"type": "str"}, + }, + "type": "list", + }, + "passive_interface": { + "type": "dict", + "options": { + "interface_list": {"type": "str"}, + "default": {"type": "bool"}, + }, + }, + "point_to_point": {"type": "bool"}, + "redistribute": { + "elements": "dict", + "options": { + "isis_level": {"type": "str"}, + "route_map": {"type": "str"}, + "routes": {"type": "str"}, + }, + "type": "list", + }, + "retransmission_threshold": {"type": "int"}, + "rfc1583compatibility": {"type": "bool"}, + "router_id": {"type": "str"}, + "shutdown": {"type": "bool"}, + "summary_address": { + "options": { + "address": {"type": "str"}, + "attribute_map": {"type": "str"}, + "mask": {"type": "str"}, + "not_advertise": {"type": "bool"}, + "prefix": {"type": "str"}, + "tag": {"type": "int"}, + }, + "type": "dict", + }, + "timers": { + "elements": "dict", + "options": { + "lsa": { + "options": { + "rx": { + "options": { + "min_interval": { + "type": "int", + }, + }, + "type": "dict", + }, + "tx": { + "options": { + "delay": { + "options": { + "initial": { + "type": "int", + }, + "max": {"type": "int"}, + "min": {"type": "int"}, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + }, + "type": "dict", + }, + "out_delay": {"type": "int"}, + "pacing": {"type": "int"}, + "spf": { + "options": { + "initial": {"type": "int"}, + "max": {"type": "int"}, + "min": {"type": "int"}, + "seconds": {"type": "int"}, + }, + "type": "dict", + }, + "throttle": { + "options": { + "attr": {"type": "str"}, + "initial": {"type": "int"}, + "max": {"type": "int"}, + "min": {"type": "int"}, + }, + "type": "dict", + }, + }, + "type": "list", + }, + }, + "type": "list", + }, + }, + "type": "dict", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv3/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv3/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv3/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv3/ospfv3.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv3/ospfv3.py new file mode 100644 index 000000000..ece4db0cd --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/ospfv3/ospfv3.py @@ -0,0 +1,532 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# 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 eos_ospfv3 module +""" + + +class Ospfv3Args(object): # pylint: disable=R0903 + """The arg spec for the eos_ospfv3 module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + }, + "config": { + "type": "dict", + "options": { + "processes": { + "elements": "dict", + "type": "list", + "options": { + "router_id": {"type": "str"}, + "shutdown": {"type": "bool"}, + "fips_restrictions": {"type": "bool"}, + "graceful_restart_helper": {"type": "bool"}, + "adjacency": { + "type": "dict", + "options": { + "exchange_start": { + "type": "dict", + "options": {"threshold": {"type": "int"}}, + }, + }, + }, + "max_metric": { + "type": "dict", + "options": { + "router_lsa": { + "type": "dict", + "options": { + "external_lsa": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "max_metric_value": { + "type": "int", + }, + }, + }, + "summary_lsa": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "max_metric_value": { + "type": "int", + }, + }, + }, + "set": {"type": "bool"}, + "on_startup": { + "type": "dict", + "options": { + "wait_for_bgp": { + "type": "bool", + }, + "wait_period": {"type": "int"}, + }, + }, + "include_stub": {"type": "bool"}, + }, + }, + }, + }, + "log_adjacency_changes": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "detail": {"type": "bool"}, + }, + }, + "graceful_restart": { + "type": "dict", + "options": { + "grace_period": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + "timers": { + "type": "dict", + "options": { + "throttle": { + "type": "dict", + "options": { + "max": {"type": "int"}, + "initial": {"type": "int"}, + "min": {"type": "int"}, + "spf": {"type": "bool"}, + "lsa": {"type": "bool"}, + }, + }, + "spf": { + "type": "dict", + "options": { + "max": {"type": "int"}, + "initial": {"type": "int"}, + "min": {"type": "int"}, + }, + }, + "lsa": { + "type": "raw", + "options": { + "max": {"type": "int"}, + "initial": {"type": "int"}, + "min": {"type": "int"}, + "direction": { + "type": "str", + "choices": ["rx", "tx"], + }, + }, + }, + "out_delay": {"type": "int"}, + "pacing": {"type": "int"}, + }, + }, + "vrf": {"type": "str"}, + "auto_cost": { + "type": "dict", + "options": { + "reference_bandwidth": {"type": "int"}, + }, + }, + "passive_interface": {"type": "bool"}, + "bfd": { + "type": "dict", + "options": {"all_interfaces": {"type": "bool"}}, + }, + "areas": { + "elements": "dict", + "type": "list", + "options": { + "area_id": {"type": "str"}, + "encryption": { + "type": "dict", + "options": { + "hidden_key": {"type": "bool"}, + "key": {"type": "str", "no_log": True}, + "algorithm": { + "type": "str", + "choices": ["sha1", "md5"], + }, + "encrypt_key": {"type": "bool"}, + "encryption": { + "type": "str", + "choices": [ + "3des-cbc", + "aes-128-cbc", + "aes-192-cbc", + "aes-256-cbc", + "null", + ], + }, + "spi": {"type": "int"}, + "passphrase": { + "type": "str", + "no_log": True, + }, + }, + }, + "nssa": { + "type": "dict", + "options": { + "translate": {"type": "bool"}, + "default_information_originate": { + "type": "dict", + "options": { + "metric_type": {"type": "int"}, + "metric": {"type": "int"}, + "nssa_only": {"type": "bool"}, + "set": {"type": "bool"}, + }, + }, + "nssa_only": {"type": "bool"}, + "set": {"type": "bool"}, + "no_summary": {"type": "bool"}, + }, + }, + "stub": { + "type": "dict", + "options": { + "summary_lsa": {"type": "bool"}, + "set": {"type": "bool"}, + }, + }, + "default_cost": {"type": "int"}, + "authentication": { + "type": "dict", + "options": { + "hidden_key": {"type": "bool"}, + "key": {"type": "str", "no_log": True}, + "algorithm": { + "type": "str", + "choices": ["md5", "sha1"], + }, + "encrypt_key": {"type": "bool"}, + "spi": {"type": "int"}, + "passphrase": { + "type": "str", + "no_log": True, + }, + }, + }, + }, + }, + "address_family": { + "elements": "dict", + "type": "list", + "options": { + "router_id": {"type": "str"}, + "distance": {"type": "int"}, + "redistribute": { + "elements": "dict", + "type": "list", + "options": { + "routes": { + "type": "str", + "choices": [ + "bgp", + "connected", + "static", + ], + }, + "route_map": {"type": "str"}, + }, + }, + "default_information": { + "type": "dict", + "options": { + "metric_type": {"type": "int"}, + "always": {"type": "bool"}, + "metric": {"type": "int"}, + "originate": {"type": "bool"}, + "route_map": {"type": "str"}, + }, + }, + "afi": { + "choices": ["ipv4", "ipv6"], + "type": "str", + }, + "fips_restrictions": {"type": "bool"}, + "default_metric": {"type": "int"}, + "maximum_paths": {"type": "int"}, + "adjacency": { + "type": "dict", + "options": { + "exchange_start": { + "type": "dict", + "options": { + "threshold": {"type": "int"}, + }, + }, + }, + }, + "max_metric": { + "type": "dict", + "options": { + "router_lsa": { + "type": "dict", + "options": { + "external_lsa": { + "type": "dict", + "options": { + "set": { + "type": "bool", + }, + "max_metric_value": { + "type": "int", + }, + }, + }, + "summary_lsa": { + "type": "dict", + "options": { + "set": { + "type": "bool", + }, + "max_metric_value": { + "type": "int", + }, + }, + }, + "set": {"type": "bool"}, + "on_startup": { + "type": "dict", + "options": { + "wait_for_bgp": { + "type": "bool", + }, + "wait_period": { + "type": "int", + }, + }, + }, + "include_stub": { + "type": "bool", + }, + }, + }, + }, + }, + "log_adjacency_changes": { + "type": "dict", + "options": { + "set": {"type": "bool"}, + "detail": {"type": "bool"}, + }, + }, + "timers": { + "type": "dict", + "options": { + "throttle": { + "type": "dict", + "options": { + "max": {"type": "int"}, + "initial": {"type": "int"}, + "min": {"type": "int"}, + "spf": {"type": "bool"}, + "lsa": {"type": "bool"}, + }, + }, + "spf": { + "type": "dict", + "options": { + "max": {"type": "int"}, + "initial": {"type": "int"}, + "min": {"type": "int"}, + }, + }, + "lsa": { + "type": "raw", + "options": { + "max": {"type": "int"}, + "initial": {"type": "int"}, + "min": {"type": "int"}, + "direction": { + "type": "str", + "choices": ["rx", "tx"], + }, + }, + }, + "out_delay": {"type": "int"}, + "pacing": {"type": "int"}, + }, + }, + "shutdown": {"type": "bool"}, + "auto_cost": { + "type": "dict", + "options": { + "reference_bandwidth": {"type": "int"}, + }, + }, + "graceful_restart_helper": {"type": "bool"}, + "passive_interface": {"type": "bool"}, + "bfd": { + "type": "dict", + "options": { + "all_interfaces": {"type": "bool"}, + }, + }, + "areas": { + "elements": "dict", + "type": "list", + "options": { + "ranges": { + "elements": "dict", + "type": "list", + "options": { + "subnet_mask": {"type": "str"}, + "advertise": {"type": "bool"}, + "cost": {"type": "int"}, + "subnet_address": { + "type": "str", + }, + "address": {"type": "str"}, + }, + }, + "area_id": {"type": "str"}, + "encryption": { + "type": "dict", + "options": { + "hidden_key": {"type": "bool"}, + "key": { + "type": "str", + "no_log": True, + }, + "algorithm": { + "type": "str", + "choices": ["sha1", "md5"], + }, + "encrypt_key": { + "type": "bool", + }, + "encryption": { + "type": "str", + "choices": [ + "3des-cbc", + "aes-128-cbc", + "aes-192-cbc", + "aes-256-cbc", + "null", + ], + }, + "spi": {"type": "int"}, + "passphrase": { + "type": "str", + "no_log": True, + }, + }, + }, + "nssa": { + "type": "dict", + "options": { + "translate": {"type": "bool"}, + "default_information_originate": { + "type": "dict", + "options": { + "metric_type": { + "type": "int", + }, + "metric": { + "type": "int", + }, + "nssa_only": { + "type": "bool", + }, + "set": { + "type": "bool", + }, + }, + }, + "nssa_only": {"type": "bool"}, + "set": {"type": "bool"}, + "no_summary": {"type": "bool"}, + }, + }, + "stub": { + "type": "dict", + "options": { + "summary_lsa": { + "type": "bool", + }, + "set": {"type": "bool"}, + }, + }, + "default_cost": {"type": "int"}, + "authentication": { + "type": "dict", + "options": { + "hidden_key": {"type": "bool"}, + "key": { + "type": "str", + "no_log": True, + }, + "algorithm": { + "type": "str", + "choices": ["md5", "sha1"], + }, + "encrypt_key": { + "type": "bool", + }, + "spi": {"type": "int"}, + "passphrase": { + "type": "str", + "no_log": True, + }, + }, + }, + }, + }, + "graceful_restart": { + "type": "dict", + "options": { + "grace_period": {"type": "int"}, + "set": {"type": "bool"}, + }, + }, + }, + }, + }, + }, + }, + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/prefix_lists/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/prefix_lists/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/prefix_lists/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/prefix_lists/prefix_lists.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/prefix_lists/prefix_lists.py new file mode 100644 index 000000000..665c899a2 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/prefix_lists/prefix_lists.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the eos_prefix_lists module +""" + + +class Prefix_listsArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_prefix_lists module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "type": "list", + "elements": "dict", + "options": { + "afi": { + "type": "str", + "required": True, + "choices": ["ipv4", "ipv6"], + }, + "prefix_lists": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "entries": { + "type": "list", + "elements": "dict", + "options": { + "action": { + "type": "str", + "choices": ["deny", "permit"], + }, + "address": {"type": "str"}, + "match": { + "type": "dict", + "options": { + "operator": { + "type": "str", + "choices": ["eq", "le", "ge"], + }, + "masklen": {"type": "int"}, + }, + }, + "sequence": {"type": "int"}, + "resequence": { + "type": "dict", + "options": { + "default": {"type": "bool"}, + "start_seq": {"type": "int"}, + "step": {"type": "int"}, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/route_maps/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/route_maps/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/route_maps/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/route_maps/route_maps.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/route_maps/route_maps.py new file mode 100644 index 000000000..ab6cb7574 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/route_maps/route_maps.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# 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 eos_route_maps module +""" + + +class Route_mapsArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_route_maps module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "running_config": {"type": "str"}, + "state": { + "default": "merged", + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + }, + "config": { + "elements": "dict", + "type": "list", + "options": { + "route_map": {"type": "str"}, + "entries": { + "elements": "dict", + "type": "list", + "options": { + "set": { + "type": "dict", + "options": { + "extcommunity": { + "type": "dict", + "options": { + "rt": { + "type": "dict", + "options": { + "vpn": {"type": "str"}, + "additive": {"type": "bool"}, + "delete": {"type": "bool"}, + }, + }, + "none": {"type": "bool"}, + "soo": { + "type": "dict", + "options": { + "vpn": {"type": "str"}, + "additive": {"type": "bool"}, + "delete": {"type": "bool"}, + }, + }, + "lbw": { + "type": "dict", + "options": { + "aggregate": {"type": "bool"}, + "divide": { + "type": "str", + "choices": [ + "equal", + "ration", + ], + }, + "value": {"type": "str"}, + }, + }, + }, + }, + "origin": { + "type": "str", + "choices": ["egp", "igp", "incomplete"], + }, + "isis_level": {"type": "str"}, + "weight": {"type": "int"}, + "distance": {"type": "int"}, + "ip": { + "type": "dict", + "options": { + "peer_address": {"type": "bool"}, + "unchanged": {"type": "bool"}, + "address": {"type": "str"}, + }, + }, + "metric": { + "type": "dict", + "options": { + "add": { + "type": "str", + "choices": [ + "igp-metric", + "igp-nexthop-cost", + ], + }, + "igp_param": { + "type": "str", + "choices": [ + "igp-metric", + "igp-nexthop-cost", + ], + }, + "value": {"type": "str"}, + }, + }, + "nexthop": { + "type": "dict", + "options": { + "value": {"type": "int"}, + "max_metric": {"type": "bool"}, + }, + }, + "as_path": { + "type": "dict", + "options": { + "match": { + "type": "dict", + "options": { + "as_number": {"type": "str"}, + "none": {"type": "bool"}, + }, + }, + "prepend": { + "type": "dict", + "options": { + "last_as": {"type": "int"}, + "as_number": {"type": "str"}, + }, + }, + }, + }, + "community_attributes": { + "type": "dict", + "options": { + "none": {"type": "bool"}, + "graceful_shutdown": {"type": "bool"}, + "community": { + "type": "dict", + "options": { + "additive": {"type": "bool"}, + "local_as": {"type": "bool"}, + "no_export": {"type": "bool"}, + "list": {"type": "str"}, + "number": {"type": "str"}, + "no_advertise": { + "type": "bool", + }, + "internet": {"type": "bool"}, + "graceful_shutdown": { + "type": "bool", + }, + "delete": {"type": "bool"}, + }, + }, + }, + }, + "bgp": {"type": "int"}, + "tag": {"type": "int"}, + "local_preference": {"type": "int"}, + "segment_index": {"type": "int"}, + "ipv6": { + "type": "dict", + "options": { + "peer_address": {"type": "bool"}, + "unchanged": {"type": "bool"}, + "address": {"type": "str"}, + }, + }, + "metric_type": { + "type": "str", + "choices": ["type-1", "type-2"], + }, + "evpn": {"type": "bool"}, + }, + }, + "description": {"type": "str"}, + "sequence": {"type": "int"}, + "source": {"type": "dict"}, + "continue_sequence": {"type": "int"}, + "statement": {"type": "str"}, + "action": { + "type": "str", + "choices": ["deny", "permit"], + }, + "sub_route_map": { + "type": "dict", + "options": { + "name": {"type": "str"}, + "invert_result": {"type": "bool"}, + }, + }, + "match": { + "type": "dict", + "options": { + "extcommunity": { + "type": "dict", + "options": { + "community_list": {"type": "str"}, + "exact_match": {"type": "bool"}, + }, + }, + "router_id": {"type": "str"}, + "invert_result": { + "type": "dict", + "options": { + "extcommunity": { + "type": "dict", + "options": { + "community_list": { + "type": "str", + }, + "exact_match": { + "type": "bool", + }, + }, + }, + "large_community": { + "type": "dict", + "options": { + "community_list": { + "type": "str", + }, + "exact_match": { + "type": "bool", + }, + }, + }, + "aggregate_role": { + "type": "dict", + "options": { + "contributor": { + "type": "bool", + }, + "route_map": {"type": "str"}, + }, + }, + "as_path": { + "type": "dict", + "options": { + "path_list": {"type": "str"}, + "length": {"type": "str"}, + }, + }, + "community": { + "type": "dict", + "options": { + "community_list": { + "type": "str", + }, + "instances": {"type": "str"}, + "exact_match": { + "type": "bool", + }, + }, + }, + }, + }, + "large_community": { + "type": "dict", + "options": { + "community_list": {"type": "str"}, + "exact_match": {"type": "bool"}, + }, + }, + "ip": { + "type": "dict", + "options": { + "resolved_next_hop": {"type": "str"}, + "next_hop": {"type": "str"}, + "address": { + "type": "dict", + "options": { + "prefix_list": {"type": "str"}, + "dynamic": {"type": "bool"}, + "access_list": {"type": "str"}, + }, + }, + }, + }, + "aggregate_role": { + "type": "dict", + "options": { + "contributor": {"type": "bool"}, + "route_map": {"type": "str"}, + }, + }, + "isis_level": {"type": "str"}, + "community": { + "type": "dict", + "options": { + "community_list": {"type": "str"}, + "instances": {"type": "str"}, + "exact_match": {"type": "bool"}, + }, + }, + "as_path": { + "type": "dict", + "options": { + "path_list": {"type": "str"}, + "length": {"type": "str"}, + }, + }, + "route_type": {"type": "str"}, + "as": {"type": "int"}, + "tag": {"type": "int"}, + "local_preference": {"type": "int"}, + "ipv6": { + "type": "dict", + "options": { + "resolved_next_hop": {"type": "str"}, + "next_hop": {"type": "str"}, + "address": { + "type": "dict", + "options": { + "prefix_list": {"type": "str"}, + "dynamic": {"type": "bool"}, + "access_list": {"type": "str"}, + }, + }, + }, + }, + "metric_type": { + "type": "str", + "choices": ["type-1", "type-2"], + }, + "interface": {"type": "str"}, + "source_protocol": {"type": "str"}, + "metric": {"type": "int"}, + }, + }, + }, + }, + }, + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/snmp_server/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/snmp_server/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/snmp_server/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/snmp_server/snmp_server.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/snmp_server/snmp_server.py new file mode 100644 index 000000000..654201e44 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/snmp_server/snmp_server.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the eos_snmp_server module +""" + + +class Snmp_serverArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_snmp_server module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "chassis_id": {"type": "str"}, + "communities": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str"}, + "acl_v4": {"type": "str"}, + "acl_v6": {"type": "str"}, + "ro": {"type": "bool"}, + "rw": {"type": "bool"}, + "view": {"type": "str"}, + }, + }, + "contact": {"type": "str"}, + "traps": { + "type": "dict", + "options": { + "bgp": { + "type": "dict", + "options": { + "arista_backward_transition": {"type": "bool"}, + "arista_established": {"type": "bool"}, + "backward_transition": {"type": "bool"}, + "established": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "bridge": { + "type": "dict", + "options": { + "arista_mac_age": {"type": "bool"}, + "arista_mac_learn": {"type": "bool"}, + "arista_mac_move": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "capacity": { + "type": "dict", + "options": { + "arista_hardware_utilization_alert": { + "type": "bool", + }, + "enabled": {"type": "bool"}, + }, + }, + "entity": { + "type": "dict", + "options": { + "arista_ent_sensor_alarm": {"type": "bool"}, + "ent_config_change": {"type": "bool"}, + "ent_state_oper": {"type": "bool"}, + "ent_state_oper_disabled": {"type": "bool"}, + "ent_state_oper_enabled": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "external_alarm": { + "type": "dict", + "options": { + "arista_external_alarm_asserted_notif": { + "type": "bool", + }, + "arista_external_alarm_deasserted_notif": { + "type": "bool", + }, + "enabled": {"type": "bool"}, + }, + }, + "isis": { + "type": "dict", + "options": { + "adjacency_change": {"type": "bool"}, + "area_mismatch": {"type": "bool"}, + "attempt_to_exceed_max_sequence": { + "type": "bool", + }, + "authentication_type_failure": { + "type": "bool", + }, + "database_overload": {"type": "bool"}, + "own_lsp_purge": {"type": "bool"}, + "rejected_adjacency": {"type": "bool"}, + "sequence_number_skip": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "lldp": { + "type": "dict", + "options": { + "rem_tables_change": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "mpls_ldp": { + "type": "dict", + "options": { + "mpls_ldp_session_down": {"type": "bool"}, + "mpls_ldp_session_up": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "msdp": { + "type": "dict", + "options": { + "backward_transition": {"type": "bool"}, + "established": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "ospf": { + "type": "dict", + "options": { + "if_config_error": {"type": "bool"}, + "if_auth_failure": {"type": "bool"}, + "if_state_change": {"type": "bool"}, + "nbr_state_change": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "ospfv3": { + "type": "dict", + "options": { + "if_config_error": {"type": "bool"}, + "if_rx_bad_packet": {"type": "bool"}, + "if_state_change": {"type": "bool"}, + "nbr_state_change": {"type": "bool"}, + "nbr_restart_helper_status_change": { + "type": "bool", + }, + "nssa_translator_status_change": { + "type": "bool", + }, + "restart_status_change": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "pim": { + "type": "dict", + "options": { + "neighbor_loss": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "snmp": { + "type": "dict", + "options": { + "authentication": {"type": "bool"}, + "link_down": {"type": "bool"}, + "link_up": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "snmpConfigManEvent": { + "type": "dict", + "options": { + "arista_config_man_event": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "switchover": { + "type": "dict", + "options": { + "arista_redundancy_switch_over_notif": { + "type": "bool", + }, + "enabled": {"type": "bool"}, + }, + }, + "test": { + "type": "dict", + "options": { + "arista_test_notification": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + "vrrp": { + "type": "dict", + "options": { + "trap_new_master": {"type": "bool"}, + "enabled": {"type": "bool"}, + }, + }, + }, + }, + "engineid": { + "type": "dict", + "options": { + "local": {"type": "str"}, + "remote": { + "type": "dict", + "options": { + "host": {"type": "str"}, + "udp_port": {"type": "int"}, + "id": {"type": "str"}, + }, + }, + }, + }, + "extension": { + "type": "dict", + "options": { + "root_oid": {"type": "str"}, + "script_location": {"type": "str"}, + "oneshot": {"type": "bool"}, + }, + }, + "groups": { + "type": "list", + "elements": "dict", + "options": { + "group": {"type": "str"}, + "version": { + "type": "str", + "choices": ["v1", "v3", "v2c"], + }, + "auth_privacy": { + "type": "str", + "choices": ["auth", "noauth", "priv"], + }, + "context": {"type": "str"}, + "notify": {"type": "str"}, + "read": {"type": "str"}, + "write": {"type": "str"}, + }, + }, + "hosts": { + "type": "list", + "elements": "dict", + "options": { + "host": {"type": "str"}, + "user": {"type": "str"}, + "udp_port": {"type": "int"}, + "informs": {"type": "bool"}, + "traps": {"type": "bool"}, + "version": { + "type": "str", + "choices": [ + "1", + "2c", + "3 auth", + "3 noauth", + "3 priv", + ], + }, + "vrf": {"type": "str"}, + }, + }, + "acls": { + "type": "list", + "elements": "dict", + "options": { + "afi": {"type": "str", "choices": ["ipv4", "ipv6"]}, + "acl": {"type": "str"}, + "vrf": {"type": "str"}, + }, + }, + "local_interface": {"type": "str"}, + "location": {"type": "str"}, + "notification": {"type": "int"}, + "objects": { + "type": "dict", + "options": { + "mac_address_tables": {"type": "bool"}, + "route_tables": {"type": "bool"}, + }, + }, + "qos": {"type": "int"}, + "qosmib": {"type": "int"}, + "transmit": {"type": "int"}, + "transport": {"type": "str"}, + "users": { + "type": "list", + "elements": "dict", + "options": { + "user": {"type": "str"}, + "group": {"type": "str"}, + "remote": {"type": "str"}, + "udp_port": {"type": "int"}, + "version": { + "type": "str", + "choices": ["v1", "v2c", "v3"], + }, + "auth": { + "type": "dict", + "options": { + "algorithm": {"type": "str"}, + "auth_passphrase": { + "type": "str", + "no_log": True, + }, + "encryption": {"type": "str"}, + "priv_passphrase": { + "type": "str", + "no_log": True, + }, + }, + }, + "localized": { + "type": "dict", + "options": { + "engineid": {"type": "str"}, + "algorithm": {"type": "str"}, + "auth_passphrase": { + "type": "str", + "no_log": True, + }, + "encryption": {"type": "str"}, + "priv_passphrase": { + "type": "str", + "no_log": True, + }, + }, + }, + }, + }, + "views": { + "type": "list", + "elements": "dict", + "options": { + "view": {"type": "str"}, + "mib": {"type": "str"}, + "action": { + "type": "str", + "choices": ["excluded", "included"], + }, + }, + }, + "vrfs": { + "type": "list", + "elements": "dict", + "options": { + "vrf": {"type": "str"}, + "local_interface": {"type": "str"}, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/static_routes/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/static_routes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/static_routes/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/static_routes/static_routes.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/static_routes/static_routes.py new file mode 100644 index 000000000..86d790606 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/static_routes/static_routes.py @@ -0,0 +1,97 @@ +# +# -*- 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 eos_static_routes module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class Static_routesArgs(object): + """The arg spec for the eos_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": { + "dest": {"required": True, "type": "str"}, + "next_hops": { + "elements": "dict", + "options": { + "admin_distance": {"type": "int"}, + "description": {"type": "str"}, + "forward_router_address": { + "type": "str", + }, + "interface": {"type": "str"}, + "nexthop_grp": {"type": "str"}, + "mpls_label": {"type": "int"}, + "tag": {"type": "int"}, + "track": {"type": "str"}, + "vrf": {"type": "str"}, + }, + "type": "list", + }, + }, + "type": "list", + }, + }, + "type": "list", + }, + "vrf": {"type": "str"}, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/vlans/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/vlans/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/vlans/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/vlans/vlans.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/vlans/vlans.py new file mode 100644 index 000000000..5bdefddb0 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/argspec/vlans/vlans.py @@ -0,0 +1,64 @@ +# -*- 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 eos_vlans module +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +class VlansArgs(object): + """The arg spec for the eos_vlans module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "vlan_id": {"required": True, "type": "int"}, + "name": {"type": "str"}, + "state": {"choices": ["active", "suspend"], "type": "str"}, + }, + "type": "list", + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + "type": "str", + }, + } diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acl_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acl_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acl_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acl_interfaces/acl_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acl_interfaces/acl_interfaces.py new file mode 100644 index 000000000..c76e79595 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acl_interfaces/acl_interfaces.py @@ -0,0 +1,481 @@ +# +# -*- 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 eos_acl_interfaces 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 + +import itertools + +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 ( + search_obj_in_list, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) + + +class Acl_interfaces(ConfigBase): + """ + The eos_acl_interfaces class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["acl_interfaces"] + + def __init__(self, module): + super(Acl_interfaces, self).__init__(module) + + def get_acl_interfaces_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, + ) + acl_interfaces_facts = facts["ansible_network_resources"].get( + "acl_interfaces", + ) + if not acl_interfaces_facts: + return [] + return acl_interfaces_facts + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + warnings = list() + commands = list() + changed = False + + if self.state in self.ACTION_STATES: + existing_acl_interfaces_facts = self.get_acl_interfaces_facts() + else: + existing_acl_interfaces_facts = [] + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_acl_interfaces_facts)) + if commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + self._connection.edit_config(commands) + changed = True + if changed: + 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_acl_interfaces_facts = self.get_acl_interfaces_facts() + elif self.state == "rendered": + result["rendered"] = commands + elif self.state == "parsed": + if not self._module.params["running_config"]: + self._module.fail_json( + msg="Value of running_config parameter must not be empty for state parsed", + ) + result["parsed"] = self.get_acl_interfaces_facts( + data=self._module.params["running_config"], + ) + else: + changed_acl_interfaces_facts = [] + if self.state in self.ACTION_STATES: + result["before"] = existing_acl_interfaces_facts + if result["changed"]: + result["after"] = changed_acl_interfaces_facts + elif self.state == "gathered": + result["gathered"] = changed_acl_interfaces_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_acl_interfaces_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_acl_interfaces_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") and not want: + self._module.fail_json( + msg="value of config parameter must not be empty for state {0}".format( + self.state, + ), + ) + state = self._module.params["state"] + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or self.state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + 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 + """ + commandset = [] + want_interface = [] + for w in want: + commands = [] + diff_access_group = [] + want_interface.append(w["name"]) + obj_in_have = search_obj_in_list(w["name"], have, "name") + if not obj_in_have or "access_groups" not in obj_in_have.keys(): + commands.append(add_commands(w["access_groups"], w["name"])) + else: + if "access_groups" in obj_in_have.keys(): + obj = self.get_acl_diff(obj_in_have, w) + if obj[0]: + to_delete = { + "access_groups": [{"acls": obj[0], "afi": "ipv4"}], + } + commands.append(remove_commands(to_delete, w["name"])) + if obj[1]: + to_delete = { + "access_groups": [{"acls": obj[1], "afi": "ipv6"}], + } + commands.append(remove_commands(to_delete, w["name"])) + diff = self.get_acl_diff(w, obj_in_have) + if diff[0]: + diff_access_group.append( + {"afi": "ipv4", "acls": diff[0]}, + ) + if diff[1]: + diff_access_group.append( + {"afi": "ipv6", "acls": diff[1]}, + ) + if diff_access_group: + commands.append( + add_commands(diff_access_group, w["name"]), + ) + if commands: + intf_command = ["interface " + w["name"]] + commands = list(itertools.chain(*commands)) + commandset.append(intf_command) + commandset.append(commands) + + if commandset: + commandset = list(itertools.chain(*commandset)) + return commandset + + 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 + """ + commandset = [] + want_interface = [] + for w in want: + commands = [] + diff_access_group = [] + want_interface.append(w["name"]) + obj_in_have = search_obj_in_list(w["name"], have, "name") + if not obj_in_have or "access_groups" not in obj_in_have.keys(): + commands.append(add_commands(w["access_groups"], w["name"])) + else: + if "access_groups" in obj_in_have.keys(): + obj = self.get_acl_diff(obj_in_have, w) + if obj[0]: + to_delete = { + "access_groups": [{"acls": obj[0], "afi": "ipv4"}], + } + commands.append(remove_commands(to_delete, w["name"])) + if obj[1]: + to_delete = { + "access_groups": [{"acls": obj[1], "afi": "ipv6"}], + } + commands.append(remove_commands(to_delete, w["name"])) + diff = self.get_acl_diff(w, obj_in_have) + if diff[0]: + diff_access_group.append( + {"afi": "ipv4", "acls": diff[0]}, + ) + if diff[1]: + diff_access_group.append( + {"afi": "ipv6", "acls": diff[1]}, + ) + if diff_access_group: + commands.append( + add_commands(diff_access_group, w["name"]), + ) + if commands: + intf_command = ["interface " + w["name"]] + commands = list(itertools.chain(*commands)) + commandset.append(intf_command) + commandset.append(commands) + for h in have: + commands = [] + if "access_groups" in h.keys() and h["access_groups"]: + if h["name"] not in want_interface: + for h_group in h["access_groups"]: + to_delete = {"access_groups": [h_group]} + commands.append(remove_commands(to_delete, h["name"])) + if commands: + intf_command = ["interface " + h["name"]] + commands = list(itertools.chain(*commands)) + commandset.append(intf_command) + commandset.append(commands) + + if commandset: + commandset = list(itertools.chain(*commandset)) + + return commandset + + 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 + """ + commandset = [] + for w in want: + commands = [] + diff_access_group = [] + obj_in_have = search_obj_in_list(w["name"], have, "name") + if not obj_in_have: + commands = add_commands(w["access_groups"], w["name"]) + else: + if "access_groups" in obj_in_have.keys(): + diff = self.get_acl_diff(w, obj_in_have) + if diff[0]: + diff_access_group.append( + {"afi": "ipv4", "acls": diff[0]}, + ) + if diff[1]: + diff_access_group.append( + {"afi": "ipv6", "acls": diff[1]}, + ) + if diff_access_group: + commands = add_commands(diff_access_group, w["name"]) + else: + commands = add_commands(w["access_groups"], w["name"]) + if commands: + intf_command = ["interface " + w["name"]] + commandset.append(intf_command) + commandset.append(commands) + if commandset: + commandset = list(itertools.chain(*commandset)) + return commandset + + 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 + """ + commandset = [] + for w in want: + commands = [] + intf_command = ["interface " + w["name"]] + obj_in_have = search_obj_in_list(w["name"], have, "name") + if "access_groups" not in w.keys() or not w["access_groups"]: + commands = remove_commands(obj_in_have, w["name"]) + if w["access_groups"]: + for w_grp in w["access_groups"]: + if "acls" not in w_grp.keys() or not w_grp["acls"]: + obj = self.get_acls_from_afi( + w["name"], + w_grp["afi"], + have, + ) + to_delete = { + "access_groups": [ + {"acls": obj, "afi": w_grp["afi"]}, + ], + } + commands = remove_commands(to_delete, w["name"]) + else: + if ( + "access_groups" not in obj_in_have.keys() + or not obj_in_have["access_groups"] + ): + continue + group = {"access_groups": [w_grp]} + obj = self.get_acl_diff(group, obj_in_have, True) + if obj[0]: + to_delete = { + "access_groups": [ + {"acls": obj[0], "afi": "ipv4"}, + ], + } + commands.append( + remove_commands(to_delete, w["name"]), + ) + if obj[1]: + to_delete = { + "access_groups": [ + {"acls": obj[1], "afi": "ipv6"}, + ], + } + commands.append( + remove_commands(to_delete, w["name"]), + ) + if commands: + commands = list(itertools.chain(*commands)) + if commands: + commandset.append(intf_command) + commandset.append(commands) + + if commandset: + commandset = list(itertools.chain(*commandset)) + return commandset + + def get_acl_diff(self, w, h, intersection=False): + diff_v4 = [] + diff_v6 = [] + w_acls_v4 = [] + w_acls_v6 = [] + h_acls_v4 = [] + h_acls_v6 = [] + for w_group in w["access_groups"]: + if w_group["afi"] == "ipv4": + w_acls_v4 = w_group["acls"] + if w_group["afi"] == "ipv6": + w_acls_v6 = w_group["acls"] + for h_group in h["access_groups"]: + if h_group["afi"] == "ipv4": + h_acls_v4 = h_group["acls"] + if h_group["afi"] == "ipv6": + h_acls_v6 = h_group["acls"] + for item in w_acls_v4: + match = list( + filter(lambda x: x["name"] == item["name"], h_acls_v4), + ) + if match: + if item["direction"] == match[0]["direction"]: + if intersection: + diff_v4.append(item) + else: + if not intersection: + diff_v4.append(item) + else: + if not intersection: + diff_v4.append(item) + for item in w_acls_v6: + match = list( + filter(lambda x: x["name"] == item["name"], h_acls_v6), + ) + if match: + if item["direction"] == match[0]["direction"]: + if intersection: + diff_v6.append(item) + else: + if not intersection: + diff_v6.append(item) + else: + if not intersection: + diff_v6.append(item) + return diff_v4, diff_v6 + + def get_acls_from_afi(self, interface, afi, have): + config = [] + for h in have: + if h["name"] == interface: + if "access_groups" not in h.keys() or not h["access_groups"]: + continue + if h["access_groups"]: + for h_grp in h["access_groups"]: + if h_grp["afi"] == afi: + config = h_grp["acls"] + return config + + +def add_commands(want, interface): + commands = [] + + for w in want: + # This module was verified on an ios device since vEOS doesnot support + # acl_interfaces cnfiguration. In ios, ipv6 acl is configured as + # traffic-filter and in eos it is access-group + + # a_cmd = "traffic-filter" if w['afi'] == 'ipv6' else "access-group" + a_cmd = "access-group" + afi = "ip" if w["afi"] == "ipv4" else w["afi"] + if "acls" in w.keys(): + for acl in w["acls"]: + commands.append( + afi + + " " + + a_cmd + + " " + + acl["name"] + + " " + + acl["direction"], + ) + return commands + + +def remove_commands(want, interface): + commands = [] + if "access_groups" not in want.keys() or not want["access_groups"]: + return commands + for w in want["access_groups"]: + # This module was verified on an ios device since vEOS doesnot support + # acl_interfaces cnfiguration. In ios, ipv6 acl is configured as + # traffic-filter and in eos it is access-group + + # a_cmd = "traffic-filter" if w['afi'] == 'ipv6' else "access-group" + a_cmd = "access-group" + + afi = "ip" if w["afi"] == "ipv4" else w["afi"] + if "acls" in w.keys(): + for acl in w["acls"]: + commands.append( + "no " + + afi + + " " + + a_cmd + + " " + + acl["name"] + + " " + + acl["direction"], + ) + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acls/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acls/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acls/acls.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acls/acls.py new file mode 100644 index 000000000..17fe6cfd3 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/acls/acls.py @@ -0,0 +1,661 @@ +# +# -*- 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 eos_acls 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 + +import itertools +import re +import socket + +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 ( + dict_diff, + remove_empties, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) + + +class Acls(ConfigBase): + """ + The eos_acls class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["acls"] + + def __init__(self, module): + super(Acls, self).__init__(module) + + def get_acls_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, + ) + acls_facts = facts["ansible_network_resources"].get("acls") + if not acls_facts: + return [] + return acls_facts + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + warnings = list() + commands = list() + changed = False + + if self.state in self.ACTION_STATES: + existing_acls_facts = self.get_acls_facts() + else: + existing_acls_facts = [] + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_acls_facts)) + if commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + self._connection.edit_config(commands) + changed = True + if changed: + 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_acls_facts = self.get_acls_facts() + elif self.state == "rendered": + commands = list(itertools.chain(*commands)) + result["rendered"] = commands + elif self.state == "parsed": + if not self._module.params["running_config"]: + self._module.fail_json( + msg="Value of running_config parameter must not be empty for state parsed", + ) + result["parsed"] = self.get_acls_facts( + data=self._module.params["running_config"], + ) + else: + changed_acls_facts = [] + if self.state in self.ACTION_STATES: + result["before"] = existing_acls_facts + if result["changed"]: + result["after"] = changed_acls_facts + elif self.state == "gathered": + result["gathered"] = changed_acls_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_acls_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 + """ + config = self._module.params.get("config") + want = [] + onbox_configs = [] + for h in existing_acls_facts: + have_configs = add_commands(remove_empties(h)) + onbox_configs.append(have_configs) + if config: + for w in config: + want.append(remove_empties(w)) + have = existing_acls_facts + resp = self.set_state(want, have) + if self.state == "merged": + to_config = self.compare_configs(onbox_configs, to_list(resp)) + else: + to_config = resp + return to_config + + def compare_configs(self, have, want): + commands = [] + want = list(itertools.chain(*want)) + have = list(itertools.chain(*have)) + h_index = 0 + config = list(want) + for w in want: + access_list = re.findall(r"(ip.*) access-list (.*)", w) + if access_list: + if w in have: + h_index = have.index(w) + else: + for num, h in enumerate(have, start=h_index + 1): + if "access-list" not in h: + seq_num = re.search(r"(\d+) (.*)", w) + if seq_num: + have_seq_num = re.search(r"(\d+) (.*)", h) + if seq_num.group(1) == have_seq_num.group( + 1, + ) and have_seq_num.group(2) != seq_num.group(2): + negate_cmd = "no " + seq_num.group(1) + config.insert(config.index(w), negate_cmd) + if w in h: + config.pop(config.index(w)) + break + for c in config: + access_list = re.findall(r"(ip.*) access-list (.*)", c) + if access_list: + acl_index = config.index(c) + else: + if config[acl_index] not in commands: + commands.append(config[acl_index]) + commands.append(c) + return commands + + 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, + ), + ) + state = self._module.params["state"] + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or self.state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + 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 = [] + config_cmds = [] + remove_cmds = [] + ace_names = [] + diff = {} + if not have: + commands = set_commands(want, []) + for w in want: + afi = "ipv6" if w["afi"] == "ipv6" else "ipv4" + for acl in w["acls"]: + name = acl["name"] + want_ace = acl["aces"] + for h in have: + if h["afi"] == afi: + for h_acl in h["acls"]: + if h_acl["name"] == name: + if name not in ace_names: + ace_names.append(name) + h = {"afi": afi, "acls": [{"name": name}]} + for h_ace in h_acl.get("aces", []): + diff = get_ace_diff(h_ace, want_ace) + if diff: + h = { + "afi": afi, + "acls": [ + { + "name": name, + "aces": [h_ace], + }, + ], + } + remove_cmds.append( + del_commands(h, have), + ) + for w_ace in want_ace: + w_diff = get_ace_diff( + w_ace, + h_acl.get("aces", []), + ) + if w_diff: + w = [ + { + "afi": afi, + "acls": [ + { + "name": name, + "aces": [w_ace], + }, + ], + }, + ] + cmds = set_commands(w, have) + config_cmds.append( + list(itertools.chain(*cmds)), + ) + if name not in ace_names: + for w_ace in want_ace: + w = [ + { + "afi": afi, + "acls": [ + { + "name": name, + "aces": [w_ace], + }, + ], + }, + ] + cmds = set_commands(w, have) + config_cmds.append( + list(itertools.chain(*cmds)), + ) + + if remove_cmds: + remove_cmds = list(itertools.chain(*remove_cmds)) + commands.append(remove_cmds) + if config_cmds: + config_cmds = list(itertools.chain(*config_cmds)) + commands.append(config_cmds) + commands = list(itertools.chain(*commands)) + commandset = [] + [commandset.append(cmd) for cmd in commands if cmd not in commandset] + return commandset + + 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 = [] + ace_diff = {} + h_afi_list = [] + w_afi_list = [] + for h in have: + h_afi_list.append(h["afi"]) + for w in want: + w_afi_list.append(w["afi"]) + if not h_afi_list: + commands = set_commands(want, []) + for hafi in h_afi_list: + if hafi not in w_afi_list: + h = {"afi": hafi} + remove_cmds = del_commands(h, have) + commands.append(remove_cmds) + for w in want: + w_names = [] + for h in have: + h_names = [] + if w["afi"] == h["afi"]: + for w_acl in w["acls"]: + for h_acl in h["acls"]: + h_names.append(h_acl["name"]) + if h_acl["name"] == w_acl["name"]: + for h_ace in h_acl.get("aces", []): + ace_diff = get_ace_diff( + h_ace, + w_acl["aces"], + ) + if ace_diff: + h = { + "afi": h["afi"], + "acls": [ + { + "name": h_acl["name"], + "aces": [h_ace], + }, + ], + } + remove_cmds = del_commands(h, have) + commands.append(remove_cmds) + for w_ace in w_acl["aces"]: + if w_acl["name"] not in w_names: + w_ace_diff = get_ace_diff( + w_ace, + h_acl.get("aces", []), + ) + if w_ace_diff: + w_diff = [ + { + "afi": w["afi"], + "acls": [ + { + "name": w_acl["name"], + "aces": [w_ace], + }, + ], + }, + ] + config_cmds = set_commands( + w_diff, + have, + ) + config_cmds = list( + itertools.chain(*config_cmds), + ) + commands.append(config_cmds) + w_names.append(w_acl["name"]) + for hname in h_names: + if hname not in w_names: + h = {"afi": h["afi"], "acls": [{"name": hname}]} + remove_cmds = del_commands(h, have) + if remove_cmds not in commands: + commands.append(remove_cmds) + + if commands: + commands = list(itertools.chain(*commands)) + + commandset = [] + for c in commands: + access_list = re.findall(r"(ip.*) access-list (.*)", c) + if access_list and "no" in c: + commandset.append(c) + continue + if access_list: + acl_index = commands.index(c) + else: + if commands[acl_index] not in commandset: + commandset.append(commands[acl_index]) + commandset.append(c) + + return commandset + + 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 + """ + return set_commands(want, have) + + 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 not want: + for h in have: + return_command = add_commands(h) + for command in return_command: + command = "no " + command + commands.append(command) + else: + for w in want: + return_command = del_commands(w, have, True) + if return_command[:5] == "Warn ": + self._module.warn(return_command[5:]) + return commands + else: + commands.append(return_command) + commands = list(itertools.chain(*commands)) + return commands + + +def set_commands(want, have): + commands = [] + for w in want: + wace_updated = [] + for h in have: + if w["afi"] == h["afi"]: + for wacl in w["acls"]: + for hacl in h["acls"]: + if wacl["name"] == hacl["name"]: + want_aces = wacl["aces"] + for wace in wacl["aces"]: + for hace in hacl.get("aces", []): + if ( + "sequence" in wace.keys() + and "sequence" in hace.keys() + ): + if ( + wace["sequence"] + == hace["sequence"] + ): + wace_updated = get_updated_ace( + wace, + hace, + ) + if wace_updated: + want_aces.pop( + want_aces.index(wace), + ) + want_aces.append(wace_updated) + return_command = add_commands(w) + commands.append(return_command) + return commands + + +def get_updated_ace(w, h): + # gives the ace to be updated in case of merge update. + if not dict_diff(w, h): + return + w_updated = w.copy() + for hkey in h.keys(): + if hkey not in w.keys(): + w_updated.update({hkey: h[hkey]}) + else: + w_updated.update({hkey: w[hkey]}) + return w_updated + + +def add_commands(want): + commandset = [] + protocol_name = { + "51": "ahp", + "47": "gre", + "1": "icmp", + "2": "igmp", + "4": "ip", + "89": "ospf", + "103": "pim", + "6": "tcp", + "17": "udp", + "112": "vrrp", + } + if not want: + return commandset + command = "" + afi = "ip" if want["afi"] == "ipv4" else "ipv6" + for acl in want["acls"]: + if "standard" in acl.keys() and acl["standard"]: + command = afi + " access-list standard " + acl["name"] + else: + command = afi + " access-list " + acl["name"] + commandset.append(command) + if "aces" not in acl.keys(): + continue + for ace in acl["aces"]: + command = "" + if "sequence" in ace.keys(): + command = str(ace["sequence"]) + if "remark" in ace.keys(): + command = command + " remark " + ace["remark"] + if "fragment_rules" in ace.keys() and ace["fragment_rules"]: + command = command + " fragment-rules" + if "grant" in ace.keys(): + command = command + " " + ace["grant"] + if "vlan" in ace.keys(): + command = command + " vlan " + ace["vlan"] + if "protocol" in ace.keys(): + protocol = ace["protocol"] + if protocol.isdigit(): + if protocol in protocol_name.keys(): + protocol = protocol_name[protocol] + command = command + " " + protocol + if "source" in ace.keys(): + if "any" in ace["source"].keys(): + command = command + " any" + elif "subnet_address" in ace["source"].keys(): + command = command + " " + ace["source"]["subnet_address"] + elif "host" in ace["source"].keys(): + command = command + " host " + ace["source"]["host"] + elif "address" in ace["source"].keys(): + command = ( + command + + " " + + ace["source"]["address"] + + " " + + ace["source"]["wildcard_bits"] + ) + if "port_protocol" in ace["source"].keys(): + for op, val in ace["source"]["port_protocol"].items(): + if val.isdigit(): + val = socket.getservbyport(int(val)) + command = ( + command + " " + op + " " + val.replace("_", "-") + ) + if "destination" in ace.keys(): + if "any" in ace["destination"].keys(): + command = command + " any" + elif "subnet_address" in ace["destination"].keys(): + command = ( + command + " " + ace["destination"]["subnet_address"] + ) + elif "host" in ace["destination"].keys(): + command = command + " host " + ace["destination"]["host"] + elif "address" in ace["destination"].keys(): + command = ( + command + + " " + + ace["destination"]["address"] + + " " + + ace["destination"]["wildcard_bits"] + ) + if "port_protocol" in ace["destination"].keys(): + for op in ace["destination"]["port_protocol"].keys(): + command = ( + command + + " " + + op + + " " + + ace["destination"]["port_protocol"][op].replace( + "_", + "-", + ) + ) + if "protocol_options" in ace.keys(): + for proto in ace["protocol_options"].keys(): + if proto == "icmp" or proto == "icmpv6": + for icmp_msg in ace["protocol_options"][proto].keys(): + command = ( + command + " " + icmp_msg.replace("_", "-") + ) + elif proto == "ip" or proto == "ipv6": + command = ( + command + + " nexthop-group " + + ace["protocol_options"][proto]["nexthop_group"] + ) + elif proto == "tcp": + for flag, val in ace["protocol_options"][proto][ + "flags" + ].items(): + if val: + command = command + " " + flag + if "hop_limit" in ace.keys(): + for op, val in ace["hop_limit"].items(): + command = command + " hop-limit " + op + " " + val + if "tracked" in ace.keys() and ace["tracked"]: + command = command + " tracked" + if "ttl" in ace.keys(): + for op, val in ace["ttl"].items(): + command = command + " ttl " + op + " " + str(val) + if "fragments" in ace.keys(): + command = command + " fragments" + if "log" in ace.keys(): + command = command + " log" + commandset.append(command.strip()) + return commandset + + +def del_commands(want, have, name_only=False): + commandset = [] + command = "" + have_command = [] + for h in have: + have_configs = add_commands(h) + have_command.append(have_configs) + have_command = list(itertools.chain(*have_command)) + afi = "ip" if want["afi"] == "ipv4" else "ipv6" + if "acls" not in want.keys(): + for have_cmd in have_command: + access_list = re.search(r"(ip.*)\s+access-list .*", have_cmd) + if access_list and access_list.group(1) == afi: + commandset.append("no " + have_cmd) + return commandset + + for acl in want["acls"]: + ace_present = True + if "standard" in acl.keys() and acl["standard"]: + command = afi + " access-list standard " + acl["name"] + else: + command = afi + " access-list " + acl["name"] + if "aces" not in acl.keys(): + ace_present = False + commandset.append("no " + command) + if ace_present: + if name_only: + msg = "Deleted operation allows deletion of access-list only and not the entries !!" + return "Warn " + msg + return_command = add_commands(want) + for cmd in return_command: + if "access-list" in cmd: + commandset.append(cmd) + continue + seq = re.search( + r"(\d+) (permit|deny|fragment-rules|remark) .*", + cmd, + ) + if seq: + commandset.append("no " + seq.group(1)) + else: + commandset.append("no " + cmd) + return commandset + + +def get_ace_diff(want_ace, have_ace): + # gives the diff of the aces passed. + if not have_ace: + return dict_diff({}, want_ace) + for h_a in have_ace: + d = dict_diff(want_ace, h_a) + if not d: + break + return d diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_address_family/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_address_family/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_address_family/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_address_family/bgp_address_family.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_address_family/bgp_address_family.py new file mode 100644 index 000000000..021074805 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_address_family/bgp_address_family.py @@ -0,0 +1,293 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_bgp_address_family config file. +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 its desired end-state is +created. +""" + +import re + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.bgp_address_family import ( + Bgp_afTemplate, +) + + +class Bgp_af(ResourceModule): + """ + The eos_bgp_address_family config class + """ + + def __init__(self, module): + super(Bgp_af, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="bgp_address_family", + tmplt=Bgp_afTemplate(), + ) + self.parsers = [ + "router", + "address_family", + "bgp_params_additional_paths", + "bgp_params.nexthop_address_family", + "bgp_params.nexthop_unchanged", + "bgp_params.redistribute_internal", + "bgp_params.route", + "graceful_restart", + "neighbor.activate", + "neighbor.additional_paths", + "neighbor.default_originate", + "neighbor.graceful_restart", + "neighbor.next_hop_unchanged", + "neighbor.next_hop_address_family", + "neighbor.prefix_list", + "neighbor.route_map", + "neighbor.weight", + "neighbor.encapsulation", + "network", + "redistribute", + "route_target", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = {} + haved = {} + + if self.want: + wantd = {self.want["as_number"]: self.want} + if self.have: + haved = {self.have["as_number"]: self.have} + + # turn all lists of dicts into dicts prior to merge + for entry in wantd, haved: + self._bgp_af_list_to_dict(entry) + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + if len(wantd.keys()) > 1: + self._module.fail_json( + msg="Only one bgp instance is allowed per device", + ) + wantd = {} + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + h_del = {} + for k, v in iteritems(haved): + if k in wantd or not wantd: + h_del.update({k: v}) + haved = h_del + for wk, wv in iteritems(wantd): + self._compare(want=wv, have=haved.pop(wk, {})) + + wantd = {} + + # remove superfluous config for overridden + if self.state == "overridden": + for k, have in iteritems(haved): + if k not in wantd: + self._compare(want={}, have=have) + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _delete_af(self, want, have): + waf = want.get("address_family", {}) + haf = have.get("address_family", {}) + for hkey, entry in iteritems(haf): + if hkey in waf.keys(): + af_no_command = self._tmplt.render( + entry, + "address_family", + True, + ).split("\n") + if re.search(r"\S+_\S+", hkey): + af_no_command[0] = af_no_command[0][3:] + af_no_command[1] = "no " + af_no_command[1] + for cmd in af_no_command: + self.commands.append(cmd) + else: + self.addcmd(entry, "address_family", True) + have = {} + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Bgp_af network resource. + """ + for name, entry in iteritems(want): + if name != "as_number": + if self.state == "deleted": + self._delete_af(want, have) + else: + self._compare_af({name: entry}, {name: have.get(name, {})}) + + if self.commands and "router bgp" not in self.commands[0]: + self.commands.insert( + 0, + self._tmplt.render( + {"as_number": want.get("as_number") or have["as_number"]}, + "router", + False, + ), + ) + + def _compare_af(self, want, have): + waf = want.get("address_family", {}) + haf = have.get("address_family", {}) + for name, entry in iteritems(waf): + begin = len(self.commands) + self._compare_lists(entry, have=haf.get(name, {})) + self._compare_neighbor(entry, have=haf.get(name, {})) + # Removing the alias key + if "route_target" in entry.keys(): + entry["route_target"].pop("mode", "") + self.compare( + parsers=self.parsers, + want=entry, + have=haf.pop(name, {}), + ) + if len(self.commands) != begin: + af_command = self._tmplt.render( + entry, + "address_family", + False, + ).split("\n") + for cmd in af_command: + self.commands.insert(begin, cmd) + self.commands.append("exit") + begin += 1 + for name, entry in iteritems(haf): + # skip superfluous configs for replaced + if self.state in ["replaced"]: + if name in waf.keys(): + self.addcmd(entry, "address_family", True) + else: + # overridden + # check if want has vrf or not + # if want doesnot have vrf, device's vrf config will not + # be touched. + vrf_present = False + for w_key in waf.keys(): + if re.search(r"\S+_\S+", w_key): + vrf_present = True + break + if vrf_present: + if re.search(r"\S+_\S+", name): + af_no_command = self._tmplt.render( + entry, + "address_family", + True, + ).split("\n") + if name not in waf.keys(): + af_no_command[0] = af_no_command[0][3:] + af_no_command[1] = "no " + af_no_command[1] + for cmd in af_no_command: + self.commands.append(cmd) + else: + self.addcmd(entry, "address_family", True) + else: + if not re.search(r"\S+_\S+", name): + self.addcmd(entry, "address_family", True) + + def _compare_neighbor(self, want, have): + parsers = [ + "neighbor.activate", + "neighbor.additional_paths", + "neighbor.default_originate", + "neighbor.graceful_restart", + "neighbor.next_hop_unchanged", + "neighbor.next_hop_address_family", + "neighbor.prefix_list", + "neighbor.route_map", + "neighbor.weight", + "neighbor.encapsulation", + ] + wneigh = want.get("neighbor", {}) + hneigh = have.get("neighbor", {}) + for name, entry in iteritems(wneigh): + self.compare( + parsers=parsers, + want={"neighbor": entry}, + have={"neighbor": hneigh.pop(name, {})}, + ) + for name, entry in iteritems(hneigh): + self.compare(parsers=parsers, want={}, have={"neighbor": entry}) + + def _compare_lists(self, want, have): + for attrib in ["redistribute", "network"]: + wdict = want.pop(attrib, {}) + hdict = have.pop(attrib, {}) + for key, entry in iteritems(wdict): + if entry != hdict.pop(key, {}): + self.addcmd(entry, attrib, False) + # remove remaining items in have for replaced + for entry in hdict.values(): + self.addcmd(entry, attrib, True) + + def _bgp_af_list_to_dict(self, entry): + for name, proc in iteritems(entry): + if "address_family" in proc: + addr_dict = {} + for entry in proc.get("address_family", []): + addr_dict.update( + {entry["afi"] + "_" + entry.get("vrf", ""): entry}, + ) + proc["address_family"] = addr_dict + self._bgp_af_list_to_dict(proc["address_family"]) + + if "neighbor" in proc: + neigh_dict = {} + for entry in proc.get("neighbor", []): + neigh_dict.update({entry["peer"]: entry}) + proc["neighbor"] = neigh_dict + + if "network" in proc: + network_dict = {} + for entry in proc.get("network", []): + network_dict.update({entry["address"]: entry}) + proc["network"] = network_dict + + if "redistribute" in proc: + redis_dict = {} + for entry in proc.get("redistribute", []): + redis_dict.update({entry["protocol"]: entry}) + proc["redistribute"] = redis_dict diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_global/bgp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_global/bgp_global.py new file mode 100644 index 000000000..4658ced75 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/bgp_global/bgp_global.py @@ -0,0 +1,422 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_bgp_global config file. +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 its desired end-state is +created. +""" + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.bgp_global import ( + Bgp_globalTemplate, +) + + +class Bgp_global(ResourceModule): + """ + The eos_bgp_global config class + """ + + def __init__(self, module): + super(Bgp_global, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="bgp_global", + tmplt=Bgp_globalTemplate(), + ) + self.parsers = [ + "router", + "vrf", + "default_metric", + "distance", + "graceful_restart", + "graceful_restart_helper", + "maximum_paths", + "monitoring", + "route_target", + "router_id", + "shutdown", + "timers", + "ucmp_fec", + "ucmp_link_bandwidth", + "ucmp_mode", + "update", + "vlan", + "vlan_aware_bundle", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = {} + haved = {} + if ( + self.want.get("as_number") == self.have.get("as_number") + or not self.have + ): + if self.want: + wantd = {self.want["as_number"]: self.want} + if self.have: + haved = {self.have["as_number"]: self.have} + else: + self._module.fail_json( + msg="Only one bgp instance is allowed per device", + ) + + # turn all lists of dicts into dicts prior to merge + for entry in wantd, haved: + self._bgp_global_list_to_dict(entry) + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state in ["deleted", "purged"]: + h_del = {} + for k, v in iteritems(haved): + if k in wantd or not wantd: + h_del.update({k: v}) + wantd = {} + haved = h_del + + if self.state == "deleted": + self._compare(want={}, have=self.have) + + if self.state == "purged": + for num, entry in iteritems(haved): + self.commands.append( + self._tmplt.render({"as_number": num}, "router", True), + ) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Bgp_global network resource. + """ + self._compare_vrfs(want, have) + self._compare_neighbor(want, have) + self._compare_lists(want, have) + self._compare_bgp_params(want, have) + for name, entry in iteritems(want): + if name != "as_number": + self.compare( + parsers=self.parsers, + want={name: entry}, + have={name: have.pop(name, {})}, + ) + for name, entry in iteritems(have): + if name != "as_number": + self.compare( + parsers=self.parsers, + want={}, + have={name: have.get(name)}, + ) + + if self.commands and "router bgp" not in self.commands[0]: + self.commands.insert( + 0, + self._tmplt.render(want or have, "router", False), + ) + + def _compare_bgp_params(self, want, have): + parsers = [ + "bgp_params_additional_paths", + "bgp_params_advertise_inactive", + "bgp_params_allowas_in", + "bgp_params_always_compare_med", + "bgp_params_asn", + "bgp_params_auto_local_addr", + "bgp_params_bestpath_as_path", + "bgp_params_bestpath_ecmp_fast", + "bgp_params_bestpath_med", + "bgp_params_bestpath_skip", + "bgp_params_tie_break", + "bgp_params_client_to_client", + "bgp_params_cluster_id", + "bgp_params_confederation", + "bgp_params_control_plane_filter", + "bgp_params_convergence", + "bgp_params_default", + "bgp_params.enforce_first_as", + "bgp_params.host_routes", + "bgp_params.labelled_unicast", + "bgp_params.listen_limit", + "bgp_params.listen_range", + "bgp_params.log_neighbor_changes", + "bgp_params.missing_policy", + "bgp_params.monitoring", + "bgp_params.nexthop_unchanged", + "bgp_params.redistribute_internal", + "bgp_params.route", + "bgp_params.route_reflector", + "bgp_params.transport", + ] + wbgp = want.pop("bgp_params", {}) + hbgp = have.pop("bgp_params", {}) + for name, entry in iteritems(wbgp): + if name == "bestpath": + for k, v in iteritems(entry): + h = {} + if hbgp.get(name): + h = {name: hbgp[name].pop(v, {})} + self.compare( + parsers=parsers, + want={"bgp_params": {name: {k: v}}}, + have={"bgp_params": h}, + ) + hbgp.pop(name, {}) + continue + self.compare( + parsers=parsers, + want={"bgp_params": {name: entry}}, + have={"bgp_params": {name: hbgp.pop(name, {})}}, + ) + for name, entry in iteritems(hbgp): + self.compare( + parsers=parsers, + want={}, + have={"bgp_params": {name: entry}}, + ) + + def _compare_vrfs(self, want, have): + wvrf = want.pop("vrfs", {}) + hvrf = have.pop("vrfs", {}) + begin = len(self.commands) + for name, entry in iteritems(wvrf): + self._compare_neighbor(entry, hvrf.get(name, {})) + self._compare_lists(entry, hvrf.get(name, {})) + self._compare_bgp_params(entry, hvrf.get(name, {})) + for k, v in entry.items(): + if hvrf.get(name): + h = {k: hvrf[name].pop(k, {})} + else: + h = {} + if k != "vrf": + self.compare(parsers=self.parsers, want={k: v}, have=h) + + if len(self.commands) != begin: + self.commands.insert( + begin, + self._tmplt.render({"vrf": name}, "vrf", False), + ) + self.commands.append("exit") + begin_negate = len(self.commands) + for name, entry in iteritems(hvrf): + if name not in wvrf.keys(): + if self._check_af(name): + self._module.fail_json( + msg="Use the _bgp_address_family module to delete the address_family under vrf, before replacing/deleting the vrf.", + ) + else: + self.commands.append( + self._tmplt.render({"vrf": name}, "vrf", True), + ) + continue + self.compare(parsers=self.parsers, want={}, have=entry) + after_negate = len(self.commands) + if after_negate != begin_negate: + if "vrf " + name in self.commands: + index = self.commands.index("vrf " + name) + i = begin_negate + while i < after_negate: + cmd = self.commands.pop(i) + if cmd != "exit": + self.commands.insert(index + 1, cmd) + i += 1 + else: + self.commands.insert( + begin_negate, + self._tmplt.render({"vrf": name}, "vrf", False), + ) + self.commands.append("exit") + + def _get_config(self, connection): + return connection.get("show running-config | section bgp ") + + def _check_af(self, vrf): + af_present = False + if self._connection: + config_lines = self._get_config(self._connection).splitlines() + index = [i + 1 for i, el in enumerate(config_lines) if vrf in el] + if index: + # had to do this to escape flake8 and black errors + ind = index[0] + for line in config_lines[ind:]: + if "vrf" in line: + break + if "address-family" in line: + af_present = True + break + return af_present + + def _compare_neighbor(self, want, have): + parsers = [ + "neighbor.additional_paths", + "neighbor.allowas_in", + "neighbor.auto_local_addr", + "neighbor.bfd", + "neighbor.default_originate", + "neighbor.description", + "neighbor.dont_capability_negotiate", + "neighbor.ebgp_multihop", + "neighbor.encryption_password", + "neighbor.enforce_first_as", + "neighbor.export_localpref", + "neighbor.fall_over", + "neighbor.graceful_restart", + "neighbor.graceful_restart_helper", + "neighbor.idle_restart_timer", + "neighbor.import_localpref", + "neighbor.link_bandwidth", + "neighbor.local_as", + "neighbor.local_v6_addr", + "neighbor.maximum_accepted_routes", + "neighbor.maximum_received_routes", + "neighbor.metric_out", + "neighbor.monitoring", + "neighbor.next_hop_self", + "neighbor.next_hop_unchanged", + "neighbor.next_hop_v6_addr", + "neighbor.out_delay", + "neighbor.remote_as", + "neighbor.remove_private_as", + "neighbor.peer_group", + "neighbor.prefix_list", + "neighbor.route_map", + "neighbor.route_reflector_client", + "neighbor.route_to_peer", + "neighbor.send_community", + "neighbor.shutdown", + "neighbor.soft_reconfiguration", + "neighbor.transport", + "neighbor.timers", + "neighbor.ttl", + "neighbor.update_source", + "neighbor.weight", + ] + wneigh = want.pop("neighbor", {}) + hneigh = have.pop("neighbor", {}) + for name, entry in iteritems(wneigh): + for k, v in entry.items(): + if entry.get("peer"): + peer = entry["peer"] + else: + peer = entry["neighbor_address"] + if hneigh.get(name): + h = {"neighbor_address": peer, k: hneigh[name].pop(k, {})} + else: + h = {} + self.compare( + parsers=parsers, + want={"neighbor": {"neighbor_address": peer, k: v}}, + have={"neighbor": h}, + ) + for name, entry in iteritems(hneigh): + if name not in wneigh.keys() and "peer_group" not in entry.keys(): + self.commands.append("no neighbor " + name) + continue + for k, v in entry.items(): + self.compare( + parsers=parsers, + want={}, + have={"neighbor": {"neighbor_address": name, k: v}}, + ) + + def _compare_lists(self, want, have): + for attrib in [ + "redistribute", + "network", + "aggregate_address", + "access_group", + ]: + wdict = want.pop(attrib, {}) + hdict = have.pop(attrib, {}) + for key, entry in iteritems(wdict): + if entry != hdict.pop(key, {}): + self.addcmd(entry, attrib, False) + # remove remaining items in have for replaced + for entry in hdict.values(): + self.addcmd(entry, attrib, True) + + def _bgp_global_list_to_dict(self, entry): + for name, proc in iteritems(entry): + if "neighbor" in proc: + neigh_dict = {} + for entry in proc.get("neighbor", []): + if entry.get("peer"): + peer = entry["peer"] + else: + peer = entry["neighbor_address"] + neigh_dict.update({peer: entry}) + proc["neighbor"] = neigh_dict + + if "network" in proc: + network_dict = {} + for entry in proc.get("network", []): + network_dict.update({entry["address"]: entry}) + proc["network"] = network_dict + + if "aggregate_address" in proc: + agg_dict = {} + for entry in proc.get("aggregate_address", []): + agg_dict.update({entry["address"]: entry}) + proc["aggregate_address"] = agg_dict + + if "access_group" in proc: + access_dict = {} + for entry in proc.get("access_group", []): + access_dict.update({entry["afi"]: entry}) + proc["access_group"] = access_dict + + if "redistribute" in proc: + redis_dict = {} + for entry in proc.get("redistribute", []): + redis_dict.update({entry["protocol"]: entry}) + proc["redistribute"] = redis_dict + + if "vrfs" in proc: + vrf_dict = {} + for entry in proc.get("vrfs", []): + vrf_dict.update({entry["vrf"]: entry}) + proc["vrfs"] = vrf_dict + self._bgp_global_list_to_dict(proc["vrfs"]) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/hostname/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/hostname/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/hostname/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/hostname/hostname.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/hostname/hostname.py new file mode 100644 index 000000000..026e1ae19 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/hostname/hostname.py @@ -0,0 +1,76 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_hostname config file. +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 its desired end-state is +created. +""" + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.hostname import ( + HostnameTemplate, +) + + +class Hostname(ResourceModule): + """ + The eos_hostname config class + """ + + def __init__(self, module): + super(Hostname, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="hostname", + tmplt=HostnameTemplate(), + ) + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = self.want + haved = self.have + + if self.state == "deleted": + wantd = {} + + self._compare(want=wantd, have=haved) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Hostname network resource. + """ + self.compare(parsers="hostname", want=want, have=have) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/interfaces/interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/interfaces/interfaces.py new file mode 100644 index 000000000..fdaa9e979 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/interfaces/interfaces.py @@ -0,0 +1,309 @@ +# -*- 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 eos_interfaces 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 + +import re + +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 ( + dict_diff, + param_list_to_dict, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, +) + + +class Interfaces(ConfigBase): + """ + The eos_interfaces class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["interfaces"] + + def get_interfaces_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, + ) + interfaces_facts = facts["ansible_network_resources"].get("interfaces") + if not interfaces_facts: + return [] + return interfaces_facts + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + commands = list() + warnings = list() + + if self.state in self.ACTION_STATES: + existing_interfaces_facts = self.get_interfaces_facts() + else: + existing_interfaces_facts = [] + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_interfaces_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_interfaces_facts = self.get_interfaces_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_interfaces_facts(data=running_config) + + if self.state in self.ACTION_STATES: + result["before"] = existing_interfaces_facts + if result["changed"]: + result["after"] = changed_interfaces_facts + + elif self.state == "gathered": + result["gathered"] = changed_interfaces_facts + result["warnings"] = warnings + return result + + def set_config(self, existing_interfaces_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_interfaces_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + want = param_list_to_dict(want) + have = param_list_to_dict(have) + commands = [] + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + for key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + if ( + "speed" in add_config.keys() + and "duplex" not in add_config.keys() + ): + add_config.update({"duplex": desired.get("duplex")}) + + commands.extend(generate_commands(key, add_config, del_config)) + + return commands + + @staticmethod + def _state_overridden(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 = [] + for key, extant in have.items(): + if key in want: + desired = want[key] + else: + desired = dict() + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + if ( + "speed" in add_config.keys() + and "duplex" not in add_config.keys() + ): + add_config.update({"duplex": desired.get("duplex")}) + + commands.extend(generate_commands(key, add_config, del_config)) + + return commands + + @staticmethod + def _state_merged(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 key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + + add_config = dict_diff(extant, desired) + if ( + "speed" in add_config.keys() + and "duplex" not in add_config.keys() + ): + add_config.update({"duplex": desired.get("duplex")}) + commands.extend(generate_commands(key, add_config, {})) + + return commands + + @staticmethod + def _state_deleted(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 = [] + for key in want: + desired = dict() + if key in have: + extant = have[key] + else: + continue + + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(key, {}, del_config)) + + return commands + + +def generate_commands(interface, to_set, to_remove): + commands = [] + for key, value in to_set.items(): + if value is None: + continue + + if key == "enabled": + commands.append("{0}shutdown".format("no " if value else "")) + elif key == "speed": + if value == "auto": + commands.append("{0} {1}".format(key, value)) + else: + commands.append("speed {0}{1}".format(value, to_set["duplex"])) + elif key == "duplex": + # duplex is handled with speed + continue + elif key == "mode": + if not re.search(r"(M|m)anagement.*", interface): + if value == "layer3": + # switching from default (layer2) mode to layer3 + commands.append("no switchport") + else: + # setting to default (layer 2) mode + commands.append("switchport") + else: + commands.append("{0} {1}".format(key, value)) + + # Don't try to also remove the same key, if present in to_remove + to_remove.pop(key, None) + + for key in to_remove.keys(): + if key == "enabled": + commands.append("no shutdown") + elif key == "speed": + commands.append("speed auto") + elif key == "duplex": + # duplex is handled with speed + continue + elif key == "mode": + if not re.search(r"(M|m)anagement.*", interface): + commands.append("switchport") + else: + commands.append("no {0}".format(key)) + + if commands: + commands.insert(0, "interface {0}".format(interface)) + + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l2_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l2_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l2_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l2_interfaces/l2_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l2_interfaces/l2_interfaces.py new file mode 100644 index 000000000..a34c1614a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l2_interfaces/l2_interfaces.py @@ -0,0 +1,365 @@ +# -*- 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 eos_l2_interfaces 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 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 ( + param_list_to_dict, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, + vlan_range_to_list, +) + + +class L2_interfaces(ConfigBase): + """ + The eos_l2_interfaces class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["l2_interfaces"] + + def get_l2_interfaces_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, + ) + l2_interfaces_facts = facts["ansible_network_resources"].get( + "l2_interfaces", + ) + if not l2_interfaces_facts: + return [] + return l2_interfaces_facts + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + commands = list() + warnings = list() + + if self.state in self.ACTION_STATES: + existing_l2_interfaces_facts = self.get_l2_interfaces_facts() + else: + existing_l2_interfaces_facts = [] + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_l2_interfaces_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_l2_interfaces_facts = self.get_l2_interfaces_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_l2_interfaces_facts( + data=running_config, + ) + + if self.state in self.ACTION_STATES: + result["before"] = existing_l2_interfaces_facts + if result["changed"]: + result["after"] = changed_l2_interfaces_facts + + elif self.state == "gathered": + result["gathered"] = changed_l2_interfaces_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_l2_interfaces_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_l2_interfaces_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + want = param_list_to_dict(want) + have = param_list_to_dict(have) + commands = [] + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + for key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + + intf_commands = set_interface(desired, extant) + intf_commands.extend(clear_interface(desired, extant)) + + if intf_commands: + commands.append("interface {0}".format(interface_name)) + commands.extend(intf_commands) + + return commands + + @staticmethod + def _state_overridden(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 = [] + for key, extant in have.items(): + if key in want: + desired = want[key] + else: + desired = dict() + + intf_commands = set_interface(desired, extant) + intf_commands.extend(clear_interface(desired, extant)) + + if intf_commands: + commands.append("interface {0}".format(key)) + commands.extend(intf_commands) + + return commands + + @staticmethod + def _state_merged(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 key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + + intf_commands = set_interface(desired, extant) + + if intf_commands: + commands.append("interface {0}".format(interface_name)) + commands.extend(intf_commands) + + return commands + + @staticmethod + def _state_deleted(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 = [] + for key in want: + desired = dict() + if key in have: + extant = have[key] + else: + continue + intf_commands = clear_interface(desired, extant) + + if intf_commands: + commands.append("interface {0}".format(key)) + commands.extend(intf_commands) + + return commands + + +def set_interface(want, have): + commands = [] + + want_mode = want.get("mode") + if want_mode and want_mode != have.get("mode"): + commands.append("switchport mode {0}".format(want_mode)) + + wants_access = want.get("access") + if wants_access: + access_vlan = wants_access.get("vlan") + if access_vlan and access_vlan != have.get("access", {}).get("vlan"): + commands.append("switchport access vlan {0}".format(access_vlan)) + + wants_trunk = want.get("trunk") + if wants_trunk: + allowed_vlans = [] + has_allowed_vlans = {} + want_allowed_vlans = {} + has_trunk = have.get("trunk", {}) + native_vlan = wants_trunk.get("native_vlan") + if native_vlan and native_vlan != has_trunk.get("native_vlan"): + commands.append( + "switchport trunk native vlan {0}".format(native_vlan), + ) + for con in [want, have]: + expand_trunk_allowed_vlans(con) + want_allowed_vlans = want["trunk"].get("trunk_allowed_vlans") + if has_trunk: + has_allowed_vlans = has_trunk.get("trunk_allowed_vlans") + + if want_allowed_vlans and has_allowed_vlans: + allowed_vlans = list( + set(want_allowed_vlans.split(",")) + - set(has_allowed_vlans.split(",")), + ) + elif want_allowed_vlans: + allowed_vlans = want_allowed_vlans.split(",") + if allowed_vlans: + allowed_vlans.sort() + allowed_vlans = ",".join( + ["{0}".format(vlan) for vlan in allowed_vlans], + ) + if has_allowed_vlans: + commands.append( + "switchport trunk allowed vlan add {0}".format( + allowed_vlans, + ), + ) + else: + commands.append( + "switchport trunk allowed vlan {0}".format(allowed_vlans), + ) + return commands + + +def expand_trunk_allowed_vlans(want): + if not want: + return None + if want.get("trunk"): + if "trunk_allowed_vlans" in want["trunk"]: + allowed_vlans = vlan_range_to_list( + want["trunk"]["trunk_allowed_vlans"], + ) + vlans_list = [str(num) for num in sorted(allowed_vlans)] + want["trunk"]["trunk_allowed_vlans"] = ",".join(vlans_list) + + +def clear_interface(want, have): + commands = [] + + if "mode" in have and want.get("mode") is None: + commands.append("no switchport mode") + + if "access" in have and not want.get("access"): + commands.append("no switchport access vlan") + + has_trunk = have.get("trunk") or {} + wants_trunk = want.get("trunk") or {} + if ( + "trunk_allowed_vlans" in has_trunk + and "trunk_allowed_vlans" not in wants_trunk + ): + commands.append("no switchport trunk allowed vlan") + if ( + "trunk_allowed_vlans" in has_trunk + and "trunk_allowed_vlans" in wants_trunk + ): + for con in [want, have]: + expand_trunk_allowed_vlans(con) + want_allowed_vlans = want["trunk"].get("trunk_allowed_vlans") + has_allowed_vlans = has_trunk.get("trunk_allowed_vlans") + allowed_vlans = list( + set(has_allowed_vlans.split(",")) + - set(want_allowed_vlans.split(",")), + ) + if allowed_vlans: + allowed_vlans = ",".join( + ["{0}".format(vlan) for vlan in allowed_vlans], + ) + + commands.append( + "switchport trunk allowed vlan remove {0}".format( + allowed_vlans, + ), + ) + + if "native_vlan" in has_trunk and "native_vlan" not in wants_trunk: + commands.append("no switchport trunk native vlan") + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l3_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l3_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l3_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l3_interfaces/l3_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l3_interfaces/l3_interfaces.py new file mode 100644 index 000000000..a34e23619 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/l3_interfaces/l3_interfaces.py @@ -0,0 +1,351 @@ +# -*- 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 eos_l3_interfaces 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 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 ( + param_list_to_dict, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, +) + + +class L3_interfaces(ConfigBase): + """ + The eos_l3_interfaces class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["l3_interfaces"] + + def get_l3_interfaces_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, + ) + l3_interfaces_facts = facts["ansible_network_resources"].get( + "l3_interfaces", + ) + if not l3_interfaces_facts: + return [] + return l3_interfaces_facts + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + commands = list() + warnings = list() + + if self.state in self.ACTION_STATES: + existing_l3_interfaces_facts = self.get_l3_interfaces_facts() + else: + existing_l3_interfaces_facts = [] + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_l3_interfaces_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_l3_interfaces_facts = self.get_l3_interfaces_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_l3_interfaces_facts( + data=running_config, + ) + + if self.state in self.ACTION_STATES: + result["before"] = existing_l3_interfaces_facts + if result["changed"]: + result["after"] = changed_l3_interfaces_facts + + elif self.state == "gathered": + result["gathered"] = changed_l3_interfaces_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_l3_interfaces_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_l3_interfaces_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + want = param_list_to_dict(want) + have = param_list_to_dict(have) + commands = [] + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + for key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + intf_commands = set_interface(desired, extant) + intf_commands.extend(clear_interface(desired, extant)) + + if intf_commands: + commands.append("interface {0}".format(interface_name)) + commands.extend(intf_commands) + return commands + + @staticmethod + def _state_overridden(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 = [] + for key, extant in have.items(): + interface_name = normalize_interface(key) + desired = dict() + if "Management" in interface_name: + continue + for k in want.keys(): + k_want = normalize_interface(k) + if key == k_want: + desired = want[k] + break + if desired.get("ipv4"): + for ipv4 in desired["ipv4"]: + for k in ["secondary", "virtual"]: + if ipv4[k] is None: + del ipv4[k] + intf_commands = set_interface(desired, extant) + intf_commands.extend(clear_interface(desired, extant)) + + if intf_commands: + commands.append("interface {0}".format(interface_name)) + commands.extend(intf_commands) + + # new interfaces in want + new_intf_commands = [] + for key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name not in have: + extant = dict() + new_intf_commands = set_interface(desired, extant) + + if new_intf_commands: + commands.append("interface {0}".format(interface_name)) + commands.extend(new_intf_commands) + + return commands + + @staticmethod + def _state_merged(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 key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + + intf_commands = set_interface(desired, extant) + if intf_commands: + commands.append("interface {0}".format(interface_name)) + commands.extend(intf_commands) + + return commands + + @staticmethod + def _state_deleted(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 = [] + for key in want: + interface_name = normalize_interface(key) + desired = dict() + if interface_name in have: + extant = have[interface_name] + else: + continue + + intf_commands = clear_interface(desired, extant) + + if intf_commands: + commands.append("interface {0}".format(interface_name)) + commands.extend(intf_commands) + + return commands + + +def set_interface(want, have): + commands = [] + want_ipv4 = set( + tuple(sorted(address.items())) for address in want.get("ipv4") or [] + ) + have_ipv4 = set( + tuple(sorted(address.items())) for address in have.get("ipv4") or [] + ) + for address in want_ipv4 - have_ipv4: + address = dict(address) + for param in ["secondary", "virtual"]: + if param in address and not address[param]: + del address[param] + if tuple(sorted(address.items())) in have_ipv4: + continue + + address_cmd = "ip address {0}".format(address["address"]) + if address.get("secondary"): + address_cmd += " secondary" + if address.get("virtual"): + address_cmd = "ip address virtual {0}".format(address["address"]) + commands.append(address_cmd) + + want_ipv6 = set( + tuple(sorted(address.items())) for address in want.get("ipv6") or [] + ) + have_ipv6 = set( + tuple(sorted(address.items())) for address in have.get("ipv6") or [] + ) + for address in want_ipv6 - have_ipv6: + address = dict(address) + commands.append("ipv6 address {0}".format(address["address"])) + return commands + + +def clear_interface(want, have): + commands = [] + want_ipv4 = set( + tuple(sorted(address.items())) for address in want.get("ipv4") or [] + ) + have_ipv4 = set( + tuple(sorted(address.items())) for address in have.get("ipv4") or [] + ) + if not want_ipv4 and have_ipv4: + commands.append("no ip address") + else: + for address in have_ipv4 - want_ipv4: + address = dict(address) + for param in ["secondary", "virtual"]: + if param not in address: + address[param] = None + if tuple(sorted(address.items())) in want_ipv4: + continue + + if address.get("secondary"): + commands.append( + "no ip address {0} secondary".format(address["address"]), + ) + if address.get("virtual"): + commands.append( + "no ip address virtual {0}".format(address["address"]), + ) + + if "secondary" not in address: + # Removing non-secondary removes all other interfaces + break + + want_ipv6 = set( + tuple(sorted(address.items())) for address in want.get("ipv6") or [] + ) + have_ipv6 = set( + tuple(sorted(address.items())) for address in have.get("ipv6") or [] + ) + for address in have_ipv6 - want_ipv6: + address = dict(address) + commands.append("no ipv6 address {0}".format(address["address"])) + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp/lacp.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp/lacp.py new file mode 100644 index 000000000..0d7ba850f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp/lacp.py @@ -0,0 +1,201 @@ +# -*- 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 eos_lacp 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 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 ( + dict_diff, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) + + +class Lacp(ConfigBase): + """ + The eos_lacp class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["lacp"] + + def get_lacp_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, + ) + lacp_facts = facts["ansible_network_resources"].get("lacp") + if not lacp_facts: + return {} + return lacp_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_lacp_facts = self.get_lacp_facts() + else: + existing_lacp_facts = {} + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_lacp_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_lacp_facts = self.get_lacp_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_lacp_facts(data=running_config) + + if self.state in self.ACTION_STATES: + result["before"] = existing_lacp_facts + if result["changed"]: + result["after"] = changed_lacp_facts + + elif self.state == "gathered": + result["gathered"] = changed_lacp_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_lacp_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"] or {} + have = existing_lacp_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 + """ + state = self._module.params["state"] + if state in ("merged", "replaced", "rendered") and not want: + self._module.fail_json( + msg="value of config parameter must not be empty for state {0}".format( + state, + ), + ) + if state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + to_set = dict_diff(have, want) + if "system" in to_set: + system = to_set["system"] + if "priority" in system: + commands.append( + "lacp system-priority {0}".format(system["priority"]), + ) + + to_del = dict_diff(want, have) + if "system" in to_del: + system = to_del["system"] + system_set = to_set.get("system", {}) + if "priority" in system and "priority" not in system_set: + commands.append("no lacp system-priority") + + return commands + + @staticmethod + def _state_merged(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 = [] + to_set = dict_diff(have, want) + if "system" in to_set: + system = to_set["system"] + if "priority" in system: + commands.append( + "lacp system-priority {0}".format(system["priority"]), + ) + + return commands + + @staticmethod + def _state_deleted(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 = [] + to_del = dict_diff(want, have) + if "system" in to_del: + system = to_del["system"] + if "priority" in system: + commands.append("no lacp system-priority") + + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp_interfaces/lacp_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp_interfaces/lacp_interfaces.py new file mode 100644 index 000000000..64746f880 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lacp_interfaces/lacp_interfaces.py @@ -0,0 +1,262 @@ +# -*- 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 eos_lacp_interfaces 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 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 ( + dict_diff, + param_list_to_dict, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, +) + + +class Lacp_interfaces(ConfigBase): + """ + The eos_lacp_interfaces class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["lacp_interfaces"] + + def get_lacp_interfaces_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, + ) + lacp_interfaces_facts = facts["ansible_network_resources"].get( + "lacp_interfaces", + ) + if not lacp_interfaces_facts: + return [] + return lacp_interfaces_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_lacp_interfaces_facts = self.get_lacp_interfaces_facts() + else: + existing_lacp_interfaces_facts = {} + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_lacp_interfaces_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_lacp_interfaces_facts = self.get_lacp_interfaces_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_lacp_interfaces_facts( + data=running_config, + ) + if self.state in self.ACTION_STATES: + result["before"] = existing_lacp_interfaces_facts + if result["changed"]: + result["after"] = changed_lacp_interfaces_facts + elif self.state == "gathered": + result["gathered"] = changed_lacp_interfaces_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_lacp_interfaces_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_lacp_interfaces_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + want = param_list_to_dict(want) + have = param_list_to_dict(have) + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + for key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(key, add_config, del_config)) + + return commands + + @staticmethod + def _state_overridden(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 = [] + for key, extant in have.items(): + if key in want: + desired = want[key] + else: + desired = dict() + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(key, add_config, del_config)) + + return commands + + @staticmethod + def _state_merged(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 key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict() + + add_config = dict_diff(extant, desired) + + commands.extend(generate_commands(key, add_config, {})) + + return commands + + @staticmethod + def _state_deleted(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 = [] + for key in want: + desired = dict() + if key in have: + extant = have[key] + else: + continue + + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(key, {}, del_config)) + + return commands + + +def generate_commands(interface, to_set, to_remove): + commands = [] + for key in to_remove.keys(): + if key == "rate": + continue + commands.append("no lacp {0}".format(key.replace("_", "-"))) + + for key, value in to_set.items(): + if key == "rate": + continue + if value is None: + continue + + commands.append("lacp {0} {1}".format(key.replace("_", "-"), value)) + + if commands: + commands.insert(0, "interface {0}".format(interface)) + + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lag_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lag_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lag_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lag_interfaces/lag_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lag_interfaces/lag_interfaces.py new file mode 100644 index 000000000..b69ae175f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lag_interfaces/lag_interfaces.py @@ -0,0 +1,275 @@ +# -*- 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 eos_lag_interfaces 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 + +import re + + +__metaclass__ = type + +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 ( + dict_diff, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, +) + + +class Lag_interfaces(ConfigBase): + """ + The eos_lag_interfaces class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["lag_interfaces"] + + def get_lag_interfaces_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, + ) + lag_interfaces_facts = facts["ansible_network_resources"].get( + "lag_interfaces", + ) + if not lag_interfaces_facts: + return [] + return lag_interfaces_facts + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {"changed": False} + commands = list() + warnings = list() + + if self.state in self.ACTION_STATES: + existing_lag_interfaces_facts = self.get_lag_interfaces_facts() + else: + existing_lag_interfaces_facts = {} + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_lag_interfaces_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_lag_interfaces_facts = self.get_lag_interfaces_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_lag_interfaces_facts( + data=running_config, + ) + + if self.state in self.ACTION_STATES: + result["before"] = existing_lag_interfaces_facts + if result["changed"]: + result["after"] = changed_lag_interfaces_facts + elif self.state == "gathered": + result["gathered"] = changed_lag_interfaces_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_lag_interfaces_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_lag_interfaces_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + for interface in want: + interface_name = normalize_interface(interface["name"]) + for extant in have: + if extant["name"] == interface_name: + break + else: + extant = dict(name=interface_name) + + commands.extend(set_config(interface, extant)) + commands.extend(remove_config(interface, extant)) + + return commands + + @staticmethod + def _state_overridden(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 = [] + for extant in have: + for interface in want: + if normalize_interface(interface["name"]) == extant["name"]: + break + else: + interface = dict(name=extant["name"]) + commands.extend(remove_config(interface, extant)) + + for interface in want: + interface_name = normalize_interface(interface["name"]) + for extant in have: + if extant["name"] == interface_name: + break + else: + extant = dict(name=interface_name) + commands.extend(set_config(interface, extant)) + + return commands + + @staticmethod + def _state_merged(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 interface in want: + interface_name = normalize_interface(interface["name"]) + for extant in have: + if extant["name"] == interface_name: + break + else: + extant = dict(name=interface_name) + + commands.extend(set_config(interface, extant)) + + return commands + + @staticmethod + def _state_deleted(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 = [] + for interface in want: + interface_name = normalize_interface(interface["name"]) + for extant in have: + if extant["name"] == interface_name: + break + else: + extant = dict(name=interface_name) + + # Clearing all args, send empty dictionary + interface = dict(name=interface_name) + commands.extend(remove_config(interface, extant)) + + return commands + + +def set_config(want, have): + commands = [] + to_set = dict_diff(have, want) + for member in to_set.get("members", []): + channel_id = re.search(r"\d.*", want["name"]) + if channel_id: + commands.extend( + [ + "interface {0}".format(member["member"]), + "channel-group {0} mode {1}".format( + channel_id.group(0), + member["mode"], + ), + ], + ) + + return commands + + +def remove_config(want, have): + commands = [] + if not want.get("members"): + return ["no interface {0}".format(want["name"])] + + to_remove = dict_diff(want, have) + for member in to_remove.get("members", []): + commands.extend( + ["interface {0}".format(member["member"]), "no channel-group"], + ) + + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_global/lldp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_global/lldp_global.py new file mode 100644 index 000000000..f42471ff9 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_global/lldp_global.py @@ -0,0 +1,215 @@ +# +# -*- 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 eos_lldp_global 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 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 ( + dict_diff, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) + + +class Lldp_global(ConfigBase): + """ + The eos_lldp_global class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["lldp_global"] + + def __init__(self, module): + super(Lldp_global, self).__init__(module) + + def get_lldp_global_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, + ) + lldp_global_facts = facts["ansible_network_resources"].get( + "lldp_global", + ) + if not lldp_global_facts: + return {} + return lldp_global_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_lldp_global_facts = self.get_lldp_global_facts() + else: + existing_lldp_global_facts = {} + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_lldp_global_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_lldp_global_facts = self.get_lldp_global_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_lldp_global_facts(data=running_config) + + if self.state in self.ACTION_STATES: + result["before"] = existing_lldp_global_facts + if result["changed"]: + result["after"] = changed_lldp_global_facts + + elif self.state == "gathered": + result["gathered"] = changed_lldp_global_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_lldp_global_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"] or {} + have = existing_lldp_global_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + if state == "deleted": + commands = state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = state_merged(want, have) + elif state == "replaced": + commands = state_replaced(want, have) + return commands + + +def state_replaced(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 = set() + # merged and deleted are likely to emit duplicate tlv-select commands + commands.update(state_merged(want, have)) + commands.update(state_deleted(want, have)) + + return list(commands) + + +def state_merged(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 = [] + to_set = dict_diff(have, want) + tlv_options = to_set.pop("tlv_select", {}) + for key, value in to_set.items(): + if key == "holdtime": + key = "hold-time" + if key == "reinit": + key = "timer reinitialization" + commands.append("lldp {0} {1}".format(key, value)) + for key, value in tlv_options.items(): + device_option = key.replace("_", "-") + if value is True: + commands.append("lldp tlv transmit {0}".format(device_option)) + elif value is False: + commands.append("no lldp tlv transmit {0}".format(device_option)) + + return commands + + +def state_deleted(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 = [] + to_remove = dict_diff(want, have) + tlv_options = to_remove.pop("tlv_select", {}) + for key in to_remove: + if key == "holdtime": + key = "hold-time" + if key == "reinit": + key = "timer reinitialization" + commands.append("no lldp {0}".format(key)) + for key, value in tlv_options.items(): + device_option = key.replace("_", "-") + if value is False: + commands.append("lldp tlv transmit {0}".format(device_option)) + elif value is True: + commands.append("no lldp tlv transmit {0}".format(device_option)) + + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_interfaces/lldp_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_interfaces/lldp_interfaces.py new file mode 100644 index 000000000..c0325102b --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/lldp_interfaces/lldp_interfaces.py @@ -0,0 +1,264 @@ +# -*- 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 eos_lldp_interfaces 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 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 ( + dict_diff, + param_list_to_dict, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, +) + + +class Lldp_interfaces(ConfigBase): + """ + The eos_lldp_interfaces class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["lldp_interfaces"] + + def get_lldp_interfaces_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, + ) + lldp_interfaces_facts = facts["ansible_network_resources"].get( + "lldp_interfaces", + ) + if not lldp_interfaces_facts: + return [] + return lldp_interfaces_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_lldp_interfaces_facts = self.get_lldp_interfaces_facts() + else: + existing_lldp_interfaces_facts = [] + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_lldp_interfaces_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_lldp_interfaces_facts = self.get_lldp_interfaces_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_lldp_interfaces_facts( + data=running_config, + ) + + if self.state in self.ACTION_STATES: + result["before"] = existing_lldp_interfaces_facts + if result["changed"]: + result["after"] = changed_lldp_interfaces_facts + + elif self.state == "gathered": + result["gathered"] = changed_lldp_interfaces_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_lldp_interfaces_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_lldp_interfaces_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + want = param_list_to_dict(want, remove_key=False) + have = param_list_to_dict(have, remove_key=False) + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + for key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict(name=interface_name) + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + commands.extend( + generate_commands(interface_name, add_config, del_config), + ) + + return commands + + @staticmethod + def _state_overridden(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 = [] + for key, extant in have.items(): + if key in want: + desired = want[key] + else: + desired = dict(name=key) + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(key, add_config, del_config)) + + return commands + + @staticmethod + def _state_merged(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 key, desired in want.items(): + interface_name = normalize_interface(key) + if interface_name in have: + extant = have[interface_name] + else: + extant = dict(name=interface_name) + + add_config = dict_diff(extant, desired) + + commands.extend(generate_commands(interface_name, add_config, {})) + + return commands + + @staticmethod + def _state_deleted(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 = [] + for key in want.keys(): + interface_name = normalize_interface(key) + desired = dict(name=interface_name) + if interface_name in have: + extant = have[interface_name] + else: + continue + + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(interface_name, {}, del_config)) + + return commands + + +def generate_commands(name, to_set, to_remove): + commands = [] + for key, value in to_set.items(): + if value is None: + continue + + prefix = "" if value else "no " + commands.append("{0}lldp {1}".format(prefix, key)) + + for key in to_remove: + commands.append("lldp {0}".format(key)) + + if commands: + commands.insert(0, "interface {0}".format(name)) + + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/logging_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/logging_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/logging_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/logging_global/logging_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/logging_global/logging_global.py new file mode 100644 index 000000000..8016b29d7 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/logging_global/logging_global.py @@ -0,0 +1,190 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_logging_global config file. +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 its desired end-state is +created. +""" + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.logging_global import ( + Logging_globalTemplate, +) + + +class Logging_global(ResourceModule): + """ + The eos_logging_global config class + """ + + def __init__(self, module): + super(Logging_global, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="logging_global", + tmplt=Logging_globalTemplate(), + ) + self.parsers = [ + "buffered", + "event", + "facility", + "console", + "format", + "format.timestamp.traditional", + "format.timestamp.highresolution", + "level", + "monitor", + "on", + "persistent", + "policy", + "relogging_interval", + "repeat_messages", + "src_interface", + "synchronous", + "trap", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + + wantd = {"logging_global": self.want} + haved = {"logging_global": self.have} + # turn all lists of dicts into dicts prior to merge + for entry in wantd["logging_global"], haved["logging_global"]: + self._logging_global_list_to_dict(entry) + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + wantd = {} + for k, have in iteritems(haved): + self._compare(want={}, have=have) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Logging_global network resource. + """ + self._hosts_compare(want=want, have=have) + self._vrfs_compare(want=want, have=have) + self.compare(parsers=self.parsers, want=want, have=have) + + def _hosts_compare(self, want, have): + host_want = want.pop("hosts", {}) + host_have = have.pop("hosts", {}) + for name, entry in iteritems(host_want): + h = {} + if host_have: + h = {"hosts": host_have.pop(name, {})} + self.compare(parsers="host", want={"hosts": entry}, have=h) + for name, entry in iteritems(host_have): + self.compare(parsers="host", want={}, have={"hosts": entry}) + + def _vrfs_hosts_compare(self, vrf, want, have): + host_want = want.pop("hosts", {}) + host_have = have.pop("hosts", {}) + for name, entry in iteritems(host_want): + h = {} + if host_have: + h = {"vrfs": {"name": vrf, "hosts": host_have.pop(name, {})}} + w = {"vrfs": {"name": vrf, "hosts": entry}} + self.compare(parsers="vrf.host", want=w, have=h) + for name, entry in iteritems(host_have): + self.compare( + parsers="vrf.host", + want={}, + have={"vrfs": {"name": vrf, "hosts": entry}}, + ) + + def _vrfs_compare(self, want, have): + vrf_want = want.pop("vrfs", {}) + vrf_have = have.pop("vrfs", {}) + for name, entry in iteritems(vrf_want): + self._vrfs_hosts_compare( + name, + want=entry, + have=vrf_have.get(name, {}), + ) + if entry.get("source_interface"): + h = {} + if vrf_have.get(name): + h = { + "vrfs": { + "name": name, + "source_interface": vrf_have[name].pop( + "source_interface", + "", + ), + }, + } + w = { + "vrfs": { + "name": name, + "source_interface": entry["source_interface"], + }, + } + self.compare(parsers="vrf.source_interface", want=w, have=h) + for name, entry in iteritems(vrf_have): + self._vrfs_hosts_compare(name, want={}, have=entry) + self.compare( + parsers="vrf.source_interface", + want={}, + have={"vrfs": entry}, + ) + + def _logging_global_list_to_dict(self, entry): + if "hosts" in entry: + hosts_dict = {} + for el in entry["hosts"]: + hosts_dict.update({el["name"]: el}) + entry["hosts"] = hosts_dict + + if "vrfs" in entry: + vrf_dict = {} + for el in entry["vrfs"]: + vrf_dict.update({el["name"]: el}) + entry["vrfs"] = vrf_dict + for k, v in iteritems(entry["vrfs"]): + self._logging_global_list_to_dict(v) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ntp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ntp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ntp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ntp_global/ntp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ntp_global/ntp_global.py new file mode 100644 index 000000000..0b8323d15 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ntp_global/ntp_global.py @@ -0,0 +1,249 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_ntp_global config file. +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 its desired end-state is +created. +""" + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.ntp_global import ( + Ntp_globalTemplate, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, +) + + +class Ntp_global(ResourceModule): + """ + The eos_ntp_global config class + """ + + def __init__(self, module): + super(Ntp_global, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="ntp_global", + tmplt=Ntp_globalTemplate(), + ) + self.parsers = [ + "authenticate", + "local_interface", + "qos_dscp", + "trusted_key", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = {"ntp_global": self.want} + haved = {"ntp_global": self.have} + # turn all lists of dicts into dicts prior to merge + for entry in wantd["ntp_global"], haved["ntp_global"]: + self._ntp_global_list_to_dict(entry) + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + wantd = {} + for k, have in iteritems(haved): + if k not in wantd: + self._compare(want={}, have=have) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Ntp_global network resource. + """ + self._serve_compare(want=want, have=have) + self._authentication_keys_compare(want=want, have=have) + self._servers_compare(want=want, have=have) + self.compare(parsers=self.parsers, want=want, have=have) + add_cmd = [] + del_cmd = [] + if self.commands: + for cmd in self.commands: + if "no ntp" in cmd: + del_cmd.append(cmd) + else: + add_cmd.append(cmd) + self.commands = del_cmd + add_cmd + + def _authentication_keys_compare(self, want, have): + w = want.pop("authentication_keys", {}) + h = have.pop("authentication_keys", {}) + for name, entry in iteritems(w): + h_key = {} + if h.get(name): + h_key = {"authentication_keys": h.pop(name)} + self.compare( + parsers="authentication_keys", + want={"authentication_keys": entry}, + have=h_key, + ) + for name, entry in iteritems(h): + self.compare( + parsers="authentication_keys", + want={}, + have={"authentication_keys": entry}, + ) + + def _servers_compare(self, want, have): + w = want.pop("servers", {}) + h = have.pop("servers", {}) + for name, entry in iteritems(w): + if entry.get("source"): + entry["source"] = normalize_interface(entry["source"]) + h_key = {} + if h.get(name): + h_key = {"servers": h.pop(name)} + self.compare( + parsers="servers", + want={"servers": entry}, + have=h_key, + ) + for name, entry in iteritems(h): + self.compare(parsers="servers", want={}, have={"servers": entry}) + + def _serve_compare(self, want, have): + serve_want = want.pop("serve", {}) + serve_have = have.pop("serve", {}) + for name, entry in iteritems(serve_want): + if name == "all" and entry: + w = {"serve": {"all": True}} + self.compare( + parsers="serve_all", + want=w, + have={"serve": {"all": serve_have.pop("all", False)}}, + ) + else: + for k_afi, v_afi in iteritems(entry): + for k, v in iteritems(v_afi): + afi = v_afi["afi"] + if k == "afi": + continue + h = {} + if k == "acls": + for ace, ace_entry in iteritems(v): + if serve_have.get("access_lists"): + for hk, hv in iteritems( + serve_have["access_lists"], + ): + for h_k, h_v in iteritems(hv): + h_afi = hv["afi"] + if h_k == "afi": + continue + if h_afi == afi: + if ace in h_v: + h_acc = { + "afi": h_afi, + "acls": h_v.pop(ace), + } + h = { + "serve": { + "access_lists": h_acc, + }, + } + w = { + "serve": { + "access_lists": { + "afi": afi, + "acls": ace_entry, + }, + }, + } + self.compare(parsers="serve", want=w, have=h) + for k, v in iteritems(serve_have): + if k == "all" and v: + h = {"serve": {"all": True}} + self.compare(parsers="serve_all", want={}, have=h) + else: + for k_afi, v_afi in iteritems(v): + for k, v in iteritems(v_afi): + hafi = v_afi["afi"] + if k == "afi": + continue + for k_acl, v_acl in iteritems(v): + h = { + "serve": { + "access_lists": { + "afi": hafi, + "acls": v_acl, + }, + }, + } + self.compare(parsers="serve", want={}, have=h) + + def _ntp_global_list_to_dict(self, entry): + if "authentication_keys" in entry: + key_dict = {} + for el in entry["authentication_keys"]: + key_dict.update({el["id"]: el}) + entry["authentication_keys"] = key_dict + + if "servers" in entry: + server_dict = {} + for el in entry["servers"]: + server_dict.update({el["server"]: el}) + entry["servers"] = server_dict + + if "serve" in entry: + serve_dict = {} + main_dict = {} + if entry["serve"].get("all"): + main_dict.update({"all": entry["serve"]["all"]}) + if entry["serve"].get("access_lists"): + for el in entry["serve"].get("access_lists"): + if "acls" in el: + acl_dict = {} + for acl in el["acls"]: + acl_dict.update({acl["acl_name"]: acl}) + el["acls"] = acl_dict + serve_dict.update({el["afi"]: el}) + if serve_dict: + main_dict.update({"access_lists": serve_dict}) + if serve_dict: + entry["serve"] = main_dict diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospf_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospf_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospf_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospf_interfaces/ospf_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospf_interfaces/ospf_interfaces.py new file mode 100644 index 000000000..38d46bd7c --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospf_interfaces/ospf_interfaces.py @@ -0,0 +1,212 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_ospf_interfaces config file. +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 its desired end-state is +created. +""" +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.ospf_interfaces import ( + Ospf_interfacesTemplate, +) + + +class Ospf_interfaces(ResourceModule): + """ + The eos_ospf_interfaces config class + """ + + def __init__(self, module): + super(Ospf_interfaces, self).__init__( + empty_fact_val=[], + facts_module=Facts(module), + module=module, + resource="ospf_interfaces", + tmplt=Ospf_interfacesTemplate(), + ) + self.parsers = [ + "interfaces", + "area", + "authentication_v2", + "authentication_v3", + "authentication_key", + "deadinterval", + "encryption", + "hellointerval", + "bfd", + "cost", + "ip_params_area", + "ip_params_bfd", + "ip_params_cost", + "ip_params_dead_interval", + "ip_params_hello_interval", + "ip_params_mtu_ignore", + "ip_params_network", + "ip_params_priority", + "ip_params_passive_interface", + "ip_params_retransmit_interval", + "ip_params_transmit_delay", + "mtu_ignore", + "network", + "priority", + "passive_interface", + "retransmit_interval", + "transmit_delay", + "message_digest_key", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + + # convert list of dicts to dicts of dicts + wantd = {} + haved = {} + for entry in self.want: + wantd.update({entry["name"]: entry}) + for entry in self.have: + haved.update({entry["name"]: entry}) + + # turn all lists of dicts into dicts prior to merge + for entry in wantd, haved: + self._ospf_int_list_to_dict(entry) + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + h_del = {} + for k, v in iteritems(haved): + if k in wantd or not wantd: + h_del.update({k: v}) + haved = h_del + for k, have in iteritems(haved): + self._compare(want={}, have=have) + wantd = {} + + # remove superfluous config for overridden + if self.state == "overridden": + for k, have in iteritems(haved): + if k not in wantd: + self._compare(want={}, have=have) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Ospf_interfaces network resource. + """ + begin = len(self.commands) + self._compare_addr_family(want=want, have=have) + if len(self.commands) != begin: + tmp = want or have + tmp.pop("address_family", {}) + self.commands.insert( + begin, + self._tmplt.render(tmp, "interfaces", False), + ) + + def _compare_addr_family(self, want, have): + wdict = want.get("address_family", {}) + hdict = have.get("address_family", {}) + for afi in ["ipv4", "ipv6"]: + w_family = wdict.pop(afi, {}) + h_family = hdict.pop(afi, {}) + for k in w_family.keys(): + if k == "afi": + continue + w = {"afi": afi, k: w_family[k]} + h = {"afi": afi, k: h_family.pop(k, {})} + if k == "ip_params": + self._compare_ip_params(want=w, have=h) + self.compare(parsers=self.parsers, want=w, have=h) + for k in h_family.keys(): + if k in ["afi"]: + continue + w = {"afi": afi, k: None} + h = {"afi": afi, k: h_family[k]} + if k == "ip_params": + w = {"afi": afi, k: {}} + self._compare_ip_params(want=w, have=h) + self.compare(parsers=self.parsers, want=w, have=h) + + def _compare_ip_params(self, want, have): + w_params = want.get("ip_params", {}) + h_params = have.get("ip_params", {}) + for afi in ["ipv4", "ipv6"]: + w_p = w_params.pop(afi, {}) + h_p = h_params.pop(afi, {}) + for k, params in iteritems(w_p): + if k == "afi": + continue + w = {"afi": afi, k: params} + h = {"afi": afi, k: h_p.pop(k, None)} + self.compare( + parsers=self.parsers, + want={"ip_params": w}, + have={"ip_params": h}, + ) + for k, params in iteritems(h_p): + if k == "afi": + continue + w = {"afi": afi, k: None} + h = {"afi": afi, k: params} + self.compare( + parsers=self.parsers, + want={"ip_params": w}, + have={"ip_params": h}, + ) + + def _ospf_int_list_to_dict(self, entry): + for name, family in iteritems(entry): + if family.get("ip_params"): + family_dict = {} + for entry in family["ip_params"]: + family_dict.update({entry["afi"]: entry}) + family["ip_params"] = family_dict + + if "address_family" in family: + addr_dict = {} + for entry in family.get("address_family", []): + addr_dict.update({entry["afi"]: entry}) + family["address_family"] = addr_dict + self._ospf_int_list_to_dict(family["address_family"]) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv2/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv2/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv2/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv2/ospfv2.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv2/ospfv2.py new file mode 100644 index 000000000..f7e85d4a2 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv2/ospfv2.py @@ -0,0 +1,840 @@ +# +# -*- 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 eos_ospfv2 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 + +import re + +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 ( + dict_diff, + remove_empties, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) + + +class Ospfv2(ConfigBase): + """ + The eos_ospfv2 class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["ospfv2"] + + def __init__(self, module): + super(Ospfv2, self).__init__(module) + + def get_ospfv2_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, + ) + + ospfv2_facts = facts["ansible_network_resources"].get("ospfv2") + if not ospfv2_facts: + return [] + return ospfv2_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_ospfv2_facts = self.get_ospfv2_facts() + else: + existing_ospfv2_facts = [] + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_ospfv2_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_ospfv2_facts = self.get_ospfv2_facts() + elif self.state == "rendered": + result["rendered"] = commands + elif self.state == "parsed": + if not self._module.params["running_config"]: + self._module.fail_json( + msg="value of running_config parameter must not be empty for state parsed", + ) + result["parsed"] = self.get_ospfv2_facts( + data=self._module.params["running_config"], + ) + else: + changed_ospfv2_facts = self.get_ospfv2_facts() + if self.state in self.ACTION_STATES: + result["before"] = existing_ospfv2_facts + if result["changed"]: + result["after"] = changed_ospfv2_facts + elif self.state == "gathered": + result["gathered"] = changed_ospfv2_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_ospfv2_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_ospfv2_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 = self._state_overridden(want, have) + elif self.state == "deleted": + commands = self._state_deleted(want, have) + elif self.state == "merged" or self.state == "rendered": + commands = self._state_merged(want, have) + elif self.state == "replaced": + commands = self._state_replaced(want, have) + return commands + + def _get_os_version(self): + os_version = "4.20" + if self._connection(): + os_version = self._connection.get_device_info()[ + "network_os_version" + ] + return os_version + + 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 = [] + for w in want["processes"]: + del_cmds = w.copy() + add_cmds = {} + for h in have["processes"]: + if h["process_id"] != w["process_id"]: + continue + if w.get("vrf"): + if w["vrf"] != h["vrf"]: + self._module.fail_json( + msg="Value of vrf and process_id does not match the config present in the device", + ) + break + del_instance_list = self.compare_dicts(h, w) + if del_instance_list: + del_cmds = {"processes": del_instance_list} + add_instance_list = self.compare_dicts(w, h) + if add_instance_list: + add_cmds = {"processes": add_instance_list} + + return_command = self.del_commands(del_cmds, have) + for command in return_command: + if "exit" not in command: + commands.append(command) + return_command = self.add_commands(add_cmds, have) + for command in return_command: + if "router ospf" in command: + if command not in commands: + commands.append(command) + else: + commands.append(command) + commandset = [] + if commands: + commandset.append(commands[0]) + for cmd in commands[1::]: + if "router ospf" in cmd and commandset[-1] != "exit": + commandset.append("exit") + commandset.append(cmd) + if commandset[-1] != "exit": + commandset.append("exit") + return commandset + + 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 = [] + for h in have["processes"]: + present = False + for w in want["processes"]: + if h["process_id"] == w["process_id"]: + present = True + break + if not present: + commands.append("no router ospf " + str(h["process_id"])) + replace_cmds = self._state_replaced(want, have) + for cmd in replace_cmds: + commands.append(cmd) + 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 + """ + + return self.set_commands(want, have) + + 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 = [] + return_command = self.del_commands(want, have) + if return_command: + for cmd in return_command: + if "no exit" in cmd: + cmd = "exit" + commands.append(cmd) + return commands + + def set_commands(self, want, have): + commands = [] + instance_list = [] + for w in want["processes"]: + present = False + c = [] + if have and not have.get("processes"): + instance_list = want["processes"] + break + if have: + for h in have["processes"]: + if w["process_id"] == h["process_id"]: + if w.get("vrf"): + if w["vrf"] != h["vrf"]: + self._module.fail_json( + msg="Value of vrf and process_id does not match the config present in the device", + ) + continue + present = True + c = self.compare_dicts(w, h) + break + if c: + instance_list.append(c[0]) + if not present: + if w["vrf"] in _get_vrf_list(have): + self._module.fail_json( + msg="Value of vrf and process_id does not match the config present in the device", + ) + instance_list.append(w) + instance_dict = {"processes": instance_list} + return_command = self.add_commands(instance_dict, have) + for command in return_command: + commands.append(command) + return commands + + def compare_dicts(self, want_inst, have_inst): + want_dict = remove_empties(want_inst) + have = have_inst + ospf_list = [] + return_ospf_dict = {} + for w_key in want_dict.keys(): + if not have.get(w_key): + return_ospf_dict.update({w_key: want_dict[w_key]}) + elif ( + isinstance(want_dict[w_key], str) + or isinstance(want_dict[w_key], bool) + or isinstance(want_dict[w_key], int) + ): + if want_dict[w_key] != have[w_key]: + return_ospf_dict.update({w_key: want_dict[w_key]}) + elif isinstance(want_dict[w_key], dict): + diff = dict_diff(have.get(w_key, {}), want_dict[w_key]) + if diff: + return_ospf_dict.update({w_key: diff}) + elif isinstance(want_dict[w_key], list): + if have.get(w_key): + compare_list = self.compare_ospf_list( + want_dict[w_key], + have.get(w_key), + w_key, + ) + if compare_list: + return_ospf_dict.update({w_key: compare_list}) + else: + if want_dict[w_key] != have.get(w_key): + return_ospf_dict.update({w_key: want_dict[w_key]}) + + if return_ospf_dict: + if want_dict.get("vrf"): + return_ospf_dict.update( + { + "process_id": want_dict["process_id"], + "vrf": want_dict["vrf"], + }, + ) + else: + return_ospf_dict.update( + {"process_id": want_dict["process_id"]}, + ) + ospf_list.append(return_ospf_dict) + return ospf_list + + def compare_ospf_list(self, w_list, h_list, l_key): + return_list = [] + for w in w_list: + present = False + for h in h_list: + diff = dict_diff(h, w) + if not diff: + present = True + break + if not present: + return_list.append(w) + return return_list + + def add_commands(self, want, have): + commands = [] + if not want: + return commands + for ospf_params in want["processes"]: + commands.append(_get_router_command(ospf_params)) + if ospf_params.get("traffic_engineering"): + commands.append("traffic-engineering") + if ospf_params.get("adjacency"): + threshold = ospf_params["adjacency"]["exchange_start"][ + "threshold" + ] + commands.append( + "adjacency exchange-start threshold " + str(threshold), + ) + if ospf_params.get("areas"): + command_list = _parse_areas(ospf_params["areas"]) + for c in command_list: + commands.append(c) + if ospf_params.get("auto_cost"): + commands.append( + "auto-cost reference-bandwidth " + + ospf_params["auto_cost"], + ) + if ospf_params.get("bfd"): + os_version = self._get_os_version() + if os_version < "4.23": + commands.append("bfd all-interfaces") + else: + commands.append("bfd default") + if ospf_params.get("default_information"): + commands.append( + _parse_default_information( + ospf_params["default_information"], + ), + ) + if ospf_params.get("default_metric"): + commands.append( + "default-metric" + + " " + + str(ospf_params["default_metric"]), + ) + if ospf_params.get("distance"): + for k, v in ospf_params["distance"].items(): + if v: + k = re.sub(r"_", "-", k) + commands.append("distance ospf " + k + " " + str(v)) + if ospf_params.get("distribute_list"): + commands.append( + "distribute-list " + + ospf_params["distribute_list"].keys()[0] + + " " + + ospf_params["distribute_list"].values()[0] + + " in", + ) + if ospf_params.get("dn_bit_ignore"): + commands.append("dn-bit-ignore") + if ospf_params.get("graceful_restart"): + if ospf_params["graceful_restart"].get("set"): + commands.append("graceful-restart") + else: + commands.append( + "graceful-restart grace-period " + + str( + ospf_params["graceful_restart"].get( + "grace_period", + ), + ), + ) + if ospf_params.get("graceful_restart_helper"): + commands.append("graceful-restart-helper") + if ospf_params.get("log_adjacency_changes"): + cmd = "log-adjacency-changes" + if ospf_params["log_adjacency_changes"].get("detail"): + cmd = cmd + " detail" + commands.append(cmd) + if ospf_params.get("max_lsa"): + commands.append(_parse_max_lsa(ospf_params["max_lsa"])) + if ospf_params.get("max_metric"): + commands.append(_parse_max_metric(ospf_params["max_metric"])) + if ospf_params.get("maximum_paths"): + commands.append( + "maximum-paths " + str(ospf_params["maximum_paths"]), + ) + if ospf_params.get("mpls_ldp"): + commands.append("mpls ldp sync default") + if ospf_params.get("networks"): + command_list = _parse_networks(ospf_params["networks"]) + for c in command_list: + commands.append(c) + if ospf_params.get("passive_interface"): + if "interface_list" in ospf_params["passive_interface"].keys(): + commands.append( + "passive-interface " + + ospf_params["passive_interface"]["interface_list"], + ) + else: + commands.append("passive-interface default") + if ospf_params.get("point_to_point"): + commands.append("point-to-point routes") + if ospf_params.get("redistribute"): + command_list = _parse_redistribute(ospf_params["redistribute"]) + for c in command_list: + commands.append(c) + if ospf_params.get("retransmission_threshold"): + commands.append( + "retransmission-threshold lsa " + + str(ospf_params["retransmission_threshold"]), + ) + if ospf_params.get("rfc1583compatibility"): + commands.append("compatible rfc1583") + if ospf_params.get("router_id"): + commands.append("router-id " + ospf_params.get("router_id")) + if ospf_params.get("summary_address"): + commands.append( + _parse_summary_address(ospf_params["summary_address"]), + ) + if ospf_params.get("timers"): + os_version = self._get_os_version() + command_list = _parse_timers(ospf_params["timers"], os_version) + for c in command_list: + commands.append(c) + commands.append("exit") + commandset = [] + for command in commands: + commandset.append(command.strip()) + return commandset + + def del_commands(self, want, have): + commands = [] + other_commands = 0 + want = remove_empties(want) + if want.get("processes"): + for w_inst in want["processes"]: + router_context = 0 + d_cmds = [] + instance_list = [] + if have.get("processes"): + for h_inst in have["processes"]: + if h_inst["process_id"] == w_inst["process_id"]: + if w_inst.get("vrf") and w_inst.get( + "vrf", + ) == h_inst.get("vrf"): + if list(w_inst.keys()) == [ + "process_id", + "vrf", + ]: + commands.append( + "no router ospf " + + str(w_inst["process_id"]) + + " vrf " + + w_inst["vrf"], + ) + router_context = 1 + if len(w_inst.keys()) == 1 and list( + w_inst.keys(), + ) == ["process_id"]: + commands.append( + "no router ospf " + + str(w_inst["process_id"]), + ) + router_context = 1 + if not router_context: + instance_list = self.compare_dicts( + w_inst, + h_inst, + ) + if not instance_list: + del_want = {"processes": [w_inst]} + d_cmds = self.add_commands(del_want, have) + for cmd in d_cmds: + if "router ospf" in cmd: + other_commands = 0 + if cmd not in commands: + commands.append(cmd) + else: + cmd = "no " + cmd + if cmd not in commands: + commands.append(cmd) + other_commands += 1 + if ( + not other_commands + and len(commands) == 1 + and not router_context + ): + if ( + "no" not in commands[0] + and "router ospf" in commands[0] + ): + commands[0] = "no " + commands[0] + return commands + + +def _get_router_command(inst): + command = "" + if inst.get("vrf") and inst.get("vrf") != "default": + command = ( + "router ospf " + str(inst["process_id"]) + " vrf " + inst["vrf"] + ) + else: + command = "router ospf " + str(inst["process_id"]) + return command + + +def _get_vrf_list(want): + vrf_list = [] + if not want: + return vrf_list + for w in want["processes"]: + if w.get("vrf"): + vrf_list.append(w["vrf"]) + return vrf_list + + +def _parse_areas(areas): + command = [] + for area in areas: + area_cmd = "area " + area["area_id"] + if area.get("default_cost"): + command.append( + area_cmd + " default-cost " + str(area.get("default_cost")), + ) + elif area.get("filter"): + command.append( + area_cmd + " " + _parse_areas_filter(area["filter"]), + ) + elif area.get("not_so_stubby"): + command.append( + area_cmd + + " " + + _parse_areas_filter_notsostubby(area["not_so_stubby"]), + ) + elif area.get("nssa"): + command.append( + area_cmd + " " + _parse_areas_filter_nssa(area["nssa"]), + ) + elif area.get("range"): + command.append(area_cmd + " " + _parse_areas_range(area["range"])) + return command + + +def _parse_areas_filter(filter_dict): + filter_cmd = "filter " + if filter_dict.get("prefix_list"): + filter_cmd = filter_cmd + filter_dict.get("filter") + elif filter_dict.get("address"): + filter_cmd = filter_cmd + filter_dict.get("address") + else: + filter_cmd = ( + filter_cmd + + filter_dict.get("subnet_address") + + " " + + filter_dict.get("subnet_mask") + ) + return filter_cmd + + +def _parse_areas_filter_notsostubby(nss_dict): + nss_cmd = "not-so-stubby " + if nss_dict.get("default_information_originate"): + nss_cmd = nss_cmd + "default-information-originate " + for def_keys in nss_dict["default_information_originate"].keys(): + if ( + def_keys == "nssa_only" + and nss_dict["default_information_originate"]["nssa_only"] + ): + nss_cmd = nss_cmd + " nssa-only " + elif nss_dict["default_information_originate"].get(def_keys): + nss_cmd = ( + nss_cmd + + def_keys + + " " + + nss_dict["default_information_originate"][def_keys] + ) + elif "lsa" in nss_dict.keys() and nss_dict.get("lsa"): + nss_cmd = nss_cmd + " lsa type-7 convert type-5" + elif "no_summary" in nss_dict.keys() and nss_dict.get("no_summary"): + nss_cmd = nss_cmd + " no-summary" + elif "nssa_only" in nss_dict.keys() and nss_dict.get("nssa_only"): + nss_cmd = nss_cmd + " nssa-only" + return nss_cmd + + +def _parse_areas_filter_nssa(nss_dict): + nss_cmd = "nssa " + if nss_dict.get("default_information_originate"): + nss_cmd = nss_cmd + "default-information-originate " + for def_keys in nss_dict["default_information_originate"].keys(): + if ( + def_keys == "nssa_only" + and nss_dict["default_information_originate"]["nssa_only"] + ): + nss_cmd = nss_cmd + " nssa-only " + elif nss_dict["default_information_originate"].get(def_keys): + nss_cmd = ( + nss_cmd + + def_keys + + " " + + nss_dict["default_information_originate"][def_keys] + ) + elif "no_summary" in nss_dict.keys() and nss_dict.get("no_summary"): + nss_cmd = nss_cmd + " no-summary" + elif "nssa_only" in nss_dict.keys() and nss_dict.get("nssa_only"): + nss_cmd = nss_cmd + " nssa-only" + return nss_cmd + + +def _parse_areas_range(range_dict): + range_cmd = " range " + if range_dict.get("address"): + range_cmd = range_cmd + range_dict["address"] + if range_dict.get("subnet_address"): + range_cmd = ( + range_cmd + + range_dict["subnet_address"] + + " " + + range_dict["subnet_mask"] + ) + if range_dict.get("advertise") is not None: + if range_dict["advertise"]: + range_cmd = range_cmd + " advertise " + else: + range_cmd = range_cmd + " not-advertise " + if range_dict.get("cost"): + range_cmd = range_cmd + " cost " + str(range_dict["cost"]) + return range_cmd + + +def _parse_default_information(default_dict): + def_cmd = "default-information originate" + for def_key in sorted(default_dict.keys()): + if def_key == "always": + if default_dict.get(def_key): + def_cmd = def_cmd + " " + def_key + elif def_key in ["metric", "metric_type", "route_map"]: + if default_dict.get(def_key): + k = re.sub(r"_", "-", def_key) + def_cmd = def_cmd + " " + k + " " + str(default_dict[def_key]) + return def_cmd + + +def _parse_max_lsa(max_lsa_dict): + max_lsa_cmd = "max-lsa " + if max_lsa_dict.get("count"): + max_lsa_cmd = max_lsa_cmd + " " + str(max_lsa_dict["count"]) + if max_lsa_dict.get("threshold"): + max_lsa_cmd = max_lsa_cmd + " " + str(max_lsa_dict["threshold"]) + for lsa_key, lsa_val in sorted(max_lsa_dict.items()): + if lsa_key == "warning" and lsa_val: + max_lsa_cmd = max_lsa_cmd + " warning-only" + elif lsa_key in ["ignore_count", "reset_time", "ignore_time"]: + if lsa_val: + k = re.sub(r"_", "-", lsa_key) + max_lsa_cmd = max_lsa_cmd + " " + k + " " + str(lsa_val) + " " + return max_lsa_cmd + + +def _parse_max_metric(max_metric_dict): + metric_cmd = "max-metric router-lsa " + for k, v in max_metric_dict["router_lsa"].items(): + if not v: + continue + if k == "include_stub" and v: + metric_cmd = metric_cmd + " include-stub" + elif k == "on_startup": + metric_cmd = metric_cmd + " on-startup " + str(v["wait_period"]) + elif k in ["summary_lsa", "external_lsa"]: + k = re.sub(r"_", "-", k) + if v.get("set"): + metric_cmd = metric_cmd + " " + k + else: + metric_cmd = ( + metric_cmd + " " + k + " " + str(v.get("max_metric_value")) + ) + return metric_cmd + + +def _parse_networks(net_list): + network_cmd = [] + for net_dict in net_list: + net_cmd = "network " + if net_dict.get("prefix"): + net_cmd = net_cmd + net_dict.get("prefix") + else: + net_cmd = ( + net_cmd + + net_dict.get("network_address") + + " " + + net_dict.get("mask") + ) + if net_dict.get("area"): + net_cmd = net_cmd + " area " + net_dict.get("area") + network_cmd.append(net_cmd) + return network_cmd + + +def _parse_redistribute(r_list): + rcmd_list = [] + for r_dict in r_list: + r_cmd = "redistribute " + r_cmd = r_cmd + r_dict["routes"] + if r_dict.get("isis_level"): + k = re.sub(r"_", "-", r_dict["isis_level"]) + r_cmd = r_cmd + " " + k + if r_dict.get("route_map"): + r_cmd = r_cmd + " route-map " + r_dict["route_map"] + rcmd_list.append(r_cmd) + return rcmd_list + + +def _parse_summary_address(addr_dict): + sum_cmd = "summary-address " + if addr_dict.get("prefix"): + sum_cmd = sum_cmd + addr_dict.get("prefix") + else: + sum_cmd = ( + sum_cmd + addr_dict.get("address") + " " + addr_dict.get("mask") + ) + if "attribute_map" in addr_dict.keys(): + sum_cmd = sum_cmd + " attribute-map " + addr_dict["attribute_map"] + elif addr_dict.get("not_advertise"): + sum_cmd = sum_cmd + " not-advertise " + elif "tag" in addr_dict.keys(): + sum_cmd = sum_cmd + " tag " + addr_dict["tag"] + return sum_cmd + + +def _parse_timers(timers_list, os_version="4.20"): + timers_cmd = [] + for t_dict in timers_list: + t_cmd = "timers " + for t_key in t_dict.keys(): + if not t_dict.get(t_key): + break + if t_key == "lsa": + if t_dict["lsa"].get("rx"): + if os_version < "4.23": + t_cmd = ( + t_cmd + + "lsa arrival " + + str(t_dict["lsa"]["rx"]["min_interval"]) + ) + else: + t_cmd = ( + t_cmd + + "lsa rx min interval " + + str(t_dict["lsa"]["rx"]["min_interval"]) + ) + else: + t_cmd = ( + t_cmd + + "lsa tx delay initial " + + str(t_dict["lsa"]["tx"]["delay"]["initial"]) + + " " + + str(t_dict["lsa"]["tx"]["delay"]["min"]) + + " " + + str(t_dict["lsa"]["tx"]["delay"]["max"]) + ) + elif t_key == "out_delay": + t_cmd = t_cmd + " out-delay " + str(t_dict["out_delay"]) + elif t_key == "pacing": + t_cmd = t_cmd + " pacing flood " + str(t_dict["pacing"]) + elif t_key == "spf": + if "seconds" in t_dict["spf"].keys(): + t_cmd = t_cmd + " spf " + str(t_dict["spf"]["seconds"]) + else: + t_cmd = ( + t_cmd + + "spf delay initial " + + str(t_dict["spf"]["initial"]) + + " " + + str(t_dict["spf"]["max"]) + + " " + + str(t_dict["spf"]["min"]) + ) + elif t_key == "throttle": + if t_dict["throttle"]["attr"] == "lsa": + t_cmd = t_cmd + "throttle lsa all " + else: + t_cmd = t_cmd + "throttle spf " + t_cmd = ( + t_cmd + + str(t_dict["throttle"]["initial"]) + + " " + + str(t_dict["throttle"]["min"]) + + " " + + str(t_dict["throttle"]["max"]) + ) + timers_cmd.append(t_cmd) + return timers_cmd diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv3/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv3/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv3/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv3/ospfv3.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv3/ospfv3.py new file mode 100644 index 000000000..a16d163e5 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/ospfv3/ospfv3.py @@ -0,0 +1,392 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_ospfv3 config file. +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 its desired end-state is +created. +""" + +import re + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, + get_from_dict, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.ospfv3 import ( + Ospfv3Template, +) + + +class Ospfv3(ResourceModule): + """ + The eos_ospfv3 config class + """ + + def __init__(self, module): + super(Ospfv3, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="ospfv3", + tmplt=Ospfv3Template(module=module), + ) + self.parsers = [ + "vrf", + "address_family", + "adjacency", + "auto_cost", + "area.default_cost", + "area.authentication", + "area.encryption", + "area.nssa", + "area.ranges", + "area.stub", + "bfd", + "default_information", + "default_metric", + "distance", + "fips_restrictions", + "graceful_restart", + "graceful_restart_period", + "graceful_restart_helper", + "log_adjacency_changes", + "max_metric", + "maximum_paths", + "passive_interface", + "redistribute", + "router_id", + "shutdown", + "timers.out_delay", + "timers.pacing", + "timers.lsa", + "timers.spf", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = {} + haved = {} + for entry in self.want.get("processes", []): + wantd.update({entry["vrf"]: entry}) + for entry in self.have.get("processes", []): + haved.update({entry["vrf"]: entry}) + + # turn all lists of dicts into dicts prior to merge + for entry in wantd, haved: + self._ospf_list_to_dict(entry) + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + h_del = {} + for k, v in iteritems(haved): + if k in wantd or not wantd: + h_del.update({k: v}) + wantd = {} + haved = h_del + + # remove superfluous config for overridden and deleted + if self.state in ["overridden", "deleted"]: + for k, have in iteritems(haved): + if k not in wantd and have.get("vrf") == k: + self.commands.append(self._tmplt.render(have, "vrf", True)) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Ospfv3 network resource. + """ + begin = len(self.commands) + self._af_compare(want=want, have=have) + self._global_compare(want=want, have=have) + + if len(self.commands) != begin or (not have and want): + self.commands.insert( + begin, + self._tmplt.render(want or have, "vrf", False), + ) + self.commands.append("exit") + + def _global_compare(self, want, have): + for name, entry in iteritems(want): + if name == "timers": + if entry.get("throttle"): + throttle = entry.pop("throttle") + modified = {} + if throttle.get("lsa"): + modified["lsa"] = { + "max": throttle["max"], + "min": throttle["min"], + "initial": throttle["initial"], + "direction": "tx", + } + if throttle.get("spf"): + modified["spf"] = { + "max": throttle["max"], + "min": throttle["min"], + "initial": throttle["initial"], + } + entry.update(modified) + self._module.warn( + " ** The 'timers' argument has been changed to have separate 'lsa' and 'spf' keys and 'throttle' has been deprecated. ** " + " \n** Your task has been modified to use {0}. ** " + " \n** timers.throttle will be removed after '2024-01-01' ** ".format( + entry, + ), + ) + if entry.get("lsa") and not isinstance(entry["lsa"], dict): + modified = {} + if not isinstance(entry["lsa"], int): + # if neither old or new format, fail ! + self._module.fail_json( + msg="The lsa key takes a dictionary of arguments. Please consult the documentation for more details", + ) + modified = { + "timers": { + "lsa": {"direction": "rx", "min": entry["lsa"]}, + }, + } + self._module.warn( + " ** 'timers lsa arrival' has changed to 'timers lsa rx min interval' from eos 4.23 onwards. ** " + " \n** Your task has been modified to use {0}. ** " + " \n** timers.lsa of type int will be removed after '2024-01-01' ** ".format( + modified, + ), + ) + entry["lsa"] = modified["timers"]["lsa"] + if name in ["vrf", "address_family"]: + continue + if not isinstance(entry, dict) and name != "areas": + self.compare( + parsers=self.parsers, + want={name: entry}, + have={name: have.pop(name, None)}, + ) + else: + if name == "areas" and entry: + self._areas_compare( + want={name: entry}, + have={name: have.get(name, {})}, + ) + else: + # passing dict without vrf, inorder to avoid no router ospfv3 command + h = {} + for i in have: + if i != "vrf": + h.update({i: have[i]}) + self.compare( + parsers=self.parsers, + want={name: entry}, + have={name: h.pop(name, {})}, + ) + # remove remaining items in have for replaced + for name, entry in iteritems(have): + if name in ["vrf", "address_family"]: + continue + if not isinstance(entry, dict): + self.compare( + parsers=self.parsers, + want={name: want.pop(name, None)}, + have={name: entry}, + ) + else: + # passing dict without vrf, inorder to avoid no router ospfv3 command + self.compare( + parsers=self.parsers, + want={name: want.pop(name, {})}, + have={name: entry}, + ) + + def _af_compare(self, want, have): + wafs = want.get("address_family", {}) + hafs = have.get("address_family", {}) + for name, entry in iteritems(wafs): + begin = len(self.commands) + if "timers" in entry: + if entry["timers"].get("throttle"): + throttle = entry["timers"].pop("throttle") + modified = {} + if throttle.get("lsa"): + modified["lsa"] = { + "max": throttle["max"], + "min": throttle["min"], + "initial": throttle["initial"], + "direction": "tx", + } + if throttle.get("spf"): + modified["spf"] = { + "max": throttle["max"], + "min": throttle["min"], + "initial": throttle["initial"], + } + entry["timers"].update(modified) + self._module.warn( + " ** The 'timers' argument has been changed to have separate 'lsa' and 'spf' keys and 'throttle' has been deprecated. ** " + " \n** Your task has been modified to use {0}. ** " + " \n** timers.throttle will be removed after '2024-01-01' ** ".format( + entry["timers"], + ), + ) + if entry["timers"].get("lsa") and not isinstance( + entry["timers"]["lsa"], + dict, + ): + if not isinstance(entry["timers"]["lsa"], int): + # It doesn't match the new format or the old format, fail here + self._module.fail_json( + msg="The lsa key takes a dictionary of arguments. Please consult the documentation for more details", + ) + modified = { + "timers": { + "lsa": { + "direction": "rx", + "min": entry["timers"]["lsa"], + }, + }, + } + self._module.warn( + " ** 'timers lsa arrival' has changed to 'timers lsa rx min interval' from eos 4.23 onwards. ** " + " \n** Your task has been modified to use {0}. ** " + " \n** timers.lsa of type int will be removed after '2024-01-01' ** ".format( + modified, + ), + ) + entry["timers"]["lsa"] = modified["timers"]["lsa"] + self._compare_lists(want=entry, have=hafs.get(name, {})) + self._areas_compare(want=entry, have=hafs.get(name, {})) + self.compare( + parsers=self.parsers, + want=entry, + have=hafs.pop(name, {}), + ) + if ( + len(self.commands) != begin + and "afi" in entry + and entry["afi"] != "router" + ): + self._rotate_commands(begin=begin) + self.commands.insert( + begin, + self._tmplt.render(entry, "address_family", False), + ) + self.commands.append("exit") + for name, entry in iteritems(hafs): + self.addcmd(entry, "address_family", True) + + def _rotate_commands(self, begin=0): + # move negate commands to beginning + for cmd in self.commands[begin::]: + negate = re.match(r"^no .*", cmd) + if negate: + self.commands.insert( + begin, + self.commands.pop(self.commands.index(cmd)), + ) + begin += 1 + + def _areas_compare(self, want, have): + wareas = want.get("areas", {}) + hareas = have.get("areas", {}) + for name, entry in iteritems(wareas): + self._area_compare(want=entry, have=hareas.pop(name, {})) + for name, entry in iteritems(hareas): + self._area_compare(want={}, have=entry) + + def _area_compare(self, want, have): + parsers = [ + "area.default_cost", + "area.encryption", + "area.authentication", + "area.nssa", + "area.stub", + ] + self.compare(parsers=parsers, want=want, have=have) + self._area_compare_lists(want=want, have=have) + + def _area_compare_lists(self, want, have): + for attrib in ["ranges"]: + wdict = want.get(attrib, {}) + hdict = have.get(attrib, {}) + for key, entry in iteritems(wdict): + if entry != hdict.pop(key, {}): + entry["area_id"] = want["area_id"] + self.addcmd(entry, "area.{0}".format(attrib), False) + # remove remaining items in have for replaced + for entry in hdict.values(): + entry["area_id"] = have["area_id"] + self.addcmd(entry, "area.{0}".format(attrib), True) + + def _compare_lists(self, want, have): + for attrib in ["redistribute"]: + wdict = get_from_dict(want, attrib) or {} + hdict = get_from_dict(have, attrib) or {} + for key, entry in iteritems(wdict): + if entry != hdict.pop(key, {}): + self.addcmd(entry, attrib, False) + # remove remaining items in have for replaced + for entry in hdict.values(): + self.addcmd(entry, attrib, True) + + def _ospf_list_to_dict(self, entry): + for name, proc in iteritems(entry): + for area in proc.get("areas", []): + if "ranges" in area: + range_dict = {} + for entry in area.get("ranges", []): + range_dict.update({entry["address"]: entry}) + area["ranges"] = range_dict + areas_dict = {} + for entry in proc.get("areas", []): + areas_dict.update({entry["area_id"]: entry}) + proc["areas"] = areas_dict + + redis_dict = {} + for entry in proc.get("redistribute", []): + redis_dict.update({entry["routes"]: entry}) + proc["redistribute"] = redis_dict + + if "address_family" in proc: + addr_dict = {} + for entry in proc.get("address_family", []): + addr_dict.update({entry["afi"]: entry}) + proc["address_family"] = addr_dict + self._ospf_list_to_dict(proc["address_family"]) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/prefix_lists/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/prefix_lists/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/prefix_lists/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/prefix_lists/prefix_lists.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/prefix_lists/prefix_lists.py new file mode 100644 index 000000000..347f8d426 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/prefix_lists/prefix_lists.py @@ -0,0 +1,217 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_prefix_lists config file. +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 its desired end-state is +created. +""" + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_diff, + dict_merge, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.prefix_lists import ( + Prefix_listsTemplate, +) + + +class Prefix_lists(ResourceModule): + """ + The eos_prefix_lists config class + """ + + def __init__(self, module): + super(Prefix_lists, self).__init__( + empty_fact_val=[], + facts_module=Facts(module), + module=module, + resource="prefix_lists", + tmplt=Prefix_listsTemplate(), + ) + self.parsers = [] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + + wantd = {} + haved = {} + + for entry in self.want: + wantd.update({entry["afi"]: entry}) + for entry in self.have: + haved.update({entry["afi"]: entry}) + + # turn all lists of dicts into dicts prior to merge + for entry in wantd, haved: + self._prefix_lists_list_to_dict(entry) + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + h_del = {} + for k, v in iteritems(haved): + if k in wantd or not wantd: + h_del.update({k: v}) + haved = h_del + wantd = {} + + # remove superfluous config for overridden and deleted + if self.state in ["overridden", "deleted"]: + for k, have in iteritems(haved): + if k not in wantd: + self._compare(want={}, have=have) + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Prefix_lists network resource. + """ + for k, v in iteritems(want): + if k == "afi": + continue + afi = want["afi"] + for pk, pv in iteritems(v): + begin = len(self.commands) + w_parent = {"afi": afi, "prefix_lists": {"name": pk}} + if pv.get("entries"): + self._compare_prefix_lists(afi, pk, pv, have) + if have.get("prefix_lists"): + if have["prefix_lists"].get(pk): + h_parent = {"afi": afi, "prefix_lists": {"name": pk}} + if begin != len(self.commands): + self.commands.insert( + begin, + self._tmplt.render( + w_parent or h_parent, + "prefixlist.name", + False, + ), + ) + for hk, hv in iteritems(have): + if hk == "afi": + continue + h_afi = have["afi"] + for hpk, hpv in iteritems(hv): + self.commands.append( + self._tmplt.render( + {"afi": h_afi, "prefix_lists": {"name": hpk}}, + "prefixlist.name", + True, + ), + ) + + def _compare_prefix_lists(self, afi, pk, w_list, have): + parser = ["prefixlist.entry", "prefixlist.resequence"] + for ek, ev in iteritems(w_list): + if ek == "name": + continue + h_child = {} + if have.get("prefix_lists"): + if have["prefix_lists"].get(pk): + if have["prefix_lists"][pk].get(ek): + self._compare_seq( + afi, + w_list["entries"], + have["prefix_lists"][pk][ek], + ) + for seq, seq_val in iteritems( + have["prefix_lists"][pk][ek], + ): + h_child = { + "afi": afi, + "prefix_lists": {"entries": {seq: seq_val}}, + } + self.compare(parsers=parser, want={}, have=h_child) + have["prefix_lists"].pop(pk) + else: + self._compare_seq(afi, w_list["entries"], {}) + else: + self._compare_seq(afi, w_list["entries"], {}) + + def _compare_seq(self, afi, w, h): + wl_child = {} + hl_child = {} + parser = ["prefixlist.entry", "prefixlist.resequence"] + for seq, ent in iteritems(w): + seq_diff = {} + wl_child = {"afi": afi, "prefix_lists": {"entries": {seq: ent}}} + if h.get(seq): + hl_child = { + "afi": afi, + "prefix_lists": {"entries": {seq: h.pop(seq)}}, + } + seq_diff = dict_diff( + hl_child["prefix_lists"]["entries"][seq], + wl_child["prefix_lists"]["entries"][seq], + ) + if seq_diff: + if self.state == "merged": + self._module.fail_json( + msg="Sequence number " + + str(seq) + + " is already present. Use replaced/overridden operation to change the configuration", + ) + + self.compare( + parsers="prefixlist.entry", + want={}, + have=hl_child, + ) + self.compare(parsers=parser, want=wl_child, have=hl_child) + + def _prefix_lists_list_to_dict(self, entry): + for afi, plist in iteritems(entry): + if "prefix_lists" in plist: + pl_dict = {} + for el in plist["prefix_lists"]: + if "entries" in el: + ent_dict = {} + for en in el["entries"]: + if "sequence" not in en.keys(): + num = "seq" + else: + num = en["sequence"] + ent_dict.update({num: en}) + el["entries"] = ent_dict + for el in plist["prefix_lists"]: + pl_dict.update({el["name"]: el}) + plist["prefix_lists"] = pl_dict diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/route_maps/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/route_maps/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/route_maps/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/route_maps/route_maps.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/route_maps/route_maps.py new file mode 100644 index 000000000..7f799d793 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/route_maps/route_maps.py @@ -0,0 +1,349 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_route_maps config file. +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 its desired end-state is +created. +""" + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.route_maps import ( + Route_mapsTemplate, +) + + +class Route_maps(ResourceModule): + """ + The eos_route_maps config class + """ + + def __init__(self, module): + super(Route_maps, self).__init__( + empty_fact_val=[], + facts_module=Facts(module), + module=module, + resource="route_maps", + tmplt=Route_mapsTemplate(), + ) + self.parsers = [ + "continue", + "route_map.copy", + "route_map.rename", + "description", + "sub_route_map", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = {} + haved = {} + + for entry in self.want: + wantd.update({entry["route_map"]: entry}) + for entry in self.have: + haved.update({entry["route_map"]: entry}) + + # turn all lists of dicts into dicts prior to merge + for entry in wantd, haved: + self._route_maps_list_to_dict(entry) + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + h_del = {} + for k, v in iteritems(haved): + if k in wantd or not wantd: + h_del.update({k: v}) + haved = h_del + for rmap, val in iteritems(haved): + self.addcmd({"route_map": rmap}, "route_map.name", True) + wantd = {} + + # remove superfluous config for overridden + if self.state in ["overridden"]: + for k, have in iteritems(haved): + for entry, val in iteritems(have.get("entries", {})): + if not wantd.get(k) or entry not in wantd[k].get( + "entries", + {}, + ): + self._compare_maps(want={}, have={entry: val}) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Route_maps network resource. + """ + self._compare_entries(want=want, have=have) + + def _compare_entries(self, want, have): + w_entries = want.get("entries", {}) + h_entries = have.get("entries", {}) + # overridden + if not w_entries: + for k, v in iteritems(h_entries): + self._compare_maps({}, {k: v}) + for k, v in iteritems(w_entries): + before_maps = len(self.commands) + self._compare_maps({k: v}, {k: h_entries.get(k, {})}) + after_maps = len(self.commands) + self._compare_match(v, h_entries.get(k, {})) + self._comapre_set(v, h_entries.get(k, {})) + for entry_k, entry_v in iteritems(v): + h = {} + if h_entries.get(k): + h = {"entries": {entry_k: h_entries[k].pop(entry_k, {})}} + self.compare( + parsers=self.parsers, + want={"entries": {entry_k: entry_v}}, + have=h, + ) + for h_k, h_v in iteritems(h_entries.pop(k, {})): + self.compare( + parsers=self.parsers, + have={"entries": {h_k: h_v}}, + want={}, + ) + + parent_present = False + for c in self.commands[before_maps::]: + if c.startswith("route-map"): + parent_present = True + break + if ( + before_maps == after_maps and len(self.commands) > after_maps + ) or (not parent_present and len(self.commands) > after_maps): + self._compare_maps({k: v}, {}) + self.commands.insert(after_maps, self.commands.pop(-1)) + + def _compare_maps(self, want, have): + map_in_want = [] + for k, v in iteritems(want): + map_name = k.split(" ")[0] + map_in_want.append(map_name) + w = {} + h = {} + h_entry = {} + for entry_k, entry_v in iteritems(v): + if entry_k not in [ + "continue_sequence", + "sub_route_map", + "description", + "match", + "set", + ]: + w.update({entry_k: entry_v}) + if have.get(k): + h.update({entry_k: have[k].pop(entry_k, {})}) + if h: + h_entry = {"route_map": map_name, "entries": h} + parser = self._select_parser(w) + self.compare( + parsers=parser, + want={"route_map": map_name, "entries": w}, + have=h_entry, + ) + for k, v in iteritems(have): + map_name = k.split(" ")[0] + if k not in want.keys() and self.state in [ + "replaced", + "overridden", + ]: + w_negate = {} + if map_name not in map_in_want and self.state == "replaced": + continue + parser = self._select_parser(v) + w_negate.update({"route_map": map_name, "entries": v}) + self.addcmd(w_negate, parser, True) + + def _select_parser(self, w): + parser = "" + if ( + "statement" in w.keys() + and "action" in w.keys() + and "sequence" in w.keys() + ): + parser = "route_map.statement.entries" + elif "statement" in w.keys() and "action" in w.keys(): + parser = "route_map.statement.action" + elif "statement" in w.keys(): + parser = "route_map.statement.name" + elif "action" in w.keys() and "sequence" in w.keys(): + parser = "route_map.entries" + elif "action" in w.keys(): + parser = "route_map.action" + else: + parser = "route_map.name" + return parser + + def _compare_match(self, want, have): + parsers = [ + "match.aggregate_role", + "match.as", + "match.as_path", + "match.community.instances", + "match.community.list", + "match.extcommunity", + "match.invert.aggregate_role", + "match.invert.as_path", + "match.invert.community.instances", + "match.invert.community.list", + "match.invert.extcommunity", + "match.interface", + "match.ip", + "match.ipaddress", + "match.ipv6", + "match.ipv6address", + "match.largecommunity", + "match.isis", + "match.local_pref", + "match.metric", + "match.metric_type", + "match.route_type", + "match.routerid", + "match.source_protocol", + "match.tag", + ] + w_match = want.pop("match", {}) + h_match = have.pop("match", {}) + for k, v in iteritems(w_match): + if k in ["ip", "ipv6"]: + for k_ip, v_ip in iteritems(v): + if h_match.get(k): + h = {k_ip: h_match[k].pop(k_ip, {})} + else: + h = {} + self.compare( + parsers=[ + "match.ip", + "match.ipaddress", + "match.ipv6address", + "match.ipv6", + ], + want={"entries": {"match": {k: {k_ip: v_ip}}}}, + have={"entries": {"match": {k: h}}}, + ) + h_match.pop(k, {}) + continue + self.compare( + parsers=parsers, + want={"entries": {"match": {k: v}}}, + have={"entries": {"match": {k: h_match.pop(k, {})}}}, + ) + for k, v in iteritems(h_match): + if k in ["ip", "ipv6"]: + for hk, hv in iteritems(v): + self.compare( + parsers=[ + "match.ip", + "match.ipaddress", + "match.ipv6address", + "match.ipv6", + ], + want={}, + have={"entries": {"match": {k: {hk: hv}}}}, + ) + continue + self.compare( + parsers=parsers, + want={}, + have={"entries": {"match": {k: v}}}, + ) + + def _comapre_set(self, want, have): + parsers = [ + "set.as_path.prepend", + "set.as_path.match", + "set.bgp", + "set.community.graceful_shutdown", + "set.community.none", + "set.community.number", + "set.community.list", + "set.community.internet", + "set.distance", + "set.evpn", + "set.extcommunity.lbw", + "set.extcommunity.none", + "set.extcommunity.rt", + "set.extcommunity.soo", + "set.ip", + "set.ipv6", + "set.isis", + "set.local_pref", + "set.metric.value", + "set.metric_type", + "set.nexthop", + "set.origin", + "set.segment_index", + "set.tag", + "set.weight", + ] + + w_set = want.pop("set", {}) + h_set = have.pop("set", {}) + for k, v in iteritems(w_set): + self.compare( + parsers=parsers, + want={"entries": {"set": {k: v}}}, + have={"entries": {"set": {k: h_set.pop(k, {})}}}, + ) + for k, v in iteritems(h_set): + self.compare( + parsers=parsers, + want={}, + have={"entries": {"set": {k: v}}}, + ) + + def _route_maps_list_to_dict(self, entry): + for name, r_map in iteritems(entry): + if r_map.get("entries"): + map_dict = {} + for entry in r_map["entries"]: + if entry.get("sequence"): + seq = entry["sequence"] + else: + seq = "seq" + mapkey = name + " " + str(seq) + map_dict.update({mapkey: entry}) + r_map["entries"] = map_dict diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/snmp_server/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/snmp_server/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/snmp_server/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/snmp_server/snmp_server.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/snmp_server/snmp_server.py new file mode 100644 index 000000000..7d850fea2 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/snmp_server/snmp_server.py @@ -0,0 +1,243 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos_snmp_server config file. +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 its desired end-state is +created. +""" + +import re + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, + get_from_dict, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.snmp_server import ( + Snmp_serverTemplate, +) + + +class Snmp_server(ResourceModule): + """ + The eos_snmp_server config class + """ + + def __init__(self, module): + super(Snmp_server, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="snmp_server", + tmplt=Snmp_serverTemplate(), + ) + self.parsers = [ + "chassis_id", + "contact", + "traps.bgp", + "traps.bridge", + "traps.capacity", + "traps.entity", + "traps.external_alarm", + "traps.isis", + "traps.lldp", + "traps.mpls_ldp", + "traps.msdp", + "traps.ospf", + "traps.ospfv3", + "traps.pim", + "traps.snmp", + "traps.snmpConfigManEvent", + "traps.switchover", + "traps.test", + "traps.vrrp", + "engineid", + "extension", + "local_interface", + "location", + "notification", + "objects.mac", + "objects.route", + "qos", + "qosmib", + "transmit", + "transport", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + + wantd = {"snmp_server": self.want} + haved = {"snmp_server": self.have} + + # turn all lists of dicts into dicts prior to merge + for entry in wantd["snmp_server"], haved["snmp_server"]: + self._snmp_server_list_to_dict(entry) + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + haved = { + k: v for k, v in iteritems(haved) if k in wantd or not wantd + } + wantd = {} + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + wantd = {} + for k, have in iteritems(haved): + self._compare(want={}, have=have) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Snmp_server network resource. + """ + self._compare_hosts(want, have) + self._compare_lists(want, have) + for name, entry in iteritems(want): + self.compare( + parsers=self.parsers, + want={name: entry}, + have={name: have.pop(name, {})}, + ) + for name, entry in iteritems(have): + self.compare(parsers=self.parsers, want={}, have={name: entry}) + + self._modify_traps_negate() + + def _modify_traps_negate(self): + command_set = [] + for cmd in self.commands: + if re.search("no snmp-server enable traps", cmd): + command_set.append(cmd.replace("no ", "default ")) + else: + command_set.append(cmd) + self.commands = command_set + + def _compare_lists(self, want, have): + parsers = [ + "communities_ipv6_acl", + "communities_ipv4_acl", + "groups", + "acls", + "views", + "users.auth", + "users.localized", + "vrfs", + ] + for attrib in [ + "communities", + "groups", + "acls", + "users", + "views", + "vrfs", + ]: + wdict = get_from_dict(want, attrib) or {} + hdict = get_from_dict(have, attrib) or {} + for key, entry in iteritems(wdict): + # self.addcmd(entry, attrib, False) + self.compare( + parsers=parsers, + want={attrib: entry}, + have={attrib: hdict.pop(key, {})}, + ) + # remove remaining items in have for replaced + for entry in hdict.values(): + self.compare(parsers=parsers, want={}, have={attrib: entry}) + + def _compare_hosts(self, want, have): + wdict = get_from_dict(want, "hosts") or {} + hdict = get_from_dict(have, "hosts") or {} + for key, entry in iteritems(wdict): + # self.addcmd(entry, attrib, False) + self.compare( + parsers="hosts", + want={"hosts": {key: entry}}, + have={"hosts": {key: hdict.pop(key, {})}}, + ) + # remove remaining items in have for replaced + for key, entry in iteritems(hdict): + self.compare( + parsers="hosts", + want={}, + have={"hosts": {key: entry}}, + ) + + def _snmp_server_list_to_dict(self, entry): + param_dict = { + "communities": "name", + "groups": "group", + "acls": "afi", + "users": "user", + "views": "view", + "vrfs": "vrf", + } + for k, v in iteritems(param_dict): + if k in entry: + a_dict = {} + for el in entry[k]: + a_dict.update({el[v]: el}) + entry[k] = a_dict + if "hosts" in entry: + host_dict = {} + for el in entry["hosts"]: + tr = "" + inf = "" + if el.get("traps"): + tr = "traps" + if el.get("informs"): + inf = "informs" + host_dict.update( + { + ( + el.get("host"), + el.get("user"), + el.get("version"), + inf, + tr, + el.get("udp_port"), + ): el, + }, + ) + entry["hosts"] = host_dict diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/static_routes/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/static_routes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/static_routes/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/static_routes/static_routes.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/static_routes/static_routes.py new file mode 100644 index 000000000..0c4198edf --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/static_routes/static_routes.py @@ -0,0 +1,369 @@ +# +# -*- 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 eos_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 + +import re + +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 ( + remove_empties, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) + + +class Static_routes(ConfigBase): + """ + The eos_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": + if not self._module.params["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=self._module.params["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 + """ + commands = [] + onbox_configs = [] + for h in existing_static_routes_facts: + return_command = add_commands(h) + for command in return_command: + onbox_configs.append(command) + config = self._module.params.get("config") + want = [] + if config: + for w in config: + want.append(remove_empties(w)) + have = existing_static_routes_facts + resp = self.set_state(want, have) + for want_config in resp: + if want_config not in onbox_configs: + commands.append(want_config) + return commands + + 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") and not want: + self._module.fail_json( + msg="value of config parameter must not be empty for state {0}".format( + self.state, + ), + ) + state = self._module.params["state"] + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or self.state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + haveconfigs = [] + vrf = get_vrf(want) + dest = get_dest(want) + for h in have: + return_command = add_commands(h) + for command in return_command: + for d in dest: + if d in command: + if vrf is None: + if "vrf" not in command: + haveconfigs.append(command) + else: + if vrf in command: + haveconfigs.append(command) + wantconfigs = set_commands(want, have) + + removeconfigs = list(set(haveconfigs) - set(wantconfigs)) + for command in removeconfigs: + commands.append("no " + command) + for wantcmd in wantconfigs: + commands.append(wantcmd) + return commands + + @staticmethod + def _state_overridden(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 = [] + haveconfigs = [] + for h in have: + return_command = add_commands(h) + for command in return_command: + haveconfigs.append(command) + wantconfigs = set_commands(want, have) + idempotentconfigs = list(set(haveconfigs) - set(wantconfigs)) + if not idempotentconfigs: + return idempotentconfigs + removeconfigs = list(set(haveconfigs) - set(wantconfigs)) + for command in removeconfigs: + commands.append("no " + command) + for wantcmd in wantconfigs: + commands.append(wantcmd) + return commands + + @staticmethod + def _state_merged(want, have): + """The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + return set_commands(want, have) + + @staticmethod + def _state_deleted(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 not want: + for h in have: + return_command = add_commands(h) + for command in return_command: + command = "no " + command + commands.append(command) + else: + for w in want: + return_command = del_commands(w, have) + for command in return_command: + commands.append(command) + return commands + + +def set_commands(want, have): + commands = [] + for w in want: + return_command = add_commands(w) + for command in return_command: + commands.append(command) + return commands + + +def add_commands(want): + commandset = [] + if not want: + return commandset + vrf = ( + want["vrf"] + if "vrf" in want.keys() and want["vrf"] is not None + else None + ) + for address_family in want["address_families"]: + for route in address_family["routes"]: + for next_hop in route["next_hops"]: + commands = [] + if address_family["afi"] == "ipv4": + commands.append("ip route") + else: + commands.append("ipv6 route") + if vrf: + commands.append(" vrf " + vrf) + if not re.search(r"/", route["dest"]): + mask = route["dest"].split()[1] + cidr = get_net_size(mask) + commands.append( + " " + route["dest"].split()[0] + "/" + cidr, + ) + else: + commands.append(" " + route["dest"]) + if "interface" in next_hop.keys(): + commands.append(" " + next_hop["interface"]) + if "nexthop_grp" in next_hop.keys(): + commands.append( + " Nexthop-Group" + " " + next_hop["nexthop_grp"], + ) + if "forward_router_address" in next_hop.keys(): + commands.append(" " + next_hop["forward_router_address"]) + if "mpls_label" in next_hop.keys(): + commands.append(" label " + str(next_hop["mpls_label"])) + if "track" in next_hop.keys(): + commands.append(" track " + next_hop["track"]) + if "admin_distance" in next_hop.keys(): + commands.append(" " + str(next_hop["admin_distance"])) + if "description" in next_hop.keys(): + commands.append(" name " + str(next_hop["description"])) + if "tag" in next_hop.keys(): + commands.append(" tag " + str(next_hop["tag"])) + + config_commands = "".join(commands) + commandset.append(config_commands) + return commandset + + +def del_commands(want, have): + commandset = [] + haveconfigs = [] + for h in have: + return_command = add_commands(h) + for command in return_command: + command = "no " + command + haveconfigs.append(command) + if want is None or "address_families" not in want.keys(): + commandset = haveconfigs + if "address_families" not in want.keys() and "vrf" in want.keys(): + commandset = [] + for command in haveconfigs: + if want["vrf"] in command: + commandset.append(command) + elif ( + want is not None + and "vrf" not in want.keys() + and "address_families" not in want.keys() + ): + commandset = [] + for command in haveconfigs: + if "vrf" not in command: + commandset.append(command) + + elif want["address_families"]: + for address_family in want["address_families"]: + for command in haveconfigs: + afi = "ip " if address_family["afi"] == "ipv4" else "ipv6" + if afi in command: + commandset.append(command) + return commandset + + +def get_net_size(netmask): + binary_str = "" + netmask = netmask.split(".") + for octet in netmask: + binary_str += bin(int(octet))[2:].zfill(8) + return str(len(binary_str.rstrip("0"))) + + +def get_vrf(config): + vrf = "" + for c in config: + vrf = c["vrf"] if "vrf" in c.keys() and c["vrf"] else None + return vrf + + +def get_dest(config): + dest = [] + for c in config: + for address_family in c["address_families"]: + for route in address_family["routes"]: + dest.append(route["dest"]) + return dest diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/vlans/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/vlans/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/vlans/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/vlans/vlans.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/vlans/vlans.py new file mode 100644 index 000000000..664216e6d --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/config/vlans/vlans.py @@ -0,0 +1,262 @@ +# -*- 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 eos_vlans 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 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 ( + dict_diff, + param_list_to_dict, + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + Facts, +) + + +class Vlans(ConfigBase): + """ + The eos_vlans class + """ + + gather_subset = ["!all", "!min"] + + gather_network_resources = ["vlans"] + + def get_vlans_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, + ) + vlans_facts = facts["ansible_network_resources"].get("vlans") + if not vlans_facts: + return [] + return vlans_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_vlans_facts = self.get_vlans_facts() + else: + existing_vlans_facts = {} + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_vlans_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_vlans_facts = self.get_vlans_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_vlans_facts(data=running_config) + + if self.state in self.ACTION_STATES: + result["before"] = existing_vlans_facts + if result["changed"]: + result["after"] = changed_vlans_facts + elif self.state == "gathered": + result["gathered"] = changed_vlans_facts + result["warnings"] = warnings + return result + + def set_config(self, existing_vlans_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_vlans_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 + """ + state = self._module.params["state"] + if ( + 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( + state, + ), + ) + want = param_list_to_dict(want, "vlan_id", remove_key=False) + have = param_list_to_dict(have, "vlan_id", remove_key=False) + if state == "overridden": + commands = self._state_overridden(want, have) + elif state == "deleted": + commands = self._state_deleted(want, have) + elif state == "merged" or state == "rendered": + commands = self._state_merged(want, have) + elif state == "replaced": + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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 = [] + for vlan_id, desired in want.items(): + if vlan_id in have: + extant = have[vlan_id] + else: + extant = dict() + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(vlan_id, add_config, del_config)) + + return commands + + @staticmethod + def _state_overridden(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 = [] + for vlan_id, extant in have.items(): + if vlan_id in want: + desired = want[vlan_id] + else: + desired = dict() + + add_config = dict_diff(extant, desired) + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(vlan_id, add_config, del_config)) + + # Handle vlans not already in config + new_vlans = [vlan_id for vlan_id in want if vlan_id not in have] + for vlan_id in new_vlans: + desired = want[vlan_id] + extant = dict(vlan_id=vlan_id) + add_config = dict_diff(extant, desired) + + commands.extend(generate_commands(vlan_id, add_config, {})) + + return commands + + @staticmethod + def _state_merged(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 vlan_id, desired in want.items(): + if vlan_id in have: + extant = have[vlan_id] + else: + extant = dict() + + add_config = dict_diff(extant, desired) + + commands.extend(generate_commands(vlan_id, add_config, {})) + + return commands + + @staticmethod + def _state_deleted(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 = [] + for vlan_id in want: + desired = dict() + if vlan_id in have: + extant = have[vlan_id] + else: + continue + + del_config = dict_diff(desired, extant) + + commands.extend(generate_commands(vlan_id, {}, del_config)) + + return commands + + +def generate_commands(vlan_id, to_set, to_remove): + commands = [] + if "vlan_id" in to_remove: + return ["no vlan {0}".format(vlan_id)] + + for key in to_remove: + if key in to_set.keys(): + continue + commands.append("no {0}".format(key)) + + for key, value in to_set.items(): + if key == "vlan_id" or value is None: + continue + + commands.append("{0} {1}".format(key, value)) + + if commands: + commands.insert(0, "vlan {0}".format(vlan_id)) + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/eos.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/eos.py new file mode 100644 index 000000000..292ad274f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/eos.py @@ -0,0 +1,568 @@ +# +# This code is part of Ansible, but is an independent component. +# +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2017 Red Hat, Inc. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type +import json +import os +import time + +from ansible.module_utils._text import to_text +from ansible.module_utils.connection import Connection, ConnectionError +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, + dumps, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + ComplexList, + to_list, +) + + +_DEVICE_CONNECTION = None + + +def get_connection(module): + global _DEVICE_CONNECTION + if not _DEVICE_CONNECTION: + connection_proxy = Connection(module._socket_path) + cap = json.loads(connection_proxy.get_capabilities()) + if cap["network_api"] == "cliconf": + conn = Cli(module) + elif cap["network_api"] == "eapi": + conn = HttpApi(module) + _DEVICE_CONNECTION = conn + return _DEVICE_CONNECTION + + +def transform_commands(module): + transform = ComplexList( + dict( + command=dict(key=True), + output=dict(), + prompt=dict(type="list"), + answer=dict(type="list"), + newline=dict(type="bool", default=True), + sendonly=dict(type="bool", default=False), + check_all=dict(type="bool", default=False), + version=dict( + type="str", + default="latest", + choices=["latest", "1"], + ), + ), + module, + ) + + return transform(module.params["commands"]) + + +def session_name(): + """Generate a unique string to be used as a configuration session name.""" + return "ansible_%d" % (time.time() * 100) + + +class Cli: + def __init__(self, module): + self._module = module + self._device_configs = {} + self._session_support = None + self._connection = None + + @property + def supports_sessions(self): + if self._session_support is None: + self._session_support = self._get_connection().supports_sessions() + return self._session_support + + def _get_connection(self): + if self._connection: + return self._connection + self._connection = Connection(self._module._socket_path) + + return self._connection + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache""" + flags = [] if flags is None else flags + + cmd = "show running-config " + cmd += " ".join(flags) + cmd = cmd.strip() + + try: + return self._device_configs[cmd] + except KeyError: + conn = self._get_connection() + try: + out = conn.get_config(flags=flags) + except ConnectionError as exc: + self._module.fail_json( + msg=to_text(exc, errors="surrogate_then_replace"), + ) + + cfg = to_text(out, errors="surrogate_then_replace").strip() + self._device_configs[cmd] = cfg + return cfg + + def run_commands(self, commands, check_rc=True): + """Run list of commands on remote device and return results""" + connection = self._get_connection() + try: + response = connection.run_commands( + commands=commands, + check_rc=check_rc, + ) + except ConnectionError as exc: + self._module.fail_json( + msg=to_text(exc, errors="surrogate_then_replace"), + ) + return response + + def get_session_config(self, commands, commit=False, replace=False): + """Loads the config commands onto the remote device""" + conn = self._get_connection() + try: + response = conn.get_session_config(commands, commit, replace) + except ConnectionError as exc: + message = getattr(exc, "err", to_text(exc)) + if ( + "check mode is not supported without configuration session" + in message + ): + self._module.warn( + "EOS can not check config without config session", + ) + response = {"changed": True} + else: + self._module.fail_json( + msg="%s" % message, + data=to_text(message, errors="surrogate_then_replace"), + ) + + return response + + def load_config(self, commands, commit=False, replace=False): + """Loads the config commands onto the remote device""" + conn = self._get_connection() + try: + response = conn.edit_config(commands, commit, replace) + except ConnectionError as exc: + message = getattr(exc, "err", to_text(exc)) + if ( + "check mode is not supported without configuration session" + in message + ): + self._module.warn( + "EOS can not check config without config session", + ) + response = {"changed": True} + else: + self._module.fail_json( + msg="%s" % message, + data=to_text(message, errors="surrogate_then_replace"), + ) + + return response + + def get_diff( + self, + candidate=None, + running=None, + diff_match="line", + diff_ignore_lines=None, + path=None, + diff_replace="line", + ): + conn = self._get_connection() + try: + diff = conn.get_diff( + candidate=candidate, + running=running, + diff_match=diff_match, + diff_ignore_lines=diff_ignore_lines, + path=path, + diff_replace=diff_replace, + ) + except ConnectionError as exc: + self._module.fail_json( + msg=to_text(exc, errors="surrogate_then_replace"), + ) + return diff + + def get_capabilities(self): + """Returns platform info of the remove device""" + if hasattr(self._module, "_capabilities"): + return self._module._capabilities + + connection = self._get_connection() + try: + capabilities = connection.get_capabilities() + except ConnectionError as exc: + self._module.fail_json( + msg=to_text(exc, errors="surrogate_then_replace"), + ) + self._module._capabilities = json.loads(capabilities) + return self._module._capabilities + + +class HttpApi: + def __init__(self, module): + self._module = module + self._device_configs = {} + self._session_support = None + self._connection_obj = None + + @property + def _connection(self): + if not self._connection_obj: + self._connection_obj = Connection(self._module._socket_path) + + return self._connection_obj + + @property + def supports_sessions(self): + if self._session_support is None: + self._session_support = self._connection.supports_sessions() + return self._session_support + + def run_commands(self, commands, check_rc=True): + """Runs list of commands on remote device and returns results""" + output = None + queue = list() + responses = list() + + def run_queue(queue, output, version): + try: + response = to_list( + self._connection.send_request( + queue, + output=output, + version=version, + ), + ) + except ConnectionError as exc: + if check_rc: + raise + return to_list(to_text(exc)) + + if output == "json": + response = [json.loads(item) for item in response] + return response + + for item in to_list(commands): + cmd_output = "text" + if isinstance(item, dict): + command = item["command"] + if "output" in item: + cmd_output = item["output"] + if "version" in item: + version = item["version"] + else: + command = item + + # Emulate '| json' from CLI + if is_json(command): + command = command.rsplit("|", 1)[0] + cmd_output = "json" + + if output and output != cmd_output: + responses.extend(run_queue(queue, output, version)) + queue = list() + + output = cmd_output + queue.append(command) + + if queue: + responses.extend(run_queue(queue, output, version)) + + return responses + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache""" + flags = [] if flags is None else flags + + cmd = "show running-config " + cmd += " ".join(flags) + cmd = cmd.strip() + + try: + return self._device_configs[cmd] + except KeyError: + try: + out = self._connection.send_request(cmd) + except ConnectionError as exc: + self._module.fail_json( + msg=to_text(exc, errors="surrogate_then_replace"), + ) + + cfg = to_text(out).strip() + self._device_configs[cmd] = cfg + return cfg + + def get_diff( + self, + candidate=None, + running=None, + diff_match="line", + diff_ignore_lines=None, + path=None, + diff_replace="line", + ): + diff = {} + + # prepare candidate configuration + candidate_obj = NetworkConfig(indent=3) + candidate_obj.load(candidate) + + if running and diff_match != "none" and diff_replace != "config": + # running configuration + running_obj = NetworkConfig( + indent=3, + contents=running, + ignore_lines=diff_ignore_lines, + ) + configdiffobjs = candidate_obj.difference( + running_obj, + path=path, + match=diff_match, + replace=diff_replace, + ) + + else: + configdiffobjs = candidate_obj.items + + diff["config_diff"] = ( + dumps(configdiffobjs, "commands") if configdiffobjs else {} + ) + return diff + + def load_config(self, config, commit=False, replace=False): + """Loads the configuration onto the remote devices + + If the device doesn't support configuration sessions, this will + fallback to using configure() to load the commands. If that happens, + there will be no returned diff or session values + """ + return self.edit_config(config, commit, replace) + + def edit_config(self, config, commit=False, replace=False): + """Loads the configuration onto the remote devices + + If the device doesn't support configuration sessions, this will + fallback to using configure() to load the commands. If that happens, + there will be no returned diff or session values + """ + session = session_name() + result = {"session": session} + banner_cmd = None + banner_input = [] + + commands = ["configure session %s" % session] + if replace: + commands.append("rollback clean-config") + + for command in config: + if command.startswith("banner"): + banner_cmd = command + banner_input = [] + elif banner_cmd: + if command == "EOF": + command = { + "cmd": banner_cmd, + "input": "\n".join(banner_input), + } + banner_cmd = None + commands.append(command) + else: + banner_input.append(command) + continue + else: + commands.append(command) + + try: + response = self._connection.send_request(commands) + except Exception: + commands = ["configure session %s" % session, "abort"] + response = self._connection.send_request(commands, output="text") + raise + + commands = [ + "configure session %s" % session, + "show session-config diffs", + ] + if commit: + commands.append("commit") + else: + commands.append("abort") + + response = self._connection.send_request(commands, output="text") + diff = response[1].strip() + if diff: + result["diff"] = diff + + return result + + def get_session_config(self, config, commit=False, replace=False): + """Loads the configuration onto the remote devices + + If the device doesn't support configuration sessions, this will + fallback to using configure() to load the commands. If that happens, + there will be no returned diff or session values + """ + resp = "" + use_session = os.getenv("ANSIBLE_EOS_USE_SESSIONS", True) + try: + use_session = int(use_session) + except ValueError: + pass + + if not all((bool(use_session), self.supports_sessions)): + if commit: + return self.configure(config) + else: + self._module.warn( + "EOS can not check config without config session", + ) + result = {"changed": True} + return result + session = session_name() + result = {"session": session} + commands = ["configure session %s" % session] + + if replace: + commands.append("rollback clean-config") + + commands.extend(config) + response = self._connection.send_request(commands) + if "error" in response: + commands = ["configure session %s" % session, "abort"] + self._connection.send_request(commands) + err = response["error"] + error_text = [] + for data in err["data"]: + error_text.extend(data.get("errors", [])) + error_text = "\n".join(error_text) or err["message"] + self._module.fail_json(msg=error_text, code=err["code"]) + + commands = [ + "configure session %s" % session, + "show session-config", + ] + if commit: + commands.append("commit") + else: + commands.append("abort") + response = self._connection.send_request(commands, output="text") + for out in response: + if out: + resp += out + "" + return resp.rstrip() + + def get_capabilities(self): + """Returns platform info of the remove device""" + try: + capabilities = self._connection.get_capabilities() + except ConnectionError as exc: + self._module.fail_json( + msg=to_text(exc, errors="surrogate_then_replace"), + ) + + return json.loads(capabilities) + + +def is_json(cmd): + return to_text(cmd, errors="surrogate_then_replace").endswith("| json") + + +def to_command(module, commands): + transform = ComplexList( + dict( + command=dict(key=True), + output=dict(type="str", default="text"), + prompt=dict(type="list"), + answer=dict(type="list"), + newline=dict(type="bool", default=True), + sendonly=dict(type="bool", default=False), + check_all=dict(type="bool", default=False), + version=dict(type="str", default="latest"), + ), + module, + ) + + return transform(to_list(commands)) + + +def get_config(module, flags=None): + flags = None if flags is None else flags + + conn = get_connection(module) + return conn.get_config(flags) + + +def run_commands(module, commands, check_rc=True): + conn = get_connection(module) + return conn.run_commands(to_command(module, commands), check_rc=check_rc) + + +def load_config(module, config, commit=False, replace=False): + conn = get_connection(module) + return conn.load_config(config, commit, replace) + + +def get_session_config(module, config, commit=False, replace=False): + conn = get_connection(module) + return conn.get_session_config(config, commit, replace) + + +def get_diff( + self, + candidate=None, + running=None, + diff_match="line", + diff_ignore_lines=None, + path=None, + diff_replace="line", +): + conn = self.get_connection() + return conn.get_diff( + candidate=candidate, + running=running, + diff_match=diff_match, + diff_ignore_lines=diff_ignore_lines, + path=path, + diff_replace=diff_replace, + ) + + +def get_capabilities(module): + conn = get_connection(module) + return conn.get_capabilities() diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acl_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acl_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acl_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acl_interfaces/acl_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acl_interfaces/acl_interfaces.py new file mode 100644 index 000000000..2e53931de --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acl_interfaces/acl_interfaces.py @@ -0,0 +1,149 @@ +# +# -*- 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 eos acl_interfaces 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 + + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.acl_interfaces.acl_interfaces import ( + Acl_interfacesArgs, +) + + +class Acl_interfacesFacts(object): + """The eos acl_interfaces fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Acl_interfacesArgs.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( + "show running-config | include interface | access-group | traffic-filter", + ) + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for acl_interfaces + :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) + # split the config into instances of the resource + resource_delim = "interface" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts["ansible_network_resources"].pop("acl_interfaces", None) + facts = {} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["acl_interfaces"] = [ + utils.remove_empties(cfg) for cfg in params["config"] + ] + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + access_group_list = [] + access_group_v6_list = [] + acls_list = [] + group_list = [] + group_dict = {} + config["name"] = utils.parse_conf_arg(conf, "^interface") + conf_lines = conf.split("\n") + for line in conf_lines: + if config["name"] in line: + continue + access_group = utils.parse_conf_arg(line, "^ip access-group") + # This module was verified on an ios device since vEOS doesnot support + # acl_interfaces cnfiguration. In ios, ipv6 acl is configured as + # traffic-filter and in eos it is access-group + + # access_group_v6 = utils.parse_conf_arg(line, 'ipv6 traffic-filter') + access_group_v6 = utils.parse_conf_arg(line, "^ipv6 access-group") + if access_group: + access_group_list.append(access_group) + if access_group_v6: + access_group_v6_list.append(access_group_v6) + if access_group_list: + for acl in access_group_list: + a_name = acl.split()[0] + a_dir = acl.split()[1] + acls_dict = {"name": a_name, "direction": a_dir} + acls_list.append(acls_dict) + group_dict = {"afi": "ipv4", "acls": acls_list} + group_list.append(group_dict) + acls_list = [] + if group_list: + config["access_groups"] = group_list + if access_group_v6_list: + for acl in access_group_v6_list: + a_name = acl.split()[0] + a_dir = acl.split()[1] + acls_dict = {"name": a_name, "direction": a_dir} + acls_list.append(acls_dict) + group_dict = {"acls": acls_list, "afi": "ipv6"} + group_list.append(group_dict) + acls_list = [] + if group_list: + config["access_groups"] = group_list + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acls/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acls/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acls/acls.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acls/acls.py new file mode 100644 index 000000000..567fca8ba --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/acls/acls.py @@ -0,0 +1,392 @@ +# +# -*- 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 eos acls 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.acls.acls import ( + AclsArgs, +) + + +class AclsFacts(object): + """The eos acls fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = AclsArgs.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("show running-config | section access-list") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for acls + :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) + + # split the config into instances of the resource + find_pattern = r"(?:^|\n)(?:ip|ipv6) access\-list.*?(?=(?:^|\n)(?:ip|ipv6) access\-list|$)" + resources = [] + for p in re.findall(find_pattern, data, re.DOTALL): + resources.append(p) + + objs = [] + ipv4list = [] + ipv6list = [] + for resource in resources: + if "ipv6" in resource: + ipv6list.append(resource) + else: + ipv4list.append(resource) + ipv4list = ["\n".join(ipv4list)] + ipv6list = ["\n".join(ipv6list)] + for resource in ipv4list: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + for resource in ipv6list: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts["ansible_network_resources"].pop("acls", None) + facts = {} + if objs: + facts["acls"] = [] + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + for cfg in params["config"]: + facts["acls"].append(utils.remove_empties(cfg)) + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + afi_list = [] + acls_list = [] + name_dict = {} + standard = 0 + operator = ["eq", "lt", "neq", "range", "gt"] + flags = ["ack", "established", "fin", "psh", "rst", "syn", "urg"] + others = ["hop_limit", "log", "ttl", "fragments", "tracked"] + for dev_config in conf.split("\n"): + ace_dict = {} + if not dev_config: + continue + if dev_config == "!": + continue + dev_config = dev_config.strip() + matches = re.findall(r"(ip.*?) access-list (.*)", dev_config) + if matches: + afi = "ipv4" if matches[0][0] == "ip" else "ipv6" + ace_list = [] + if bool(name_dict): + acls_list.append(name_dict.copy()) + name_dict = {} + if afi not in afi_list: + afi_list.append(afi) + config.update({"afi": afi}) + if "standard" in matches[0][1]: + standard = 1 + name = matches[0][1].split() + name_dict.update({"name": name[1]}) + name_dict.update({"standard": True}) + else: + name_dict.update({"name": matches[0][1]}) + else: + source_dict = {} + dest_dict = {} + dev_config = re.sub("-", "_", dev_config) + dev_config_remainder = dev_config.split() + if "fragment_rules" in dev_config: + ace_dict.update({"sequence": dev_config_remainder.pop(0)}) + ace_dict.update({"fragment_rules": True}) + if "remark" in dev_config: + ace_dict.update({"sequence": dev_config_remainder.pop(0)}) + ace_dict.update( + {"remark": " ".join(dev_config_remainder[1:])}, + ) + seq = re.search(r"\d+ (permit|deny) .*", dev_config) + if seq: + ace_dict.update({"sequence": dev_config_remainder.pop(0)}) + ace_dict.update({"grant": dev_config_remainder.pop(0)}) + if ( + dev_config_remainder + and dev_config_remainder[0] == "vlan" + ): + vlan_str = "" + dev_config_remainder.pop(0) + if ( + dev_config_remainder + and dev_config_remainder[0] == "inner" + ): + vlan_str = dev_config_remainder.pop(0) + " " + vlan_str = ( + dev_config_remainder.pop(0) + + " " + + dev_config_remainder.pop(0) + ) + ace_dict.update({"vlan": vlan_str}) + if not standard: + protocol = dev_config_remainder[0] + ace_dict.update( + {"protocol": dev_config_remainder.pop(0)}, + ) + src_prefix = re.search(r"/", dev_config_remainder[0]) + src_address = re.search( + r"[a-z\d:\.]+", + dev_config_remainder[0], + ) + if ( + dev_config_remainder + and dev_config_remainder[0] == "host" + ): + source_dict.update( + {"host": dev_config_remainder.pop(1)}, + ) + dev_config_remainder.pop(0) + elif ( + dev_config_remainder + and dev_config_remainder[0] == "any" + ): + source_dict.update({"any": True}) + dev_config_remainder.pop(0) + elif src_prefix: + source_dict.update( + {"subnet_address": dev_config_remainder.pop(0)}, + ) + elif src_address: + source_dict.update( + {"address": dev_config_remainder.pop(0)}, + ) + source_dict.update( + {"wildcard_bits": dev_config_remainder.pop(0)}, + ) + if dev_config_remainder: + if ( + dev_config_remainder + and dev_config_remainder[0] in operator + ): + port_dict = {} + src_port = "" + src_opr = dev_config_remainder.pop(0) + portlist = dev_config_remainder[:] + for config_remainder in portlist: + addr = re.search(r"[\.\:]", config_remainder) + if ( + config_remainder == "any" + or config_remainder == "host" + or addr + ): + break + src_port = src_port + " " + config_remainder + dev_config_remainder.pop(0) + src_port = src_port.strip() + port_dict.update({src_opr: src_port}) + source_dict.update({"port_protocol": port_dict}) + ace_dict.update({"source": source_dict}) + if not dev_config_remainder or standard: + if ( + dev_config_remainder + and "log" in dev_config_remainder + ): + ace_dict.update({"log": True}) + if bool(ace_dict): + ace_list.append(ace_dict.copy()) + if len(ace_list): + name_dict = name_dict.copy() + name_dict.update({"aces": ace_list[:]}) + # acls_list.append(name_dict) + continue + dest_prefix = re.search(r"/", dev_config_remainder[0]) + dest_address = re.search( + r"[a-z\d:\.]+", + dev_config_remainder[0], + ) + if ( + dev_config_remainder + and dev_config_remainder[0] == "host" + ): + dest_dict.update({"host": dev_config_remainder.pop(1)}) + dev_config_remainder.pop(0) + elif ( + dev_config_remainder + and dev_config_remainder[0] == "any" + ): + dest_dict.update({"any": True}) + dev_config_remainder.pop(0) + elif dest_prefix: + dest_dict.update( + {"subnet_address": dev_config_remainder.pop(0)}, + ) + elif dest_address: + dest_dict.update( + {"address": dev_config_remainder.pop(0)}, + ) + dest_dict.update( + {"wildcard_bits": dev_config_remainder.pop(0)}, + ) + if dev_config_remainder: + if dev_config_remainder[0] in operator: + port_dict = {} + dest_port = "" + dest_opr = dev_config_remainder.pop(0) + portlist = dev_config_remainder[:] + for config_remainder in portlist: + if ( + config_remainder in operator + or config_remainder in others + ): + break + dest_port = dest_port + " " + config_remainder + dev_config_remainder.pop(0) + dest_port = dest_port.strip() + port_dict.update({dest_opr: dest_port}) + dest_dict.update({"port_protocol": port_dict}) + ace_dict.update({"destination": dest_dict}) + protocol_option_dict = {} + tcp_dict = {} + icmp_dict = {} + ip_dict = {} + if not dev_config_remainder: + if bool(ace_dict): + ace_list.append(ace_dict.copy()) + if len(ace_list): + name_dict = name_dict.copy() + name_dict.update({"aces": ace_list[:]}) + # acls_list.append(name_dict) + continue + if protocol in ["tcp", "6"]: + protocol = "tcp" + flags_dict = {} + if ( + dev_config_remainder + and dev_config_remainder[0] in flags + ): + flaglist = dev_config_remainder[:] + for config_remainder in flaglist: + if config_remainder not in flags: + break + flags_dict.update({config_remainder: True}) + dev_config_remainder.pop(0) + if bool(flags_dict): + tcp_dict.update({"flags": flags_dict}) + if bool(tcp_dict): + protocol_option_dict.update({"tcp": tcp_dict}) + if ( + protocol == "icmp" + or protocol == "icmpv6" + or protocol == "1" + or protocol == "58" + ): + if protocol == "1": + protocol = "icmp" + elif protocol == "58": + protocol = "icmpv6" + if ( + dev_config_remainder + and dev_config_remainder[0] not in others + ): + icmp_dict.update({dev_config_remainder[0]: True}) + dev_config_remainder.pop(0) + if bool(icmp_dict): + protocol_option_dict.update({protocol: icmp_dict}) + if protocol in ["ip", "ipv6"]: + if ( + dev_config_remainder + and dev_config_remainder[0] == "nexthop_group" + ): + dev_config_remainder.pop(0) + ip_dict.update( + {"nexthop_group": dev_config_remainder.pop(0)}, + ) + if bool(ip_dict): + protocol_option_dict.update({protocol: ip_dict}) + if bool(protocol_option_dict): + ace_dict.update( + {"protocol_options": protocol_option_dict}, + ) + if ( + dev_config_remainder + and dev_config_remainder[0] == "ttl" + ): + dev_config_remainder.pop(0) + op = dev_config_remainder.pop(0) + ttl_dict = {op: dev_config_remainder.pop(0)} + ace_dict.update({"ttl": ttl_dict}) + for config_remainder in dev_config_remainder: + if config_remainder in others: + if config_remainder == "hop_limit": + hop_index = dev_config_remainder.index( + config_remainder, + ) + hoplimit_dict = { + dev_config_remainder[ + hop_index + 1 + ]: dev_config_remainder[hop_index + 2], + } + ace_dict.update({"hop_limit": hoplimit_dict}) + dev_config_remainder.pop(0) + continue + ace_dict.update({config_remainder: True}) + dev_config_remainder.pop(0) + if dev_config_remainder: + config.update({"line": dev_config}) + return utils.remove_empties(config) + if bool(ace_dict): + ace_list.append(ace_dict.copy()) + if len(ace_list): + name_dict = name_dict.copy() + name_dict.update({"aces": ace_list[:]}) + acls_list.append(name_dict.copy()) + config.update({"acls": acls_list}) + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_address_family/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_address_family/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_address_family/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_address_family/bgp_address_family.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_address_family/bgp_address_family.py new file mode 100644 index 000000000..649e5c6eb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_address_family/bgp_address_family.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos bgp_address_family 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. +""" + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.bgp_address_family.bgp_address_family import ( + Bgp_afArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.bgp_address_family import ( + Bgp_afTemplate, +) + + +class Bgp_afFacts(object): + """The eos bgp_address_family facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Bgp_afArgs.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_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section router\\sbgp ") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Bgp_af network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + + if not data: + data = self.get_config(connection) + + # remove global configs from bgp_address_family + bgp_af_config = [] + vrf_set = "" + start = False + for bgp_line in data.splitlines(): + if "router bgp" in bgp_line: + bgp_af_config.append(bgp_line) + vrf_present = re.search(r"vrf\s\S+", bgp_line) + if vrf_present: + vrf_set = vrf_present.group(0) + if start: + bgp_af_config.append(bgp_line) + if "address-family" in bgp_line: + af_line = vrf_set + bgp_line + bgp_af_config.append(af_line) + start = True + if start and "!" in bgp_line: + start = False + + # parse native config using the Bgp_af template + bgp_af_parser = Bgp_afTemplate(lines=bgp_af_config) + objs = bgp_af_parser.parse() + if objs: + if "address_family" in objs: + objs["address_family"] = list(objs["address_family"].values()) + for af in objs["address_family"]: + if "neighbor" in af: + af["neighbor"] = list(af["neighbor"].values()) + if "network" in af: + af["network"] = list(af["network"].values()) + af["network"] = sorted( + af["network"], + key=lambda k: k["address"], + ) + + ansible_facts["ansible_network_resources"].pop( + "bgp_address_family", + None, + ) + + params = utils.remove_empties( + utils.validate_config(self.argument_spec, {"config": objs}), + ) + + facts["bgp_address_family"] = params.get("config", []) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_global/bgp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_global/bgp_global.py new file mode 100644 index 000000000..9cb785a3c --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/bgp_global/bgp_global.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos bgp_global 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 ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.bgp_global.bgp_global import ( + Bgp_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.bgp_global import ( + Bgp_globalTemplate, +) + + +class Bgp_globalFacts(object): + """The eos bgp_global facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Bgp_globalArgs.argument_spec + + def get_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section router\\sbgp ") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Bgp_global network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + if not data: + data = self.get_config(connection) + + # remove address_family configs from bgp_global + bgp_global_config = [] + start = False + self._af = False + not_bgp = False + for bgp_line in data.splitlines(): + if "router " in bgp_line: + # Skip other protocol configs like router ospf etc + if "router bgp" not in bgp_line: + not_bgp = True + continue + not_bgp = False + if not start and not not_bgp: + bgp_global_config.append(bgp_line) + if "address-family" in bgp_line: + start = True + self._af = True + if start and "!" in bgp_line: + start = False + + # parse native config using the Bgp_global template + bgp_global_parser = Bgp_globalTemplate( + lines=bgp_global_config, + module=self._module, + ) + objs = bgp_global_parser.parse() + + if objs: + global_vals = objs.get("vrfs", {}).pop("vrf_", {}) + for key, value in iteritems(global_vals): + objs[key] = value + + if "vrfs" in objs: + objs["vrfs"] = list(objs["vrfs"].values()) + for vrf in objs["vrfs"]: + if "neighbor" in vrf: + vrf["neighbor"] = list(vrf["neighbor"].values()) + if "network" in vrf: + vrf["network"] = list(vrf["network"].values()) + vrf["network"] = sorted( + vrf["network"], + key=lambda k: k["address"], + ) + if "aggregate_address" in vrf: + vrf["aggregate_address"] = sorted( + vrf["aggregate_address"], + key=lambda k: k["address"], + ) + + if "neighbor" in objs: + objs["neighbor"] = list(objs["neighbor"].values()) + + if "network" in objs: + objs["network"] = list(objs["network"].values()) + objs["network"] = sorted( + objs["network"], + key=lambda k: k["address"], + ) + if "aggregate_address" in objs: + objs["aggregate_address"] = sorted( + objs["aggregate_address"], + key=lambda k: k["address"], + ) + + ansible_facts["ansible_network_resources"].pop("bgp_global", None) + + params = utils.remove_empties( + bgp_global_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["bgp_global"] = params.get("config", []) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/facts.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/facts.py new file mode 100644 index 000000000..87fd402eb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/facts.py @@ -0,0 +1,160 @@ +# -*- 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 facts class for eos +this file validates each subset of facts and selectively +calls the appropriate facts gathering function +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts import ( + FactsBase, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.acl_interfaces.acl_interfaces import ( + Acl_interfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.acls.acls import ( + AclsFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.bgp_address_family.bgp_address_family import ( + Bgp_afFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.bgp_global.bgp_global import ( + Bgp_globalFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.hostname.hostname import ( + HostnameFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.interfaces.interfaces import ( + InterfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.l2_interfaces.l2_interfaces import ( + L2_interfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.l3_interfaces.l3_interfaces import ( + L3_interfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.lacp.lacp import ( + LacpFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.lacp_interfaces.lacp_interfaces import ( + Lacp_interfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.lag_interfaces.lag_interfaces import ( + Lag_interfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.legacy.base import ( + Config, + Default, + Hardware, + Interfaces, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.lldp_global.lldp_global import ( + Lldp_globalFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.lldp_interfaces.lldp_interfaces import ( + Lldp_interfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.logging_global.logging_global import ( + Logging_globalFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.ntp_global.ntp_global import ( + Ntp_globalFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.ospf_interfaces.ospf_interfaces import ( + Ospf_interfacesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.ospfv2.ospfv2 import ( + Ospfv2Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.ospfv3.ospfv3 import ( + Ospfv3Facts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.prefix_lists.prefix_lists import ( + Prefix_listsFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.route_maps.route_maps import ( + Route_mapsFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.snmp_server.snmp_server import ( + Snmp_serverFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.static_routes.static_routes import ( + Static_routesFacts, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.vlans.vlans import ( + VlansFacts, +) + + +FACT_LEGACY_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) +FACT_RESOURCE_SUBSETS = dict( + interfaces=InterfacesFacts, + l2_interfaces=L2_interfacesFacts, + l3_interfaces=L3_interfacesFacts, + lacp=LacpFacts, + lacp_interfaces=Lacp_interfacesFacts, + lag_interfaces=Lag_interfacesFacts, + lldp_global=Lldp_globalFacts, + lldp_interfaces=Lldp_interfacesFacts, + vlans=VlansFacts, + acl_interfaces=Acl_interfacesFacts, + acls=AclsFacts, + static_routes=Static_routesFacts, + ospfv2=Ospfv2Facts, + ospfv3=Ospfv3Facts, + ospf_interfaces=Ospf_interfacesFacts, + bgp_address_family=Bgp_afFacts, + bgp_global=Bgp_globalFacts, + route_maps=Route_mapsFacts, + prefix_lists=Prefix_listsFacts, + logging_global=Logging_globalFacts, + ntp_global=Ntp_globalFacts, + snmp_server=Snmp_serverFacts, + hostname=HostnameFacts, +) + + +class Facts(FactsBase): + """The fact class for eos""" + + VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) + VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) + + def get_facts( + self, + legacy_facts_type=None, + resource_facts_type=None, + data=None, + ): + """Collect the facts for eos + :param legacy_facts_type: List of legacy facts types + :param resource_facts_type: List of resource fact types + :param data: previously collected conf + :rtype: dict + :return: the facts gathered + """ + if self.VALID_RESOURCE_SUBSETS: + self.get_network_resources_facts( + FACT_RESOURCE_SUBSETS, + resource_facts_type, + data, + ) + + if self.VALID_LEGACY_GATHER_SUBSETS: + self.get_network_legacy_facts( + FACT_LEGACY_SUBSETS, + legacy_facts_type, + ) + return self.ansible_facts, self._warnings diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/hostname/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/hostname/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/hostname/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/hostname/hostname.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/hostname/hostname.py new file mode 100644 index 000000000..1c95fe576 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/hostname/hostname.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos hostname 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 ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.hostname.hostname import ( + HostnameArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.hostname import ( + HostnameTemplate, +) + + +class HostnameFacts(object): + """The eos hostname facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = HostnameArgs.argument_spec + + def get_config(self, connection): + return connection.get("show running-config | section ^hostname") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Hostname network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + + if not data: + data = self.get_config(connection) + + # parse native config using the Hostname template + hostname_parser = HostnameTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = hostname_parser.parse() + + ansible_facts["ansible_network_resources"].pop("hostname", None) + + params = utils.remove_empties( + hostname_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["hostname"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/interfaces/interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/interfaces/interfaces.py new file mode 100644 index 000000000..f4d77637e --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/interfaces/interfaces.py @@ -0,0 +1,116 @@ +# -*- 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 eos interfaces 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.interfaces.interfaces import ( + InterfacesArgs, +) + + +class InterfacesFacts(object): + """The eos interfaces fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = InterfacesArgs.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("show running-config | section ^interface") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for interfaces + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected configuration + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + # operate on a collection of resource x + config = ("\n" + data).split("\ninterface ") + objs = [] + for conf in config: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + facts = {"interfaces": []} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + for cfg in params["config"]: + facts["interfaces"].append(utils.remove_empties(cfg)) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + + # populate the facts from the configuration + config["name"] = re.match(r"(\S+)", conf).group(1) + description = utils.parse_conf_arg(conf, "description") + if description is not None: + config["description"] = description.replace('"', "") + shutdown = utils.parse_conf_cmd_arg(conf, "shutdown", False) + config["enabled"] = shutdown if shutdown is False else True + config["mtu"] = utils.parse_conf_arg(conf, "mtu") + config["mode"] = utils.parse_conf_cmd_arg( + conf, + "switchport", + "layer2", + "layer3", + ) + + state = utils.parse_conf_arg(conf, "speed") + if state: + if state == "auto": + config["duplex"] = state + else: + # remaining options are all e.g., 10half or 40gfull + config["speed"] = state[:-4] + config["duplex"] = state[-4:] + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l2_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l2_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l2_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l2_interfaces/l2_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l2_interfaces/l2_interfaces.py new file mode 100644 index 000000000..b77b1a611 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l2_interfaces/l2_interfaces.py @@ -0,0 +1,120 @@ +# -*- 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 eos l2_interfaces 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.l2_interfaces.l2_interfaces import ( + L2_interfacesArgs, +) + + +class L2_interfacesFacts(object): + """The eos l2_interfaces fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = L2_interfacesArgs.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("show running-config | section ^interface") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for l2_interfaces + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected configuration + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + # operate on a collection of resource x + config = ("\n" + data).split("\ninterface ") + objs = [] + for conf in config: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + facts = {} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["l2_interfaces"] = [ + utils.remove_empties(cfg) for cfg in params["config"] + ] + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + + # populate the facts from the configuration + config["name"] = re.match(r"(\S+)", conf).group(1).replace('"', "") + has_mode = re.search(r"switchport mode (\S+)", conf) + if has_mode: + config["mode"] = has_mode.group(1) + + has_access = re.search(r"switchport access vlan (\d+)", conf) + if has_access: + config["access"] = {"vlan": int(has_access.group(1))} + + has_trunk = re.findall(r"switchport trunk (.+)", conf) + if has_trunk: + trunk = {} + for match in has_trunk: + has_native = re.match(r"native vlan (\d+)", match) + if has_native: + trunk["native_vlan"] = int(has_native.group(1)) + continue + + has_allowed = re.match(r"allowed vlan (\S+)", match) + if has_allowed: + # TODO: listify? + trunk["trunk_allowed_vlans"] = has_allowed.group(1) + continue + config["trunk"] = trunk + + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l3_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l3_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l3_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l3_interfaces/l3_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l3_interfaces/l3_interfaces.py new file mode 100644 index 000000000..3a617507e --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/l3_interfaces/l3_interfaces.py @@ -0,0 +1,124 @@ +# -*- 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 eos l3_interfaces 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.l3_interfaces.l3_interfaces import ( + L3_interfacesArgs, +) + + +class L3_interfacesFacts(object): + """The eos l3_interfaces fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = L3_interfacesArgs.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("show running-config | section ^interface") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for l3_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected configuration + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + # split the config into instances of the resource + resource_delim = "interface" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + facts = {} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["l3_interfaces"] = [ + utils.remove_empties(cfg) for cfg in params["config"] + ] + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + + config["name"] = utils.parse_conf_arg(conf, "interface") + + matches = re.findall(r".*ip address (.+)$", conf, re.MULTILINE) + if matches: + config["ipv4"] = [] + for match in matches: + address, dummy, remainder = match.partition(" ") + if address == "virtual": + ipv4 = {"virtual": True, "address": remainder} + else: + ipv4 = {"address": address} + if remainder == "secondary": + ipv4["secondary"] = True + config["ipv4"].append(ipv4) + + matches = re.findall(r".*ipv6 address (.+)$", conf, re.MULTILINE) + if matches: + config["ipv6"] = [] + for match in matches: + address, dummy, remainder = match.partition(" ") + ipv6 = {"address": address} + config["ipv6"].append(ipv6) + + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp/lacp.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp/lacp.py new file mode 100644 index 000000000..28ba476b8 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp/lacp.py @@ -0,0 +1,106 @@ +# -*- 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 eos lacp 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lacp.lacp import ( + LacpArgs, +) + + +class LacpFacts(object): + """The eos lacp fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = LacpArgs.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("show running-config | section ^lacp") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for lacp + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected configuration + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + # split the config into instances of the resource + resource_delim = "lacp" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + + objs = {} + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.update(obj) + + ansible_facts["ansible_network_resources"].pop("lacp", None) + facts = {"lacp": {}} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["lacp"] = utils.remove_empties(params["config"]) + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + config["system"]["priority"] = utils.parse_conf_arg( + conf, + "system-priority", + ) + + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp_interfaces/lacp_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp_interfaces/lacp_interfaces.py new file mode 100644 index 000000000..6961ca1b6 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lacp_interfaces/lacp_interfaces.py @@ -0,0 +1,107 @@ +# -*- 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 eos lacp_interfaces 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lacp_interfaces.lacp_interfaces import ( + Lacp_interfacesArgs, +) + + +class Lacp_interfacesFacts(object): + """The eos lacp_interfaces fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Lacp_interfacesArgs.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("show running-config | section lacp") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for lacp_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected configuration + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + # split the config into instances of the resource + resource_delim = "interface" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts["ansible_network_resources"].pop("lacp_interfaces", None) + facts = {} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["lacp_interfaces"] = [ + utils.remove_empties(cfg) for cfg in params["config"] + ] + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + config["name"] = utils.parse_conf_arg(conf, "interface") + config["port_priority"] = utils.parse_conf_arg(conf, "port-priority") + config["timer"] = utils.parse_conf_arg(conf, "timer") + + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lag_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lag_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lag_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lag_interfaces/lag_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lag_interfaces/lag_interfaces.py new file mode 100644 index 000000000..73b6be3bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lag_interfaces/lag_interfaces.py @@ -0,0 +1,124 @@ +# -*- 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 eos lag_interfaces 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lag_interfaces.lag_interfaces import ( + Lag_interfacesArgs, +) + + +class Lag_interfacesFacts(object): + """The eos lag_interfaces fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Lag_interfacesArgs.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("show running-config | section ^interface") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for lag_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected configuration + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + # split the config into instances of the resource + resource_delim = "interface" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + + objs = {} + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + group_name = obj["name"] + if group_name in objs and "members" in obj: + config = objs[group_name] + if "members" not in config: + config["members"] = [] + objs[group_name]["members"].extend(obj["members"]) + else: + objs[group_name] = obj + objs = list(objs.values()) + facts = {"lag_interfaces": []} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["lag_interfaces"] = [ + utils.remove_empties(cfg) for cfg in params["config"] + ] + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + interface_name = utils.parse_conf_arg(conf, "interface") + if interface_name.startswith("Port-Channel"): + config["name"] = interface_name + return utils.remove_empties(config) + + interface = {"member": interface_name} + match = re.match( + r".*channel-group (\d+) mode (\S+)", + conf, + re.MULTILINE | re.DOTALL, + ) + if match: + config["name"], interface["mode"] = match.groups() + config["name"] = "Port-Channel" + config["name"] + config["members"] = [interface] + + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/legacy/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/legacy/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/legacy/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/legacy/base.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/legacy/base.py new file mode 100644 index 000000000..6e0424f15 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/legacy/base.py @@ -0,0 +1,180 @@ +# -*- 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) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import platform +import re + +from ansible.module_utils.six import iteritems + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + get_capabilities, + run_commands, +) + + +class FactsBase(object): + COMMANDS = frozenset() + + def __init__(self, module): + self.module = module + self.warnings = list() + self.facts = dict() + self.responses = None + + def populate(self): + self.responses = run_commands( + self.module, + list(self.COMMANDS), + check_rc=False, + ) + + +class Default(FactsBase): + SYSTEM_MAP = {"serialNumber": "serialnum"} + + COMMANDS = ["show version | json", "show hostname | json"] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + for key, value in iteritems(self.SYSTEM_MAP): + if key in data: + self.facts[value] = data[key] + + self.facts.update(self.responses[1]) + self.facts.update(self.platform_facts()) + + def platform_facts(self): + platform_facts = {} + + resp = get_capabilities(self.module) + device_info = resp["device_info"] + + platform_facts["system"] = device_info["network_os"] + + for item in ("model", "image", "version", "platform", "hostname"): + val = device_info.get("network_os_%s" % item) + if val: + platform_facts[item] = val + + platform_facts["api"] = resp["network_api"] + platform_facts["python_version"] = platform.python_version() + + return platform_facts + + +class Hardware(FactsBase): + COMMANDS = ["dir all-filesystems", "show version | json"] + + def populate(self): + super(Hardware, self).populate() + self.facts.update(self.populate_filesystems()) + self.facts.update(self.populate_memory()) + + def populate_filesystems(self): + data = self.responses[0] + + if isinstance(data, dict): + data = data["messages"][0] + + fs = re.findall(r"^Directory of (.+)/", data, re.M) + return dict(filesystems=fs) + + def populate_memory(self): + values = self.responses[1] + return dict( + memfree_mb=int(values["memFree"]) / 1024, + memtotal_mb=int(values["memTotal"]) / 1024, + ) + + +class Config(FactsBase): + COMMANDS = ["show running-config"] + + def populate(self): + super(Config, self).populate() + self.facts["config"] = self.responses[0] + + +class Interfaces(FactsBase): + INTERFACE_MAP = { + "description": "description", + "physicalAddress": "macaddress", + "mtu": "mtu", + "bandwidth": "bandwidth", + "duplex": "duplex", + "lineProtocolStatus": "lineprotocol", + "interfaceStatus": "operstatus", + "forwardingModel": "type", + } + + COMMANDS = ["show interfaces | json", "show lldp neighbors | json"] + + def populate(self): + super(Interfaces, self).populate() + + self.facts["all_ipv4_addresses"] = list() + self.facts["all_ipv6_addresses"] = list() + + data = self.responses[0] + if data and "LLDP is not enabled" not in data: + self.facts["interfaces"] = self.populate_interfaces(data) + + if len(self.responses) > 1: + data = self.responses[1] + if data: + self.facts["neighbors"] = self.populate_neighbors( + data["lldpNeighbors"], + ) + + def populate_interfaces(self, data): + facts = dict() + for key, value in iteritems(data["interfaces"]): + intf = dict() + + for remote, local in iteritems(self.INTERFACE_MAP): + if remote in value: + intf[local] = value[remote] + + if "interfaceAddress" in value: + intf["ipv4"] = dict() + for entry in value["interfaceAddress"]: + intf["ipv4"]["address"] = entry["primaryIp"]["address"] + intf["ipv4"]["masklen"] = entry["primaryIp"]["maskLen"] + self.add_ip_address(entry["primaryIp"]["address"], "ipv4") + + if "interfaceAddressIp6" in value: + intf["ipv6"] = dict() + for entry in value["interfaceAddressIp6"]["globalUnicastIp6s"]: + intf["ipv6"]["address"] = entry["address"] + intf["ipv6"]["subnet"] = entry["subnet"] + self.add_ip_address(entry["address"], "ipv6") + + facts[key] = intf + + return facts + + def add_ip_address(self, address, family): + if family == "ipv4": + self.facts["all_ipv4_addresses"].append(address) + else: + self.facts["all_ipv6_addresses"].append(address) + + def populate_neighbors(self, neighbors): + facts = dict() + for value in neighbors: + port = value["port"] + if port not in facts: + facts[port] = list() + lldp = dict() + lldp["host"] = value["neighborDevice"] + lldp["port"] = value["neighborPort"] + facts[port].append(lldp) + return facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_global/lldp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_global/lldp_global.py new file mode 100644 index 000000000..d59be7c54 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_global/lldp_global.py @@ -0,0 +1,102 @@ +# +# -*- 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 eos lldp_global 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lldp_global.lldp_global import ( + Lldp_globalArgs, +) + + +class Lldp_globalFacts(object): + """The eos lldp_global fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Lldp_globalArgs.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("show running-config | section lldp") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for lldp_global + :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) + + obj = {} + if data: + obj.update(self.render_config(self.generated_spec, data)) + + ansible_facts["ansible_network_resources"].pop("lldp_global", None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {"config": obj}) + facts["lldp_global"] = utils.remove_empties(params["config"]) + else: + facts["lldp_global"] = {} + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + config["holdtime"] = utils.parse_conf_arg(conf, "hold-time") + config["reinit"] = utils.parse_conf_arg(conf, "timer reinitialization") + config["timer"] = utils.parse_conf_arg(conf, "timer") + if config.get("timer") and "reinitialization" in config["timer"]: + config["timer"] = None + + for match in re.findall( + r"^(no)? lldp tlv transmit (\S+)", + conf, + re.MULTILINE, + ): + tlv_option = match[1].replace("-", "_") + config["tlv_select"][tlv_option] = bool(match[0] != "no") + + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_interfaces/lldp_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_interfaces/lldp_interfaces.py new file mode 100644 index 000000000..569986ef2 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/lldp_interfaces/lldp_interfaces.py @@ -0,0 +1,106 @@ +# -*- 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 eos lldp_interfaces 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lldp_interfaces.lldp_interfaces import ( + Lldp_interfacesArgs, +) + + +class Lldp_interfacesFacts(object): + """The eos lldp_interfaces fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Lldp_interfacesArgs.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 populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for lldp_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected configuration + :rtype: dictionary + :returns: facts + """ + if not data: + data = connection.get("show running-config | section lldp") + + # split the config into instances of the resource + resource_delim = "interface" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts["ansible_network_resources"].pop("lldp_interfaces", None) + facts = {} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["lldp_interfaces"] = [ + utils.remove_empties(cfg) for cfg in params["config"] + ] + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + config["name"] = utils.parse_conf_arg(conf, "interface") + + matches = re.findall(r"(no )?lldp (\S+)", conf) + for match in matches: + config[match[1]] = not bool(match[0]) + + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/logging_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/logging_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/logging_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/logging_global/logging_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/logging_global/logging_global.py new file mode 100644 index 000000000..e2e0e2cfd --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/logging_global/logging_global.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos logging_global 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 ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.logging_global.logging_global import ( + Logging_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.logging_global import ( + Logging_globalTemplate, +) + + +class Logging_globalFacts(object): + """The eos logging_global facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Logging_globalArgs.argument_spec + + def get_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section logging") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Logging_global network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = {} + + if not data: + data = self.get_config(connection) + + # parse native config using the Prefix_lists template + logging_parser = Logging_globalTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = logging_parser.parse() + if objs: + if "hosts" in objs: + objs["hosts"] = sorted( + list(objs["hosts"].values()), + key=lambda k, sk="name": k[sk], + ) + if "vrfs" in objs: + for k, v in iteritems(objs["vrfs"]): + if "hosts" in v: + v["hosts"] = sorted( + list(v["hosts"].values()), + key=lambda k, sk="name": k[sk], + ) + objs["vrfs"] = sorted( + list(objs["vrfs"].values()), + key=lambda k, sk="name": k[sk], + ) + else: + objs = {} + ansible_facts["ansible_network_resources"].pop("logging_global", None) + + params = utils.remove_empties( + logging_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["logging_global"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ntp_global/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ntp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ntp_global/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ntp_global/ntp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ntp_global/ntp_global.py new file mode 100644 index 000000000..96c2321c7 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ntp_global/ntp_global.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos ntp_global 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 ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ntp_global.ntp_global import ( + Ntp_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.ntp_global import ( + Ntp_globalTemplate, +) + + +class Ntp_globalFacts(object): + """The eos ntp_global facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Ntp_globalArgs.argument_spec + + def get_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section ntp") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Ntp_global network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = {} + + if not data: + data = self.get_config(connection) + + # parse native config using the Ntp_global template + ntp_global_parser = Ntp_globalTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = ntp_global_parser.parse() + if objs: + if "authentication_keys" in objs: + objs["authentication_keys"] = sorted( + list(objs["authentication_keys"].values()), + key=lambda k, sk="id": k[sk], + ) + if "serve" in objs: + if "access_lists" in objs["serve"]: + objs["serve"]["access_lists"] = sorted( + list(objs["serve"]["access_lists"].values()), + key=lambda k, sk="afi": k[sk], + ) + if "servers" in objs: + objs["servers"] = sorted( + list(objs["servers"].values()), + key=lambda k, sk="server": k[sk], + ) + else: + objs = {} + ansible_facts["ansible_network_resources"].pop("ntp_global", None) + + params = utils.remove_empties( + ntp_global_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["ntp_global"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospf_interfaces/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospf_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospf_interfaces/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospf_interfaces/ospf_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospf_interfaces/ospf_interfaces.py new file mode 100644 index 000000000..981b6bca7 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospf_interfaces/ospf_interfaces.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos ospf_interfaces 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. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ospf_interfaces.ospf_interfaces import ( + Ospf_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.ospf_interfaces import ( + Ospf_interfacesTemplate, +) + + +class Ospf_interfacesFacts(object): + """The eos ospf_interfaces facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Ospf_interfacesArgs.argument_spec + + def get_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section interface ") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Ospf_interfaces network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + + if not data: + data = self.get_config(connection) + + resource_delim = "interface" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + # parse native config using the Ospf_interfaces template + ospf_interfaces_facts = [] + for resource in resources: + ospf_interfaces_parser = Ospf_interfacesTemplate( + lines=resource.splitlines(), + module=self._module, + ) + entry = ospf_interfaces_parser.parse() + if entry: + if "address_family" in entry and entry["address_family"]: + entry["address_family"] = sorted( + list(entry["address_family"].values()), + key=lambda k, sk="afi": k[sk], + ) + if entry: + if entry.get("address_family"): + for addr in entry["address_family"]: + if "ip_params" in addr: + addr["ip_params"] = sorted( + list(addr["ip_params"].values()), + key=lambda k, sk="afi": k[sk], + ) + ospf_interfaces_facts.append(entry) + + ansible_facts["ansible_network_resources"].pop("ospf_interfaces", None) + facts = {"ospf_interfaces": []} + params = utils.remove_empties( + ospf_interfaces_parser.validate_config( + self.argument_spec, + {"config": ospf_interfaces_facts}, + redact=True, + ), + ) + + if params.get("config"): + for cfg in params["config"]: + facts["ospf_interfaces"].append(utils.remove_empties(cfg)) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv2/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv2/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv2/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv2/ospfv2.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv2/ospfv2.py new file mode 100644 index 000000000..f5f35541a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv2/ospfv2.py @@ -0,0 +1,510 @@ +# +# -*- 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) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos ospfv2 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. +""" +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ospfv2.ospfv2 import ( + Ospfv2Args, +) + + +class Ospfv2Facts(object): + """The eos ospfv2 fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Ospfv2Args.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("show running-config | section ospf") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for ospfv2 + :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) + + # split the config into instances of the resource + resource_delim = "router ospf" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + objs_list = [] + objs = {} + for resource in resources: + if resource and "router ospfv3" not in resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs_list.append(obj) + objs = {"processes": objs_list} + ansible_facts["ansible_network_resources"].pop("ospfv2", None) + + facts = {} + if objs: + facts["ospfv2"] = {} + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["ospfv2"].update(utils.remove_empties(params["config"])) + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + instance_list = [] + ospf_params_dict = {} + areas_list = [] + distance_dict = {} + network_list = [] + redistribute_list = [] + timers_list = [] + areas_list = [] + for dev_config in conf.split("\n"): + if not dev_config: + continue + network_dict = {} + redistribute_dict = {} + dev_config = dev_config.strip() + dev_config = re.sub(r"-", "_", dev_config).strip() + matches = re.findall(r"router (ospf) (.*)", dev_config) + if matches: + if ospf_params_dict: + instance_list.append(ospf_params_dict) + ospf_params_dict = {} + instance = matches[0][1].split() + ospf_params_dict.update({"process_id": str(instance[0])}) + if "vrf" in dev_config: + vrf_name = instance[-1] + else: + vrf_name = None + ospf_params_dict.update({"vrf": vrf_name}) + if "traffic_engineering" in dev_config: + ospf_params_dict.update({"traffic_engineering": True}) + config_params = dev_config.split() + if config_params[0] == "adjacency": + threshold = config_params[-1] + adjacency_dict = {"exchange_start": {"threshold": threshold}} + ospf_params_dict.update({"adjacency": adjacency_dict}) + elif "auto_cost" in dev_config: + bw = config_params[-1] + ospf_params_dict.update( + {"auto_cost": {"reference_bandwidth": bw}}, + ) + elif "bfd" in dev_config: + ospf_params_dict.update({"bfd": {"all_interfaces": True}}) + elif config_params[0] == "default_information": + def_dict = {"originate": True} + for i, val in enumerate(config_params[2::]): + if val == "always": + def_dict.update({"always": True}) + elif val in ["route_map", "metric", "metric_type"]: + def_dict.update({val: config_params[i + 3]}) + ospf_params_dict.update({"default_information": def_dict}) + elif "default_metric" in dev_config: + ospf_params_dict.update({"default_metric": config_params[-1]}) + elif "distance" in dev_config: + distance_dict.update({config_params[-2]: config_params[-1]}) + ospf_params_dict.update({"distance": distance_dict}) + elif "distribute_list" in dev_config: + ospf_params_dict.update( + {"distribute_list": {config_params[1]: config_params[2]}}, + ) + elif "dn_bit_ignore" in dev_config: + ospf_params_dict.update({"dn_bit_ignore": True}) + elif "fips_restrictions" in dev_config: + ospf_params_dict.update({"fips_restrictions": True}) + elif "graceful_restart" in dev_config: + if "grace_period" in dev_config: + ospf_params_dict.update( + { + "graceful_restart": { + "grace_period": config_params[-1], + }, + }, + ) + else: + ospf_params_dict.update( + {"graceful_restart": {"set": True}}, + ) + elif "graceful_restart_helper" in dev_config: + ospf_params_dict.update({"graceful_restart_helper": True}) + elif "log_adjacency_changes" in dev_config: + detail = True if "detail" in dev_config else False + ospf_params_dict.update( + {"log_adjacency_changes": {"detail": detail}}, + ) + elif "max_lsa" in dev_config: + max_lsa_dict = {} + config_params.pop(0) + max_lsa_dict.update({"count": config_params.pop(0)}) + if config_params: + if config_params[0].isdigit(): + max_lsa_dict.update( + {"threshold": config_params.pop(0)}, + ) + for i, el in enumerate(config_params): + if el == "warning_only": + max_lsa_dict.update({"warning": True}) + if el in ["ignore_count", "ignore_time", "reset_time"]: + max_lsa_dict.update({el: config_params[i + 1]}) + ospf_params_dict.update({"max_lsa": max_lsa_dict}) + elif "maximum_paths" in dev_config: + ospf_params_dict.update({"maximum_paths": config_params[1]}) + elif "mpls ldp sync default" in dev_config: + ospf_params_dict.update({"mpls_ldp": True}) + elif config_params[0] == "network": + config_params.pop(0) + prefix = re.search(r"\/", config_params[0]) + if prefix: + network_dict.update({"prefix": config_params.pop(0)}) + else: + network_dict.update( + {"network_address": config_params.pop(0)}, + ) + network_dict.update({"mask": config_params.pop(0)}) + network_dict.update({"area": config_params[-1]}) + network_list.append(network_dict) + ospf_params_dict.update({"networks": network_list}) + elif "passive_interface" in dev_config: + if config_params[1] == "default": + ospf_params_dict.update( + {"passive_interface": {"default": True}}, + ) + else: + ospf_params_dict.update( + { + "passive_interface": { + "interface_list": config_params[1], + }, + }, + ) + elif "point_to_point" in dev_config: + ospf_params_dict.update({"point_to_point": True}) + elif "redistribute" in dev_config: + redistribute_dict.update({"routes": config_params[1]}) + if config_params[1] == "isis": + if "level" in config_params[2]: + k = re.sub(r"_", "-", config_params[2]) + redistribute_dict.update({"isis_level": k}) + if "route_map" in dev_config: + redistribute_dict.update({"route_map": config_params[-1]}) + redistribute_list.append(redistribute_dict) + ospf_params_dict.update({"redistribute": redistribute_list}) + elif "router_id" in dev_config: + ospf_params_dict.update({"router_id": config_params[-1]}) + elif "retransmission_threshold" in dev_config: + ospf_params_dict.update( + {"retransmission_threshold": config_params[-1]}, + ) + elif config_params[0] == "compatible": + ospf_params_dict.update({"rfc1583compatibility": True}) + elif "shutdown" in dev_config: + ospf_params_dict.update({"shutdown": True}) + elif "summary_address" in dev_config: + summary_address_dict = {} + config_params.pop(0) + prefix = re.search(r"\/", config_params[0]) + if prefix: + summary_address_dict.update( + {"prefix": config_params.pop(0)}, + ) + else: + summary_address_dict.update( + {"address": config_params.pop(0)}, + ) + summary_address_dict.update({"mask": config_params.pop(0)}) + if "not_advertise" in dev_config: + summary_address_dict.update({"not_advertise": True}) + config_params.pop(0) + else: + if config_params: + summary_address_dict.update( + {config_params[0]: config_params[1]}, + ) + ospf_params_dict.update( + {"summary_address": summary_address_dict}, + ) + elif "timers" in dev_config: + timers_dict = {} + if config_params[1] == "lsa": + if config_params[2] in ["rx", "arrival"]: + timers_dict.update( + { + "lsa": { + "rx": {"min_interval": config_params[-1]}, + }, + }, + ) + else: + timers_dict.update( + { + "lsa": { + "tx": { + "delay": { + "initial": config_params[-3], + "min": config_params[-2], + "max": config_params[-1], + }, + }, + }, + }, + ) + elif config_params[1] == "out_delay": + timers_dict.update({"out_delay": config_params[-1]}) + elif config_params[1] == "pacing": + timers_dict.update({"pacing": config_params[-1]}) + elif config_params[1] == "spf": + if config_params[2] == "delay": + timers_dict.update( + { + "spf": { + "tx": { + "delay": { + "initial": config_params[-3], + "min": config_params[-2], + "max": config_params[-1], + }, + }, + }, + }, + ) + else: + timers_dict.update( + {"spf": {"seconds": config_params[-1]}}, + ) + elif config_params[1] == "throttle": + timers_dict.update( + { + "throttle": { + "attr": config_params[2], + "initial": config_params[-3], + "min": config_params[-2], + "max": config_params[-1], + }, + }, + ) + timers_list.append(timers_dict) + ospf_params_dict.update({"timers": timers_list}) + elif config_params[0] == "area": + areas_dict = {} + areas_dict.update({"area_id": config_params[1]}) + if config_params[2] == "default_cost": + areas_dict.update({"default_cost": config_params[-1]}) + elif config_params[2] == "filter": + prefix = re.search(r"\/", config_params[3]) + if prefix: + areas_dict.update( + {"filter": {"address": config_params[3]}}, + ) + elif config_params[3] == "prefix_list": + areas_dict.update( + {"filter": {"prefix_list": config_params[-1]}}, + ) + else: + areas_dict.update( + {"filter": {"subnet_address": config_params[3]}}, + ) + areas_dict.update( + {"filter": {"subnet_mask": config_params[4]}}, + ) + elif config_params[2] == "not_so_stubby": + if len(config_params) == 3: + areas_dict.update({"not_so_stubby": {"set": True}}) + continue + if config_params[3] == "lsa": + areas_dict.update({"not_so_stubby": {"lsa": True}}) + elif config_params[3] == "default_information_originate": + default_dict = {} + for i, val in enumerate(config_params): + if val == "nssa_only": + default_dict.update({"nssa_only": True}) + if val == "metric_type": + default_dict.update( + {"metric_type": config_params[i + 1]}, + ) + if val == "metric": + default_dict.update( + {"metric": config_params[i + 1]}, + ) + areas_dict.update( + { + "not_so_stubby": { + "default_information_originate": default_dict, + }, + }, + ) + elif config_params[3] == "no_summary": + areas_dict.update( + {"not_so_stubby": {"no_summary": True}}, + ) + elif config_params[3] == "nssa_only": + areas_dict.update( + {"not_so_stubby": {"nssa_only": True}}, + ) + elif config_params[2] == "nssa": + if len(config_params) == 3: + areas_dict.update({"nssa": {"set": True}}) + continue + if config_params[3] == "default_information_originate": + default_dict = {} + for i, val in enumerate(config_params): + if val == "nssa_only": + default_dict.update({"nssa_only": True}) + if val == "metric_type": + default_dict.update( + {"metric_type": config_params[i + 1]}, + ) + if val == "metric": + default_dict.update( + {"metric": config_params[i + 1]}, + ) + areas_dict.update( + { + "nssa": { + "default_information_originate": default_dict, + }, + }, + ) + elif config_params[3] == "no_summary": + areas_dict.update({"nssa": {"no_summary": True}}) + elif config_params[3] == "nssa_only": + areas_dict.update({"nssa": {"nssa_only": True}}) + elif config_params[2] == "range": + prefix = re.search(r"\/", config_params[3]) + range_dict = {} + if prefix: + range_dict.update({"address": config_params[3]}) + else: + range_dict.update({"subnet_address": config_params[3]}) + range_dict.update({"subnet_mask": config_params[4]}) + if "advertise" in dev_config: + range_dict.update({"advertise": True}) + if "not_advertise" in dev_config: + range_dict.update({"advertise": False}) + if "cost" in dev_config: + range_dict.update({"cost": config_params[-1]}) + areas_dict.update({"range": range_dict}) + elif config_params[2] == "stub": + if "no_summary" in dev_config: + areas_dict.update({"stub": {"no_summary": True}}) + else: + areas_dict.update({"stub": {"set": True}}) + areas_list.append(areas_dict) + ospf_params_dict.update({"areas": areas_list}) + elif config_params[0] == "max_metric": + config_params.pop(0) + router_lsa_dict = {} + config_params.pop(0) + if not config_params: + ospf_params_dict.update( + {"max_metric": {"router_lsa": {"set": True}}}, + ) + else: + for i, val in enumerate(config_params): + if val == "include_stub": + router_lsa_dict.update({"include_stub": True}) + elif val == "on_startup": + if config_params[i + 1] == "wait_for_bgp": + router_lsa_dict.update( + {"on_startup": {"wait_for_bgp": True}}, + ) + else: + router_lsa_dict.update( + { + "on_startup": { + "time": config_params[i + 1], + }, + }, + ) + elif val == "external_lsa": + if ( + i < len(config_params) + and config_params[i + 1].isdigit() + ): + router_lsa_dict.update( + { + "external_lsa": { + "max_metric_value": config_params[ + i + 1 + ], + }, + }, + ) + else: + router_lsa_dict.update( + {"external_lsa": {"set": True}}, + ) + elif val == "summary_lsa": + if ( + i < len(config_params) - 1 + and config_params[i + 1].isdigit() + ): + router_lsa_dict.update( + { + "summary_lsa": { + "max_metric_value": config_params[ + i + 1 + ], + }, + }, + ) + else: + router_lsa_dict.update( + {"summary_lsa": {"set": True}}, + ) + ospf_params_dict.update( + {"max_metric": {"router_lsa": router_lsa_dict}}, + ) + # instance_list.append(ospf_params_dict) + # config.update({"ospf_version": "v2", "ospf_processes": instance_list}) + return utils.remove_empties(ospf_params_dict) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv3/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv3/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv3/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv3/ospfv3.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv3/ospfv3.py new file mode 100644 index 000000000..3efa27cdf --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/ospfv3/ospfv3.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos ospfv3 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. +""" + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ospfv3.ospfv3 import ( + Ospfv3Args, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.ospfv3 import ( + Ospfv3Template, +) + + +class Ospfv3Facts(object): + """The eos ospfv3 facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Ospfv3Args.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_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section ospfv3") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Ospfv3 network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + ospfv3_parser = Ospfv3Template(lines=[], module=self._module) + + if not data: + data = self.get_config(connection) + + # split the config into instances of the resource + resource_delim = "router ospfv3" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + + # parse native config using the Ospfv3 template + ospfv3_facts = {"processes": []} + for resource in resources: + ospfv3_parser = Ospfv3Template( + lines=resource.splitlines(), + module=self._module, + ) + objs = ospfv3_parser.parse() + for key, sortv in [("address_family", "afi")]: + if key in objs["processes"] and objs["processes"][key]: + objs["processes"][key] = list( + objs["processes"][key].values(), + ) + + for addr_family in objs["processes"]["address_family"]: + if "areas" in addr_family: + addr_family["areas"] = list(addr_family["areas"].values()) + + for addr_family in objs["processes"]["address_family"]: + if not addr_family.get("afi"): + # global vars + objs["processes"].update(addr_family) + objs["processes"]["address_family"].remove(addr_family) + + ospfv3_facts["processes"].append(objs["processes"]) + + ansible_facts["ansible_network_resources"].pop("ospfv3", None) + params = ospfv3_parser.validate_config( + self.argument_spec, + {"config": ospfv3_facts}, + redact=True, + ) + params = utils.remove_empties(params) + + facts["ospfv3"] = params.get("config", []) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/prefix_lists/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/prefix_lists/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/prefix_lists/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/prefix_lists/prefix_lists.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/prefix_lists/prefix_lists.py new file mode 100644 index 000000000..aa1f81b84 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/prefix_lists/prefix_lists.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos prefix_lists 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 ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.prefix_lists.prefix_lists import ( + Prefix_listsArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.prefix_lists import ( + Prefix_listsTemplate, +) + + +class Prefix_listsFacts(object): + """The eos prefix_lists facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Prefix_listsArgs.argument_spec + + def get_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section prefix-list") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Prefix_lists network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + + if not data: + data = self.get_config(connection) + + # parse native config using the Prefix_lists template + prefix_lists_parser = Prefix_listsTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = prefix_lists_parser.parse() + if objs: + for afi, pl in iteritems(objs): + if "prefix_lists" in pl: + pl["prefix_lists"] = sorted( + list(pl["prefix_lists"].values()), + key=lambda k, sk="name": k[sk], + ) + for plist in pl["prefix_lists"]: + for en in plist: + if "entries" in en: + plist["entries"] = sorted( + list(plist["entries"].values()), + key=lambda k, sk="sequence": k[sk], + ) + objs = sorted(list(objs.values()), key=lambda k, sk="afi": k[sk]) + else: + objs = [] + ansible_facts["ansible_network_resources"].pop("prefix_lists", None) + + params = utils.remove_empties( + prefix_lists_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["prefix_lists"] = params.get("config", []) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/route_maps/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/route_maps/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/route_maps/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/route_maps/route_maps.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/route_maps/route_maps.py new file mode 100644 index 000000000..e5ca11162 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/route_maps/route_maps.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos route_maps 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. +""" + +import re + +from copy import deepcopy + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.route_maps.route_maps import ( + Route_mapsArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.route_maps import ( + Route_mapsTemplate, +) + + +class Route_mapsFacts(object): + """The eos route_maps facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Route_mapsArgs.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_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section route-map ") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Route_maps network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + + if not data: + data = self.get_config(connection) + + resource_delim = "route-map" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + # parse native config using the Ospf_interfaces template + route_maps_facts = [] + # parse native config using the Route_maps template + for resource in resources: + route_maps_parser = Route_mapsTemplate(lines=resource.splitlines()) + objs = route_maps_parser.parse() + if objs: + dict_update = {} + for k, v in iteritems(objs): + if k == "entries": + e_list = [] + match_dict = {} + match_ip = {} + match_ipv6 = {} + set_dict = {} + for el in v: + for entry_k, entry_v in iteritems(el): + if entry_k == "match": + if "ip" in entry_v or "ipv6" in entry_v: + for ipk, ipv in iteritems(entry_v): + if "ip" in entry_v: + match_ip.update(ipv) + if "ipv6" in entry_v: + match_ipv6.update(ipv) + matchv = { + "ip": match_ip, + "ipv6": match_ipv6, + } + else: + matchv = entry_v + match_dict.update(matchv) + elif entry_k == "set": + set_dict.update(entry_v) + else: + dict_update.update(el) + dict_update.update( + {"match": match_dict, "set": set_dict}, + ) + e_list.append(dict_update) + objs.update({"entries": e_list}) + route_maps_facts.append(objs) + maps = [] + r_facts = [] + for r_map in route_maps_facts: + if r_map["route_map"] in maps: + for r_f in r_facts: + if r_f["route_map"] == r_map["route_map"]: + r_f["entries"].extend(r_map["entries"]) + else: + maps.append(r_map["route_map"]) + r_facts.append(r_map) + ansible_facts["ansible_network_resources"].pop("route_maps", None) + facts = {"route_maps": []} + params = utils.remove_empties( + utils.validate_config(self.argument_spec, {"config": r_facts}), + ) + if params.get("config"): + for cfg in params["config"]: + facts["route_maps"].append(utils.remove_empties(cfg)) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/snmp_server/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/snmp_server/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/snmp_server/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/snmp_server/snmp_server.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/snmp_server/snmp_server.py new file mode 100644 index 000000000..3746be94c --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/snmp_server/snmp_server.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The eos snmp_server 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 ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.snmp_server.snmp_server import ( + Snmp_serverArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.snmp_server import ( + Snmp_serverTemplate, +) + + +class Snmp_serverFacts(object): + """The eos snmp_server facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Snmp_serverArgs.argument_spec + + def get_config(self, connection): + """Wrapper method for `connection.get()` + This method exists solely to allow the unit test framework to mock device connection calls. + """ + return connection.get("show running-config | section snmp-server") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Snmp_server network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + + if not data: + data = self.get_config(connection) + + # parse native config using the Snmp_server template + snmp_server_parser = Snmp_serverTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = snmp_server_parser.parse() + if objs: + if "communities" in objs: + objs["communities"] = sorted( + list(objs["communities"].values()), + key=lambda k, sk="name": k[sk], + ) + if "groups" in objs: + objs["groups"] = sorted( + list(objs["groups"].values()), + key=lambda k, sk="group": k[sk], + ) + if "hosts" in objs: + objs["hosts"] = sorted( + list(objs["hosts"].values()), + key=lambda k, sk="host": k[sk], + ) + if "acls" in objs: + objs["acls"] = sorted( + list(objs["acls"].values()), + key=lambda k, sk="afi": k[sk], + ) + if "users" in objs: + objs["users"] = sorted( + list(objs["users"].values()), + key=lambda k, sk="user": k[sk], + ) + if "views" in objs: + objs["views"] = sorted( + list(objs["views"].values()), + key=lambda k, sk="view": k[sk], + ) + if "vrfs" in objs: + objs["vrfs"] = sorted( + list(objs["vrfs"].values()), + key=lambda k, sk="vrf": k[sk], + ) + else: + objs = {} + + ansible_facts["ansible_network_resources"].pop("snmp_server", None) + + params = utils.remove_empties( + snmp_server_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["snmp_server"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/static_routes/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/static_routes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/static_routes/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/static_routes/static_routes.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/static_routes/static_routes.py new file mode 100644 index 000000000..85b0e7475 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/static_routes/static_routes.py @@ -0,0 +1,243 @@ +# +# -*- 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 eos 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.static_routes.static_routes import ( + Static_routesArgs, +) + + +class Static_routesFacts(object): + """The eos 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("show running-config | grep route") + + 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) + + # split the config into instances of the resource + resource_delim = "ip.* route" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [p.strip() for p in re.findall(find_pattern, data)] + resources_without_vrf = [] + resource_vrf = {} + for resource in resources: + if resource and "vrf" not in resource: + resources_without_vrf.append(resource) + else: + vrf = re.search(r"ip(v6)* route vrf (.*?) .*", resource) + if vrf.group(2) in resource_vrf.keys(): + vrf_val = resource_vrf[vrf.group(2)] + vrf_val.append(resource) + resource_vrf.update({vrf.group(2): vrf_val}) + else: + resource_vrf.update({vrf.group(2): [resource]}) + resources_without_vrf = ["\n".join(resources_without_vrf)] + for vrf in resource_vrf.keys(): + vrflist = ["\n".join(resource_vrf[vrf])] + resource_vrf.update({vrf: vrflist}) + objs = [] + for resource in resources_without_vrf: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + for resource in resource_vrf.keys(): + if resource: + obj = self.render_config( + self.generated_spec, + resource_vrf[resource][0], + ) + if obj: + objs.append(obj) + 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, spec, 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 + """ + config = deepcopy(spec) + address_family_dict = {} + route_dict = {} + dest_list = [] + afi_list = [] + vrf_list = [] + routes = [] + config["address_families"] = [] + next_hops = {} + interface_list = [ + "Ethernet", + "Loopback", + "Management", + "Port-Channel", + "Tunnel", + "Vlan", + "Vxlan", + "vtep", + ] + conf_list = conf.split("\n") + for conf_elem in conf_list: + matches = re.findall( + r"(ip|ipv6) route ([\d\.\/:]+|vrf) (.+)$", + conf_elem, + ) + if matches: + remainder = matches[0][2].split() + route_update = False + if matches[0][1] == "vrf": + vrf = remainder.pop(0) + # new vrf + if vrf not in vrf_list and vrf_list: + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + address_family_dict.update({"routes": routes}) + config["address_families"].append(address_family_dict) + route_update = True + config.update({"vrf": vrf}) + vrf_list.append(vrf) + dest = remainder.pop(0) + else: + config["vrf"] = None + dest = matches[0][1] + afi = "ipv4" if matches[0][0] == "ip" else "ipv6" + if afi not in afi_list: + if afi_list and not route_update: + # new afi and not the first updating all prev configs + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + address_family_dict.update({"routes": routes}) + config["address_families"].append(address_family_dict) + route_update = True + address_family_dict = {} + address_family_dict.update({"afi": afi}) + routes = [] + afi_list.append(afi) + # To check the format of the dest + prefix = re.search(r"/", dest) + if not prefix: + dest = dest + " " + remainder.pop(0) + if dest not in dest_list: + # For new dest and not the first dest + if dest_list and not route_update: + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + dest_list.append(dest) + next_hops = [] + route_dict = {} + route_dict.update({"dest": dest}) + nexthops = {} + nxthop_addr = re.search(r"[\.\:]", remainder[0]) + if nxthop_addr: + nexthops.update({"interface": remainder.pop(0)}) + if remainder and remainder[0] == "label": + nexthops.update({"mpls_label": remainder.pop(1)}) + remainder.pop(0) + elif re.search(r"Nexthop-Group", remainder[0], re.IGNORECASE): + nexthops.update({"nexthop_grp": remainder.pop(1)}) + remainder.pop(0) + else: + interface = remainder.pop(0) + if interface in interface_list: + interface = interface + " " + remainder.pop(0) + nexthops.update({"interface": interface}) + for attribute in remainder: + forward_addr = re.search( + r"([\dA-Fa-f]+[:\.]+)+[\dA-Fa-f]+", + attribute, + ) + if forward_addr: + nexthops.update( + { + "forward_router_address": remainder.pop( + remainder.index(attribute), + ), + }, + ) + for attribute in remainder: + for params in ["tag", "name", "track"]: + if attribute == params: + keyname = params + if attribute == "name": + keyname = "description" + nexthops.update( + { + keyname: remainder.pop( + remainder.index(attribute) + 1, + ), + }, + ) + remainder.pop(remainder.index(attribute)) + if remainder: + metric = re.search(r"\d+", remainder[0]) + if metric: + nexthops.update({"admin_distance": remainder.pop(0)}) + next_hops.append(nexthops) + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + address_family_dict.update({"routes": routes}) + config["address_families"].append(address_family_dict) + return utils.remove_empties(config) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/vlans/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/vlans/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/vlans/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/vlans/vlans.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/vlans/vlans.py new file mode 100644 index 000000000..876891253 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/facts/vlans/vlans.py @@ -0,0 +1,125 @@ +# -*- 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 eos vlans 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 + +import re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.vlans.vlans import ( + VlansArgs, +) + + +class VlansFacts(object): + """The eos vlans fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = VlansArgs.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 populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for vlans + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = connection.get("show running-config | section ^vlan") + + # split the config into instances of the resource + resource_delim = "vlan" + find_pattern = r"(?:^|\n)%s.*?(?=(?:^|\n)%s|$)" % ( + resource_delim, + resource_delim, + ) + resources = [ + p.strip() for p in re.findall(find_pattern, data, re.DOTALL) + ] + + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.extend(obj) + + ansible_facts["ansible_network_resources"].pop("vlans", None) + facts = {} + if objs: + params = utils.validate_config( + self.argument_spec, + {"config": objs}, + ) + facts["vlans"] = [ + utils.remove_empties(cfg) for cfg in params["config"] + ] + + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def render_config(self, spec, 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 + """ + config = deepcopy(spec) + vlans = [] + + parse_result = utils.parse_conf_arg(conf, "vlan") + if not parse_result.split(" ")[0].isalpha(): + vlan_list = vlan_to_list(parse_result) + for vlan in vlan_list: + config["vlan_id"] = vlan + config["name"] = utils.parse_conf_arg(conf, "name") + config["state"] = utils.parse_conf_arg(conf, "state") + if config["state"] is None: + config["state"] = "active" + vlans.append(utils.remove_empties(config)) + + return vlans + + +def vlan_to_list(vlan_str): + vlans = [] + for vlan in vlan_str.split(","): + if "-" in vlan: + start, stop = vlan.split("-") + vlans.extend(range(int(start), int(stop) + 1)) + else: + vlans.append(int(vlan)) + + return vlans diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/address_family.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/address_family.py new file mode 100644 index 000000000..dcbf45a4a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/address_family.py @@ -0,0 +1,149 @@ +# +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type +import re + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.cli.config.bgp.neighbors import ( + AFNeighbors, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.providers import ( + CliProvider, +) + + +class AddressFamily(CliProvider): + def render(self, config=None): + commands = list() + safe_list = list() + + router_context = "router bgp %s" % self.get_value("config.bgp_as") + context_config = None + + for item in self.get_value("config.address_family"): + context = "address-family %s" % item["afi"] + context_commands = list() + + if config: + context_path = [router_context, context] + context_config = self.get_config_context( + config, + context_path, + indent=2, + ) + + for key, value in iteritems(item): + if value is not None: + meth = getattr(self, "_render_%s" % key, None) + if meth: + resp = meth(item, context_config) + if resp: + context_commands.extend(to_list(resp)) + + if context_commands: + commands.append(context) + commands.extend(context_commands) + commands.append("exit") + + safe_list.append(context) + + if self.params["operation"] == "replace": + if config: + resp = self._negate_config(config, safe_list) + commands.extend(resp) + + return commands + + def _negate_config(self, config, safe_list=None): + commands = list() + matches = re.findall(r"(address-family .+)$", config, re.M) + for item in set(matches).difference(safe_list): + commands.append("no %s" % item) + return commands + + def _render_auto_summary(self, item, config=None): + cmd = "auto-summary" + if item["auto_summary"] is False: + cmd = "no %s" % cmd + if not config or cmd not in config: + return cmd + + def _render_synchronization(self, item, config=None): + cmd = "synchronization" + if item["synchronization"] is False: + cmd = "no %s" % cmd + if not config or cmd not in config: + return cmd + + def _render_networks(self, item, config=None): + commands = list() + safe_list = list() + + for entry in item["networks"]: + network = entry["prefix"] + if entry["masklen"]: + network = "%s/%s" % (entry["prefix"], entry["masklen"]) + safe_list.append(network) + + cmd = "network %s" % network + + if entry["route_map"]: + cmd += " route-map %s" % entry["route_map"] + + if not config or cmd not in config: + commands.append(cmd) + + if self.params["operation"] == "replace": + if config: + matches = re.findall(r"network (\S+)", config, re.M) + for entry in set(matches).difference(safe_list): + commands.append("no network %s" % entry) + + return commands + + def _render_redistribute(self, item, config=None): + commands = list() + safe_list = list() + + for entry in item["redistribute"]: + option = entry["protocol"] + + cmd = "redistribute %s" % entry["protocol"] + + if entry["route_map"]: + cmd += " route-map %s" % entry["route_map"] + + if not config or cmd not in config: + commands.append(cmd) + + safe_list.append(option) + + if self.params["operation"] == "replace": + if config: + matches = re.findall( + r"redistribute (\S+)(?:\s*)(\d*)", + config, + re.M, + ) + for i in range(0, len(matches)): + matches[i] = " ".join(matches[i]).strip() + for entry in set(matches).difference(safe_list): + commands.append("no redistribute %s" % entry) + + return commands + + def _render_neighbors(self, item, config): + """generate bgp neighbor configuration""" + return AFNeighbors(self.params).render( + config, + nbr_list=item["neighbors"], + ) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/neighbors.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/neighbors.py new file mode 100644 index 000000000..760827a93 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/neighbors.py @@ -0,0 +1,194 @@ +# +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type +import re + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.providers import ( + CliProvider, +) + + +class Neighbors(CliProvider): + def render(self, config=None, nbr_list=None): + commands = list() + safe_list = list() + if not nbr_list: + nbr_list = self.get_value("config.neighbors") + + for item in nbr_list: + neighbor_commands = list() + context = "neighbor %s" % item["neighbor"] + cmd = "%s remote-as %s" % (context, item["remote_as"]) + + if not config or cmd not in config: + neighbor_commands.append(cmd) + + for key, value in iteritems(item): + if value is not None: + meth = getattr(self, "_render_%s" % key, None) + if meth: + resp = meth(item, config) + if resp: + neighbor_commands.extend(to_list(resp)) + commands.extend(neighbor_commands) + safe_list.append(context) + + if self.params["operation"] == "replace": + if config and safe_list: + commands.extend(self._negate_config(config, safe_list)) + + return commands + + def _negate_config(self, config, safe_list=None): + commands = list() + matches = re.findall(r"(neighbor \S+)", config, re.M) + for item in set(matches).difference(safe_list): + commands.append("no %s" % item) + return commands + + def _render_description(self, item, config=None): + cmd = "neighbor %s description %s" % ( + item["neighbor"], + item["description"], + ) + if not config or cmd not in config: + return cmd + + def _render_enabled(self, item, config=None): + cmd = "neighbor %s shutdown" % item["neighbor"] + if item["enabled"] is True: + if not config or cmd in config: + cmd = "no %s" % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_update_source(self, item, config=None): + cmd = "neighbor %s update-source %s" % ( + item["neighbor"], + item["update_source"], + ) + if not config or cmd not in config: + return cmd + + def _render_password(self, item, config=None): + cmd = "neighbor %s password %s" % (item["neighbor"], item["password"]) + if not config or cmd not in config: + return cmd + + def _render_ebgp_multihop(self, item, config=None): + cmd = "neighbor %s ebgp-multihop %s" % ( + item["neighbor"], + item["ebgp_multihop"], + ) + if not config or cmd not in config: + return cmd + + def _render_peer_group(self, item, config=None): + cmd = "neighbor %s peer group %s" % ( + item["neighbor"], + item["peer_group"], + ) + if not config or cmd not in config: + return cmd + + def _render_route_reflector_client(self, item, config=None): + cmd = "neighbor %s route-reflector-client" % item["neighbor"] + if item["route_reflector_client"] is False: + if not config or cmd in config: + cmd = "no %s" % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_maximum_prefix(self, item, config=None): + cmd = "neighbor %s maximum-routes %s" % ( + item["neighbor"], + item["maximum_prefix"], + ) + if not config or cmd not in config: + return cmd + + def _render_remove_private_as(self, item, config=None): + cmd = "neighbor %s remove-private-AS" % item["neighbor"] + if item["remove_private_as"] is False: + if not config or cmd in config: + cmd = "no %s" % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_timers(self, item, config): + """generate bgp timer related configuration""" + keepalive = item["timers"]["keepalive"] + holdtime = item["timers"]["holdtime"] + neighbor = item["neighbor"] + + if keepalive and holdtime: + cmd = "neighbor %s timers %s %s" % (neighbor, keepalive, holdtime) + if not config or cmd not in config: + return cmd + + +class AFNeighbors(CliProvider): + def render(self, config=None, nbr_list=None): + commands = list() + if not nbr_list: + return + + for item in nbr_list: + neighbor_commands = list() + for key, value in iteritems(item): + if value is not None: + meth = getattr(self, "_render_%s" % key, None) + if meth: + resp = meth(item, config) + if resp: + neighbor_commands.extend(to_list(resp)) + + commands.extend(neighbor_commands) + + return commands + + def _render_activate(self, item, config=None): + cmd = "neighbor %s activate" % item["neighbor"] + if item["activate"] is False: + if not config or cmd in config: + cmd = "no %s" % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_default_originate(self, item, config=None): + cmd = "neighbor %s default-originate" % item["neighbor"] + if item["default_originate"] is False: + if not config or cmd in config: + cmd = "no %s" % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_graceful_restart(self, item, config=None): + cmd = "neighbor %s graceful-restart" % item["neighbor"] + if item["graceful_restart"] is False: + cmd = "no " + cmd + if config: + config_el = [x.strip() for x in config.split("\n")] + if cmd in config_el: + return + return cmd + + def _render_weight(self, item, config=None): + cmd = "neighbor %s weight %s" % (item["neighbor"], item["weight"]) + if not config or cmd not in config: + return cmd diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/process.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/process.py new file mode 100644 index 000000000..79cecb29c --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/cli/config/bgp/process.py @@ -0,0 +1,186 @@ +# +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type +import re + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.cli.config.bgp.address_family import ( + AddressFamily, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.cli.config.bgp.neighbors import ( + Neighbors, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.providers import ( + CliProvider, + register_provider, +) + + +REDISTRIBUTE_PROTOCOLS = frozenset( + ["ospf", "ospfv3", "rip", "isis", "static", "connected"], +) + + +@register_provider("eos", "eos_bgp") +class Provider(CliProvider): + def render(self, config=None): + commands = list() + + existing_as = None + if config: + match = re.search(r"router bgp (\d+)", config, re.M) + if match: + existing_as = match.group(1) + + operation = self.params["operation"] + + context = None + if self.params["config"]: + context = "router bgp %s" % self.get_value("config.bgp_as") + + if operation == "delete": + if existing_as: + commands.append("no router bgp %s" % existing_as) + elif context: + commands.append("no %s" % context) + + else: + self._validate_input(config) + if operation == "replace": + if existing_as and int(existing_as) != self.get_value( + "config.bgp_as", + ): + commands.append("no router bgp %s" % existing_as) + config = None + + elif operation == "override": + if existing_as: + commands.append("no router bgp %s" % existing_as) + config = None + + context_commands = list() + + for key, value in iteritems(self.get_value("config")): + if value is not None: + meth = getattr(self, "_render_%s" % key, None) + if meth: + resp = meth(config) + if resp: + context_commands.extend(to_list(resp)) + + if context and context_commands: + commands.append(context) + commands.extend(context_commands) + commands.append("exit") + return commands + + def _render_router_id(self, config=None): + cmd = "router-id %s" % self.get_value("config.router_id") + if not config or cmd not in config: + return cmd + + def _render_log_neighbor_changes(self, config=None): + cmd = "bgp log-neighbor-changes" + log_neighbor_changes = self.get_value("config.log_neighbor_changes") + if log_neighbor_changes is True: + if not config or cmd not in config: + return cmd + elif log_neighbor_changes is False: + if config and cmd in config: + return "no %s" % cmd + + def _render_networks(self, config=None): + commands = list() + safe_list = list() + + for entry in self.get_value("config.networks"): + network = entry["prefix"] + if entry["masklen"]: + network = "%s/%s" % (entry["prefix"], entry["masklen"]) + safe_list.append(network) + + cmd = "network %s" % network + + if entry["route_map"]: + cmd += " route-map %s" % entry["route_map"] + + if not config or cmd not in config: + commands.append(cmd) + + if self.params["operation"] == "replace": + if config: + matches = re.findall(r"network (\S+)", config, re.M) + for entry in set(matches).difference(safe_list): + commands.append("no network %s" % entry) + + return commands + + def _render_redistribute(self, config=None): + commands = list() + safe_list = list() + + for entry in self.get_value("config.redistribute"): + option = entry["protocol"] + + cmd = "redistribute %s" % entry["protocol"] + + if entry["route_map"]: + cmd += " route-map %s" % entry["route_map"] + + if not config or cmd not in config: + commands.append(cmd) + + safe_list.append(option) + + if self.params["operation"] == "replace": + if config: + matches = re.findall( + r"redistribute (\S+)(?:\s*)(\d*)", + config, + re.M, + ) + for i in range(0, len(matches)): + matches[i] = " ".join(matches[i]).strip() + for entry in set(matches).difference(safe_list): + commands.append("no redistribute %s" % entry) + + return commands + + def _render_neighbors(self, config): + """generate bgp neighbor configuration""" + return Neighbors(self.params).render(config) + + def _render_address_family(self, config): + """generate address-family configuration""" + return AddressFamily(self.params).render(config) + + def _validate_input(self, config): + def device_has_AF(config): + return re.search(r"address-family (?:.*)", config) + + address_family = self.get_value("config.address_family") + root_networks = self.get_value("config.networks") + operation = self.params["operation"] + + if operation == "replace" and root_networks: + if address_family: + for item in address_family: + if item["networks"]: + raise ValueError( + "operation is replace but provided both root level networks and networks under %s address family" + % item["afi"], + ) + + if config and device_has_AF(config): + raise ValueError( + "operation is replace and device has one or more address family activated but root level network(s) provided", + ) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/module.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/module.py new file mode 100644 index 000000000..12d606b70 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/module.py @@ -0,0 +1,75 @@ +# +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers import ( + providers, +) + + +class NetworkModule(AnsibleModule): + fail_on_missing_provider = True + + def __init__(self, connection=None, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + + if connection is None: + connection = Connection(self._socket_path) + + self.connection = connection + + @property + def provider(self): + if not hasattr(self, "_provider"): + capabilities = self.from_json(self.connection.get_capabilities()) + + network_os = capabilities["device_info"]["network_os"] + network_api = capabilities["network_api"] + + if network_api == "cliconf": + connection_type = "network_cli" + + cls = providers.get( + network_os, + self._name.split(".")[-1], + connection_type, + ) + + if not cls: + msg = ( + "unable to find suitable provider for network os %s" + % network_os + ) + if self.fail_on_missing_provider: + self.fail_json(msg=msg) + else: + self.warn(msg) + + obj = cls(self.params, self.connection, self.check_mode) + + setattr(self, "_provider", obj) + + return getattr(self, "_provider") + + def get_facts(self, subset=None): + try: + self.provider.get_facts(subset) + except Exception as exc: + self.fail_json(msg=to_text(exc)) + + def edit_config(self, config_filter=None): + current_config = self.connection.get_config(flags=config_filter) + try: + commands = self.provider.edit_config(current_config) + changed = bool(commands) + return {"commands": commands, "changed": changed} + except Exception as exc: + self.fail_json(msg=to_text(exc)) diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/providers.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/providers.py new file mode 100644 index 000000000..31adec628 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/providers/providers.py @@ -0,0 +1,127 @@ +# +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type +import json + +from threading import RLock + +from ansible.module_utils.six import itervalues +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) + + +_registered_providers = {} +_provider_lock = RLock() + + +def register_provider(network_os, module_name): + def wrapper(cls): + _provider_lock.acquire() + try: + if network_os not in _registered_providers: + _registered_providers[network_os] = {} + for ct in cls.supported_connections: + if ct not in _registered_providers[network_os]: + _registered_providers[network_os][ct] = {} + for item in to_list(module_name): + for entry in itervalues(_registered_providers[network_os]): + entry[item] = cls + finally: + _provider_lock.release() + return cls + + return wrapper + + +def get(network_os, module_name, connection_type): + network_os_providers = _registered_providers.get(network_os) + if network_os_providers is None: + raise ValueError("unable to find a suitable provider for this module") + if connection_type not in network_os_providers: + raise ValueError("provider does not support this connection type") + elif module_name not in network_os_providers[connection_type]: + raise ValueError("could not find a suitable provider for this module") + return network_os_providers[connection_type][module_name] + + +class ProviderBase(object): + supported_connections = () + + def __init__(self, params, connection=None, check_mode=False): + self.params = params + self.connection = connection + self.check_mode = check_mode + + @property + def capabilities(self): + if not hasattr(self, "_capabilities"): + resp = self.from_json(self.connection.get_capabilities()) + setattr(self, "_capabilities", resp) + return getattr(self, "_capabilities") + + def get_value(self, path): + params = self.params.copy() + for key in path.split("."): + params = params[key] + return params + + def get_facts(self, subset=None): + raise NotImplementedError(self.__class__.__name__) + + def edit_config(self): + raise NotImplementedError(self.__class__.__name__) + + +class CliProvider(ProviderBase): + supported_connections = ("network_cli",) + + @property + def capabilities(self): + if not hasattr(self, "_capabilities"): + resp = self.from_json(self.connection.get_capabilities()) + setattr(self, "_capabilities", resp) + return getattr(self, "_capabilities") + + def get_config_context(self, config, path, indent=2): + if config is not None: + netcfg = NetworkConfig(indent=indent, contents=config) + try: + config = netcfg.get_block_config(to_list(path)) + except ValueError: + config = None + return config + + def render(self, config=None): + raise NotImplementedError(self.__class__.__name__) + + def cli(self, command): + try: + if not hasattr(self, "_command_output"): + setattr(self, "_command_output", {}) + return self._command_output[command] + except KeyError: + out = self.connection.get(command) + try: + out = json.loads(out) + except ValueError: + pass + self._command_output[command] = out + return out + + def get_facts(self, subset=None): + return self.populate() + + def edit_config(self, config=None): + commands = self.render(config) + if commands and self.check_mode is False: + self.connection.edit_config(commands) + return commands diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/bgp_address_family.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/bgp_address_family.py new file mode 100644 index 000000000..555ffd30e --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/bgp_address_family.py @@ -0,0 +1,672 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Bgp_global parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_router_bgp_cmd(config_data): + command = "router bgp {as_number}".format(**config_data) + return command + + +def _tmplt_bgp_address_family(config_data): + command = "" + if config_data.get("vrf"): + command = "vrf {vrf}\n".format(**config_data) + command += "address-family {afi}".format(**config_data) + if config_data.get("safi"): + command += " {safi}".format(**config_data) + return command + + +def _tmplt_bgp_params(config_data): + command = "bgp" + if config_data["bgp_params"].get("additional_paths"): + command += " additional-paths {additional_paths}".format( + **config_data["bgp_params"] + ) + if config_data["bgp_params"]["additional_paths"] == "send": + command += " any" + elif config_data["bgp_params"].get("next_hop_address_family"): + command += " next-hop address-family ipv6" + elif config_data["bgp_params"].get("next_hop_unchanged"): + command += " next-hop-unchanged" + elif config_data["bgp_params"].get("redistribute_internal"): + command += " redistribute-internal" + elif config_data["bgp_params"].get("route"): + command += " route install-map {route}".format( + **config_data["bgp_params"] + ) + return command + + +def _tmplt_bgp_graceful_restart(config_data): + command = "graceful-restart" + return command + + +def _tmplt_bgp_neighbor(config_data): + command = "neighbor {peer}".format(**config_data["neighbor"]) + if config_data["neighbor"].get("additional_paths"): + command += " additional-paths {additional_paths}".format( + **config_data["neighbor"] + ) + if config_data["neighbor"]["additional_paths"] == "send": + command += "any" + elif config_data["neighbor"].get("activate"): + command += " activate" + elif config_data["neighbor"].get("default_originate"): + command += " default-originate" + if config_data["neighbor"]["default_originate"].get("route_map"): + command += " route-map {route_map}".format( + **config_data["neighbor"]["default_originate"] + ) + if config_data["neighbor"]["default_originate"].get("always"): + command += " always" + elif config_data["neighbor"].get("graceful_restart"): + command += " graceful-restart" + elif config_data["neighbor"].get("next_hop_unchanged"): + command += " next-hop-unchanged" + elif config_data["neighbor"].get("next_hop_address_family"): + command += " next-hop addres-family ipv6" + elif config_data["neighbor"].get("prefix_list"): + command += " prefix-list {name} {direction}".format( + **config_data["neighbor"]["prefix_list"] + ) + elif config_data["neighbor"].get("route_map"): + command += " route-map {name} {direction}".format( + **config_data["neighbor"]["route_map"] + ) + elif config_data["neighbor"].get("weight"): + command += " weight {weight}".format(**config_data["neighbor"]) + elif config_data["neighbor"].get("encapsulation"): + command += " encapsulation {transport}".format( + **config_data["neighbor"] + ) + if config_data["neighbor"]["encapsulation"].get("source_interface"): + command += ( + " next-hop-self source-interface {source_interface}".format( + **config_data["neighbor"] + ) + ) + return command + + +def _tmplt_bgp_network(config_data): + command = "network {address}".format(**config_data) + if config_data.get("route_map"): + command += " route-map {route_map}".format(**config_data) + return command + + +def _tmplt_bgp_redistribute(config_data): + command = "redistribute {protocol}".format(**config_data) + if config_data.get("isis_level"): + command += " {isis_level}".format(**config_data) + if config_data.get("ospf_route"): + command += " match {ospf_route}".format(**config_data) + if config_data.get("route_map"): + command += " route-map {route_map}".format(**config_data) + return command + + +def _tmplt_bgp_route_target(config_data): + command = "route-target {action}".format(**config_data["route_target"]) + if config_data["route_target"].get("type"): + command += " {type}".format(**config_data["route_target"]) + if config_data["route_target"].get("route_map"): + command += " {route_map}".format(**config_data["route_target"]) + if config_data["route_target"].get("target"): + command += " {target}".format(**config_data["route_target"]) + return command + + +class Bgp_afTemplate(NetworkTemplate): + def __init__(self, lines=None): + super(Bgp_afTemplate, self).__init__(lines=lines, tmplt=self) + + # fmt: off + PARSERS = [ + { + "name": "router", + "getval": re.compile( + r""" + ^router\s + bgp + \s(?P<as_num>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_router_bgp_cmd, + "compval": "as_number", + "result": {"as_number": "{{ as_num }}"}, + "shared": True, + }, + { + "name": "address_family", + "getval": re.compile( + r""" + \s*(?P<vrf>vrf\s\S+)* + \s*address-family + \s(?P<afi>ipv4|ipv6|evpn) + \s*(?P<type>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_address_family, + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "afi": "{{ afi }}", + "safi": "{{ type }}", + "vrf": "{{ vrf.split(" ")[1] }}", + }, + }, + }, + "shared": True, + }, + { + "name": "bgp_params_additional_paths", + "getval": re.compile( + r""" + \s*bgp + \s+additional-paths + \s+(?P<action>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.additional_paths", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "bgp_params": { + "additional_paths": "{{ action }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.nexthop_address_family", + "getval": re.compile( + r""" + \s*bgp + \s+next-hop + \s+address-family + \s+ipv6 + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.next_hop_address_family", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "bgp_params": { + "next_hop_unchanged": "{{ 'ipv6' }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.nexthop_unchanged", + "getval": re.compile( + r""" + \s*bgp + \s+next-hop-unchanged + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.next_hop_unchanged", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "bgp_params": { + "next_hop_unchanged": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.redistribute_internal", + "getval": re.compile( + r""" + \s*bgp + \s+redistribute-internal + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.redistribute_internal", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "bgp_params": { + "redistribute_internal": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.route", + "getval": re.compile( + r""" + \s*bgp + \s+route + \s+install-map + \s+(?P<route>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.route", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "bgp_params": { + "route": "{{ route }}", + }, + }, + }, + }, + }, + { + "name": "graceful_restart", + "getval": re.compile( + r""" + \s*graceful-restart + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_graceful_restart, + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "graceful_restart": "{{ True }}", + }, + }, + }, + }, + { + "name": "neighbor.activate", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+activate + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.activate", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "activate": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.additional_paths", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+additional-paths + \s+(?P<action>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.additional_paths", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "additional_paths": "{{ action }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.default_originate", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+default-originate + \s*(?P<route_map>route-map\s\S+)* + \s*(?P<always>always)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.default_originate", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "default_originate": { + "route_map": "{{ route_map.split(" ")[1] }}", + "always": "{{ True if always is defined }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.graceful_restart", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+graceful-restart + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.graceful_restart", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "graceful_restart": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.next_hop_unchanged", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+next-hop-unchanged + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.next_hop_unchanged", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "next_hop_unchanged": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.next_hop_address_family", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+next-hop + \s+address-family + \s+ipv6 + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.next_hop_address_family", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "next_hop_address_family": "{{ 'ipv6' }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.prefix_list", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+prefix-list + \s+(?P<name>\S+) + \s+(?P<dir>in|out) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.prefix_list", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "prefix_list": { + "name": "{{ name }}", + "direction": "{{ dir }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.route_map", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+route-map + \s+(?P<name>\S+) + \s+(?P<dir>in|out) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.route_map", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "route_map": { + "name": "{{ name }}", + "direction": "{{ dir }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.weight", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+weight + \s+(?P<weight>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.weight", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "weight": "{{ weight }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.encapsulation", + "getval": re.compile( + r""" + \s*neighbor + \s+default + \s+encapsulation + \s+(?P<type>mpls|vxlan) + \s*(next-hop-self)* + \s*(source-interface)* + \s*(?P<interface>\S+\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.encapsulation", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "encapsulation": { + "transport": "{{ type }}", + "source_interface": "{{ interface }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "network", + "getval": re.compile( + r""" + \s*network + \s+(?P<address>\S+) + \s*(route-map)* + \s*(?P<route_map>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_network, + "compval": "network", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "network": { + "{{ address }}": { + "address": "{{ address }}", + "route_map": "{{ route_map }}", + }, + }, + }, + }, + }, + }, + { + "name": "redistribute", + "getval": re.compile( + r""" + \s*redistribute + \s+(?P<route>\S+) + \s*(?P<level>level-1|level-2|level-1-2)* + \s*(?P<match>match\s\S+)* + \s*(?P<route_map>route-map\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_redistribute, + "compval": "redistribute", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "redistribute": [ + { + "protocol": "{{ route }}", + "route_map": "{{ route_map.split(" ")[1] }}", + "isis_level": "{{ level }}", + "ospf_route": "{{ match.split(" ")[1] }}", + }, + ], + }, + }, + }, + }, + { + "name": "route_target", + "getval": re.compile( + r""" + \s*route-target + \s+(?P<action>\S+) + \s*(?P<type>evpn|vpn-ipv4|vpn-ipv6)* + \s*(?P<map>route-map\s\S+)* + \s+(?P<target>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_route_target, + "compval": "route_target", + "result": { + "address_family": { + '{{ afi + "_" + vrf|d() }}': { + "route_target": { + "action": "{{ action }}", + "type": "{{ type }}", + "route_map": "{{ map.split(" ")[1] }}", + "target": "{{ target }}", + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/bgp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/bgp_global.py new file mode 100644 index 000000000..a1fcee400 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/bgp_global.py @@ -0,0 +1,2965 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Bgp_global parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_router_bgp_cmd(config_data): + command = "router bgp {as_number}".format(**config_data) + return command + + +def _tmplt_bgp_vrf(config_data): + command = "vrf {vrf}".format(**config_data) + return command + + +def _tmplt_bgp_aggregate_address(config_data): + command = "aggregate-address {address}".format(**config_data) + if config_data.get("as_set"): + command += " as-set" + if config_data.get("summary_only"): + command += " summary-only" + if config_data.get("attribute_map"): + command += " attribute-map {attribute_map}".format(**config_data) + if config_data.get("match_map"): + command += " match-map {match_map}".format(**config_data) + if config_data.get("advertise_only"): + command += " advertise-only" + return command + + +def _tmplt_bgp_params(config_data): + command = "bgp" + if config_data["bgp_params"].get("additional_paths"): + command += ( + " additional-paths " + + config_data["bgp_params"]["additional_paths"] + ) + if config_data["bgp_params"]["additional_paths"] == "send": + command += " any" + elif config_data["bgp_params"].get("advertise_inactive"): + command += " advertise-inactive" + elif config_data["bgp_params"].get("allowas_in"): + command += " allowas-in" + if config_data["bgp_params"]["allowas_in"].get("count"): + command += " {count}".format( + **config_data["bgp_params"]["allowas_in"] + ) + elif config_data["bgp_params"].get("always_compare_med"): + command += " always-comapre-med" + elif config_data["bgp_params"].get("asn"): + command += " asn notaion {asn}".format(**config_data["bgp_params"]) + elif config_data["bgp_params"].get("auto_local_addr"): + command += " auto-local-addr" + elif config_data["bgp_params"].get("bestpath"): + if config_data["bgp_params"]["bestpath"].get("as_path"): + command += " bestpath as-path {as_path}".format( + **config_data["bgp_params"]["bestpath"] + ) + elif config_data["bgp_params"]["bestpath"].get("ecmp_fast"): + command += " bestpath ecmp-fast" + elif config_data["bgp_params"]["bestpath"].get("med"): + command += " bestpath med" + if config_data["bgp_params"]["bestpath"]["med"].get("confed"): + command += " confed" + else: + command += " missing-as-worst" + elif config_data["bgp_params"]["bestpath"].get("skip"): + command += " bestpath skip next-hop igp-cost" + elif config_data["bgp_params"]["bestpath"].get("tie_break"): + tie = re.sub( + r"_", + r"-", + config_data["bgp_params"]["bestpath"]["tie_break"], + ) + command += " tie-break " + tie + elif config_data["bgp_params"].get("client_to_client"): + command += " client-to-client" + elif config_data["bgp_params"].get("cluster_id"): + command += " cluster-id {cluster_id}".format( + **config_data["bgp_params"] + ) + elif config_data["bgp_params"].get("confederation"): + command += " confederation" + if config_data["bgp_params"]["confederation"].get("identifier"): + command += " identifier {identifier}".format( + **config_data["bgp_params"]["confederation"] + ) + else: + command += " peers {peers}".format( + **config_data["bgp_params"]["confederation"] + ) + elif config_data["bgp_params"].get("control_plane_filter"): + command += " control-plane-filter default-allow" + elif config_data["bgp_params"].get("convergence"): + command += " convergence" + if config_data["bgp_params"]["convergence"].get("slow_peer"): + command += " slow-peer" + command += " time {time}".format( + **config_data["bgp_params"]["convergence"] + ) + elif config_data["bgp_params"].get("default"): + command += " default {default}".format(**config_data["bgp_params"]) + elif config_data["bgp_params"].get("enforce_first_as"): + command += " enforce-first-as" + elif config_data["bgp_params"].get("host_routes"): + command += " host-routes fib direct-install" + elif config_data["bgp_params"].get("labeled_unicast"): + command += " labeled-unicast rib {labeled_unicast}".format( + **config_data["bgp_params"] + ) + elif config_data["bgp_params"].get("listen"): + # from eos 4.23 , 'bgp listen limit ' is replaced by 'dynamic peer max'. + command = "dynamic peer max " + if config_data["bgp_params"]["listen"].get("limit"): + command += "{limit}".format(**config_data["bgp_params"]["listen"]) + else: + command += " range {address} peer group".format( + **config_data["bgp_params"]["listen"]["range"] + ) + if config_data["bgp_params"]["listen"]["range"]["peer_group"].get( + "peer_filter", + ): + command += " {name} peer-filter {peer_filter}".format( + **config_data["bgp_params"]["listen"]["range"][ + "peer_group" + ] + ) + else: + command += " {name} remote-as {remote_as}".format( + **config_data["bgp_params"]["listen"]["range"][ + "peer_group" + ] + ) + elif config_data["bgp_params"].get("log_neighbor_changes"): + command += " log-neighbor-changes" + elif config_data["bgp_params"].get("missing_policy"): + command += ( + " missing-policy direction {direction} action {action}".format( + **config_data["bgp_params"]["missing_policy"] + ) + ) + elif config_data["bgp_params"].get("monitoring"): + command += " monitoring" + elif config_data["bgp_params"].get("next_hop_unchanged"): + command += " next-hop-unchanged" + elif config_data["bgp_params"].get("redistribute_internal"): + command += " redistribute-internal" + elif config_data["bgp_params"].get("route"): + command += " route install-map {route}".format( + **config_data["bgp_params"] + ) + elif config_data["bgp_params"].get("route_reflector"): + command += " route-reflector preserve-attributes" + if config_data["bgp_params"]["route_reflector"].get("preserve"): + command += " always" + elif config_data["bgp_params"].get("transport"): + command += " transport listen-port {transport}".format( + **config_data["bgp_params"] + ) + return command + + +def _tmplt_bgp_redistribute(config_data): + command = "redistribute {protocol}".format(**config_data) + if config_data.get("isis_level"): + command += " {isis_level}".format(**config_data) + if config_data.get("ospf_route"): + if config_data["ospf_route"] == "nssa_external_2": + route = "nssa-external 2" + elif config_data["ospf_route"] == "nssa_external_1": + route = "nssa-external 1" + else: + route = config_data["ospf_route"] + command += " match " + route + if config_data.get("route_map"): + command += " route-map {route_map}".format(**config_data) + return command + + +def _tmplt_bgp_default_metric(config_data): + command = "default-metric {default_metric}".format(**config_data) + return command + + +def _tmplt_bgp_distance(config_data): + command = "distance bgp" + if config_data["distance"].get("external"): + command += " {external}".format(**config_data["distance"]) + if config_data["distance"].get("internal"): + command += " {internal}".format(**config_data["distance"]) + if config_data["distance"].get("local"): + command += " {local}".format(**config_data["distance"]) + return command + + +def _tmplt_bgp_graceful_restart(config_data): + command = "graceful-restart" + if config_data.get("restart_time"): + command += " restart-time {restart_time}".format(**config_data) + if config_data.get("stalepath_time"): + command += " stalepath-time {stalepath_time}".format(**config_data) + return command + + +def _tmplt_bgp_graceful_restart_helper(config_data): + command = "graceful-restart-helper" + return command + + +def _tmplt_bgp_access_group(config_data): + if config_data.get("afi") == "ipv4": + afi = "ip" + else: + afi = "ipv6" + command = afi + " access-group {acl_name}".format(**config_data) + if config_data.get("direction"): + command += " {direction}".format(**config_data) + return command + + +def _tmplt_bgp_maximum_paths(config_data): + command = "maximum-paths {max_equal_cost_paths}".format( + **config_data["maximum_paths"] + ) + if config_data["maximum_paths"].get("max_installed_ecmp_paths"): + command += " ecmp {max_installed_ecmp_paths}".format( + **config_data["maximum_paths"] + ) + return command + + +def _tmplt_bgp_monitoring(config_data): + cmd = "monitoring" + command = "" + if config_data.get("timestamp"): + command = cmd + " timestamp {timestamp}".format(**config_data) + if config_data.get("port"): + command = cmd + " port {port}".format(**config_data) + if config_data.get("received"): + command = cmd + " received routes {received}".format(**config_data) + if config_data.get("station"): + command = cmd + " station {station}".format(**config_data) + return command + + +def _tmplt_bgp_neighbor(config_data): + command = "neighbor {neighbor_address}".format(**config_data["neighbor"]) + if config_data["neighbor"].get("additional_paths"): + command += " additional-paths {additional_paths}".format( + **config_data["neighbor"] + ) + if config_data["neighbor"]["additional_paths"] == "send": + command += "any" + elif config_data["neighbor"].get("peer_group"): + command += " peer group" + if ( + config_data["neighbor"]["peer_group"] + != config_data["neighbor"]["peer_group"] + ): + command += config_data["neighbor"]["peer_group"] + elif config_data["neighbor"].get("allowas_in"): + command += " allowas-in" + if config_data["neighbor"]["allowas_in"].get("count"): + command += " {count}".format( + **config_data["neighbor"]["allowas_in"] + ) + elif config_data["neighbor"].get("auto_local_addr"): + command += " auto-local-addr" + elif config_data["neighbor"].get("bfd"): + command += " bfd" + if config_data["neighbor"]["bfd"] == "c_bit": + command += " c-bit" + elif config_data["neighbor"].get("default_originate"): + command += " default-originate" + if config_data["neighbor"]["default_originate"].get("route_map"): + command += " route-map {route_map}".format( + **config_data["neighbor"]["default_originate"] + ) + if config_data["neighbor"]["default_originate"].get("always"): + command += " always" + elif config_data["neighbor"].get("description"): + command += " description {description}".format( + **config_data["neighbor"] + ) + elif config_data["neighbor"].get("dont_capability_negotiate"): + command += " dont-capability-negotiate" + elif config_data["neighbor"].get("ebgp_multihop"): + command += " ebgp-multiphop" + if config_data["neighbor"]["ebgp_multihop"].get("ttl"): + command += " {ttl}".format( + **config_data["neighbor"]["ebgp_multihop"] + ) + elif config_data["neighbor"].get("encryption_password"): + command += " password {type} {password}".format( + **config_data["neighbor"]["encryption_password"] + ) + elif config_data["neighbor"].get("enforce_first_as"): + command += " enforce-first-as" + elif config_data["neighbor"].get("export_localpref"): + command += " export-localpref {export_localpref}".format( + **config_data["neighbor"] + ) + elif config_data["neighbor"].get("fall_over"): + command += " fall-over bfd" + elif config_data["neighbor"].get("graceful_restart"): + command += " graceful-restart" + elif config_data["neighbor"].get("graceful_restart_helper"): + command += " graceful-restart-helper" + elif config_data["neighbor"].get("idle_restart_timer"): + command += " idle-restart-timer {idle_restart_timer}".format( + **config_data["neighbor"] + ) + elif config_data["neighbor"].get("import_localpref"): + command += " import-localpref {import_localpref}".format( + **config_data["neighbor"] + ) + elif config_data["neighbor"].get("link_bandwidth"): + command += " link-bandwidth" + if config_data["neighbor"]["link_bandwidth"].get("auto"): + command += " auto" + if config_data["neighbor"]["link_bandwidth"].get("default"): + command += " default {default}".format( + **config_data["neighbor"]["link_bandwidth"] + ) + if config_data["neighbor"]["link_bandwidth"].get("update_delay"): + command += " update-delay {update_delay}".format( + **config_data["neighbor"]["link_bandwidth"] + ) + elif config_data["neighbor"].get("local_as"): + command += " local-as {as_number} no-prepend replace-as".format( + **config_data["neighbor"]["local_as"] + ) + if config_data["neighbor"]["local_as"].get("fallback"): + command += " fallback" + elif config_data["neighbor"].get("local_v6_addr"): + command += " local-v6-addr {local_v6_addr}".format( + **config_data["neighbor"] + ) + elif config_data["neighbor"].get("maximum_accepted_routes"): + command += " maximum-accepted-routes {count}".format( + **config_data["neighbor"]["maximum_accepted_routes"] + ) + if config_data["neighbor"]["maximum_accepted_routes"].get( + "warning_limit", + ): + command += " warning-limit {warning_limit}".format( + **config_data["neighbor"]["maximum_accepted_routes"] + ) + elif config_data["neighbor"].get("maximum_received_routes"): + command += " maximum-routes {count}".format( + **config_data["neighbor"]["maximum_received_routes"] + ) + if config_data["neighbor"]["maximum_received_routes"].get( + "warning_limit", + ): + if config_data["neighbor"]["maximum_received_routes"][ + "warning_limit" + ].get("limit_count"): + command += " warning-limit {limit_count}".format( + **config_data["neighbor"]["maximum_received_routes"][ + "warning_limit" + ] + ) + if config_data["neighbor"]["maximum_received_routes"][ + "warning_limit" + ].get("limit_percent"): + command += ( + " warning-limit " + + str( + config_data["neighbor"]["maximum_received_routes"][ + "warning_limit" + ]["limit_percent"], + ) + + " percent" + ) + if config_data["neighbor"]["maximum_received_routes"].get( + "warning_only", + ): + command += " warning-only" + elif config_data["neighbor"].get("metric_out"): + command += " metric-out {metric_out}".format(**config_data["neighbor"]) + elif config_data["neighbor"].get("monitoring"): + command += " monitoring" + elif config_data["neighbor"].get("next_hop_self"): + command += " next-hop-self" + elif config_data["neighbor"].get("next_hop_unchanged"): + command += " next-hop-unchanged" + elif config_data["neighbor"].get("next_hop_v6_address"): + command += " next-hop-v6-addr {next_hop_v6_address} in".format( + **config_data["neighbor"] + ) + elif config_data["neighbor"].get("out_delay"): + command += " out-delay {out_delay}".format(**config_data["neighbor"]) + elif config_data["neighbor"].get("remote_as"): + command += " remote-as {remote_as}".format(**config_data["neighbor"]) + elif config_data["neighbor"].get("remove_private_as"): + command += " remove-private-as" + if config_data["neighbor"]["remove_private_as"].get("all"): + command += " all" + if config_data["neighbor"]["remove_private_as"].get("replace_as"): + command += " replace-as" + elif config_data["neighbor"].get("peer_as"): + command += " peer-as {peer_as}".format(**config_data["neighbor"]) + elif config_data["neighbor"].get("prefix_list"): + command += " prefix-list {name} {direction}".format( + **config_data["neighbor"]["prefix_list"] + ) + elif config_data["neighbor"].get("route_map"): + command += " route-map {name} {direction}".format( + **config_data["neighbor"]["route_map"] + ) + elif config_data["neighbor"].get("route_reflector_client"): + command += " route-reflector-client" + elif config_data["neighbor"].get("route_to_peer"): + command += " route-to-peer" + elif config_data["neighbor"].get("send_community"): + command += " send-community" + if config_data["neighbor"]["send_community"].get( + "community_attribute", + ): + command += ( + " " + + config_data["neighbor"]["send_community"][ + "community_attribute" + ] + ) + if config_data["neighbor"]["send_community"].get("sub_attribute"): + command += ( + " " + + config_data["neighbor"]["send_community"]["sub_attribute"] + ) + if config_data["neighbor"]["send_community"].get( + "link_bandwidth_attribute", + ): + command += ( + " " + + config_data["neighbor"]["send_community"][ + "link_bandwidth_attribute" + ] + ) + if config_data["neighbor"]["send_community"].get("speed"): + command += " " + config_data["neighbor"]["send_community"]["speed"] + if config_data["neighbor"]["send_community"].get("divide"): + command += ( + " " + config_data["neighbor"]["send_community"]["divide"] + ) + elif config_data["neighbor"].get("shutdown"): + command += " shutdown" + elif config_data["neighbor"].get("soft_reconfiguration"): + command += " soft-reconfiguration inbound" + if config_data["neighbor"]["soft_reconfiguration"] == "all": + command += " all" + elif config_data["neighbor"].get("transport"): + command += " transport" + if config_data["neighbor"]["transport"].get("connection_mode"): + command += " connection-mode passive" + else: + command += " remote-port {remote_port}".format( + **config_data["neighbor"]["transport"] + ) + elif config_data["neighbor"].get("timers"): + command += " timers {keepalive} {holdtime}".format( + **config_data["neighbor"]["timers"] + ) + elif config_data["neighbor"].get("ttl"): + command += " ttl maximum-hops {ttl}".format(**config_data["neighbor"]) + elif config_data["neighbor"].get("update_source"): + command += " update-source {update_source}".format( + **config_data["neighbor"] + ) + elif config_data["neighbor"].get("weight"): + command += " weight {weight}".format(**config_data["neighbor"]) + return command + + +def _tmplt_bgp_network(config_data): + command = "network {address}".format(**config_data) + if config_data.get("route_map"): + command += " route-map {route_map}".format(**config_data) + return command + + +def _tmplt_bgp_route_target(config_data): + command = "route-target {action}".format(**config_data["route_target"]) + if config_data["route_target"].get("type"): + command += " {type}".format(**config_data["route_target"]) + if config_data["route_target"].get("route_map"): + command += " {route_map}".format(**config_data["route_target"]) + if config_data["route_target"].get("imported_route"): + command += " imported-route" + if config_data["route_target"].get("target"): + command += " {target}".format(**config_data["route_target"]) + + return command + + +def _tmplt_bgp_router_id(config_data): + command = "router-id {router_id}".format(**config_data) + return command + + +def _tmplt_bgp_shutdown(config_data): + return "shutdown" + + +def _tmplt_bgp_timers(config_data): + command = "timers bgp {keepalive} {holdtime}".format( + **config_data["timers"] + ) + return command + + +def _tmplt_bgp_ucmp(config_data): + command = "ucmp" + if "fec" in config_data["ucmp"]: + command += " fec threshold trigger" + command += " {trigger} clear {clear} warning-only".format( + **config_data["ucmp"]["fec"] + ) + if "link_bandwidth" in config_data["ucmp"]: + command += " link-bandwidth {mode}".format( + **config_data["ucmp"]["link_bandwidth"] + ) + if config_data["ucmp"]["link_bandwidth"].get("mode") == "update_delay": + command += " {update_delay}".format( + **config_data["ucmp"]["link_bandwidth"] + ) + if "mode" in config_data["ucmp"]: + command += " mode 1" + if config_data["ucmp"]["mode"].get("nexthops"): + command += " {nexthops}".format(**config_data["ucmp"]["mode"]) + return command + + +def _tmplt_bgp_update(config_data): + command = "update {wait_for}".format(**config_data["update"]) + if config_data["update"].get("batch_size"): + command += " {batch_size}".format(**config_data["update"]) + return command + + +def _tmplt_bgp_vlan(config_data): + command = "vlan {vlan}".format(**config_data) + return command + + +def _tmplt_bgp_vlan_aware_bundle(config_data): + command = "vlan-aware-bundle " + config_data["vlan_aware_bundle"] + return command + + +class Bgp_globalTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Bgp_globalTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "router", + "getval": re.compile( + r""" + ^router\s + bgp + \s(?P<as_num>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_router_bgp_cmd, + "compval": "as_number", + "result": {"as_number": "{{ as_num }}"}, + }, + { + "name": "vrf", + "getval": re.compile( + r""" + \s*vrf + \s(?P<vrf>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_vrf, + "compval": "vrfs.vrf", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "vrf": "{{ vrf }}", + }, + }, + }, + "shared": True, + }, + { + "name": "aggregate_address", + "getval": re.compile( + r""" + \s*aggregate-address + \s+(?P<address>\S+) + \s*(?P<as_set>as-set)* + \s*(?P<summary_only>summary-only)* + \s*(?P<attribute_map>attribute-map\s\S+)* + \s*(?P<match_map>match-map\s\S+)* + \s*(?P<advertise_only>advertise-only)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_aggregate_address, + "compval": "aggregate_address", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "aggregate_address": [ + { + "address": "{{ address }}", + "advertise_only": "{{ True if advertise_only is defined }}", + "as_set": "{{ True if as_set is defined }}", + "summary_only": "{{ True if summary_only is defined }}", + "attribute_map": "{{ attribute_map.split(" ")[1] }}", + "match_map": "{{ match_map.split(" ")[1] }}", + }, + ], + }, + }, + }, + }, + { + "name": "bgp_params_additional_paths", + "getval": re.compile( + r""" + \s*bgp + \s+additional-paths + \s+(?P<action>\S+) + \s*(any) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.additional_paths", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "additional_paths": "{{ action }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_advertise_inactive", + "getval": re.compile( + r""" + \s*bgp + \s+advertise-inactive + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.advertise_inactive", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "advertise_inactive": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_allowas_in", + "getval": re.compile( + r""" + \s*bgp + \s+allowas-in + \s*(?P<count>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.allowas_in", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "allowas_in": { + "set": "{{ True if count is undefined }}", + "count": "{{ count }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params_always_compare_med", + "getval": re.compile( + r""" + \s*bgp + \s+always-compare-med + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.always_compare_med", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "always_compare_med": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_asn", + "getval": re.compile( + r""" + \s*bgp + \s+asn + \s+notation + \s+(?P<notation>asdot|asplain) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.asn", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "asn": "{{ notation }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_auto_local_addr", + "getval": re.compile( + r""" + \s*bgp + \s+auto-local-addr + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.auto_local_addr", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "auto_local_addr": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_bestpath_as_path", + "getval": re.compile( + r""" + \s*bgp + \s+bestpath + \s*(?P<as_path>as-path\s\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.bestpath.as_path", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "bestpath": { + "as_path": "{{ as_path.split(" ")[1] }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params_bestpath_ecmp_fast", + "getval": re.compile( + r""" + \s*bgp + \s+bestpath + \s+ecmp-fast + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.bestpath.ecmp_fast", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "bestpath": { + "ecmp_fast": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params_bestpath_med", + "getval": re.compile( + r""" + \s*bgp + \s+bestpath + \s+med + \s*(?P<confed>confed)* + \s*(?P<missing>missing-as-worst)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.bestpath.med", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "med": { + "confed": "{{ True if confed is defined }}", + "missing_as_worst": "{{ True if missing is defined }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params_bestpath_skip", + "getval": re.compile( + r""" + \s*bgp + \s+bestpath + \s+(?P<skip>skip) + \s+next-hop + \s+igp-cost + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.bestpath.skip", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "bestpath": { + "skip": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params_tie_break", + "getval": re.compile( + r""" + \s*bgp + \s+tie-break + \s+(?P<tie>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.bestpath.tie_break", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "tie_break": "{{ tie }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_client_to_client", + "getval": re.compile( + r""" + \s*bgp + \s+client-to-client + \s+reflection + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.client_to_client", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "client_to_client": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_cluster_id", + "getval": re.compile( + r""" + \s*bgp + \s+cluster-id + \s+(?P<address>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.cluster_id", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "cluster_id": "{{ address }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_confederation", + "getval": re.compile( + r""" + \s*bgp + \s+confederation + \s*(?P<identifier>identifier\s.+)* + \s*(?P<peers>peers\s.+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.confederation", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "confederation": { + "identifier": "{{ identifier.split(" ")[1] }}", + "peers": "{{ peers.split(" ")[1] }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params_control_plane_filter", + "getval": re.compile( + r""" + \s*bgp + \s+control-plane-filter + \s+default-allow + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.control_plane_filter", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "control_plane_filter": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params_convergence", + "getval": re.compile( + r""" + \s*bgp + \s+convergence + \s*(?P<slow>slow-peer)* + \s+(?P<time>time\s\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.convergence", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "convergence": { + "slow_peer": "{{ True if slow is defined else False}}", + "time": "{{ time.split(" ")[1] }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params_default", + "getval": re.compile( + r""" + \s*bgp + \s+default + \s(?P<param>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.default", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "default": "{{ param }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.enforce_first_as", + "getval": re.compile( + r""" + \s*bgp + \s+enforce-first-as + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.enforce_first_as", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "enforce_first_as": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.host_routes", + "getval": re.compile( + r""" + \s*bgp + \s+host-routes + \s+fib + \s+direct-install + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.host_routes", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "host_routes": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.labelled_unicast", + "getval": re.compile( + r""" + \s*bgp + \s+labeled-unicast + \s+rib + \s+(?P<rib>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.labelled_unicast", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "labeled_unicast": "{{ rib }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.listen_limit", + "getval": re.compile( + r""" + \s*dynamic\speer\smax + \s+(?P<limit>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.listen.limit", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "listen": { + "limit": "{{ limit }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params.listen_range", + "getval": re.compile( + r""" + \s*bgp + \s+listen + \s+range + \s+(?P<address>\S+) + \s+peer\sgroup + \s+(?P<group>\S+) + \s*(?P<filter>peer-filter \S+)* + \s*(?P<remote_as>remote-as \S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.listen.range", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "listen": { + "range": { + "address": "{{ address }}", + "peer_group": { + "name": "{{ group }}", + "peer_filter": "{{ filter }}", + "remote_as": "{{ remote_as }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params.log_neighbor_changes", + "getval": re.compile( + r""" + \s*bgp + \s+log-neighbor-changes + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.log_neighbor_changes", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "log_neighbor_changes": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.missing_policy", + "getval": re.compile( + r""" + \s*bgp + \s+missing-policy + \s+direction + \s+(?P<dir>in|out) + \s+action + \s+(?P<action>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.missing_policy", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "missing_policy": { + "direction": "{{ dir }}", + "action": "{{ action }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params.monitoring", + "getval": re.compile( + r""" + \s*bgp + \s+monitoring + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.monitoring", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "monitoring": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.nexthop_unchanged", + "getval": re.compile( + r""" + \s*bgp + \s+next-hop-unchanged + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.next_hop_unchanged", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "next_hop_unchanged": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.redistribute_internal", + "getval": re.compile( + r""" + \s*bgp + \s+redistribute-internal + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.redistribute_internal", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "redistribute_internal": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.route", + "getval": re.compile( + r""" + \s*bgp + \s+route + \s+install-map + \s+(?P<route>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.route", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "route": "{{ route }}", + }, + }, + }, + }, + }, + { + "name": "bgp_params.route_reflector", + "getval": re.compile( + r""" + \s*bgp + \s+route-reflector + \s+preserve-attributes + \s*(?P<preserve>always)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.route_reflector", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "route_reflector": { + "set": "{{ True if presever is undefined }}", + "preserve": "{{ True if preserve is defined }}", + }, + }, + }, + }, + }, + }, + { + "name": "bgp_params.transport", + "getval": re.compile( + r""" + \s*bgp + \s+transport + \s+listen-port + \s+(?P<port>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_params, + "compval": "bgp_params.transport", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "bgp_params": { + "transport": "{{ port }}", + }, + }, + }, + }, + }, + { + "name": "default_metric", + "getval": re.compile( + r""" + \s*default-metric + \s(?P<metric>\d+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_default_metric, + "compval": "default_metric", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "default_metric": "{{ metric }}", + }, + }, + }, + }, + { + "name": "redistribute", + "getval": re.compile( + r""" + \s*redistribute + \s+(?P<route>\S+) + \s*(?P<level>level-1|level-2|level-1-2)* + \s*(?P<match>match\s.+)* + \s*(?P<route_map>route-map\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_redistribute, + "compval": "redistribute", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "redistribute": [ + { + "protocol": "{{ route }}", + "route_map": "{{ route_map.split(" ")[1] }}", + "isis_level": "{{ level }}", + "ospf_route": "{{ 'nssa_external_' + match.split(" ")[2] if match.split(" ")[1] == 'nssa-external' else match.split(" ")[1]}}", + }, + ], + }, + }, + }, + }, + { + "name": "distance", + "getval": re.compile( + r""" + \s*distance + \s+bgp + \s(?P<external>\d+) + \s*(?P<internal>\d+)* + \s*(?P<local>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_distance, + "compval": "distance", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "distance": { + "external": "{{ external }}", + "internal": "{{ internal }}", + "local": "{{ local }}", + }, + }, + }, + }, + }, + { + "name": "graceful_restart", + "getval": re.compile( + r""" + \s*graceful-restart + \s*(?P<restart_time>restart-time\s\d+)* + \s*(?P<stalepath_time>stalepath-time\s\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_graceful_restart, + "remval": "graceful-restart", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "graceful_restart": { + "set": "{{ True if restart_time and stalepath_time is not defined }}", + "restart_time": "{{ restart_time.split(" ")[1]|int }}", + "stalepath_time": "{{ stalepath_time.split(" ")[1]|int }}", + }, + }, + }, + }, + }, + { + "name": "graceful_restart_helper", + "getval": re.compile( + r""" + \s*graceful-restart-helper + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_graceful_restart_helper, + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "graceful_restart": { + "set": "{{ True }}", + }, + }, + }, + }, + }, + { + "name": "access_group", + "getval": re.compile( + r""" + \s*(?P<afi>ip|ipv6) + \s+access-group + \s+(?P<acl_name>\S+) + \s*(?P<direction>in)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_access_group, + "compval": "access_group", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "access_group": [ + { + "afi": "{{ ipv4 if afi == 'ip' else afi }}", + "acl_name": "{{ acl_name }}", + "direction": "{{ direction }}", + }, + ], + }, + }, + }, + }, + { + "name": "maximum_paths", + "getval": re.compile( + r""" + \s*maximum-paths + \s+(?P<max_equal_cost_paths>\d+) + \s*(ecmp)* + \s*(?P<max_installed_ecmp_paths>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_maximum_paths, + "compval": "maximum_paths", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "maximum_paths": { + "max_equal_cost_paths": "{{ max_equal_cost_paths }}", + "max_installed_ecmp_paths": "{{ max_installed_ecmp_paths }}", + }, + }, + }, + }, + }, + { + "name": "monitoring", + "getval": re.compile( + r""" + \s*monitoring + \s+(?P<port>\d+) + \s*(?P<received>received\sroutes\s\S+)* + \s*(?P<time>timestamp\s\S+)* + \s*(?P<station>station\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_monitoring, + "compval": "monitoring", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "monitoring": { + "port": "{{ port }}", + "received": "{{ received.split(" ")[2] }}", + "timestamp": "{{ timestamp.split(" ")[1] }}", + "station": "{{ station.split(" ")[1] }}", + }, + }, + }, + }, + }, + { + "name": "neighbor.additional_paths", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+additional-paths + \s+(?P<action>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.additional_paths", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "additional_paths": "{{ action }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.allowas_in", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+allowas-in + \s*(?P<count>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.allowas_in", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "allowas_in": { + "set": "{{ True if count is undefined }}", + "count": "{{ count }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.auto_local_addr", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+auto-local-addr + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.auto_local_addr", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "auto_local_addr": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.bfd", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+bfd + \s*(?P<cbit>c-bit) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.bfd", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "bfd": "{{ 'c_bit' if cbit is defined else 'enable' }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.default_originate", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+default-originate + \s*(?P<route_map>route-map\s\S+)* + \s*(?P<always>always)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.default_originate", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "default_originate": { + "route_map": "{{ route_map.split(" ")[1] }}", + "always": "{{ True if always is defined }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.description", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+description + \s+(?P<desc>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.description", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "description": "{{ desc }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.dont_capability_negotiate", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+dont-capability-negotiate + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.dont_capability_negotiate", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "dont_capability_negotiate": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.ebgp_multihop", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+ebgp-multihop + \s*(?P<ttl>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.ebgp_multihop", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "ebgp_multihop": { + "set": "{{ True if ttl is not defined }}", + "ttl": "{{ ttl }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.encryption_password", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+password + \s*(?P<type>\d+) + \s*(?P<password>\S+) + """, + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.encryption_password", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "peer": "{{ peer }}", + "encryption_password": { + "type": "{{ type }}", + "password": "{{ password }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.enforce_first_as", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+enforce-first-as + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.enforce_first_as", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "enforce_first_as": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.export_localpref", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+export-localpref + \s+(?P<pref>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.export_localpref", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "export_localpref": "{{ pref }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.fall_over", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+fall-over + \s+bfd + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.fall_over", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "fall_over": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.graceful_restart", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+graceful-restart + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "remval": "neighbor {{ peer }} graceful-restart", + "compval": "neighbor.graceful_restart", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "graceful_restart": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.graceful_restart_helper", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+graceful-restart-helper + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.graceful_restart_helper", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "graceful_restart_helper": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.idle_restart_timer", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+idle-restart-timer + \s+(?P<time>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.idle_restart_timer", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "idle_restart_timer": "{{ time }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.import_localpref", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+import-localpref + \s+(?P<pref>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.import_localpref", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "import_localpref": "{{ pref }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.link_bandwidth", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+link-bandwidth + \s*(?P<auto>auto)* + \s*(?P<default>default\s\S+)* + \s*(?P<update>update-delay\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.link_bandwidth", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "link_bandwidth": { + "set": "{{ True }}", + "auto": "{{ True if auto is defined }}", + "default": "{{ default.split(" ")[1] if default is defined }}", + "update_delay": "{{ update.split(" ")[1] if update is defined }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.local_as", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+local-as + \s+(?P<num>\S+) + \s+no-prepend + \s+replace-as + \s*(?P<fallback>fallback) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.local_as", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "local_as": { + "as_number": "{{ num }}", + "fallback": "{{ True if fallback is defined }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.local_v6_addr", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+local-v6-addr + \s+(?P<addr>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.local_v6_addr", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "local_v6_addr": "{{ addr }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.maximum_accepted_routes", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+maximum-accepted-routes + \s+(?P<count>\d+) + \s*warning-limit* + \s*(?P<limit>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.maximum_accepted_routes", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "maximum_accepted_routes": { + "count": "{{ count }}", + "warning_limit": "{{ limit }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.maximum_received_routes", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+maximum-routes + \s+(?P<count>\d+)* + \s*(warning-limit)* + \s*(?P<limit>\d+)* + \s*(?P<percent>percent)* + \s*(?P<warning_only>warning-only)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.maximum_received_routes", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "maximum_received_routes": { + "count": "{{ count }}", + "warning_limit": { + "limit_count": "{{ limit if percent is undefined }}", + "limit_percent": "{{ limit if percent is defined }}", + }, + "warning_only": "{{ True if warning_only is defined }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.metric_out", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+metric-out + \s+(?P<metric>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.metric_out", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "metric_out": "{{ metric }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.monitoring", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+monitoring + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.monitoring", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "monitoring": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.next_hop_self", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+next-hop-self + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.next_hop_self", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "next_hop_self": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.next_hop_unchanged", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+next-hop-unchanged + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.next_hop_unchanged", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "next_hop_unchanged": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.next_hop_v6_addr", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+next-hop-v6-addr + \s+(?P<addr>\S+) + \s+in + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.next_hop_v6_address", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "next_hop_v6_address": "{{ addr }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.out_delay", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+out-delay + \s+(?P<delay>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.out_delay", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "out_delay": "{{ delay }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.remote_as", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+remote-as + \s+(?P<num>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.remote_as", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "remote_as": "{{ num }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.remove_private_as", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+remove-private-as + \s*(?P<all>all)* + \s*(?P<replace>replace-as)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.remove_private_as", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "remove_private_as": { + "set": "{{ True if all is undefined and replace is undefined }}", + "all": "{{ True if all is defined }}", + "replace_as": "{{ True if replace is defined }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.peer_group", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+peer\sgroup + \s*(?P<name>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.peer_group", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "peer_group": "{{ name if name is defined else peer}}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.prefix_list", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+prefix-list + \s+(?P<name>\S+) + \s+(?P<dir>in|out) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.prefix_list", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "prefix_list": { + "name": "{{ name }}", + "direction": "{{ dir }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.route_map", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+route-map + \s+(?P<name>\S+) + \s+(?P<dir>in|out) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.route_map", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "route_map": { + "name": "{{ name }}", + "direction": "{{ dir }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.route_reflector_client", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+route-reflector-client + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.route_reflector_client", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "route_reflector_client": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.route_to_peer", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+route-to-peer + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.route_to_peer", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "route_to_peer": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.send_community", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+send-community + \s*(?P<comm>add|extended|link-bandwidth|remove|standard)* + \s*(?P<attr>extended|link-bandwidth|standard)* + \s*(?P<link>aggregate|divide)* + \s*(?P<div>equal|ratio)* + \s*(?P<speed>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.send_community", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "send_community": { + "set": "{{ True if comm is not defined }}", + "community_attribute": "{{ comm }}", + "sub_attribute": "{{ attr }}", + "link_bandwidth_attribute": "{{ link }}", + "speed": "{{ speed }}", + "divide": "{{ div }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.shutdown", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+shutdown + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.shutdown", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "shutdown": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.soft_reconfiguration", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+soft-reconfiguration + \s+inbound + \s*(?P<all>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.soft_reconfiguration", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "soft_reconfiguration": "{{ 'all' if all is defined else 'None' }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.transport", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+transport + \s+(?P<mode>\S+) + \s*(passive)* + \s*(?P<port>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.transport", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "transport": { + "connection_mode": "{{ mode }}", + "remote_port": "{{ port if port is defined }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.timers", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+timers + \s+(?P<keepalive>\d+) + \s+(?P<hold>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.timers", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "timers": { + "keepalive": "{{ keepalive }}", + "holdtime": "{{ holdtime }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.ttl", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+ttl + \s+maximum-hops + \s+(?P<hop>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.ttl", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "ttl": "{{ hop }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.update_source", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+update-source + \s+(?P<src>.+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.update_source", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "update_source": "{{ src }}", + }, + }, + }, + }, + }, + }, + { + "name": "neighbor.weight", + "getval": re.compile( + r""" + \s*neighbor + \s+(?P<peer>\S+) + \s+weight + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_neighbor, + "compval": "neighbor.weight", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "neighbor": { + "{{ peer }}": { + "neighbor_address": "{{ peer }}", + "weight": "{{ val }}", + }, + }, + }, + }, + }, + }, + { + "name": "network", + "getval": re.compile( + r""" + \s*network + \s+(?P<address>\S+) + \s*(route-map)* + \s*(?P<route_map>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_network, + "compval": "network", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "network": { + "{{ address }}": { + "address": "{{ address }}", + "route_map": "{{ route_map }}", + }, + }, + }, + }, + }, + }, + { + "name": "route_target", + "getval": re.compile( + r""" + \s*route-target + \s+(?P<action>\S+) + \s*(?P<type>evpn|vpn-ipv4|vpn-ipv6)* + \s*(?P<map>route-map\s\S+)* + \s*(?P<imp>imported-route)* + \s*(?P<target>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_route_target, + "compval": "route_target", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "route_target": { + "action": "{{ action }}", + "type": "{{ type }}", + "route_map": "{{ map.split(" ")[1] }}", + "imported_route": "{{ True if imp is defined }}", + "target": "{{ target }}", + }, + }, + }, + }, + }, + { + "name": "router_id", + "getval": re.compile( + r""" + \s*router-id + \s+(?P<id>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_router_id, + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "router_id": "{{ id }}", + }, + }, + }, + }, + { + "name": "shutdown", + "getval": re.compile( + r""" + \s*shutdown + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_shutdown, + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "shutdown": "{{ True }}", + }, + }, + }, + }, + { + "name": "timers", + "getval": re.compile( + r""" + \s*timers + \s+bgp + \s+(?P<keepalive>\d+) + \s+(?P<holdtime>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_timers, + "compval": "timers", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "timers": { + "keepalive": "{{ keepalive }}", + "holdtime": "{{ holdtime }}", + }, + }, + }, + }, + }, + { + "name": "ucmp_fec", + "getval": re.compile( + r""" + \s*ucmp + \s+fec + \s+threshold + \s+(?P<trigger>trigger \d+) + \s+(?P<clear>clear \d+) + \s+warning-only + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_ucmp, + "compval": "ucmp.fec", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "ucmp": { + "fec": { + "trigger": "{{ trigger }}", + "clear": "{{ clear }}", + }, + }, + }, + }, + }, + }, + { + "name": "ucmp_link_bandwidth", + "getval": re.compile( + r""" + \s*ucmp + \s+link-bandwidth + \s*(?P<ucmp_mode>recursive|encoding-weighted|update-delay) + \s*(?P<update_delay>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_ucmp, + "compval": "ucmp.link_bandwidth", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "ucmp": { + "link_bandwidth": { + "mode": "{{ ucmp_mode }}", + "update_delay": "{{ update_delay }}", + }, + }, + }, + }, + }, + }, + { + "name": "ucmp_mode", + "getval": re.compile( + r""" + \s*ucmp + \s+mode + \s+(?P<ucmp_set>\d+) + \s*(?P<nexthop>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_ucmp, + "compval": "ucmp.mode", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "ucmp": { + "mode": { + "set": "{{ True if ucmp_set == '1'}}", + "nexthops": "{{ nexthop }}", + }, + }, + }, + }, + }, + }, + { + "name": "update", + "getval": re.compile( + r""" + \s*update + \s+(?P<wait>\S+) + \s*(?P<size>batch-size\s\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_update, + "compval": "update", + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "update": { + "wait_for": "{{ wait }}", + "batch_size": "{{ size.split(" ")[1] }}", + }, + }, + }, + }, + }, + { + "name": "vlan", + "getval": re.compile( + r""" + \s*vlan + \s+(?P<id>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_vlan, + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "vlan": "{{ id }}", + }, + }, + }, + }, + { + "name": "vlan_aware_bundle", + "getval": re.compile( + r""" + \s*vlan-aware-bundle + \s+(?P<bundle>.+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_bgp_vlan_aware_bundle, + "result": { + "vrfs": { + '{{ "vrf_" + vrf|d() }}': { + "vlan_aware_bundle": "{{ bundle }}", + }, + }, + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/hostname.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/hostname.py new file mode 100644 index 000000000..1be361fe0 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/hostname.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Hostname parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class HostnameTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(HostnameTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "hostname", + "getval": re.compile( + r""" + ^hostname\s(?P<hostname>\S+) + $""", re.VERBOSE, + ), + "setval": "hostname {{ hostname }}", + "result": { + "hostname": "{{ hostname }}", + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/logging_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/logging_global.py new file mode 100644 index 000000000..ea7a6a0ba --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/logging_global.py @@ -0,0 +1,475 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Logging_global parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_logging_format(config_data): + command = "" + if "hostname" in config_data["format"]: + command = ( + "logging format hostname " + config_data["format"]["hostname"] + ) + if "sequence_numbers" in config_data["format"]: + command = "logging format sequence-numbers" + return command + + +def _tmplt_logging_synchronous(config_data): + command = "logging synchronous" + if "level" in config_data["synchronous"]: + command += " level " + config_data["synchronous"]["level"] + return command + + +def _tmplt_logging_trap(config_data): + command = "logging trap" + if "severity" in config_data["trap"]: + command += " " + config_data["trap"]["severity"] + return command + + +def _tmplt_logging_global_hosts(config_data): + el = config_data["hosts"] + command = "logging host " + el["name"] + if el.get("add"): + command += " add" + if el.get("remove"): + command += " remove" + if el.get("port"): + command += " " + str(el["port"]) + if el.get("protocol"): + command += " protocol " + el["protocol"] + return command + + +def _tmplt_logging_global_vrf_hosts(config_data): + el = config_data["vrfs"] + command = "logging vrf " + el["name"] + " host " + el = el["hosts"] + command += el["name"] + if el.get("add"): + command += " add" + if el.get("remove"): + command += " remove" + if el.get("port"): + command += " " + str(el["port"]) + if el.get("protocol"): + command += " protocol " + el["protocol"] + return command + + +def _tmplt_logging_global_format_timestamp(config_data): + command = "" + el = config_data["format"]["timestamp"] + if el.get("traditional"): + command = "logging format timestamp traditional" + if el["traditional"].get("year"): + if el["traditional"]["year"]: + command += " year" + if el["traditional"].get("timezone"): + if el["traditional"]["timezone"]: + command += " timezone" + return command + + +class Logging_globalTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Logging_globalTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "buffered", + "getval": re.compile( + r""" + \s*logging\sbuffered + \s*(?P<size>\d{2,})* + \s*(?P<sev>[0-7]|\w+)* + $""", + re.VERBOSE, + ), + "setval": 'logging buffered {{ buffered.buffer_size if buffered.buffer_size is defined }}' + ' {{ buffered.severity if buffered.severity is defined }}', + "result": { + "buffered": { + "buffer_size": "{{ size }}", + "severity": "{{ sev }}", + }, + }, + }, + { + "name": "console", + "getval": re.compile( + r""" + \s*logging\sconsole + \s*(?P<sev>[0-7]|\w+)* + $""", + re.VERBOSE, + ), + "setval": "logging console {{ console.severity|string if console.severity is defined else ''}}", + "result": { + "console": { + "severity": "{{ sev }}", + }, + }, + }, + { + "name": "event", + "getval": re.compile( + r""" + \s*logging\sevent + \s+(?P<event>link-status|port-channel|spanning-tree) + \s*(member-status)* + \s*global + *$""", + re.VERBOSE, + ), + "setval": "logging event {{ event }} {{ 'member-status' if event == 'port-channel' else '' }} global", + "result": { + "event": "{{ event }}", + }, + }, + { + "name": "facility", + "getval": re.compile( + r""" + \s*logging\sfacility + \s(?P<facility>\S+) + *$""", + re.VERBOSE, + ), + "setval": 'logging facility {{ facility }}', + "result": { + "facility": "{{ facility }}", + }, + }, + { + "name": "format", + "getval": re.compile( + r""" + \s*logging\sformat + \s*(?P<param>hostname\s\S+|sequence-numbers)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_logging_format, + "shared": True, + "result": { + "format": { + "hostname": '{{ param.split(" ")[1] if "hostname" in param }}', + "sequence_numbers": '{{ True if "sequence-numbers" in param }}', + }, + }, + }, + { + "name": "format.timestamp.highresolution", + "getval": re.compile( + r""" + \s*logging\sformat\stimestamp\shigh-resolution + *$""", + re.VERBOSE, + ), + "setval": 'logging format timestamp high-resolution', + "shared": True, + "compval": "format.timestamp.high_resolution", + "result": { + "format": { + "timestamp": { + "high_resolution": "{{ True }}", + }, + }, + }, + }, + { + "name": "format.timestamp.traditional", + "getval": re.compile( + r""" + \s*logging\sformat\stimestamp\straditional + \s*(?P<year>year)* + \s*(?P<zone>timezone)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_logging_global_format_timestamp, + "compval": "format.timestamp.traditional", + "shared": True, + "result": { + "format": { + "timestamp": { + "traditional": { + "year": "{{ True if year is defined}}", + "timezone": "{{ True if zone is defined}}", + "state": "{{ enabled if year and zone is undefined}}", + }, + }, + }, + }, + }, + { + "name": "host", + "getval": re.compile( + r""" + \s*logging\shost + \s*(?P<name>\S+) + \s*(?P<oper>add|remove)* + \s*(?P<port>\d+)* + \s*(protocol)* + \s*(?P<proto>tcp|udp)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_logging_global_hosts, + "compval": "hosts", + "result": { + "hosts": { + "{{ name }}": { + "name": "{{ name }}", + "add": '{{ True if oper == "add" }}', + "remove": '{{ True if oper == "remove" }}', + "port": "{{ port }}", + "protocol": "{{ proto }}", + }, + }, + }, + }, + { + "name": "level", + "getval": re.compile( + r""" + \s*logging\slevel + \s(?P<level>\S+) + \s(?P<sev>\S+) + *$""", + re.VERBOSE, + ), + "setval": 'logging level {{ level.facility }} {{ level.severity }}', + "result": { + "level": { + "facility": "{{ level }}", + "severity": "{{ sev }}", + }, + }, + }, + { + "name": "monitor", + "getval": re.compile( + r""" + \s*logging\smonitor + \s(?P<val>\S+) + *$""", + re.VERBOSE, + ), + "setval": 'logging monitor {{ val }}', + "result": { + "monitor": "{{ val }}", + }, + }, + { + "name": "on", + "getval": re.compile( + r""" + \s*logging\son + *$""", + re.VERBOSE, + ), + "setval": 'logging on', + "compval": 'turn_on', + "result": { + "turn_on": "{{ True }}", + }, + }, + { + "name": "persistent", + "getval": re.compile( + r""" + \s*logging\spersistent + \s*(?P<size>\d+)* + $""", + re.VERBOSE, + ), + "setval": "logging persistent{{ ' ' + persistent.size|string if persistent.size is defined }}", + "result": { + "persistent": { + "size": "{{ size }}", + "set": '{{ True if size is not defined }}', + }, + }, + }, + { + "name": "policy", + "getval": re.compile( + r""" + \s*logging\spolicy\smatch + \s*(?P<inv>inverse-result)* + \s+match-list + \s+(?P<match>\S+) + \s+discard + $""", + re.VERBOSE, + ), + "setval": "logging policy match {{ 'invert-result' if policy.invert_result is defined }} match-list {{ policy.match_list }} discard", + "result": { + "policy": { + "invert_result": "{{ True if inv is defined }}", + "match_list": '{{ match }}', + }, + }, + }, + { + "name": "relogging_interval", + "getval": re.compile( + r""" + \s*logging\srelogging-interval + \s(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": 'logging relogging-interval {{ relogging_interval }}', + "result": { + "relogging_interval": "{{ val }}", + }, + }, + { + "name": "repeat_messages", + "getval": re.compile( + r""" + \s*logging\srepeat-messages + *$""", + re.VERBOSE, + ), + "setval": 'logging repeat-messages', + "result": { + "repeat_messages": "{{ True }}", + }, + }, + { + "name": "src_interface", + "getval": re.compile( + r""" + \s*logging\ssource-interface + \s(?P<val>.+) + *$""", + re.VERBOSE, + ), + "setval": 'logging source-interface {{ source_interface }}', + "result": { + "source_interface": "{{ val }}", + }, + }, + { + "name": "synchronous", + "getval": re.compile( + r""" + \s*logging\ssynchronous + \s*(?P<level>level\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_logging_synchronous, + "result": { + "synchronous": { + "set": "{{ True if level is not defined }}", + "level": '{{ level.split(" ")[1] if level is defined }}', + }, + }, + }, + { + "name": "trap", + "getval": re.compile( + r""" + \s*logging\strap + \s*(?P<level>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_logging_trap, + "result": { + "trap": { + "set": "{{ True if level is not defined }}", + "severity": "{{ level }}", + }, + }, + }, + { + "name": "vrf.host", + "getval": re.compile( + r""" + \s*logging\svrf + \s+(?P<vrf>\S+) + \s+host + \s(?P<name>\S+) + \s*(?P<oper>add|remove)* + \s*(?P<port>\d+)* + \s*(protocol)* + \s*(?P<proto>tcp|udp)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_logging_global_vrf_hosts, + "compval": "vrfs.hosts", + "shared": True, + "result": { + "vrfs": { + "{{ vrf }}": { + "name": "{{ vrf }}", + "hosts": { + "{{ name }}": { + "name": "{{ name }}", + "add": '{{ True if oper == "add" }}', + "remove": '{{ True if oper == "remove" }}', + "port": "{{ port }}", + "protocol": "{{ proto }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrf.source_interface", + "getval": re.compile( + r""" + \s*logging\svrf + \s+(?P<vrf>\S+) + \s+source-interface + \s(?P<val>.+) + *$""", + re.VERBOSE, + ), + "setval": 'logging vrf {{ vrfs.name }} source-interface {{ vrfs.source_interface }}', + "compval": "vrfs.source_interface", + "shared": True, + "result": { + "vrfs": { + "{{ vrf }}": { + "name": "{{ vrf }}", + "source_interface": "{{ val }}", + }, + }, + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ntp_global.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ntp_global.py new file mode 100644 index 000000000..73359df4a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ntp_global.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Ntp_global parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( + normalize_interface, +) + + +def _tmplt_ntp_global_serve(config_data): + el = config_data["serve"] + command = "ntp serve" + if el.get("access_lists"): + command += " {afi} access-group".format(**el["access_lists"]) + if "acls" in el["access_lists"]: + command += " {acl_name} ".format(**el["access_lists"]["acls"]) + if el["access_lists"]["acls"].get("vrf"): + command += " vrf {vrf} ".format(**el["access_lists"]["acls"]) + command += el["access_lists"]["acls"]["direction"] + return command + + +def _tmplt_ntp_global_authenticate(config_data): + el = config_data["authenticate"] + if el.get("enable"): + command = "ntp authenticate" + if el.get("servers"): + command += " servers" + return command + + +def _tmplt_ntp_global_authentication_keys(config_data): + el = config_data["authentication_keys"] + command = "ntp authentication-key " + command += str(el["id"]) + command += " " + el["algorithm"] + if "encryption" in el: + command += " " + str(el["encryption"]) + if "key" in el: + command += " " + el["key"] + return command + + +def _tmplt_ntp_global_servers(config_data): + el = config_data["servers"] + command = "ntp server" + if el.get("vrf"): + command += " vrf {vrf}".format(**el) + if el.get("server"): + command += " {server}".format(**el) + if el.get("burst"): + command += " burst" + if el.get("iburst"): + command += " iburst" + if el.get("key_id"): + command += " key {key_id}".format(**el) + if el.get("local_interface"): + command += " local_interface {local_interface}".format(**el) + if el.get("maxpoll"): + command += " maxpoll {maxpoll}".format(**el) + if el.get("minpoll"): + command += " minpoll {minpoll}".format(**el) + if el.get("prefer"): + command += " prefer" + if el.get("source"): + interface_name = normalize_interface(el["source"]) + command += " source " + interface_name + if el.get("version"): + command += " version {version}".format(**el) + return command + + +class Ntp_globalTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Ntp_globalTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "authenticate", + "getval": re.compile( + r""" + \s*ntp\sauthenticate + \s*(?P<servers>servers)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ntp_global_authenticate, + "result": { + "authenticate": { + "enable": "{{ True if servers is undefined }}", + "servers": "{{ True if servers is defined }}", + }, + }, + }, + { + "name": "authentication_keys", + "getval": re.compile( + r""" + \s*ntp\sauthentication-key + \s+(?P<id>\d+) + \s+(?P<algo>md5|sha1) + \s*(?P<enc>0|7)* + \s+(?P<line>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ntp_global_authentication_keys, + "result": { + "authentication_keys": { + "{{ id }}": { + "id": "{{ id }}", + "algorithm": "{{ algo }}", + "encryption": "{{ enc }}", + "key": "{{ line }}", + }, + }, + }, + }, + { + "name": "local_interface", + "getval": re.compile( + r""" + \s*ntp\slocal-interface + \s+(?P<int>.+) + *$""", + re.VERBOSE, + ), + "setval": 'ntp local-interface {{ local_interface }}', + "result": { + "local_interface": "{{ int }}", + }, + }, + { + "name": "qos_dscp", + "getval": re.compile( + r""" + \s*ntp\sqos\sdscp + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": 'ntp qos dscp {{ qos_dscp }}', + "result": { + "qos_dscp": "{{ val }}", + }, + }, + { + "name": "serve_all", + "getval": re.compile( + r""" + \s*ntp\sserve + \s+(?P<all>all)* + $""", + re.VERBOSE, + ), + "setval": "ntp serve all", + "compval": "serve", + "result": { + "serve": { + "all": "{{ True if all is defined }}", + }, + }, + }, + { + "name": "serve", + "getval": re.compile( + r""" + \s*ntp\sserve + \s*(?P<afi>ip|ipv6)* + \s*(access-group)* + \s*(?P<name>\S+)* + \s*(?P<vrf>vrf\s\S+)* + \s*(?P<dir>in|out)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ntp_global_serve, + "shared": True, + "result": { + "serve": { + "access_lists": { + "{{ afi }}": { + "afi": "{{ afi }}", + "acls": [ + { + "acl_name": "{{ name }}", + "direction": "{{ dir }}", + "vrf": "{{ vrf.split(" ")[1] }}", + }, + ], + }, + }, + }, + }, + }, + { + "name": "servers", + "getval": re.compile( + r""" + \s*ntp\sserver + \s*(?P<vrf>vrf\s\S+)* + \s*(?P<host>\S+)* + \s*(?P<prefer>prefer)* + \s*(?P<burst>burst)* + \s*(?P<iburst>iburst)* + \s*(?P<local_int>local-interface\s.+?)* + \s*(?P<maxpoll>maxpoll\s\d+)* + \s*(?P<minpoll>minpoll\s\d+)* + \s*(?P<source>source\s.+?)* + \s*(?P<version>version\s[1-4])* + \s*(?P<key>key\s.+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ntp_global_servers, + "result": { + "servers": { + "{{ host }}": { + "vrf": "{{ vrf.split(" ")[1] if vrf is defined }}", + "server": "{{ host }}", + "burst": "{{ True if burst is defined }}", + "iburst": "{{ True if iburst is defined }}", + "key_id": "{{ key.split(" ")[1] if key is defined }}", + "local_interface": "{{ local_int.split(" ")[1:] if local_int is defined }}", + "maxpoll": "{{ maxpoll.split(" ")[1] if maxpoll is defined }}", + "minpoll": "{{ minpoll.split(" ")[1] if minpoll is defined }}", + "source": "{{ source.split(" ")[1] if source is defined }}", + "version": "{{ version.split(" ")[1] if version is defined }}", + "prefer": "{{ True if prefer is defined }}", + }, + }, + }, + }, + { + "name": "trusted_key", + "getval": re.compile( + r""" + \s*ntp\strusted-key + \s*(?P<key>.+)* + $""", + re.VERBOSE, + ), + "setval": 'ntp trusted-key {{ trusted_key }}', + "result": { + "trusted_key": "{{ key }}", + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ospf_interfaces.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ospf_interfaces.py new file mode 100644 index 000000000..31ca82bce --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ospf_interfaces.py @@ -0,0 +1,1094 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Ospf_interfaces parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_ospf_int_authentication(config_data): + if "authentication_v2" in config_data: + command = "ip ospf authentication" + if "message_digest" in config_data["authentication_v2"]: + command += " message-digest" + return command + if "authentication_v3" in config_data: + command = "ospfv3 authentication ipsec spi " + command += "{spi} {algorithm}".format( + **config_data["authentication_v3"] + ) + if "passphrase" in config_data["authentication_v3"]: + command += " passphrase" + if "keytype" in config_data["authentication_v3"]: + command += " {keytype}".format(**config_data["authentication_v3"]) + if "passphrase" not in config_data["authentication_v3"]: + command += " {key}".format(**config_data["authentication_v3"]) + else: + command += " {passphrase}".format( + **config_data["authentication_v3"] + ) + return command + + +def _tmplt_ospf_int_encryption_v3(config_data): + if "encryption" in config_data: + command = "ospfv3 encryption ipsec spi ".format(**config_data) + command += "{spi} esp {encryption} {algorithm}".format( + **config_data["encryption"] + ) + if "passphrase" in config_data["encryption"]: + command += " passphrase" + if "keytype" in config_data["encryption"]: + command += " {keytype}".format(**config_data["encryption"]) + if "passphrase" not in config_data["encryption"]: + command += " {key}".format(**config_data["encryption"]) + else: + command += " {passphrase}".format(**config_data["encryption"]) + return command + + +def _tmplt_ospf_int_authentication_key(config_data): + if "authentication_key" in config_data: + command = "ip ospf authentication-key" + if "encryption" in config_data["authentication_key"]: + command += " {encryption} {key}".format( + **config_data["authentication_key"] + ) + else: + command += " {key}".format(**config_data["authentication_key"]) + return command + + +def _tmplt_ospf_int_cost(config_data): + if config_data["afi"] == "ipv4": + command = "ip ospf cost {cost}".format(**config_data) + else: + command = "ospfv3 cost {cost}".format(**config_data) + return command + + +def _tmplt_ospf_int_bfd(config_data): + if config_data["afi"] == "ipv4": + command = "ip ospf bfd" + else: + command = "ospfv3 bfd" + return command + + +def _tmplt_ospf_int_hello_interval(config_data): + if "ip_params" in config_data: + command = "ospfv3 {afi} hello-interval {hello_interval}".format( + **config_data["ip_params"] + ) + else: + if config_data["afi"] == "ipv4": + command = "ip ospf hello-interval {hello_interval}".format( + **config_data + ) + else: + command = "ospfv3 hello-interval {hello_interval}".format( + **config_data + ) + return command + + +def _tmplt_ospf_int_mtu_ignore(config_data): + if "ip_params" in config_data: + command = "ospfv3 {afi} mtu-ignore".format(**config_data["ip_params"]) + else: + if config_data["afi"] == "ipv4": + command = "ip ospf mtu-ignore" + else: + command = "ospfv3 mtu-ignore" + return command + + +def _tmplt_ospf_int_network(config_data): + if "ip_params" in config_data: + command = "ospfv3 {afi} network {network}".format( + **config_data["ip_params"] + ) + else: + if config_data["afi"] == "ipv4": + command = "ip ospf network {network}".format(**config_data) + else: + command = "ospfv3 network {network}".format(**config_data) + return command + + +def _tmplt_ospf_int_priority(config_data): + if "ip_params" in config_data: + command = "ospfv3 {afi} priority {priority}".format( + **config_data["ip_params"] + ) + else: + if config_data["afi"] == "ipv4": + command = "ip ospf priority {priority}".format(**config_data) + else: + command = "ospfv3 priority {priority}".format(**config_data) + return command + + +def _tmplt_ospf_int_retransmit_interval(config_data): + if "ip_params" in config_data: + command = ( + "ospfv3 {afi} retransmit-interval {retransmit_interval}".format( + **config_data["ip_params"] + ) + ) + else: + if config_data["afi"] == "ipv4": + command = ( + "ip ospf retransmit-interval {retransmit_interval}".format( + **config_data + ) + ) + else: + command = ( + "ospfv3 retransmit-interval {retransmit_interval}".format( + **config_data + ) + ) + return command + + +def _tmplt_ospf_int_transmit_delay(config_data): + if "ip_params" in config_data: + command = "ospfv3 {afi} transmit-delay {transmit_delay}".format( + **config_data["ip_params"] + ) + else: + if config_data["afi"] == "ipv4": + command = "ip ospf transmit-delay {transmit_delay}".format( + **config_data + ) + else: + command = "ospfv3 transmit-delay {transmit_delay}".format( + **config_data + ) + return command + + +def _tmplt_ospf_int_dead_interval(config_data): + if "ip_params" in config_data: + command = "ospfv3 {afi} dead-interval {dead_interval}".format( + **config_data["ip_params"] + ) + else: + if config_data["afi"] == "ipv4": + command = "ip ospf dead-interval {dead_interval}".format( + **config_data + ) + else: + command = "ospfv3 dead-interval {dead_interval}".format( + **config_data + ) + return command + + +class Ospf_interfacesTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Ospf_interfacesTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + PARSERS = [ + { + "name": "interfaces", + "getval": re.compile( + r""" + ^interface + \s+(?P<name>\S+) + $""", + re.VERBOSE, + ), + "setval": "interface {{ name }}", + "result": {"name": "{{ name }}"}, + "shared": True, + }, + { + "name": "area", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+area + \s+(?P<area_id>\S+)* + $""", + re.VERBOSE, + ), + "setval": "ip ospf area {{ area.area_id }}", + "compval": "area", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "area": {"area_id": "{{ area_id }}"}, + }, + }, + }, + }, + { + "name": "authentication_v2", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+authentication + \s*(?P<message_digest>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_authentication, + "compval": "authentication_v2", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "authentication_v2": { + "set": "{{ True if message_digest is undefined }}", + "message_digest": "{{ True if message_digest is defined }}", + }, + }, + }, + }, + }, + { + "name": "authentication_v3", + "getval": re.compile( + r""" + \s*ospfv3 + \s+authentication + \s+ipsec + \s+spi + \s+(?P<val>\d+) + \s+(?P<algorithm>md5|sha1) + \s*(?P<passphrase>passphrase)* + \s*(?P<type>0|7)* + \s*(?P<line>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_authentication, + "compval": "authentication_v3", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "authentication_v3": { + "spi": "{{ val }}", + "algorithm": "{{ algorithm }}", + "keytype": "{{ type }}", + "passphrase": "{{ line if passphrase is defined }}", + "key": "{{ str(line) if passphrase is undefined }}", + }, + }, + }, + }, + }, + { + "name": "authentication_key", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+authentication-key + \s*(?P<encryption>\d+)* + \s*(?P<line>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_authentication_key, + "compval": "authentication_key", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "authentication_key": { + "encryption": "{{ encryption }}", + "key": "{{ line }}", + }, + }, + }, + }, + }, + { + "name": "bfd", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+bfd + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_bfd, + "compval": "bfd", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "bfd": "{{ True }}", + }, + }, + }, + }, + { + "name": "deadinterval", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+dead-interval + \s+(?P<interval>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_dead_interval, + "compval": "dead_interval", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "dead_interval": "{{ interval }}", + }, + }, + }, + }, + { + "name": "encryption", + "getval": re.compile( + r""" + \s*ospfv3 + \s+encryption + \s+ipsec + \s+spi + \s+(?P<val>\d+) + \s+esp + \s+(?P<encryption>\S+) + \s*(?P<algorithm>md5|sha1) + \s*(?P<passphrase>passphrase) + \s*(?P<type>0|7) + \s*(?P<line>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_encryption_v3, + "compval": "encryption_v3", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "encryption_v3": { + "spi": "{{ val }}", + "encryption": "{{ encryption }}", + "algorithm": "{{ algorithm }}", + "keytype": "{{ type }}", + "passphrase": "{{ line if passphrase is defined }}", + "key": "{{ str(line) if passphrase is undefined }}", + }, + }, + }, + }, + }, + { + "name": "hellointerval", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+hello-interval + \s+(?P<interval>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_hello_interval, + "compval": "hello_interval", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "hello_interval": "{{ interval }}", + }, + }, + }, + }, + { + "name": "bfd", + "getval": re.compile( + r""" + \s+ospfv3 + \s+bfd + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_bfd, + "compval": "bfd", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "bfd": "{{ True }}", + }, + }, + }, + }, + { + "name": "cost", + "getval": re.compile( + r""" + \s+ip + \s+ospf + \s+cost + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_cost, + "compval": "cost", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "cost": "{{ val }}", + }, + }, + }, + }, + { + "name": "cost", + "getval": re.compile( + r""" + \s+ospfv3 + \s+cost + \s+(?P<cost>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_cost, + "compval": "cost", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "cost": "{{ cost }}", + }, + }, + }, + }, + { + "name": "deadinterval", + "getval": re.compile( + r""" + \s+ospfv3 + \s+dead-interval + \s+(?P<interval>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_dead_interval, + "compval": "dead_interval", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "dead_interval": "{{ interval }}", + }, + }, + }, + }, + { + "name": "hellointerval", + "getval": re.compile( + r""" + \s+ospfv3 + \s+hello-interval + \s+(?P<interval>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_hello_interval, + "compval": "hello_interval", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "hello_interval": "{{ interval }}", + }, + }, + }, + }, + { + "name": "ip_params_area", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+area + \s+(?P<area_id>\S+) + *$""", + re.VERBOSE, + ), + "setval": "ospfv3 {{ ip_params.afi }} area {{ ip_params.area.area_id }}", + "compval": "ip_params.area", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "area": {"area_id": "{{ area_id }}"}, + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_bfd", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+bfd + *$""", + re.VERBOSE, + ), + "setval": "ospfv3 {{ afi }} bfd", + "compval": "ip_params.bfd", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "bfd": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_cost", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+cost + \s+(?P<cost>\d+) + *$""", + re.VERBOSE, + ), + "setval": "ospfv3 {{ afi }} cost {{ cost }}", + "compval": "ip_params.cost", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "cost": "{{ cost }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_dead_interval", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+dead-interval + \s+(?P<interval>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_dead_interval, + "compval": "ip_params.dead_interval", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "dead_interval": "{{ interval }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_hello_interval", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+hello-interval + \s+(?P<interval>\d+) + *$""", + re.VERBOSE, + ), + "setval": "ospfv3 {{ ip_params.afi }} hello-interval {{ ip_params.hello_interval }}", + "compval": "ip_params.hello_interval", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "hello_interval": "{{ interval }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_mtu_ignore", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+(?P<mtu>mtu-ignore) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_mtu_ignore, + "compval": "ip_params.mtu_ignore", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "mtu_ignore": "{{ True if mtu is defined}}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_network", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+network + \s+(?P<val>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_network, + "compval": "ip_params.network", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "network": "{{ val }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_priority", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+priority + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_priority, + "compval": "ip_params.priority", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "priority": "{{ val }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_passive_interface", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+passive-interface + *$""", + re.VERBOSE, + ), + "setval": "ospfv3 {{ ip_params.afi }} passive-interface", + "compval": "ip_params.passive_interface", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "passive_interface": "{{ True }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_retransmit_interval", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+retransmit-interval + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_retransmit_interval, + "compval": "ip_params.retransmit_interval", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "retransmit_interval": "{{ val }}", + }, + }, + }, + }, + }, + }, + { + "name": "ip_params_transmit_delay", + "getval": re.compile( + r""" + \s*ospfv3 + \s+(?P<afi>ipv4|ipv6) + \s+transmit-delay + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_transmit_delay, + "compval": "ip_params.transmit_delay", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "ip_params": { + "{{ afi }}": { + "afi": "{{ afi }}", + "transmit_delay": "{{ val }}", + }, + }, + }, + }, + }, + }, + { + "name": "mtu_ignore", + "getval": re.compile( + r""" + \s*ospfv3 + \s+mtu-ignore + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_mtu_ignore, + "compval": "mtu_ignore", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "mtu_ignore": "{{ True }}", + }, + }, + }, + }, + { + "name": "network", + "getval": re.compile( + r""" + \s*ospfv3 + \s+network + \s+(?P<interface>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_network, + "compval": "network", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "network": "{{ interface }}", + }, + }, + }, + }, + { + "name": "priority", + "getval": re.compile( + r""" + \s*ospfv3 + \s+priority + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_priority, + "compval": "priority", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "priority": "{{ val }}", + }, + }, + }, + }, + { + "name": "passive_interface", + "getval": re.compile( + r""" + \s*ospfv3 + \s+passive-interface + *$""", + re.VERBOSE, + ), + "setval": "ospfv3 passive-interface", + "compval": "passive_interface", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "passive_interface": "{{ True }}", + }, + }, + }, + }, + { + "name": "retransmit_interval", + "getval": re.compile( + r""" + \s*ospfv3 + \s+retransmit-interval + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_retransmit_interval, + "compval": "retransmit_interval", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "retransmit_interval": "{{ val }}", + }, + }, + }, + }, + { + "name": "transmit_delay", + "getval": re.compile( + r""" + \s*ospfv3 + \s+transmit-delay + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_transmit_delay, + "compval": "transmit_delay", + "result": { + "address_family": { + "{{ 'ipv6' }}": { + "afi": '{{ "ipv6" }}', + "transmit_delay": "{{ val }}", + }, + }, + }, + }, + { + "name": "mtu_ignore", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+mtu-ignore + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_mtu_ignore, + "compval": "mtu_ignore", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "mtu_ignore": "{{ True }}", + }, + }, + }, + }, + { + "name": "network", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+network + \s+(?P<interface>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_network, + "compval": "network", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "network": "{{ interface }}", + }, + }, + }, + }, + { + "name": "priority", + "getval": re.compile( + r""" + \s*ip ospf + \s+priority + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_priority, + "compval": "priority", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "priority": "{{ val }}", + }, + }, + }, + }, + { + "name": "retransmit_interval", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+retransmit-interval + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_retransmit_interval, + "compval": "retransmit_interval", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "retransmit_interval": "{{ val }}", + }, + }, + }, + }, + { + "name": "transmit_delay", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+transmit-delay + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_int_transmit_delay, + "compval": "transmit_delay", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "transmit_delay": "{{ val }}", + }, + }, + }, + }, + { + "name": "message_digest_key", + "getval": re.compile( + r""" + \s*ip + \s+ospf + \s+message-digest-key + \s+(?P<id>\d+) + \s+md5 + \s*(?P<type>0|7)* + \s+(?P<line>\S+) + *$""", + re.VERBOSE, + ), + "setval": "ip ospf message-digest-key {{ message_digest_key.key_id }} md5 {{ message_digest_key.encryption }} {{ message_digest_key.key }}", + "compval": "message_digest_key", + "result": { + "address_family": { + "{{ 'ipv4' }}": { + "afi": '{{ "ipv4" }}', + "message_digest_key": { + "key_id": "{{ id }}", + "encryption": "{{ type }}", + "key": "{{ line }}", + }, + }, + }, + }, + }, + ] diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ospfv3.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ospfv3.py new file mode 100644 index 000000000..67e0b7787 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/ospfv3.py @@ -0,0 +1,1091 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Ospfv3 parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_ospf_vrf_cmd(process): + command = "router ospfv3" + vrf = "{vrf}".format(**process) + if "vrf" in process and vrf != "default": + command += " vrf " + vrf + return command + + +def _tmplt_ospf_address_family_cmd(config_data): + afi = "{afi}".format(**config_data) + if afi == "router": + command = "" + else: + command = "address-family " + afi + return command + + +def _tmplt_ospf_adjacency_cmd(config_data): + command = "adjacency exchange-start threshold" + if "adjacency" in config_data: + command += " {threshold}".format( + **config_data["adjacency"]["exchange_start"] + ) + return command + + +def _tmplt_ospf_auto_cost(config_data): + if "auto_cost" in config_data: + command = "auto-cost" + if "reference_bandwidth" in config_data["auto_cost"]: + command += " reference-bandwidth {reference_bandwidth}".format( + **config_data["auto_cost"] + ) + return command + + +def _tmplt_ospf_area_authentication(config_data): + if "area_id" in config_data: + command = "area {area_id} authentication ipsec spi ".format( + **config_data + ) + command += "{spi} {algorithm}".format(**config_data["authentication"]) + if "passphrase" in config_data["authentication"]: + command += " passphrase" + if ( + "encrypt_key" in config_data["authentication"] + and config_data["authentication"]["encrypt_key"] is False + ): + command += " 0" + if ( + "hidden_key" in config_data["authentication"] + and config_data["authentication"]["hidden_key"] is True + ): + command += " 7" + if "passphrase" not in config_data["authentication"]: + command += " {key}".format(**config_data["authentication"]) + else: + command += " {passphrase}".format(**config_data["authentication"]) + return command + + +def _tmplt_ospf_area_encryption(config_data): + if "area_id" in config_data: + command = "area {area_id} encryption ipsec spi ".format(**config_data) + command += "{spi} esp {encryption} {algorithm}".format( + **config_data["encryption"] + ) + if "passphrase" in config_data["encryption"]: + command += " passphrase" + if ( + "encrypt_key" in config_data["encryption"] + and config_data["encryption"]["encrypt_key"] is False + ): + command += " 0" + if ( + "hidden_key" in config_data["encryption"] + and config_data["encryption"]["hidden_key"] is True + ): + command += " 7" + if "passphrase" not in config_data["encryption"]: + command += " {key}".format(**config_data["encryption"]) + else: + command += " {passphrase}".format(**config_data["encryption"]) + return command + + +def _tmplt_ospf_area_nssa(config_data): + if "nssa" in config_data: + command = "area {area_id} nssa".format(**config_data) + if "default_information_originate" in config_data["nssa"]: + command += " default-information-originate" + if ( + "metric" + in config_data["nssa"]["default_information_originate"] + ): + command += " metric {metric}".format( + **config_data["nssa"]["default_information_originate"] + ) + if ( + "metric_type" + in config_data["nssa"]["default_information_originate"] + ): + command += " metric-type {metric_type}".format( + **config_data["nssa"]["default_information_originate"] + ) + if ( + "nssa_only" + in config_data["nssa"]["default_information_originate"] + ): + command += " nssa-only" + if config_data["nssa"].get("nssa_only"): + command += " nssa-only" + if config_data["nssa"].get("translate"): + command += " translate type7 always" + if config_data["nssa"].get("no_summary"): + command += " no-summary" + return command + + +def _tmplt_ospf_area_range(config_data): + if "area_id" in config_data: + command = "area {area_id} range".format(**config_data) + if "address" in config_data: + command += " {address}".format(**config_data) + if "subnet_address" in config_data: + command += " {subnet_address}".format(**config_data) + if "subnet_mask" in config_data: + command += " {subnet_mask}".format(**config_data) + if "advertise" in config_data: + if config_data.get("advertise"): + command += " advertise" + else: + command += " not-advertise" + if "cost" in config_data: + command += " cost {cost}".format(**config_data) + return command + + +def _tmplt_ospf_area_stub(config_data): + if "stub" in config_data: + command = "area {area_id} stub".format(**config_data) + if "summary_lsa" in config_data["stub"]: + if not config_data["stub"]["summary_lsa"]: + command += " no-summary" + return command + + +def _tmplt_ospf_default_information(config_data): + if "default_information" in config_data: + command = "default-information" + if "originate" in config_data["default_information"]: + command += " originate" + if "always" in config_data["default_information"]: + command += " always" + if "metric" in config_data["default_information"]: + command += " metric {metric}".format( + **config_data["default_information"] + ) + if "metric_type" in config_data["default_information"]: + command += " metric-type {metric_type}".format( + **config_data["default_information"] + ) + if "route_map" in config_data["default_information"]: + command += " route-map {route_map}".format( + **config_data["default_information"] + ) + return command + + +def _tmplt_ospf_log_adjacency_changes(config_data): + if "log_adjacency_changes" in config_data: + command = "log-adjacency-changes" + if "detail" in config_data["log_adjacency_changes"]: + if config_data["log_adjacency_changes"].get("detail"): + command += " detail" + return command + + +def _tmplt_ospf_max_metric(config_data): + if "max_metric" in config_data: + command = "max-metric" + if "router_lsa" in config_data["max_metric"]: + command += " router-lsa" + if "external_lsa" in config_data["max_metric"]["router_lsa"]: + command += " external-lsa" + if ( + "max_metric_value" + in config_data["max_metric"]["router_lsa"]["external_lsa"] + ): + command += " {max_metric_value}".format( + **config_data["max_metric"]["router_lsa"]["external_lsa"] + ) + if "include_stub" in config_data["max_metric"]["router_lsa"]: + if config_data["max_metric"]["router_lsa"].get("include_stub"): + command += " include-stub" + if "on_startup" in config_data["max_metric"]["router_lsa"]: + command += " on-startup {wait_period}".format( + **config_data["max_metric"]["router_lsa"]["on_startup"] + ) + if "summary_lsa" in config_data["max_metric"]["router_lsa"]: + command += " summary-lsa" + if ( + "max_metric_value" + in config_data["max_metric"]["router_lsa"]["summary_lsa"] + ): + command += " {max_metric_value}".format( + **config_data["max_metric"]["router_lsa"]["summary_lsa"] + ) + return command + + +def _tmplt_ospf_redistribute(config_data): + command = "redistribute {routes}".format(**config_data) + if "route_map" in config_data: + command += " route-map {route_map}".format(**config_data) + return command + + +def _tmplt_ospf_timers_lsa(config_data): + command = "" + if "lsa" in config_data["timers"]: + command += "timers lsa {direction}".format( + **config_data["timers"]["lsa"] + ) + if config_data["timers"]["lsa"]["direction"] == "rx": + command += " min interval " + else: + command += " delay initial " + if config_data["timers"]["lsa"].get("initial"): + command += str(config_data["timers"]["lsa"]["initial"]) + if config_data["timers"]["lsa"].get("min"): + command += str(config_data["timers"]["lsa"]["min"]) + if config_data["timers"]["lsa"].get("max"): + command += str(config_data["timers"]["lsa"]["max"]) + return command + + +def _tmplt_ospf_timers_spf(config_data): + command = "" + if "spf" in config_data["timers"]: + command += "timers spf delay initial " + command += "{initial} {min} {max}".format( + **config_data["timers"]["spf"] + ) + return command + + +def _tmplt_ospf_bfd(config_data): + if os_version < "4.23": + command = "bfd all-interfaces" + else: + command = "bfd default" + return command + + +os_version = "4.23" + + +class Ospfv3Template(NetworkTemplate): + def __init__(self, lines=None, module=None): + global os_version + super(Ospfv3Template, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + if self._connection: + os_version = self._get_os_version() + + def _get_os_version(self): + os_version = self._connection.get_device_info()["network_os_version"] + return os_version + + PARSERS = [ + { + "name": "vrf", + "getval": re.compile( + r""" + ^router\s + ospfv3 + \svrf + \s(?P<vrf>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_vrf_cmd, + "result": {"processes": {"vrf": "{{ vrf }}"}}, + "shared": True, + }, + { + "name": "vrf", + "getval": re.compile( + r""" + ^router\s + ospfv3 + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_vrf_cmd, + "result": {"processes": {"vrf": '{{ "default" }}'}}, + "shared": True, + }, + { + "name": "address_family", + "getval": re.compile( + r""" + \s*address-family + \s(?P<afi>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_address_family_cmd, + "compval": "address_family", + "result": { + "processes": { + "address_family": {"{{ afi }}": {"afi": "{{ afi }}"}}, + }, + }, + "shared": True, + }, + { + "name": "adjacency", + "getval": re.compile( + r""" + \s*adjacency + \s+exchange-start + \s+threshold + \s+(?P<threshold>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_adjacency_cmd, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "adjacency": { + "exchange_start": { + "threshold": "{{ threshold|int }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "auto_cost", + "getval": re.compile( + r"""\s+(?P<auto_cost>auto-cost)* + \s*(?P<ref_band>reference-bandwidth\s\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_auto_cost, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "auto_cost": { + "reference_bandwidth": '{{ ref_band.split(" ")[1] }}', + }, + }, + }, + }, + }, + }, + { + "name": "area.default_cost", + "getval": re.compile( + r"""\s+area + \s(?P<area_id>\S+)* + \sdefault-cost* + \s*(?P<default_cost>\S+) + *$""", + re.VERBOSE, + ), + "setval": "area {{ area_id }} default-cost {{ default_cost }}", + "compval": "default_cost", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "areas": { + "{{ area_id }}": { + "area_id": "{{ area_id }}", + "default_cost": "{{ default_cost|int }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "area.authentication", + "getval": re.compile( + r""" + \s*area + \s+(?P<area_id>\S+) + \s+authentication + \s+ipsec + \s+spi + \s+(?P<val>\d+) + \s+(?P<algorithm>md5|sha1) + \s*(?P<passphrase>passphrase)* + \s*(?P<type>0|7)* + \s*(?P<line>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_area_authentication, + "compval": "authentication", + "remval": "area {{ area_id }} authentication", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "areas": { + "{{ area_id }}": { + "area_id": "{{ area_id }}", + "authentication": { + "spi": "{{ val }}", + "algorithm": "{{ algorithm }}", + "encrypt_key": '{{ False if type is defined and type == "0" }}', + "hidden_key": '{{ True if type is defined and type == "7" }}', + "passphrase": "{{ line if passphrase is defined }}", + "key": "{{ str(line) if passphrase is undefined }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "area.encryption", + "getval": re.compile( + r""" + \s*area + \s+(?P<area_id>\S+)* + \s+encryption + \s+ipsec + \s+spi + \s+(?P<val>\d+) + \s+esp + \s*(?P<encryption>\S+) + \s*(?P<algorithm>md5|sha1)* + \s*(?P<passphrase>passphrase)* + \s*(?P<type>0|7)* + \s*(?P<line>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_area_encryption, + "compval": "encryption", + "remval": "area {{ area_id }} encryption", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "areas": { + "{{ area_id }}": { + "area_id": "{{ area_id }}", + "encryption": { + "spi": "{{ val }}", + "encryption": "{{ encryption }}", + "algorithm": "{{ algorithm }}", + "encrypt_key": "{{ False if type is defined and type == '0'}}", + "passphrase": "{{line if passphrase is defined }}", + "hidden_key": "{{ True if type is defined and type == '7'}}", + "key": "{{ line if passphrase is not defined }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "area.nssa", + "getval": re.compile( + r""" + \s+area\s(?P<area_id>\S+) + \s(?P<nssa>nssa) + \s*(?P<def_origin>default-information-originate)* + \s*(metric)* + \s*(?P<metric>\d+)* + \s*(metric-type)* + \s*(?P<metric_type>\d+)* + \s*(?P<no_summary>no-summary)* + \s*(?P<translate>translate.*)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_area_nssa, + "compval": "nssa", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "areas": { + "{{ area_id }}": { + "area_id": "{{ area_id }}", + "nssa": { + "set": "{{ True if nssa is defined and def_origin is undefined and " + "no_summary is undefined and translate is undefined }}", + "default_information_originate": { + "set": "{{ True if def_origin is defined and metric is undefined and " + "metric_type is undefined and nssa_only is undefined }}", + "metric": "{{ metric.split(" + ")[1]|int }}", + "metric_type": "{{ metric_type.split(" + ")[1]|int }}", + "nssa_only": "{{ True if nssa_only is defined }}", + }, + "translate": "{{ True if translate is defined }}", + "no_summary": "{{ True if no_summary is defined }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "area.ranges", + "getval": re.compile( + r""" + \s*area + \s+(?P<area_id>\S+)* + \s+range + \s+(?P<address>\S+)* + \s*(?P<not_advertise>not-advertise)* + \s*(?P<subnet_mask>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})* + \s*(?P<subnet_address>\S+)* + \s*(?P<cost>cost)* + \s*(?P<cost_val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_area_range, + "compval": "ranges", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "areas": { + "{{ area_id }}": { + "area_id": "{{ area_id }}", + "ranges": [ + { + "address": "{{ address }}", + "subnet_mask": "{{ subnet_mask }}", + "advertise": "{{ not not_advertise }}", + "cost": "{{ cost_val }}", + }, + ], + }, + }, + }, + }, + }, + }, + }, + { + "name": "area.stub", + "getval": re.compile( + r"""\s+area\s(?P<area_id>\S+) + \s(?P<stub>stub)* + \s*(?P<no_sum>no-summary) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_area_stub, + "compval": "stub", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "areas": { + "{{ area_id }}": { + "area_id": "{{ area_id }}", + "stub": { + "set": "{{ True if stub is defined and no_sum is undefined }}", + "summary_lsa": "{{ True if no_sum is defined }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "bfd", + "getval": re.compile( + r"""\s+bfd* + \s*(?P<bfd>all-interfaces) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_bfd, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "bfd": { + "all_interfaces": "{{ True if bfd is defined }}", + }, + }, + }, + }, + }, + }, + { + "name": "bfd", + "getval": re.compile( + r"""\s+bfd* + \s*(?P<bfd>default) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_bfd, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "bfd": { + "all_interfaces": "{{ True if bfd is defined }}", + }, + }, + }, + }, + }, + }, + { + "name": "default_information", + "getval": re.compile( + r"""\s+default-information* + \s*(?P<originate>originate)* + \s*(?P<always>always)* + \s*(?P<metric>metric\s\d+)* + \s*(?P<metric_type>metric-type\s\d+)* + \s*(?P<route_map>route-map\s\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_default_information, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "default_information": { + "originate": "{{ True if originate is defined }}", + "always": "{{ True if always is defined }}", + "metric": "{{ metric.split(" ")[1]|int }}", + "metric_type": "{{ metric_type.split(" + ")[1]|int }}", + "route_map": "{{ route_map.split(" ")[1] }}", + }, + }, + }, + }, + }, + }, + { + "name": "default_metric", + "getval": re.compile( + r"""\s+default-metric(?P<default_metric>\s\d+) + *$""", + re.VERBOSE, + ), + "setval": "default-metric {{ default_metric }}", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "default_metric": "{{ default_metric| int}}", + }, + }, + }, + }, + }, + { + "name": "distance", + "getval": re.compile( + r"""\s+distance + \s+ospf + \s+intra-area + \s+(?P<distance>\d+) + *$""", + re.VERBOSE, + ), + "setval": "distance ospf intra-area {{ distance }}", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "distance": "{{ distance| int}}", + }, + }, + }, + }, + }, + { + "name": "fips_restrictions", + "getval": re.compile( + r""" + \s+(?P<fips>fips\s*\S+) + *$""", + re.VERBOSE, + ), + "setval": "fips restrictions", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "fips_restrictions": "{{ True if fips is defined }}", + }, + }, + }, + }, + }, + { + "name": "graceful_restart_period", + "getval": re.compile( + r""" + \s+graceful-restart + \s*grace-period* + \s*(?P<period>\d+)* + $""", + re.VERBOSE, + ), + "setval": "graceful-restart grace-period {{ graceful_restart.grace_period }}", + "remval": "graceful-restart", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "graceful_restart": { + "grace_period": "{{ period|int }}", + }, + }, + }, + }, + }, + }, + { + "name": "graceful_restart", + "getval": re.compile( + r""" + \s+graceful-restart + $""", + re.VERBOSE, + ), + "setval": "graceful-restart", + "remval": "graceful-restart", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "graceful_restart": {"set": "{{ True }}"}, + }, + }, + }, + }, + }, + { + "name": "graceful_restart_helper", + "getval": re.compile( + r"""\s+(?P<grace>graceful-restart-helper) + *$""", + re.VERBOSE, + ), + "setval": "{{ grace }}", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "graceful_restart_helper": { + "set": "{{ True if grace is defined }}", + }, + }, + }, + }, + }, + }, + { + "name": "log_adjacency_changes", + "getval": re.compile( + r"""\s+(?P<log>log-adjacency-changes)* + \s*(?P<detail>detail) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_log_adjacency_changes, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "log_adjacency_changes": { + "set": "{{ True if log is defined and detail is undefined }}", + "detail": "{{ True if detail is defined }}", + }, + }, + }, + }, + }, + }, + { + "name": "max_metric", + "getval": re.compile( + r"""\s+max-metric + \s+(?P<router_lsa>router-lsa) + \s*(?P<external_lsa>external-lsa)* + \s*(?P<external_lsa_metric>\d+)* + \s*(?P<on_startup>on-startup)* + \s*(?P<wait_for_bgp>wait-for-bgp)* + \s*(?P<startup_time>\d+)* + \s*(?P<summary_lsa>summary-lsa)* + \s*(?P<summary_lsa_metric>\d+)* + \s*(?P<include_stub>include-stub) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_max_metric, + "remval": "max-metric router-lsa", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "max_metric": { + "router_lsa": { + "set": "{{ True if router_lsa is defined and external_lsa is undefined and " + "include_stub is undefined and on_startup is undefined and " + "summary_lsa is undefined }}", + "external_lsa": { + "set": "{{ True if external_lsa is defined and external_lsa_metric is undefined }}", + "max_metric_value": "{{ external_lsa_metric }}", + }, + "include_stub": "{{ True if include_stub is defined }}", + "on_startup": { + "wait_period": "{{ startup_time }}", + "wait_for_bgp": "{{ True if wait_for_bgp is defined }}", + }, + "summary_lsa": { + "set": "{{ True if summary_lsa is defined and summary_lsa_metric is undefined }}", + "max_metric_value": "{{ summary_lsa_metric }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "maximum_paths", + "getval": re.compile( + r"""\s+maximum-paths* + \s+(?P<paths>\d+) + *$""", + re.VERBOSE, + ), + "setval": "maximum-paths {{ maximum_paths }}", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "maximum_paths": "{{ paths }}", + }, + }, + }, + }, + }, + { + "name": "passive_interface", + "getval": re.compile( + r""" + \s*(?P<passive>passive-interface.*) + *$""", + re.VERBOSE, + ), + "setval": "passive-interface default", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "passive_interface": "{{ True if passive is defined }}", + }, + }, + }, + }, + }, + { + "name": "redistribute", + "getval": re.compile( + r""" + \s+redistribute + \s(?P<route>\S+) + \s*(?P<rmpa>route-map)* + \s*(?P<map>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_redistribute, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "redistribute": [ + { + "routes": "{{ route }}", + "route_map": "{{ map }}", + }, + ], + }, + }, + }, + }, + }, + { + "name": "router_id", + "getval": re.compile( + r""" + \s+router-id + \s(?P<id>\S+)$""", + re.VERBOSE, + ), + "setval": ("router-id" " {{ router_id }}"), + "remval": "router-id", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "router_id": "{{ id }}", + }, + }, + }, + }, + }, + { + "name": "shutdown", + "getval": re.compile( + r"""\s+(?P<shutdown>shutdown) + *$""", + re.VERBOSE, + ), + "setval": "shutdown", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "shutdown": "{{ True if shutdown is defined }}", + }, + }, + }, + }, + }, + { + "name": "timers.out_delay", + "getval": re.compile( + r"""\s+timers + \sout-delay + \s(?P<out_delay>\d+) + *$""", + re.VERBOSE, + ), + "setval": "timers out-delay {{ timers.out_delay }}", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "timers": {"out_delay": "{{ out_delay }}"}, + }, + }, + }, + }, + }, + { + "name": "timers.pacing", + "getval": re.compile( + r"""\s+timers + \spacing + \sflood + \s(?P<pacing>\d+) + *$""", + re.VERBOSE, + ), + "setval": "timers pacing flood {{ timers.pacing }}", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "timers": {"pacing": "{{ pacing }}"}, + }, + }, + }, + }, + }, + { + "name": "timers.lsa", + "getval": re.compile( + r"""\s+timers + \s(?P<lsa>lsa) + \s(?P<dir>rx|tx) + \s*(min\sinterval)* + \s*(delay\sinitial)* + \s*(?P<initial>\d+)* + \s*(?P<min>\d+)* + \s*(?P<max>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_timers_lsa, + "compval": "timers.lsa", + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "timers": { + "lsa": { + "direction": "{{ dir }}", + "initial": "{{ initial }}", + "min": "{{ min_delay }}", + "max": "{{ max_delay }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "timers.spf", + "getval": re.compile( + r"""\s+timers + \s+(?P<spf>spf) + \s+(delay\sinitial) + \s*(?P<initial>\d+) + \s*(?P<min>\d+) + \s*(?P<max>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_ospf_timers_spf, + "result": { + "processes": { + "address_family": { + '{{ afi|default("router", true) }}': { + "timers": { + "spf": { + "initial": "{{ initial }}", + "min": "{{ min }}", + "max": "{{ max }}", + }, + }, + }, + }, + }, + }, + }, + ] diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/prefix_lists.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/prefix_lists.py new file mode 100644 index 000000000..1469b3af1 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/prefix_lists.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Prefix_lists parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +# diable no-self-use +# pylint: disable=R0201 +# pylint: disable=W0642 +# pylint: disable=no-self-argument + + +class Prefix_listsTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Prefix_listsTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + def _tmplt_prefix_list_ip(config_data): + command_set = [] + config_data = config_data["prefix_lists"].get("entries", {}) + for k, v in iteritems(config_data): + command = "" + if k != "seq": + command = "seq " + str(k) + " {action} {address}".format(**v) + else: + command = "{action} {address}".format(**v) + if "match" in v: + command += " {operator} {masklen}".format(**v["match"]) + if command: + command_set.append(command) + + return command_set + + def _tmplt_prefix_list_ip_del(config_data): + command_set = [] + config_data = config_data["prefix_lists"].get("entries", {}) + for k, v in iteritems(config_data): + command_set.append("seq " + str(k)) + + return command_set + + def _tmplt_prefix_list_resequence(config_data): + command = "resequence" + config_data = config_data["prefix_lists"].get("entries", {}) + for k, v in iteritems(config_data): + if v["resequence"].get("start_seq"): + command += " " + str(v["resequence"]["start_seq"]) + if v["resequence"].get("step"): + command += " " + str(v["resequence"]["step"]) + + return command + + # fmt: off + PARSERS = [ + { + "name": "prefixlist.name", + "getval": re.compile( + r""" + ^(?P<afi>ip|ipv6)\sprefix-list\s(?P<name>\S+) + *$""", + re.VERBOSE, + ), + "setval": '{{ "ip" if afi == "ipv4" else afi }} prefix-list {{ prefix_lists.name }}', + "compval": "prefix_lists", + "result": { + '{{ afi }}': { + "afi": '{{ "ipv4" if afi == "ip" else afi }}', + "prefix_lists": { + "{{ name }}": { + "name": "{{ name }}", + }, + }, + }, + }, + "shared": True, + }, + { + "name": "prefixlist.entry", + "getval": re.compile( + r""" + \s*seq + \s(?P<num>\d+) + \s+(?P<action>permit|deny) + \s+(?P<ip>\S+) + \s*(?P<oper>eq|ge|le)* + \s*(?P<len>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_prefix_list_ip, + "remval": _tmplt_prefix_list_ip_del, + "compval": "prefix_lists", + "result": { + "{{ afi }}": { + "prefix_lists": { + "{{ name }}": { + "entries": { + '{{ num|d("seq") }}': { + "sequence": "{{ num }}", + "action": "{{ action }}", + "address": "{{ ip }}", + "match": { + "operator": "{{ oper }}", + "masklen": "{{ len }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "prefixlist.resequence", + "getval": re.compile( + r""" + \s*resequence + \s*(?P<start>\d+)* + \*(?P<step>\d+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_prefix_list_resequence, + "compval": "prefix_lists", + "result": { + "{{ afi }}": { + "prefix_lists": { + "{{ name }}": { + "entries": { + '{{ num|d("seq") }}': { + "resequence": { + "default": "{{ True if start_seq is undefined and step is undefined }}", + "start_seq": "{{ start }}", + "step": "{{ step }}", + }, + }, + }, + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/route_maps.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/route_maps.py new file mode 100644 index 000000000..fb0eddfb6 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/route_maps.py @@ -0,0 +1,1697 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Route_maps parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_route_map_set_aspath_prepend(config_data): + el = config_data["entries"] + command = "set as-path prepend " + c = el["set"]["as_path"]["prepend"] + if c.get("last_as"): + command += "last-as " + str(c["last_as"]) + if c.get("as_number"): + num = " ".join(c["as_number"].split(",")) + command += num + return command + + +def _tmplt_route_map_set_aspath_match(config_data): + el = config_data["entries"] + command = "set as-path match all replacement " + c = el["set"]["as_path"]["match"] + if c.get("none"): + command += "none" + if c.get("as_number"): + num = str(c["as_number"]) + command += num + return command + + +def _tmplt_route_map_extcommunity_lbw(config_data): + config_data = config_data["entries"]["set"]["extcommunity"]["lbw"] + command = "set extcommunity lbw " + if config_data.get("aggregate"): + command += "aggregate " + config_data["value"] + if not config_data.get("aggregate") and config_data.get("value"): + command += config_data["value"] + if config_data.get("divide"): + command += "divide " + config_data["divide"] + return command + + +def _tmplt_route_map_extcommunity_rt(config_data): + config_data = config_data["entries"]["set"]["extcommunity"]["rt"] + command = "set extcommunity rt " + config_data["vpn"] + if config_data.get("additive"): + command += " additive" + if config_data.get("delete"): + command += " delete" + return command + + +def _tmplt_route_maps_subroutemap(config_data): + command = "" + if config_data["entries"].get("sub_route_map"): + command = ( + "sub-route-map " + config_data["entries"]["sub_route_map"]["name"] + ) + if config_data["entries"]["sub_route_map"].get("invert_result"): + command += " invert-result" + return command + + +def _tmplt_route_map_extcommunity_soo(config_data): + config_data = config_data["entries"]["set"]["extcommunity"]["soo"] + command = "set extcommunity soo " + config_data["vpn"] + if config_data.get("additive"): + command += " additive" + if config_data.get("delete"): + command += " delete" + return command + + +def _tmplt_route_map_ip(config_data): + config_data = config_data["entries"]["set"] + if config_data.get("ip"): + command = "set ip next-hop " + k = "ip" + elif config_data.get("ipv6"): + command = "set ip next-hop " + k = "ipv6" + if config_data[k].get("address"): + command += config_data[k]["address"] + elif config_data[k].get("unchanged"): + command += "unchanged" + elif config_data[k].get("peer_address"): + command += "peer-address" + return command + + +def _tmplt_route_maps_metric(config_data): + config_data = config_data["entries"]["set"]["metric"] + command = "set metric" + if config_data.get("value"): + command += " " + config_data["value"] + if config_data.get("add"): + command += " +" + config_data["add"] + if config_data.get("igp_param"): + command += " " + config_data["igp_param"] + return command + + +def _tmplt_route_maps_nexthop(config_data): + config_data = config_data["entries"]["set"]["nexthop"] + command = "set next-hop igp-metric " + if config_data.get("max_metric"): + command += "max-metric" + if config_data.get("value"): + command += config_data["value"] + return command + + +def _tmplt_route_map_match_aggregator_role(config_data): + config_data = config_data["entries"]["match"]["aggregator_role"] + command = "match aggregator-role contributor" + if config_data.get("route_map"): + command += " aggregate-attributes " + config_data["route_map"] + return command + + +def _tmplt_route_map_match_aspath(config_data): + config_data = config_data["entries"]["match"]["as_path"] + command = "match as-path " + if config_data.get("length"): + command += "length " + config_data["length"] + if config_data.get("path_list"): + command += config_data["path_list"] + return command + + +def _tmplt_route_map_match_invert_aggregator_role(config_data): + config_data = config_data["entries"]["match"]["invert_result"][ + "aggregate_role" + ] + command = "match invert-result as-path aggregate-role contributor" + if config_data.get("route_map"): + command += " aggregator-attributes " + config_data["route_map"] + return command + + +def _tmplt_route_map_match_invert_aspath(config_data): + config_data = config_data["entries"]["match"]["invert_result"]["as_path"] + command = "match invert-result as-path " + if config_data.get("length"): + command += "length " + config_data["length"] + if config_data.get("path_list"): + command += config_data["path_list"] + return command + + +def _tmplt_route_map_match_ip_address(config_data): + command = "" + config_data = config_data["entries"]["match"]["ip"] + if config_data.get("address"): + config_data = config_data["address"] + command = "match ip address " + if config_data.get("dynamic"): + command += "dynamic" + if config_data.get("access_list"): + command += "access-list " + config_data["access_list"] + if config_data.get("prefix_list"): + command += "prefix-list " + config_data["prefix_list"] + return command + + +def _tmplt_route_map_match_ipv6_address(config_data): + command = "" + config_data = config_data["entries"]["match"]["ipv6"] + if config_data.get("address"): + config_data = config_data["address"] + command = "match ipv6 address " + if config_data.get("dynamic"): + command += "dynamic" + if config_data.get("access_list"): + command += "access-list " + config_data["access_list"] + if config_data.get("prefix_list"): + command += "prefix-list " + config_data["prefix_list"] + return command + + +def _tmplt_route_map_match_ip(config_data): + command = "" + config_data = config_data["entries"]["match"]["ip"] + if "address" not in config_data: + command = "match ip " + if config_data.get("next_hop"): + command += "next-hop prefix-list " + config_data["next_hop"] + elif config_data.get("resolved_next_hop"): + command += ( + "resolved-next-hop prefix-list " + + config_data["resolved_next_hop"] + ) + return command + + +def _tmplt_route_map_match_ipv6(config_data): + command = "" + config_data = config_data["entries"]["match"]["ipv6"] + if "address" not in config_data: + command = "match ipv6 " + if config_data.get("next_hop"): + command += "next-hop prefix-list " + config_data["next_hop"] + elif config_data.get("resolved_next_hop"): + command += ( + "resolved-next-hop prefix-list " + + config_data["resolved_next_hop"] + ) + return command + + +def _tmplt_route_maps_match_metric(config_data): + config_data = config_data["entries"]["match"]["metric"] + command = "match metric" + if config_data.get("value"): + command += " " + config_data["value"] + return command + + +class Route_mapsTemplate(NetworkTemplate): + def __init__(self, lines=None): + super(Route_mapsTemplate, self).__init__(lines=lines, tmplt=self) + + # fmt: off + PARSERS = [ + { + "name": "route_map.entries", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + \s+(?P<action>deny|permit) + \s+(?P<seq>\d+) + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }} {{ entries.action }} {{ entries.sequence }}", + "compval": "entries", + "result": { + "route_map": "{{ map_name }}", + "entries": [ + { + "action": "{{ action }}", + "sequence": "{{ seq }}", + }, + ], + }, + "shared": True, + }, + { + "name": "route_map.action", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + \s+(?P<action>deny|permit) + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }} {{ entries.action }}", + "compval": "entries.action", + "result": { + "route_map": "{{ map_name }}", + "entries": [ + { + "action": "{{ action }}", + }, + ], + }, + "shared": True, + }, + { + "name": "route_map.name", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }}", + "compval": "route_map", + "result": { + "route_map": "{{ map_name }}", + }, + "shared": True, + }, + { + "name": "route_map.statement.entries", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + \s+statement + \s+(?P<statement>\S+) + \s+(?P<action>deny|permit) + \s+(?P<seq>\d+) + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }} statement {{ entries.statement }}" + + " {{ entries.action }} {{ entries.sequence }}", + "compval": "entries.statement", + "result": { + "route_map": "{{ map_name }}", + "entries": [ + { + "statement": "{{ statement }}", + "action": "{{ action }}", + "sequence": "{{ seq }}", + }, + ], + }, + "shared": True, + }, + { + "name": "route_map.statement.action", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + \s+statement + \s+(?P<statement>\S+) + \s+(?P<action>deny|permit) + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }} statement {{ entries.statement }} {{ entries.action }}", + "compval": "entries.statement", + "result": { + "route_map": "{{ map_name }}", + "entries": [ + { + "statement": "{{ statement }}", + "action": "{{ action }}", + }, + ], + }, + "shared": True, + }, + { + "name": "route_map.statement.name", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }} statement {{ entries.statement }}", + "compval": "entries.statement", + "result": { + "route_map": "{{ map_name }}", + "entries": [ + { + "statement": "{{ statement }}", + }, + ], + }, + "shared": True, + }, + { + "name": "continue", + "getval": re.compile( + r""" + \s*continue + \s+(?P<num>\d+) + $""", + re.VERBOSE, + ), + "setval": "continue {{ entries.continue_sequence }}", + "compval": "entries.continue_sequence", + "result": { + "entries": [ + { + "continue_sequence": "{{ num }}", + }, + ], + }, + }, + { + "name": "route_map.copy", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + \s+copy + \s+(?P<name>\S+) + \s*(?P<overwrite>overwrite)* + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }} copy {{ entries.source.source_map_name }}" + + "{{ (' ' + overwrite) if overwrite is defined }}", + "compval": "entries.source", + "result": { + "route_map": "{{ map_name }}", + "entries": [ + { + "source": { + "action": "copy", + "source_map_name": "{{ name }}", + "overwrite": "{{ True if overwrite is defined }}", + }, + }, + ], + }, + }, + { + "name": "route_map.rename", + "getval": re.compile( + r""" + \s*route-map + \s+(?P<map_name>\S+) + \s+rename + \s+(?P<name>\S+) + \s*(?P<overwrite>overwrite)* + $""", + re.VERBOSE, + ), + "setval": "route-map {{ route_map }} rename {{ entries.source.source_map_name }}" + + "{{ (' ' + overwrite) if overwrite is defined }}", + "compval": "entries.source", + "result": { + "route_map": "{{ map_name }}", + "entries": [ + { + "source": { + "action": "rename", + "source_map_name": "{{ name }}", + "overwrite": "{{ True if overwrite is defined }}", + }, + }, + ], + }, + }, + { + "name": "description", + "getval": re.compile( + r""" + \s*description + \s+(?P<desc>.+) + $""", + re.VERBOSE, + ), + "setval": "description {{ entries.description }}", + "compval": "entries.description", + "remval": "description", + "result": { + "entries": [ + { + "description": "{{ desc }}", + }, + ], + }, + }, + { + "name": "sub_route_map", + "getval": re.compile( + r""" + \s*sub-route-map + \s*(?P<invert>invert-result)* + \s+(?P<map>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_maps_subroutemap, + "compval": "entries.sub_route_map", + "result": { + "entries": [ + { + "sub_route_map": { + "name": "{{ map }}", + "invert_result": "{{ True if invert is defined }}", + }, + }, + ], + }, + }, + { + "name": "set.as_path.prepend", + "getval": re.compile( + r""" + \s*set\s+as-path\sprepend + \s*(?P<lastas>last-as .+)* + \s*(?P<as>[^a-zA-Z]+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_set_aspath_prepend, + "compval": "entries.set.as_path.prepend", + "result": { + "entries": [ + { + "set": { + "as_path": { + "prepend": { + "last_as": "{{ lastas.split(" ")[1] if lastas is defined }}", + "as_number": "{{ ','.join(as.split(' ')) if as is defined }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "set.as_path.match", + "getval": re.compile( + r""" + \s*set\s+as-path\smatch\sall\replacement + \s*(?P<none>none)* + \s*(?P<as>.+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_set_aspath_match, + "compval": "entries.set.as_path.match", + "result": { + "entries": [ + { + "set": { + "as_path": { + "match": { + "none": "{{ True if none is defined }}", + "as_number": "{{ as }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "set.bgp", + "getval": re.compile( + r""" + \s*set\sbgp\sbestpath\sas-path\sweight + \s+(?P<weight>\d+) + $""", + re.VERBOSE, + ), + "setval": "set bgp bestpath as-path weight {{ entries.set.bgp }}", + "compval": "entries.set.bgp", + "result": { + "entries": [ + { + "set": { + "bgp": "{{ weight }}", + }, + }, + ], + }, + }, + { + "name": "set.community.graceful_shutdown", + "getval": re.compile( + r""" + \s*set\scommunity\sGSHUT + *$""", + re.VERBOSE, + ), + "setval": "set community GSHUT", + "compval": "entries.set.graceful_shutdown", + "result": { + "entries": [ + { + "set": { + "graceful_shutdown": "{{ True }}", + }, + }, + ], + }, + }, + { + "name": "set.community.none", + "getval": re.compile( + r""" + \s*set\scommunity\snone + *$""", + re.VERBOSE, + ), + "setval": "set community none", + "compval": "entries.set.none", + "result": { + "entries": [ + { + "set": { + "none": "{{ True }}", + }, + }, + ], + }, + }, + { + "name": "set.community.number", + "getval": re.compile( + r""" + \s*set\scommunity + \s+(?P<num>\d+\s*)+ + \s*(?P<action>additive|delete)* + \s*(?P<donot>local-as|no-advertise|no-export)* + $""", + re.VERBOSE, + ), + "setval": "set community {{ entries.set.community.number }}" + + "{{ (' ' + action) if action is defined }}{{ (' ' + donot) if donot is defined }}", + "compval": "entries.set.community", + "result": { + "entries": [ + { + "set": { + "community": { + "number": "{{ num }}", + "additive": "{{ True if action == 'additive' }}", + "delete": "{{ True if action == 'delete' }}", + '{{ "donot" }}': "{{ True if donot is defined }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.community.list", + "getval": re.compile( + r""" + \s*set\scommunity\s+community-list + \s+(?P<name>\S+\s*)+ + \s*(?P<action>additive|delete)* + \s*(?P<donot>local-as|no-advertise|no-export)* + $""", + re.VERBOSE, + ), + "setval": "set community {{ entries.set.community.name }}" + + "{{ (' ' + action) if action is defined }}{{ (' ' + donot) if donot is defined }}", + "compval": "entries.set.community", + "result": { + "entries": [ + { + "set": { + "community": { + "community_list": "{{ name }}", + "additive": "{{ True if action == 'additive' }}", + "delete": "{{ True if action == 'delete' }}", + '{{ "donot" }}': "{{ True if donot is defined }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.community.internet", + "getval": re.compile( + r""" + \s*set\scommunity\s+internet + \s*(?P<action>additive|delete)* + \s*(?P<donot>local-as|no-advertise|no-export)* + $""", + re.VERBOSE, + ), + "setval": "set community internet" + + "{{ (' ' + action) if action is defined }}{{ (' ' + donot) if donot is defined }}", + "compval": "entries.set.community", + "result": { + "entries": [ + { + "set": { + "community": { + "internet": "{{ true }}", + "additive": "{{ True if action == 'additive' }}", + "delete": "{{ True if action == 'delete' }}", + '{{ "donot" }}': "{{ True if donot is defined }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.distance", + "getval": re.compile( + r""" + \s*set\sdistance + \s+(?P<distance>\d+) + $""", + re.VERBOSE, + ), + "setval": "set distance {{ entries.set.distance }}", + "compval": "entries.set.distance", + "result": { + "entries": [ + { + "set": { + "distance": "{{ distance }}", + }, + }, + ], + }, + }, + { + "name": "set.evpn", + "getval": re.compile( + r""" + \s*set\sevpn\snext-hop\sunchanged + $""", + re.VERBOSE, + ), + "setval": "set evpn next-hop unchanged", + "compval": "entries.set.evpn", + "result": { + "entries": [ + { + "set": { + "evpn": "{{ True }}", + }, + }, + ], + }, + }, + { + "name": "set.extcommunity.lbw", + "getval": re.compile( + r""" + \s*set\sextcommunity\slbw + \s*(?P<agg>aggregate)* + \s*(?P<divide>divide\s\S+)* + \s*(?P<value>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_extcommunity_lbw, + "compval": "entries.set.extcommunity.lbw", + "result": { + "entries": [ + { + "set": { + "extcommunity": { + "lbw": { + "value": "{{ value if value is defined }}", + "aggregate": "{{ True if agg is defined }}", + "divide": "{{ divide.split(" ")[1] if divide is defined }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "set.extcommunity.none", + "getval": re.compile( + r""" + \s*set\sextcommunity\snone + $""", + re.VERBOSE, + ), + "setval": "{{ set extcommunity none }}", + "compval": "entries.set.extcommunity.none", + "result": { + "entries": [ + { + "set": { + "extcommunity": { + "none": "{{ True }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.extcommunity.rt", + "getval": re.compile( + r""" + \s*set\sextcommunity\srt + \s+(?P<asn>\S+) + \s*(?P<action_add>additive)* + \s*(?P<action_del>delete)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_extcommunity_rt, + "compval": "entries.set.extcommunity.rt", + "result": { + "entries": [ + { + "set": { + "extcommunity": { + "rt": { + "vpn": "{{ asn }}", + "additive": "{{ True if action_add is defined }}", + "delete": "{{ True if action_del is defined }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "set.extcommunity.soo", + "getval": re.compile( + r""" + \s*set\sextcommunity\ssoo + \s+(?P<asn>\S+) + \s+(?P<action>additive|delete) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_extcommunity_soo, + "compval": "entries.set.extcommunity.soo", + "result": { + "entries": [ + { + "set": { + "extcommunity": { + "soo": { + "vpn": "{{ asn }}", + "additive": "{{ True if action == 'additive' }}", + "delete": "{{ True if action == 'delete' }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "set.ip", + "getval": re.compile( + r""" + \s*set\sip\snext-hop + \s+(?P<attr>peer-address|unchanged|[\d\.]+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_ip, + "compval": "entries.set.ip", + "result": { + "entries": [ + { + "set": { + "ip": { + "unchanged": "{{ True if attr == 'unchanged' }}", + "peer_address": "{{ True if attr == 'peer-address' }}", + "address": "{{ attr if attr != 'unchanged' and attr != 'peer-address' }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.ipv6", + "getval": re.compile( + r""" + \s*set\sipv6\snext-hop + \s+(?P<attr>peer-address|unchanged|[\d\.]+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_ip, + "compval": "entries.set.ipv6", + "result": { + "entries": [ + { + "set": { + "ipv6": { + "unchanged": "{{ True if attr == 'unchanged' }}", + "peer_address": "{{ True if attr == 'peer-address' }}", + "address": "{{ attr if attr != 'unchanged' and attr != 'peer-address' }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.isis", + "getval": re.compile( + r""" + \s*set\sisis\slevel + \s+(?P<level>\S+) + $""", + re.VERBOSE, + ), + "setval": "set isis level {{ entries.set.isis_level }}", + "compval": "entries.set.isis_level", + "result": { + "entries": [ + { + "set": { + "isis_level": "{{ level }}", + }, + }, + ], + }, + }, + { + "name": "set.local_pref", + "getval": re.compile( + r""" + \s*set\slocal-preference + \s+(?P<as>\d+) + $""", + re.VERBOSE, + ), + "setval": "set local-preference {{ entries.set.local_preference }}", + "compval": "entries.set.local_preference", + "result": { + "entries": [ + { + "set": { + "local_preference": "{{ as }}", + }, + }, + ], + }, + }, + { + "name": "set.metric.value", + "getval": re.compile( + r""" + \s*set\smetric + \s*(?P<val>\d+)* + \s*(?P<operation>\+\S+)* + \s*(?P<param>igp-metric|igp-nexthop-cost)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_maps_metric, + "compval": "entries.set.metric", + "result": { + "entries": [ + { + "set": { + "metric": { + "value": "{{ val }}", + "add": "{{ operation.strip('+') }}", + "igp_param": "{{ param }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.metric_type", + "getval": re.compile( + r""" + \s*set\smetric-type + \s+(?P<type>\S+) + $""", + re.VERBOSE, + ), + "setval": "set metric-type {{ entries.set.metric_type }}", + "compval": "entries.set.local_preference", + "result": { + "entries": [ + { + "set": { + "metric_type": "{{ type }}", + }, + }, + ], + }, + }, + { + "name": "set.nexthop", + "getval": re.compile( + r""" + \s*set\snext-hop\sigp-metric + \s*(?P<hop>\d+)* + \s*(?P<max>max-metric)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_maps_nexthop, + "compval": "entries.set.nexthop", + "result": { + "entries": [ + { + "set": { + "nexthop": { + "value": "{{ hop if hop is defined }}", + "max_metric": "{{ True if max is defined }}", + }, + }, + }, + ], + }, + }, + { + "name": "set.origin", + "getval": re.compile( + r""" + \s*set\sorigin + \s+(?P<param>\S+) + $""", + re.VERBOSE, + ), + "setval": "set origin {{ entries.set.origin }}", + "compval": "entries.set.origin", + "result": { + "entries": [ + { + "set": { + "origin": "{{ param }}", + }, + }, + ], + }, + }, + { + "name": "set.segment_index", + "getval": re.compile( + r""" + \s*set\ssegment-index + \s+(?P<index>\d+) + $""", + re.VERBOSE, + ), + "setval": "set segment-index {{ entries.set.segment_index }}", + "compval": "entries.set.segment_index", + "result": { + "entries": [ + { + "set": { + "segment_index": "{{ index }}", + }, + }, + ], + }, + }, + { + "name": "set.tag", + "getval": re.compile( + r""" + \s*set\stag + \s+(?P<val>\d+) + $""", + re.VERBOSE, + ), + "setval": "set tag {{ entries.set.tag }}", + "compval": "entries.set.tag", + "result": { + "entries": [ + { + "set": { + "tag": "{{ val }}", + }, + }, + ], + }, + }, + { + "name": "set.weight", + "getval": re.compile( + r""" + \s*set\sweight + \s+(?P<val>\d+) + $""", + re.VERBOSE, + ), + "setval": "set local-preference {{ entries.set.weight }}", + "compval": "entries.set.weight", + "result": { + "entries": [ + { + "set": { + "weight": "{{ val }}", + }, + }, + ], + }, + }, + { + "name": "match.aggregate_role", + "getval": re.compile( + r""" + \s*match\s+aggregator-role\scontributor + \s*(?P<map>aggregate-attributes \S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_aggregator_role, + "compval": "entries.match.aggregator_role", + "result": { + "entries": [ + { + "match": { + "aggregator_role": { + "contributor": "{{ True if map is not defined }}", + "route_map": "{{ map.split(" ")[1] }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.as", + "getval": re.compile( + r""" + \s*match\s+as + \s+(?P<as>\S+)* + $""", + re.VERBOSE, + ), + "setval": "match as {{ entries.match.as }}", + "compval": "entries.match.as", + "result": { + "entries": [ + { + "match": { + "as": "{{ as }}", + }, + }, + ], + }, + }, + { + "name": "match.as_path", + "getval": re.compile( + r""" + \s*match\s+as-path + \s*(?P<len>length .+)* + \s*(?P<path>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_aspath, + "compval": "entries.match.as_path.prepend", + "result": { + "entries": [ + { + "match": { + "as_path": { + "path_list": "{{ path }}", + "length": "{{ len[7:] }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.community.instances", + "getval": re.compile( + r""" + \s*match\scommunity\sinstances + \s+(?P<inst>.+) + *$""", + re.VERBOSE, + ), + "setval": "match community instances {{ entries.match.community.instances }}", + "compval": "entries.match.community.instances", + "result": { + "entries": [ + { + "match": { + "community": { + "instances": "{{ inst }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.community.list", + "getval": re.compile( + r""" + \s*match\scommunity + \s+(?P<comm>.+\s) + \s*(?P<mat>exact-match)* + $""", + re.VERBOSE, + ), + "setval": "match community {{ entries.match.community.community_list }}{{ (' exact-match') if mat is defined }}", + "compval": "entries.match.community", + "result": { + "entries": [ + { + "match": { + "community": { + "community_list": "{{ comm.strip() }}", + "exact_match": "{{ True if mat is defined }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.extcommunity", + "getval": re.compile( + r""" + \s*match\sextcommunity + \s+(?P<list>.+\s*) + \s*(?P<mat>exact-match)* + $""", + re.VERBOSE, + ), + "setval": "match extcommunity {{ entries.match.extcommunity.community_list }}{{ (' exact-match') if mat is defined }}", + "compval": "entries.match.extcommunity", + "result": { + "entries": [ + { + "match": { + "extcommunity": { + "community_list": "{{ list }}", + "exact_match": "{{ True if mat is defined }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.invert.aggregate_role", + "getval": re.compile( + r""" + \s*match\sinvert-result\saggregate-role\scontributor + \s*(?P<map>aggregate-attributes \S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_invert_aggregator_role, + "compval": "entries.match.invert_result.aggregate_role", + "result": { + "entries": [ + { + "match": { + "invert_result": { + "aggregate_role": { + "contributor": "{{ True if map is not defined }}", + "route_map": "{{ map.split(" ")[1] }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "match.invert.as_path", + "getval": re.compile( + r""" + \s*match\sinvert-result\sas-path + \s*(?P<len>length .+)* + \s*(?P<path>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_invert_aspath, + "compval": "entries.match.invert_result.as_path.prepend", + "result": { + "entries": [ + { + "match": { + "invert_result": { + "as_path": { + "path_list": "{{ path }}", + "length": "{{ len[7:] }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "match.invert.community.instances", + "getval": re.compile( + r""" + \s*match\sinvert-result\scommunity\sinstances + \s+(?P<inst>.+) + *$""", + re.VERBOSE, + ), + "setval": "match invert-result community instances {{ entries.match.community.instances }}", + "compval": "entries.match.invert_result.community.instances", + "result": { + "entries": [ + { + "match": { + "invert_result": { + "community": { + "instances": "{{ inst }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "match.invert.community.list", + "getval": re.compile( + r""" + \s*match\sinvert-result\scommunity + \s+(?P<list>.+\s*) + \s*(?P<mat>exact-match) + *$""", + re.VERBOSE, + ), + "setval": "match invert-result community {{ entries.match.community.community_list }}{{ (' exact-match') if mat is defined }}", + "compval": "entries.match.invert_result.community", + "result": { + "entries": [ + { + "match": { + "invert_result": { + "community": { + "community_list": "{{ list }}", + "exact_match": "{{ True if mat is defined }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "match.invert.extcommunity", + "getval": re.compile( + r""" + \s*match\sinvert-result\sextcommunity + \s+(?P<list>.+\s*) + \s*(?P<mat>exact-match) + *$""", + re.VERBOSE, + ), + "setval": "match invert-result extcommunity" + + " {{ entries.match.extcommunity.community_list }}{{ (' exact-match') if mat is defined }}", + "compval": "entries.match.invert_result.extcommunity", + "result": { + "entries": [ + { + "match": { + "invert_result": { + "extcommunity": { + "community_list": "{{ list }}", + "exact_match": "{{ True if mat is defined }}", + }, + }, + }, + }, + ], + }, + }, + { + "name": "match.interface", + "getval": re.compile( + r""" + \s*match\sinterface + \s+(?P<int>\S+) + $""", + re.VERBOSE, + ), + "setval": "match interface {{ entries.match.interface }}", + "compval": "entries.match.interface", + "result": { + "entries": [ + { + "match": { + "interface": "{{ int }}", + }, + }, + ], + }, + }, + { + "name": "match.ipaddress", + "getval": re.compile( + r""" + \s*match\sip\saddress + \s*(?P<dyn>dynamic)* + \s+(?P<attr>\S+\s\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_ip_address, + "compval": "entries.match.ip.address", + "shared": True, + "result": { + "entries": [ + { + "match": { + "ip": { + "address": { + "dynamic": "{{ True if dynamic is defined }}", + "access_list": '{{ attr.split(" ")[1] if attr.split(" ")[0] == "access-list" }}', + "prefix_list": '{{ attr.split(" ")[1] if attr.split(" ")[0] == "prefix-list" }}', + }, + }, + }, + }, + ], + }, + }, + { + "name": "match.ip", + "getval": re.compile( + r""" + \s*match\sip + \s+(?P<param>next-hop|resolved-next-hop) + \s+prefix-list + \s+(?P<prefix>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_ip, + "compval": "entries.match.ip", + "result": { + "entries": [ + { + "match": { + "ip": { + "next_hop": "{{ prefix if param == 'next-hop' }}", + "resolved_next_hop": "{{ prefix if param == 'resolved-next-hop' }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.ipv6address", + "getval": re.compile( + r""" + \s*match\sipv6\saddress + \s*(?P<dyn>dynamic)* + \s+(?P<attr>\S+\s\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_ipv6_address, + "compval": "entries.match.ipv6.address", + "shared": True, + "result": { + "entries": [ + { + "match": { + "ipv6": { + "address": { + "dynamic": "{{ True if dynamic is defined }}", + "access_list": '{{ attr.split(" ")[1] if attr.split(" ")[0] == "access-list" }}', + "prefix_list": '{{ attr.split(" ")[1] if attr.split(" ")[0] == "prefix-list" }}', + }, + }, + }, + }, + ], + }, + }, + { + "name": "match.ipv6", + "getval": re.compile( + r""" + \s*match\sipv6 + \s*(?P<param>next-hop|resolved-next-hop) + \s+prefix-list + \s+(?P<prefix>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_route_map_match_ipv6, + "compval": "entries.match.ipv6", + "result": { + "entries": [ + { + "match": { + "ipv6": { + "next_hop": "{{ prefix if param == 'next-hop' }}", + "resolved_next_hop": "{{ prefix if param == 'resolved-next-hop' }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.largecommunity", + "getval": re.compile( + r""" + \s*match\slarge-community + \s+(?P<list>.+\s*) + \s*(?P<mat>exact-match) + *$""", + re.VERBOSE, + ), + "setval": "match large-community {{ entries.match.large_community.community_list }}{{ (' exact-match') if mat is defined }}", + "compval": "entries.match.large_community", + "result": { + "entries": [ + { + "match": { + "large_community": { + "community_list": "{{ list }}", + "exact_match": "{{ True if mat is defined }}", + }, + }, + }, + ], + }, + }, + { + "name": "match.isis", + "getval": re.compile( + r""" + \s*match\sisis\slevel + \s+(?P<level>\S+) + $""", + re.VERBOSE, + ), + "setval": "match isis level {{ entries.match.isis_level }}", + "compval": "entries.match.isis_level", + "result": { + "entries": [ + { + "match": { + "isis_level": "{{ level }}", + }, + }, + ], + }, + }, + { + "name": "match.local_pref", + "getval": re.compile( + r""" + \s*match\slocal-preference + \s+(?P<as>\d+) + $""", + re.VERBOSE, + ), + "setval": "match local-preference {{ entries.match.local_preference }}", + "compval": "entries.match.local_preference", + "result": { + "entries": [ + { + "match": { + "local_preference": "{{ as }}", + }, + }, + ], + }, + }, + { + "name": "match.metric", + "getval": re.compile( + r""" + \s*match\smetric + \s+(?P<val>\d+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_route_maps_match_metric, + "compval": "entries.match.metric", + "result": { + "entries": [ + { + "match": { + "metric": "{{ val }}", + }, + }, + ], + }, + }, + { + "name": "match.metric_type", + "getval": re.compile( + r""" + \s*match\smetric-type + \s+(?P<type>\S+) + $""", + re.VERBOSE, + ), + "setval": "match metric-type {{ entries.match.metric_type }}", + "compval": "entries.match.local_preference", + "result": { + "entries": [ + { + "match": { + "metric_type": "{{ type }}", + }, + }, + ], + }, + }, + { + "name": "match.route_type", + "getval": re.compile( + r""" + \s*match\sroute-type + \s+(?P<type>\S+) + *$""", + re.VERBOSE, + ), + "setval": "match route-type {{ entries.match.route_type }}", + "compval": "entries.match.route_type", + "result": { + "entries": [ + { + "match": { + "route_type": "{{ type }}", + }, + }, + ], + }, + }, + { + "name": "match.routerid", + "getval": re.compile( + r""" + \s*match\srouter-id\sprefix-list + \s+(?P<id>\S+) + $""", + re.VERBOSE, + ), + "setval": "match router-id prefix-list {{ entries.match.router_id }}", + "compval": "entries.match.router_id", + "result": { + "entries": [ + { + "match": { + "router_id": "{{ id }}", + }, + }, + ], + }, + }, + { + "name": "match.source_protocol", + "getval": re.compile( + r""" + \s*match\ssource-protocol + \s+(?P<proto>\S+) + *$""", + re.VERBOSE, + ), + "setval": "match source-protocol {{ entries.match.source_protocol }}", + "compval": "entries.match.source_protocol", + "result": { + "entries": [ + { + "match": { + "source_protocol": "{{ proto }}", + }, + }, + ], + }, + }, + { + "name": "match.tag", + "getval": re.compile( + r""" + \s*match\stag + \s+(?P<val>\d+) + $""", + re.VERBOSE, + ), + "setval": "match tag {{ entries.match.tag }}", + "compval": "entries.match.tag", + "result": { + "entries": [ + { + "match": { + "tag": "{{ val }}", + }, + }, + ], + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/snmp_server.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/snmp_server.py new file mode 100644 index 000000000..52605a72e --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/rm_templates/snmp_server.py @@ -0,0 +1,1232 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Snmp_server parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_snmp_server_ipv6_comm(config_data): + command = "" + if "acl_v6" in config_data["communities"]: + command = "snmp-server community " + el = config_data["communities"] + command += el["name"] + if el.get("view"): + command += " view " + el["view"] + if el.get("ro"): + command += " ro" + if el.get("rw"): + command += " rw" + command += " ipv6 " + el["acl_v6"] + return command + + +def _tmplt_snmp_server_ipv4_comm(config_data): + command = "" + if not config_data["communities"].get("acl_v6"): + command = "snmp-server community " + el = config_data["communities"] + command += el["name"] + if el.get("view"): + command += " view " + el["view"] + if el.get("ro"): + command += " ro" + if el.get("rw"): + command += " rw" + if el.get("acl_v4"): + command += " " + el["acl_v4"] + return command + + +def _tmplt_snmp_server_traps_bgp(config_data): + command = "snmp-server enable traps bgp" + el = config_data["traps"]["bgp"] + if el.get("arista_backward_transition"): + command += " arista-backward-transition" + if el.get("arista_established"): + command += " arista-established" + if el.get("backward_transition"): + command += " backward-transition" + if el.get("established"): + command += " established" + return command + + +def _tmplt_snmp_server_traps_bridge(config_data): + command = "snmp-server enable traps bridge" + el = config_data["traps"]["bridge"] + if el.get("arista_mac_age"): + command += " arista-mac-age" + if el.get("arista_mac_learn"): + command += " arista-mac-learn" + if el.get("arista_mac_move"): + command += " arista-mac-move" + return command + + +def _tmplt_snmp_server_traps_capacity(config_data): + command = "snmp-server enable traps capacity" + el = config_data["traps"]["capacity"] + if el.get("arista_hardware_utilization_alert"): + command += " arista-hardware-utilization-alert" + return command + + +def _tmplt_snmp_server_traps_entity(config_data): + command = "snmp-server enable traps entity" + el = config_data["traps"]["entity"] + if el.get("arista_ent_sensor_alarm"): + command += " arista-ent-sensor-alarm" + if el.get("ent_config_change"): + command += " ent-config-change" + if el.get("ent_state_oper"): + command += " ent-state-oper" + if el.get("ent_state_oper_disabled"): + command += " ent-state-oper-disabled" + if el.get("ent_state_oper_enabled"): + command += " ent-state-oper-enabled" + return command + + +def _tmplt_snmp_server_traps_external_alarm(config_data): + command = "snmp-server enable traps external-alarm" + el = config_data["traps"]["external_alarm"] + if el.get("arista_external_alarm_asserted_notif"): + command += " arista-external-alarm-asserted-notif" + if el.get("arista_external_alarm_deasserted_notif"): + command += " arista-external-alarm-deasserted-notif" + return command + + +def _tmplt_snmp_server_traps_isis(config_data): + command = "snmp-server enable traps isis" + el = config_data["traps"]["isis"] + if el.get("adjacency_change"): + command += " adjacency-change" + if el.get("area_mismatch"): + command += " area-mismatch" + if el.get("attempt_to_exceed_max_sequence"): + command += " attempt-to-exceed-max-sequence" + if el.get("authentication_type_failure"): + command += " authentication-type-failure" + if el.get("database_overload"): + command += " database-overload" + if el.get("own_lsp_purge"): + command += " own-lsp-purge" + if el.get("rejected_adjacency"): + command += " rejected-adjacency" + if el.get("sequence_number_skip"): + command += " sequence-number-skip" + + return command + + +def _tmplt_snmp_server_traps_lldp(config_data): + command = "snmp-server enable traps lldp" + el = config_data["traps"]["lldp"] + if el.get("rem_tables_change"): + command += " rem-tables-change" + return command + + +def _tmplt_snmp_server_traps_mpls_ldp(config_data): + command = "snmp-server enable traps mpls-ldp" + el = config_data["traps"]["mpls_ldp"] + if el.get("mpls_ldp_session_down"): + command += " mpls-ldp-session-down" + if el.get("mpls_ldp_session_up"): + command += " mpls-ldp-session-up" + return command + + +def _tmplt_snmp_server_traps_msdp(config_data): + command = "snmp-server enable traps msdp" + el = config_data["traps"]["msdp"] + if el.get("backward_transition"): + command += " backward-transition" + if el.get("established"): + command += " established" + return command + + +def _tmplt_snmp_server_traps_ospf(config_data): + command = "snmp-server enable traps ospf" + el = config_data["traps"]["ospf"] + if el.get("if_auth_failure"): + command += " if-auth-failure" + if el.get("if_config_error"): + command += " if-config-error" + if el.get("if_state_change"): + command += " if-state-change" + if el.get("nbr_state_change"): + command += " nbr-state-change" + return command + + +def _tmplt_snmp_server_traps_ospfv3(config_data): + command = "snmp-server enable traps ospfv3" + el = config_data["traps"]["ospfv3"] + if el.get("if_config_error"): + command += " if-config-error" + if el.get("if_rx_bad_packet"): + command += " if-rx-bad-packet" + if el.get("if_state_change"): + command += " if-state-change" + if el.get("nbr_state_change"): + command += " nbr-state-change" + if el.get("nbr_restart_helper_status_change"): + command += " nbr-restart-helper-status-change" + if el.get("nssa_translator_status_change"): + command += " nssa-translator-status-change" + if el.get("restart_status_change"): + command += " restart-status-change" + return command + + +def _tmplt_snmp_server_traps_pim(config_data): + command = "snmp-server enable traps pim" + el = config_data["traps"]["pim"] + if el.get("neighbor_loss"): + command += " neighbor-loss" + return command + + +def _tmplt_snmp_server_traps_snmp(config_data): + command = "snmp-server enable traps snmp" + el = config_data["traps"]["snmp"] + if el.get("authentication"): + command += " authentication" + if el.get("link_down"): + command += " link-down" + if el.get("link_up"): + command += " link-up" + return command + + +def _tmplt_snmp_server_traps_snmpConfigManEvent(config_data): + command = "snmp-server enable traps snmpConfigManEvent" + el = config_data["traps"]["snmpConfigManEvent"] + if el.get("arista_config_man_event"): + command += " arista-config-man-event" + return command + + +def _tmplt_snmp_server_traps_switchover(config_data): + command = "snmp-server enable traps switchover" + el = config_data["traps"]["switchover"] + if el.get("arista_redundancy_switch_over_notif"): + command += " arista-redundancy-switch-over-notif" + return command + + +def _tmplt_snmp_server_traps_test(config_data): + command = "snmp-server enable traps test" + el = config_data["traps"]["test"] + if el.get("arista_test_notification"): + command += " arista-test-notification" + return command + + +def _tmplt_snmp_server_traps_vrrp(config_data): + command = "snmp-server enable traps vrrp" + el = config_data["traps"]["vrrp"] + if el.get("trap_new_master"): + command += " trap-new-master" + return command + + +def _tmplt_snmp_server_engineid(config_data): + command = [] + cmd = "snmp-server engineID" + el = config_data["engineid"] + if el.get("local"): + c = cmd + " local " + el["local"] + command.append(c) + if el.get("remote"): + c = cmd + " remote " + el["remote"]["host"] + if el["remote"].get("udp_port"): + c += " udp-port " + str(el["remote"]["udp_port"]) + if el["remote"].get("id"): + c += " " + el["remote"]["id"] + command.append(c) + return command + + +def _tmplt_snmp_server_extension(config_data): + command = "snmp-server extension " + command += config_data["extension"]["root_oid"] + command += " " + config_data["extension"]["script_location"] + if config_data["extension"].get("oneshot"): + command += " one-shot" + return command + + +def _tmplt_snmp_server_groups(config_data): + command = "snmp-server group " + config_data["groups"]["group"] + el = config_data["groups"] + command += " " + el["version"] + if el.get("auth_privacy"): + command += " " + el["auth_privacy"] + for param in ["context", "read", "write", "notify"]: + if el.get(param): + command += " " + param + " " + el[param] + return command + + +def _tmplt_snmp_server_hosts(config_data): + el = list(config_data["hosts"].values())[0] + command = "snmp-server host " + el["host"] + if el.get("vrf"): + command += " vrf" + el["vrf"] + if el.get("informs"): + command += " informs" + if el.get("traps"): + command += " traps" + if el.get("version"): + command += " version " + el["version"] + if el.get("user"): + command += " " + el["user"] + if el.get("udp_port"): + command += " udp-port " + str(el["udp_port"]) + return command + + +def _tmplt_snmp_server_acls(config_data): + command = "snmp-server " + config_data["acls"]["afi"] + " access-list " + el = config_data["acls"] + command += el["acl"] + if el.get("vrf"): + command += " vrf " + el["vrf"] + return command + + +def _tmplt_snmp_server_vrfs(config_data): + command = "snmp-server vrf " + config_data["vrfs"]["vrf"] + el = config_data["vrfs"] + if el.get("local_interface"): + command += " local-interface " + el["local_interface"] + return command + + +def _tmplt_snmp_server_users_auth(config_data): + el = config_data["users"] + command = "snmp-server user " + el["user"] + " " + el["group"] + if el.get("remote"): + command += " remote " + el["remote"] + if el.get("udp_port"): + command += " udp-port " + str(el["udp_port"]) + command += " " + el["version"] + if el.get("auth"): + command += ( + " " + el["auth"]["algorithm"] + " " + el["auth"]["auth_passphrase"] + ) + if el["auth"].get("encryption"): + command += ( + " priv " + + el["auth"]["encryption"] + + " " + + el["auth"]["priv_passphrase"] + ) + return command + + +def _tmplt_snmp_server_users_localized(config_data): + el = config_data["users"] + command = "snmp-server user " + el["user"] + " " + el["group"] + if el.get("remote"): + command += " remote " + el["remote"] + if el.get("udp_port"): + command += " udp-port " + str(el["udp_port"]) + command += " " + el["version"] + if el.get("localized"): + command += " localized " + el["localized"]["engineid"] + el = el["localized"] + command += " " + el["algorithm"] + " " + el["auth_passphrase"] + if el.get("encryption"): + command += ( + " priv " + el["encryption"] + " " + el["priv_passphrase"] + ) + return command + + +class Snmp_serverTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Snmp_serverTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "chassis_id", + "getval": re.compile( + r""" + \s*snmp-server\schassis-id + \s*(?P<id>\S+)* + $""", + re.VERBOSE, + ), + "setval": 'snmp-server chassis-id {{ chassis_id }}', + "result": { + "chassis_id": "{{ id }}", + }, + }, + { + "name": "communities_ipv6_acl", + "getval": re.compile( + r""" + \s*snmp-server\scommunity + \s+(?P<comm>\S+) + \s*(?P<view>view\s\S+)* + \s*(?P<access>ro|rw)* + \s*(?P<acl>ipv6\s\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_ipv6_comm, + "compval": "communities", + "result": { + "communities": { + "{{ comm }}": { + "name": "{{ comm }}", + "acl_v6": "{{ acl.split(" ")[1] }}", + "view": "{{ view.split(" ")[1] if view is defined }}", + "ro": '{{ True if access == "ro" }}', + "rw": '{{ True if access == "rw" }}', + }, + }, + }, + }, + { + "name": "communities_ipv4_acl", + "getval": re.compile( + r""" + \s*snmp-server\scommunity + \s+(?P<comm>\S+) + \s*(?P<view>view\s\S+)* + \s*(?P<access>ro|rw)* + \s*(?P<acl>\S+) + *$""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_ipv4_comm, + "compval": "communities", + "result": { + "communities": { + "{{ comm }}": { + "name": "{{ comm }}", + "acl_v4": '{{ acl if acl != "ipv6" }}', + "view": "{{ view.split(" ")[1] if view is defined }}", + "ro": '{{ True if access == "ro" }}', + "rw": '{{ True if access == "rw" }}', + }, + }, + }, + }, + { + "name": "contact", + "getval": re.compile( + r""" + \s*snmp-server\scontact + \s+(?P<name>.+) + *$""", + re.VERBOSE, + ), + "setval": 'snmp-server contact {{ contact }}', + "compval": "contact", + "result": { + "contact": "{{ name }}", + }, + }, + { + "name": "traps.bgp", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sbgp + \s*(?P<trap1>arista-backward-transition)* + \s*(?P<trap2>arista-established)* + \s*(?P<trap3>backward-transition)* + \s*(?P<trap4>established)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_bgp, + "result": { + "traps": { + "bgp": { + "arista_backward_transition": "{{ True if trap1 is defined }}", + "arista_established": "{{ True if trap2 is defined }}", + "backward_transition": "{{ True if trap3 is defined }}", + "established": "{{ True if trap4 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined and trap3 is undefined and trap4 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.bridge", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sbridge + \s*(?P<trap1>arista-mac-age)* + \s*(?P<trap2>arista-mac-learn)* + \s*(?P<trap3>arista-mac-move)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_bridge, + "result": { + "traps": { + "bridge": { + "enabled": "{{ True if trap1 is undefined and trap2 is undefined and trap3 is undefined }}", + "arista_mac_age": "{{ True if trap1 is defined }}", + "arista_mac_learn": "{{ True if trap2 is defined }}", + "arista_mac_move": "{{ True if trap3 is defined }}", + }, + }, + }, + }, + + { + "name": "traps.capacity", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\scapacity + \s*(?P<trap1>arista-hardware-utilization-alert)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_capacity, + "result": { + "traps": { + "capacity": { + "arista_hardware_utilization_alert": "{{ True if trap1 is defined }}", + "enabled": "{{ True if trap1 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.entity", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sentity + \s*(?P<trap1>arista-ent-sensor-alarm)* + \s*(?P<trap2>ent-config-change)* + \s*(?P<trap3>ent-state-oper)* + \s*(?P<trap4>ent-state-oper-disabled)* + \s*(?P<trap5>ent-state-oper-enabled)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_entity, + "result": { + "traps": { + "entity": { + "arista_ent_sensor_alarm": "{{ True if trap1 is defined }}", + "ent_config_change": "{{ True if trap2 is defined }}", + "ent_state_oper": "{{ True if trap3 is defined }}", + "ent_state_oper_disabled": "{{ True if trap4 is defined }}", + "ent_state_oper_enabled": "{{ True if trap4 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined and trap3 is undefined\ + and trap4 is undefined and trap5 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.external_alarm", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sexternal-alarm + \s*(?P<trap1>arista-external-alarm-asserted-notif)* + \s*(?P<trap2>arista-external-alarm-deasserted-notif)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_external_alarm, + "result": { + "traps": { + "external_alarm": { + "arista_external_alarm_asserted_notif": "{{ True if trap1 is defined }}", + "arista_external_alarm_deasserted_notif": "{{ True if trap2 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.isis", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sisis + \s*(?P<trap1>adjacency-change)* + \s*(?P<trap2>area-mismatch)* + \s*(?P<trap3>attempt-to-exceed-max-sequence)* + \s*(?P<trap4>authentication-type-failure)* + \s*(?P<trap5>database-overload)* + \s*(?P<trap6>own-lsp-purge)* + \s*(?P<trap7>rejected-adjacency)* + \s*(?P<trap8>equence-number-skip)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_isis, + "result": { + "traps": { + "isis": { + "adjacency_change": "{{ True if trap1 is defined }}", + "area_mismatch": "{{ True if trap2 is defined }}", + "attempt_to_exceed_max_sequence": "{{ True if trap3 is defined }}", + "authentication_type_failure": "{{ True if trap4 is defined }}", + "database_overload": "{{ True if trap4 is defined }}", + "own_lsp_purge": "{{ True if trap4 is defined }}", + "rejected_adjacency": "{{ True if trap4 is defined }}", + "sequence_number_skip": "{{ True if trap4 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined and trap3 is undefined and trap4 is undefined\ + and trap5 is undefined and trap6 is undefined and trap7 is undefined and trap8 is undefined }}", + + }, + }, + }, + }, + { + "name": "traps.lldp", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\slldp + \s*(?P<trap1>rem-tables-change)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_lldp, + "result": { + "traps": { + "lldp": { + "rem_tables_change": "{{ True if trap1 is defined }}", + "enabled": "{{ True if trap1 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.mpls_ldp", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\smpls-ldp + \s*(?P<trap1>mpls-ldp-session-down)* + \s*(?P<trap2>mpls-ldp-session-up)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_mpls_ldp, + "result": { + "traps": { + "mpls_ldp": { + "mpls_ldp_session_down": "{{ True if trap1 is defined }}", + "mpls_ldp_session_up": "{{ True if trap2 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.msdp", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\smsdp + \s*(?P<trap1>backward-transition)* + \s*(?P<trap2>established)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_msdp, + "result": { + "traps": { + "msdp": { + "backward_transition": "{{ True if trap1 is defined }}", + "established": "{{ True if trap2 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.ospf", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sospf + \s*(?P<trap1>if-auth-failure)* + \s*(?P<trap2>if-config-error)* + \s*(?P<trap3>if-state-change)* + \s*(?P<trap4>nbr-state-change)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_ospf, + "result": { + "traps": { + "ospf": { + "if_config_error": "{{ True if trap2 is defined }}", + "if_auth_failure": "{{ True if trap1 is defined }}", + "if_state_change": "{{ True if trap3 is defined }}", + "nbr_state_change": "{{ True if trap4 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined and trap3 is undefined and trap4 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.ospfv3", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sospfv3 + \s*(?P<trap1>if-config-error)* + \s*(?P<trap2>if-rx-bad-packet)* + \s*(?P<trap3>if-state-change)* + \s*(?P<trap4>nbr-restart-helper-status-change)* + \s*(?P<trap5>nbr-state-change)* + \s*(?P<trap6>nssa-translator-status-change)* + \s*(?P<trap7>restart-status-change)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_ospfv3, + "result": { + "traps": { + "ospfv3": { + "if_config_error": "{{ True if trap1 is defined }}", + "if_rx_bad_packet": "{{ True if trap2 is defined }}", + "if_state_change": "{{ True if trap3 is defined }}", + "nbr_state_change": "{{ True if trap5 is defined }}", + "nbr_restart_helper_status_change": "{{ True if trap4 is defined }}", + "nssa_translator_status_change": "{{ True if trap6 is defined }}", + "restart_status_change": "{{ True if trap7 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined and trap3 is undefined and trap4 is undefined\ + and trap5 is undefined and trap6 is undefined and trap7 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.pim", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\spim + \s*(?P<trap1>neighbor-loss)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_pim, + "result": { + "traps": { + "pim": { + "neighbor_loss": "{{ True if trap1 is defined }}", + "enabled": "{{ True if trap1 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.snmp", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\ssnmp + \s*(?P<trap1>authentication)* + \s*(?P<trap2>link-down)* + \s*(?P<trap3>link-up)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_snmp, + "result": { + "traps": { + "snmp": { + "authentication": "{{ True if trap1 is defined }}", + "link_down": "{{ True if trap2 is defined }}", + "link_up": "{{ True if trap3 is defined }}", + "enabled": "{{ True if trap1 is undefined and trap2 is undefined and trap3 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.snmpConfigManEvent", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\ssnmpConfigManEvent + \s*(?P<trap1>arista-config-man-event)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_snmpConfigManEvent, + "result": { + "traps": { + "snmpConfigManEvent": { + "arista_config_man_event": "{{ True if trap1 is defined }}", + "enabled": "{{ True if trap1 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.switchover", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\sswitchover + \s*(?P<trap1>arista-redundancy-switch-over-notif)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_switchover, + "result": { + "traps": { + "switchover": { + "arista_redundancy_switch_over_notif": "{{ True if trap1 is defined }}", + "enabled": "{{ True if trap1 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.test", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\stest + \s*(?P<trap1>arista-test-notification)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_test, + "result": { + "traps": { + "test": { + "arista_test_notification": "{{ True if trap1 is defined }}", + "enabled": "{{ True if trap1 is undefined }}", + }, + }, + }, + }, + { + "name": "traps.vrrp", + "getval": re.compile( + r""" + \s*snmp-server\senable\straps\svrrp + \s*(?P<trap1>trap-new-master)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_traps_vrrp, + "result": { + "traps": { + "vrrp": { + "trap_new_master": "{{ True if trap1 is defined }}", + "enabled": "{{ True if trap1 is undefined }}", + }, + }, + }, + }, + { + "name": "engineid", + "getval": re.compile( + r""" + \s*snmp-server\sengineID + \s*(?P<local>local \S+)* + \s*(?P<remote>remote \S+)* + \s*(?P<udp>udp-port\s\d+)* + \s*(?P<id>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_engineid, + "result": { + "engineid": { + "local": "{{ local.split(" ")[1] if local is defined }}", + "remote": { + "host": "{{ remote.split(" ")[1] if remote is defined }}", + "id": "{{ id }}", + "udp_port": "{{ udp.split(" ")[1] if udp is defined }}", + }, + }, + }, + }, + { + "name": "extension", + "getval": re.compile( + r""" + \s*snmp-server\sextension + \s*(?P<oid>\.\S+|str)* + \s*(?P<script>\S+)* + \s*(?P<oneshot>one-shot)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_extension, + "remval": "snmp-server extension {{ extension.root_oid.lstrip('0') }} {{ extension.script_location }}", + "result": { + "extension": { + "root_oid": "{{ oid }}", + "script_location": "{{ script }}", + "oneshot": "{{ True if oneshot is defined }}", + }, + }, + }, + { + "name": "groups", + "getval": re.compile( + r""" + \s*snmp-server\sgroup + \s*(?P<name>\S+)* + \s*(?P<version>v1|v2c|v3\sauth|v3\snoauth|v3\spriv)* + \s*(?P<context>context\s\S+)* + \s*(?P<read>read\s\S+)* + \s*(?P<write>write\s\S+)* + \s*(?P<notify>notify\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_groups, + "result": { + "groups": { + "{{ name }}": { + "group": "{{ name }}", + "version": "{{ version.split(" ")[0] }}", + "auth_privacy": '{{ version.split(" ")[1] if "v3" in version }}', + "context": "{{ context.split(" ")[1] if context is defined }}", + "notify": "{{ notify.split(" ")[1] if notify is defined }}", + "read": "{{ read.split(" ")[1] if read is defined }}", + "write": "{{ write.split(" ")[1] if write is defined }}", + }, + }, + }, + }, + { + "name": "hosts", + "getval": re.compile( + r""" + \s*snmp-server\shost + \s(?P<name>\S+) + \s*(?P<vrf>vrf\s\S+)* + \s*(?P<msg_inf1>informs)* + \s*(?P<msg_tr1>traps)* + \s*(version)* + \s*(?P<version>1|2c|3\sauth|3\snoauth|3\spriv)* + \s*(?P<msg_inf2>informs)* + \s*(?P<msg_tr2>traps)* + \s*(?P<comm>\S+)* + \s*(?P<udp>udp-port)*? + \s*(?P<port>\d+)*? + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_hosts, + "result": { + "hosts": { + '{{ name, comm|d(""), version|d("2c"), msg_inf1|d() or msg_inf2|d(), msg_tr1|d() or msg_tr2|d(), port|d() }}': { + "host": "{{ name }}", + "vrf": "{{ vrf.split(" ")[1] if vrf is defined }}", + "version": '{{ version }}', + "udp_port": "{{ port if udp is defined and port is defined else None }}", + "informs": '{{ True if msg_inf1 is defined or msg_inf2 is defined else None }}', + "traps": '{{ True if msg_tr1 is defined or msg_tr2 is defined else None }}', + "user": "{{ comm }}", + }, + }, + }, + }, + { + "name": "acls", + "getval": re.compile( + r""" + \s*snmp-server\s + \s+(?P<afi>ipv4|ipv6) + \s+access-list + \s+(?P<acl>\S+) + \s*(?P<vrf>vrf\s\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_acls, + "result": { + "acls": { + "{{ afi }}": { + "afi": "{{ afi }}", + "acl": "{{ acl }}", + "vrf": "{{ vrf.split(" ")[1] if vrf is defined }}", + }, + }, + }, + }, + { + "name": "local_interface", + "getval": re.compile( + r""" + \s*snmp-server\slocal-interface + \s+(?P<int>.+) + *$""", + re.VERBOSE, + ), + "setval": "snmp-server local-interface {{ local_interface }}", + "result": { + "local_interface": "{{ int }}", + }, + }, + { + "name": "location", + "getval": re.compile( + r""" + \s*snmp-server\slocation + \s+(?P<loc>.+) + *$""", + re.VERBOSE, + ), + "setval": "snmp-server location {{ location }}", + "result": { + "location": "{{ loc }}", + }, + }, + { + "name": "notification", + "getval": re.compile( + r""" + \s*snmp-server\snotification\slog\sentry\slimit + \s+(?P<num>\d+) + *$""", + re.VERBOSE, + ), + "setval": "snmp-server notification log entry limit {{ notification }}", + "result": { + "notification": "{{ num }}", + }, + }, + { + "name": "objects.mac", + "getval": re.compile( + r""" + \s*snmp-server\sobjects + \s+mac-address-tables + \s+disable + *$""", + re.VERBOSE, + ), + "setval": "snmp-server objects mac-address-tables disable", + "compval": "objects", + "result": { + "objects": { + "mac_address_tables": "{{ True }}", + }, + }, + }, + { + "name": "objects.route", + "getval": re.compile( + r""" + \s*snmp-server\sobjects + \s+route-tables + \s+disable + *$""", + re.VERBOSE, + ), + "setval": "snmp-server objects route-tables disable", + "compval": "objects", + "result": { + "objects": { + "route_address_tables": "{{ True }}", + }, + }, + }, + { + "name": "qos", + "getval": re.compile( + r""" + \s*snmp-server\sqos\sdscp + \s+(?P<num>\d+) + *$""", + re.VERBOSE, + ), + "setval": "snmp-server qos dscp {{ qos }}", + "result": { + "qos": "{{ num }}", + }, + }, + { + "name": "qosmib", + "getval": re.compile( + r""" + \s*snmp-server\sqosmib\scounter-interval + \s+(?P<num>\d+) + *$""", + re.VERBOSE, + ), + "setval": "snmp-server qosmib counter-interval {{ qosmib }}", + "result": { + "qosmib": "{{ num }}", + }, + }, + { + "name": "transmit", + "getval": re.compile( + r""" + \s*snmp-server\stransmit\smax-size + \s+(?P<num>\d+) + *$""", + re.VERBOSE, + ), + "setval": "snmp-server transmit max-size {{ transmit }}", + "result": { + "transmit": "{{ num }}", + }, + }, + { + "name": "transport", + "getval": re.compile( + r""" + \s*snmp-server\stransport\stcp + *$""", + re.VERBOSE, + ), + "setval": "snmp-server transport tcp", + "result": { + "transport": '{{ "tcp" }}', + }, + }, + { + "name": "views", + "getval": re.compile( + r""" + \s*snmp-server\sview + \s+(?P<name>\S+) + \s+(?P<mib>\S+) + \s+(?P<action>excluded|included) + *$""", + re.VERBOSE, + ), + "setval": "snmp-server view {{ views.view }} {{ views.mib }} {{ views.action }}", + "result": { + "views": { + "{{ name }}": { + "view": "{{ name }}", + "mib": "{{ mib }}", + "action": "{{ actions }}", + }, + }, + }, + }, + { + "name": "vrfs", + "getval": re.compile( + r""" + \s*snmp-server\svrf + \s+(?P<name>\S+) + \s*(?P<int>local-interface\s.+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_vrfs, + "result": { + "vrfs": { + "{{ name }}": { + "vrf": "{{ name }}", + "local_interface": "{{ int.split(" ")[1] }}", + }, + }, + }, + }, + { + "name": "users.auth", + "getval": re.compile( + r""" + \s*snmp-server\suser + \s*(?P<name>\S+)* + \s*(?P<group>\S+)* + \s*(?P<rem>remote\s\S+)* + \s*(?P<udp>udp-port\s\d+)* + \s*(?P<version>v1|v2c|v3)* + \s*(?P<auth>auth)* + \s*(?P<algo>\S+)* + \s*(?P<pass>\S+)* + \s*(?P<priv>priv)* + \s*(?P<enc>\S+)* + \s*(?P<privpass>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_users_auth, + "result": { + "users": { + "{{ name }}": { + "user": "{{ host }}", + "group": "{{ group }}", + "remote": "{{ rem.split(" ")[1] if rem is defined }}", + "version": '{{ version }}', + "auth": { + "algorithm": "{{ algo }}", + "auth_passphrase": "{{ pass }}", + "encryption": "{{ enc }}", + "priv_passphrase": "{{ privpass }}", + }, + "udp_port": "{{ udp.split(" ")[1] if udp is defined }}", + }, + }, + }, + }, + { + "name": "users.localized", + "getval": re.compile( + r""" + \s*snmp-server\suser + \s*(?P<name>\S+)* + \s*(?P<group>\S+)* + \s*(?P<rem>remote\s\S+)* + \s*(?P<udp>udp-port\s\d+)* + \s*(?P<version>v1|v2c|v3)* + \s*(?P<localized>localized \S+)* + \s*(?P<algo>\S+)* + \s*(?P<pass>\S+)* + \s*(?P<priv>priv)* + \s*(?P<enc>\S+)* + \s*(?P<privpass>\S+)* + $""", + re.VERBOSE, + ), + "setval": _tmplt_snmp_server_users_localized, + "result": { + "users": { + "{{ name }}": { + "user": "{{ host }}", + "group": "{{ group }}", + "remote": "{{ rem.split(" ")[1] if rem is defined }}", + "version": '{{ version }}', + "localized": { + "engineid": "{{ localized.split(" ")[1] }}", + "algorithm": "{{ algo }}", + "auth_passphrase": "{{ pass }}", + "encryption": "{{ enc }}", + "priv_passphrase": "{{ privpass }}", + }, + "udp_port": "{{ udp.split(" ")[1] if udp is defined }}", + }, + }, + }, + }, + ] + # fmt: on diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/utils/__init__.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/utils/__init__.py diff --git a/ansible_collections/arista/eos/plugins/module_utils/network/eos/utils/utils.py b/ansible_collections/arista/eos/plugins/module_utils/network/eos/utils/utils.py new file mode 100644 index 000000000..260aa1996 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/module_utils/network/eos/utils/utils.py @@ -0,0 +1,84 @@ +# -*- 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) + +# utils + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +def get_interface_number(name): + digits = "" + for char in name: + if char.isdigit() or char in "/.": + digits += char + return digits + + +def normalize_interface(name): + """Return the normalized interface name""" + if not name: + return None + + if name.lower().startswith("et"): + if_type = "Ethernet" + elif name.lower().startswith("lo"): + if_type = "Loopback" + elif name.lower().startswith("ma"): + if_type = "Management" + elif name.lower().startswith("po"): + if_type = "Port-Channel" + elif name.lower().startswith("tu"): + if_type = "Tunnel" + elif name.lower().startswith("vl"): + if_type = "Vlan" + elif name.lower().startswith("vx"): + if_type = "Vxlan" + else: + if_type = None + + number_list = name.split(" ") + if len(number_list) == 2: + number = number_list[-1].strip() + else: + number = get_interface_number(name) + + if if_type: + proper_interface = if_type + number + else: + proper_interface = name + + return proper_interface + + +def vlan_range_to_list(vlans): + result = [] + if vlans: + if isinstance(vlans, str): + vlans = vlans.split(",") + for part in vlans: + if part == "none": + break + if "-" in part: + a, b = part.split("-") + a, b = int(a), int(b) + result.extend(range(a, b + 1)) + else: + a = int(part) + result.append(a) + return numerical_sort(result) + return result + + +def numerical_sort(string_int_list): + """Sorts list of integers that are digits in numerical order.""" + as_int_list = [] + + for vlan in string_int_list: + as_int_list.append(int(vlan)) + as_int_list.sort() + return list(set(as_int_list)) diff --git a/ansible_collections/arista/eos/plugins/modules/__init__.py b/ansible_collections/arista/eos/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/__init__.py diff --git a/ansible_collections/arista/eos/plugins/modules/eos_acl_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_acl_interfaces.py new file mode 100644 index 000000000..e609a7c09 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_acl_interfaces.py @@ -0,0 +1,423 @@ +#!/usr/bin/python +# -*- 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 module file for eos_acl_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_acl_interfaces +short_description: ACL interfaces resource module +description: +- This module manages adding and removing Access Control Lists (ACLs) from interfaces + on devices running EOS software. +version_added: 1.0.0 +author: GomathiSelvi S (@GomathiselviS) +options: + config: + description: A dictionary of ACL options for interfaces. + type: list + elements: dict + suboptions: + name: + description: + - Name/Identifier for the interface. + type: str + required: true + access_groups: + type: list + elements: dict + description: + - Specifies ACLs attached to the interfaces. + suboptions: + afi: + description: + - Specifies the AFI for the ACL(s) to be configured on this interface. + type: str + choices: + - ipv4 + - ipv6 + required: true + acls: + type: list + description: + - Specifies the ACLs for the provided AFI. + elements: dict + suboptions: + name: + description: + - Specifies the name of the IPv4/IPv4 ACL for the interface. + type: str + required: true + direction: + description: + - Specifies the direction of packets that the ACL will be applied + on. + type: str + choices: + - in + - out + required: true + running_config: + description: + - The module, by default, will connect to the remote device and retrieve the current + running-config to use as a base for comparing against the contents of source. + There are times when it is not desirable to have the task get the current running-config + for every task in a playbook. The I(running_config) argument allows the implementer + to pass in the configuration to use as the base config for comparison. This + value of this option should be the output received from device by executing + command + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - parsed + - rendered + default: merged + +""" +EXAMPLES = """ +# Using Merged + +# Before state: +# ------------- +# +# eos#sh running-config | include interface|access-group +# interface Ethernet1 +# interface Ethernet2 +# interface Ethernet3 + +- name: Merge module attributes of given access-groups + arista.eos.eos_acl_interfaces: + config: + - name: Ethernet2 + access_groups: + - afi: ipv4 + acls: + name: acl01 + direction: in + - afi: ipv6 + acls: + name: acl03 + direction: out + state: merged + +# Commands Fired: +# --------------- +# +# interface Ethernet2 +# ip access-group acl01 in +# ipv6 access-group acl03 out + +# After state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Loopback888 +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 in +# ipv6 access-group acl03 out +# interface Ethernet3 + + +# Using Replaced + +# Before state: +# ------------- +# +# eos#sh running-config | include interface|access-group +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 in +# ipv6 access-group acl03 out +# interface Ethernet3 +# ip access-group acl01 in + +- name: Replace module attributes of given access-groups + arista.eos.eos_acl_interfaces: + config: + - name: Ethernet2 + access_groups: + - afi: ipv4 + acls: + name: acl01 + direction: out + state: replaced + +# Commands Fired: +# --------------- +# +# interface Ethernet2 +# no ip access-group acl01 in +# no ipv6 access-group acl03 out +# ip access-group acl01 out + +# After state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Loopback888 +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 out +# interface Ethernet3 +# ip access-group acl01 in + + +# Using Overridden + +# Before state: +# ------------- +# +# eos#sh running-config | include interface|access-group +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 in +# ipv6 access-group acl03 out +# interface Ethernet3 +# ip access-group acl01 in + +- name: Override module attributes of given access-groups + arista.eos.eos_acl_interfaces: + config: + - name: Ethernet2 + access_groups: + - afi: ipv4 + acls: + name: acl01 + direction: out + state: overridden + +# Commands Fired: +# --------------- +# +# interface Ethernet2 +# no ip access-group acl01 in +# no ipv6 access-group acl03 out +# ip access-group acl01 out +# interface Ethernet3 +# no ip access-group acl01 in + +# After state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Loopback888 +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 out +# interface Ethernet3 + + +# Using Deleted + +# Before state: +# ------------- +# +# eos#sh running-config | include interface|access-group +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 in +# ipv6 access-group acl03 out +# interface Ethernet3 +# ip access-group acl01 out + +- name: Delete module attributes of given access-groups + arista.eos.eos_acl_interfaces: + config: + - name: Ethernet2 + access_groups: + - afi: ipv4 + acls: + name: acl01 + direction: in + - afi: ipv6 + acls: + name: acl03 + direction: out + state: deleted + +# Commands Fired: +# --------------- +# +# interface Ethernet2 +# no ip access-group acl01 in +# no ipv6 access-group acl03 out + +# After state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Loopback888 +# interface Ethernet1 +# interface Ethernet2 +# interface Ethernet3 +# ip access-group acl01 out + + +# Before state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 in +# ipv6 access-group acl03 out +# interface Ethernet3 +# ip access-group acl01 out + +- name: Delete module attributes of given access-groups from ALL Interfaces + arista.eos.eos_acl_interfaces: + config: + state: deleted + +# Commands Fired: +# --------------- +# +# interface Ethernet2 +# no ip access-group acl01 in +# no ipv6 access-group acl03 out +# interface Ethernet3 +# no ip access-group acl01 out + +# After state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Loopback888 +# interface Ethernet1 +# interface Ethernet2 +# interface Ethernet3 + +# Before state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 in +# ipv6 access-group acl03 out +# interface Ethernet3 +# ip access-group acl01 out + +- name: Delete acls under afi + arista.eos.eos_acl_interfaces: + config: + - name: Ethernet3 + access_groups: + - afi: ipv4 + - name: Ethernet2 + access_groups: + - afi: ipv6 + state: deleted + +# Commands Fired: +# --------------- +# +# interface Ethernet2 +# no ipv6 access-group acl03 out +# interface Ethernet3 +# no ip access-group acl01 out + +# After state: +# ------------- +# +# eos#sh running-config | include interface| access-group +# interface Loopback888 +# interface Ethernet1 +# interface Ethernet2 +# ip access-group acl01 in +# interface Ethernet3 + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - interface Ethernet2 + - ip access-group acl01 in + - ipv6 access-group acl03 out + - interface Ethernet3 + - ip access-group acl01 out +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.acl_interfaces.acl_interfaces import ( + Acl_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.acl_interfaces.acl_interfaces import ( + Acl_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Acl_interfacesArgs.argument_spec, + supports_check_mode=True, + ) + + result = Acl_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_acls.py b/ansible_collections/arista/eos/plugins/modules/eos_acls.py new file mode 100644 index 000000000..95c961d5a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_acls.py @@ -0,0 +1,904 @@ +#!/usr/bin/python +# -*- 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 module file for eos_acls +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_acls +short_description: ACLs resource module +description: This module manages the IP access-list attributes of Arista EOS interfaces. +version_added: 1.0.0 +author: Gomathiselvi S (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +options: + config: + description: A dictionary of IP access-list options + type: list + elements: dict + suboptions: + afi: + description: + - The Address Family Indicator (AFI) for the Access Control Lists (ACL). + type: str + required: true + choices: + - ipv4 + - ipv6 + acls: + description: + - A list of Access Control Lists (ACL). + type: list + elements: dict + suboptions: + standard: + description: standard access-list or not + type: bool + name: + description: Name of the acl-list + type: str + required: true + aces: + description: Filtering data + type: list + elements: dict + suboptions: + sequence: + description: sequence number for the ordered list of rules + type: int + remark: + description: Specify a comment + type: str + fragment_rules: + description: Add fragment rules + type: bool + grant: + description: Action to be applied on the rule + type: str + choices: + - permit + - deny + line: + description: For fact gathering, any ACE that is not fully parsed, + while show up as a value of this attribute. + type: str + aliases: + - ace + protocol: + description: + - Specify the protocol to match. + - Refer to vendor documentation for valid values. + type: str + vlan: + description: Vlan options + type: str + protocol_options: + description: All the possible sub options for the protocol chosen. + type: dict + suboptions: + tcp: + description: Options for tcp protocol. + type: dict + suboptions: + flags: + description: Match TCP packet flags + type: dict + suboptions: + ack: + description: Match on the ACK bit + type: bool + established: + description: Match established connections + type: bool + fin: + description: Match on the FIN bit + type: bool + psh: + description: Match on the PSH bit + type: bool + rst: + description: Match on the RST bit + type: bool + syn: + description: Match on the SYN bit + type: bool + urg: + description: Match on the URG bit + type: bool + icmp: + description: + - Internet Control Message Protocol settings. + type: dict + suboptions: + administratively_prohibited: + description: Administratively prohibited + type: bool + alternate_address: + description: Alternate address + type: bool + conversion_error: + description: Datagram conversion + type: bool + dod_host_prohibited: + description: Host prohibited + type: bool + dod_net_prohibited: + description: Net prohibited + type: bool + echo: + description: Echo (ping) + type: bool + echo_reply: + description: Echo reply + type: bool + general_parameter_problem: + description: Parameter problem + type: bool + host_isolated: + description: Host isolated + type: bool + host_precedence_unreachable: + description: Host unreachable for precedence + type: bool + host_redirect: + description: Host redirect + type: bool + host_tos_redirect: + description: Host redirect for TOS + type: bool + host_tos_unreachable: + description: Host unreachable for TOS + type: bool + host_unknown: + description: Host unknown + type: bool + host_unreachable: + description: Host unreachable + type: bool + information_reply: + description: Information replies + type: bool + information_request: + description: Information requests + type: bool + mask_reply: + description: Mask replies + type: bool + mask_request: + description: Mask requests + type: bool + message_code: + description: ICMP message code + type: int + message_type: + description: ICMP message type + type: int + mobile_redirect: + description: Mobile host redirect + type: bool + net_redirect: + description: Network redirect + type: bool + net_tos_redirect: + description: Net redirect for TOS + type: bool + net_tos_unreachable: + description: Network unreachable for TOS + type: bool + net_unreachable: + description: Net unreachable + type: bool + network_unknown: + description: Network unknown + type: bool + no_room_for_option: + description: Parameter required but no room + type: bool + option_missing: + description: Parameter required but not present + type: bool + packet_too_big: + description: Fragmentation needed and DF set + type: bool + parameter_problem: + description: All parameter problems + type: bool + port_unreachable: + description: Port unreachable + type: bool + precedence_unreachable: + description: Precedence cutoff + type: bool + protocol_unreachable: + description: Protocol unreachable + type: bool + reassembly_timeout: + description: Reassembly timeout + type: bool + redirect: + description: All redirects + type: bool + router_advertisement: + description: Router discovery advertisements + type: bool + router_solicitation: + description: Router discovery solicitations + type: bool + source_quench: + description: Source quenches + type: bool + source_route_failed: + description: Source route failed + type: bool + time_exceeded: + description: All time exceededs + type: bool + timestamp_reply: + description: Timestamp replies + type: bool + timestamp_request: + description: Timestamp requests + type: bool + traceroute: + description: Traceroute + type: bool + ttl_exceeded: + description: TTL exceeded + type: bool + unreachable: + description: All unreachables + type: bool + message_num: + description: icmp msg type number. + type: int + icmpv6: + description: Options for icmpv6. + type: dict + suboptions: + address_unreachable: + description: address unreachable + type: bool + beyond_scope: + description: beyond_scope + type: bool + echo_reply: + description: echo_reply + type: bool + echo_request: + description: echo reques + type: bool + erroneous_header: + description: erroneous header + type: bool + fragment_reassembly_exceeded: + description: fragment_reassembly_exceeded + type: bool + hop_limit_exceeded: + description: hop limit exceeded + type: bool + neighbor_advertisement: + description: neighbor advertisement + type: bool + neighbor_solicitation: + description: neighbor_solicitation + type: bool + no_admin: + description: no admin + type: bool + no_route: + description: no route + type: bool + packet_too_big: + description: packet too big + type: bool + parameter_problem: + description: parameter problem + type: bool + port_unreachable: + description: port unreachable + type: bool + redirect_message: + description: redirect message + type: bool + reject_route: + description: reject route + type: bool + router_advertisement: + description: router_advertisement + type: bool + router_solicitation: + description: router_solicitation + type: bool + source_address_failed: + description: source_address_failed + type: bool + source_routing_error: + description: source_routing_error + type: bool + time_exceeded: + description: time_exceeded + type: bool + unreachable: + description: unreachable + type: bool + unrecognized_ipv6_option: + description: unrecognized_ipv6_option + type: bool + unrecognized_next_header: + description: unrecognized_next_header + type: bool + ip: + description: Internet Protocol. + type: dict + suboptions: + nexthop_group: + description: Nexthop-group name. + type: str + ipv6: + description: Internet V6 Protocol. + type: dict + suboptions: + nexthop_group: + description: Nexthop-group name. + type: str + source: + description: The packet's source address + type: dict + suboptions: + address: + description: dotted decimal notation of IP address + type: str + wildcard_bits: + description: Source wildcard bits + type: str + subnet_address: + description: A subnet address + type: str + host: + description: Host IP address + type: str + any: + description: Rule matches all source addresses + type: bool + port_protocol: + description: Specify source port/protocoli, along with operator. + (comes with tcp/udp). + type: dict + destination: + description: The packet's destination address + type: dict + suboptions: + address: + description: dotted decimal notation of IP address + type: str + wildcard_bits: + description: Source wildcard bits + type: str + subnet_address: + description: A subnet address + type: str + host: + description: Host IP address + type: str + any: + description: Rule matches all source addresses + type: bool + port_protocol: + description: Specify dest port/protocol, along with operator . + (comes with tcp/udp). + type: dict + ttl: + description: Compares the TTL (time-to-live) value in the packet to + a specified value + type: dict + suboptions: + eq: + description: Match a single TTL value + type: int + lt: + description: Match TTL lesser than this number + type: int + gt: + description: Match TTL greater than this number + type: int + neq: + description: Match TTL not equal to this value + type: int + fragments: + description: Match non-head fragment packets + type: bool + log: + description: Log matches against this rule + type: bool + tracked: + description: Match packets in existing ICMP/UDP/TCP connections + type: bool + hop_limit: + description: Hop limit value. + type: dict + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section access-list). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +- name: Merge provided configuration with device configuration + arista.eos.eos_acls: + config: + - afi: ipv4 + acls: + - name: test1 + aces: + - sequence: 35 + grant: deny + protocol: ospf + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + state: merged + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 35 deny ospf 20.0.0.0/8 any +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +# Using merged + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +- name: Merge to update the given configuration with an existing ace + arista.eos.eos_acls: + config: + - afi: ipv4 + acls: + - name: test1 + aces: + - sequence: 35 + log: true + ttl: + eq: 33 + state: merged + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 35 deny ospf 20.0.0.0/8 any ttl eq 33 log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +# Using replaced + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ! +# ip access-list test3 +# 10 permit ip 35.33.0.0/16 any log +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + + +- name: Replace device configuration with provided configuration + arista.eos.eos_acls: + config: + - afi: ipv4 + acls: + - name: test1 + aces: + - sequence: 35 + grant: permit + protocol: ospf + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + state: replaced + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 35 permit ospf 20.0.0.0/8 any +# ! +# ip access-list test3 +# 10 permit ip 35.33.0.0/16 any log +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + +# Using overridden + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ! +# ip access-list test3 +# 10 permit ip 35.33.0.0/16 any log +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + + +- name: override device configuration with provided configuration + arista.eos.eos_acls: + config: + - afi: ipv4 + acls: + - name: test1 + aces: + - sequence: 35 + grant: permit + protocol: ospf + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + state: overridden + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 35 permit ospf 20.0.0.0/8 any +# ! + +# Using deleted: + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +# ! + +- name: Delete provided configuration + arista.eos.eos_acls: + config: + - afi: ipv4 + acls: + - name: test1 + state: deleted + +# After state: +# ------------ +# +# show running-config | section access-list + +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + +# using gathered + +# ip access-list test1 +# 35 deny ospf 20.0.0.0/8 any +# ip access-list test2 +# 40 permit vlan 55 0xE2 icmpv6 any any log + +- name: Gather the existing configuration + arista.eos.eos_acls: + state: gathered + +# returns: + + +# arista.eos.eos_acls: +# config: +# - afi: "ipv4" +# acls: +# - name: test1 +# aces: +# - sequence: 35 +# grant: "deny" +# protocol: "ospf" +# source: +# subnet_address: 20.0.0.0/8 +# destination: +# any: true +# - afi: "ipv6" +# acls: +# - name: test2 +# aces: +# - sequence: 40 +# grant: "permit" +# vlan: "55 0xE2" +# protocol: "icmpv6" +# log: true +# source: +# any: true +# destination: +# any: true + + +# using rendered + +- name: Delete provided configuration + arista.eos.eos_acls: + config: + - afi: ipv4 + acls: + - name: test1 + aces: + - sequence: 35 + grant: deny + protocol: ospf + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + - afi: ipv6 + acls: + - name: test2 + aces: + - sequence: 40 + grant: permit + vlan: 55 0xE2 + protocol: icmpv6 + log: true + source: + any: true + destination: + any: true + state: rendered + +# returns: + +# ip access-list test1 +# 35 deny ospf 20.0.0.0/8 any +# ip access-list test2 +# 40 permit vlan 55 0xE2 icmpv6 any any log + + +# Using Parsed + +# parsed_acls.cfg + +# ipv6 access-list standard test2 +# 10 permit any log +# ! +# ip access-list test1 +# 35 deny ospf 20.0.0.0/8 any +# 45 remark Run by ansible +# 55 permit tcp any any +# ! + +- name: parse configs + arista.eos.eos_acls: + running_config: "{{ lookup('file', './parsed_acls.cfg') }}" + state: parsed + +# returns +# "parsed": [ +# { +# "acls": [ +# { +# "aces": [ +# { +# "destination": { +# "any": true +# }, +# "grant": "deny", +# "protocol": "ospf", +# "sequence": 35, +# "source": { +# "subnet_address": "20.0.0.0/8" +# } +# }, +# { +# "remark": "Run by ansible", +# "sequence": 45 +# }, +# { +# "destination": { +# "any": true +# }, +# "grant": "permit", +# "protocol": "tcp", +# "sequence": 55, +# "source": { +# "any": true +# } +# } +# ], +# "name": "test1" +# } +# ], +# "afi": "ipv4" +# }, +# { +# "acls": [ +# { +# "aces": [ +# { +# "grant": "permit", +# "log": true, +# "sequence": 10, +# "source": { +# "any": true +# } +# } +# ], +# "name": "test2", +# "standard": true +# } +# ], +# "afi": "ipv6" +# } +# ] + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - ipv6 access-list standard test2 + - 10 permit any log + - ip access-list test1 + - 35 deny ospf 20.0.0.0/8 any + - 45 remark Run by ansible + - 55 permit tcp any any +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.acls.acls import ( + AclsArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.acls.acls import ( + Acls, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=AclsArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Acls(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_banner.py b/ansible_collections/arista/eos/plugins/modules/eos_banner.py new file mode 100644 index 000000000..19a187c4c --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_banner.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_banner +author: Peter Sprygada (@privateip) +short_description: Manage multiline banners on Arista EOS devices +description: +- This will configure both login and motd banners on remote devices running Arista + EOS. It allows playbooks to add or remote banner text from the active running configuration. +version_added: 1.0.0 +notes: +- Tested against Arista EOS 4.24.6F +options: + banner: + description: + - Specifies which banner that should be configured on the remote device. + required: true + choices: + - login + - motd + type: str + text: + description: + - The banner text that should be present in the remote device running configuration. This + argument accepts a multiline string. Requires I(state=present). + type: str + state: + description: + - Specifies whether or not the configuration is present in the current devices + active running configuration. + default: present + type: str + choices: + - present + - absent +""" + +EXAMPLES = """ +- name: configure the login banner + arista.eos.eos_banner: + banner: login + text: | + this is my login banner + that contains a multiline + string + state: present + +- name: remove the motd banner + arista.eos.eos_banner: + banner: motd + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - banner login + - this is my login banner + - that contains a multiline + - string + - EOF +session_name: + description: The EOS config session name used to load the configuration + returned: if changes + type: str + sample: ansible_1479315771 +""" +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + load_config, + run_commands, +) + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + state = module.params["state"] + + if state == "absent" and have.get("text"): + if isinstance(have["text"], string_types): + commands.append("no banner %s" % module.params["banner"]) + elif have["text"].get("loginBanner") or have["text"].get("motd"): + commands.append({"cmd": "no banner %s" % module.params["banner"]}) + + elif state == "present": + if isinstance(have["text"], string_types): + if want["text"] != have["text"]: + commands.append("banner %s" % module.params["banner"]) + commands.extend(want["text"].strip().split("\n")) + commands.append("EOF") + else: + have_text = have["text"].get("loginBanner") or have["text"].get( + "motd", + ) + if have_text: + have_text = have_text.strip() + + if to_text(want["text"]) != have_text or not have_text: + # For EAPI we need to construct a dict with cmd/input + # key/values for the banner + commands.append( + { + "cmd": "banner %s" % module.params["banner"], + "input": want["text"].strip("\n"), + }, + ) + + return commands + + +def map_config_to_obj(module): + output = run_commands(module, ["show banner %s" % module.params["banner"]]) + obj = {"banner": module.params["banner"], "state": "absent"} + if output: + obj["text"] = output[0] + obj["state"] = "present" + return obj + + +def map_params_to_obj(module): + text = module.params["text"] + if text: + text = to_text(text).strip() + + return { + "banner": module.params["banner"], + "text": text, + "state": module.params["state"], + } + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + banner=dict(required=True, choices=["login", "motd"]), + text=dict(), + state=dict(default="present", choices=["present", "absent"]), + ) + + required_if = [("state", "present", ("text",))] + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + if warnings: + result["warnings"] = warnings + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result["commands"] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get("diff") and module._diff: + result["diff"] = {"prepared": response.get("diff")} + result["session_name"] = response.get("session") + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_bgp.py b/ansible_collections/arista/eos/plugins/modules/eos_bgp.py new file mode 100644 index 000000000..a9d01d60a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_bgp.py @@ -0,0 +1,469 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_bgp +author: Nilashish Chakraborty (@NilashishC) +short_description: (deprecated, removed after 2023-01-29) Configure global BGP protocol settings on Arista EOS. +description: +- This module provides configuration management of global BGP parameters on Arista + EOS devices. +version_added: 1.0.0 +deprecated: + alternative: eos_bgp_global + why: Updated module released with more functionality. + removed_at_date: '2023-01-01' +notes: +- Tested against Arista EOS 4.24.6F +options: + config: + description: + - Specifies the BGP related configuration. + type: dict + suboptions: + bgp_as: + description: + - Specifies the BGP Autonomous System (AS) number to configure on the device. + type: int + required: true + router_id: + description: + - Configures the BGP routing process router-id value. + type: str + log_neighbor_changes: + description: + - Enable/disable logging neighbor up/down and reset reason. + type: bool + neighbors: + description: + - Specifies BGP neighbor related configurations. + type: list + elements: dict + suboptions: + neighbor: + description: + - Neighbor router address. + required: true + type: str + remote_as: + description: + - Remote AS of the BGP neighbor to configure. + type: int + required: true + update_source: + description: + - Source of the routing updates. + type: str + password: + description: + - Password to authenticate the BGP peer connection. + type: str + description: + description: + - Neighbor specific description. + type: str + ebgp_multihop: + description: + - Specifies the maximum hop count for EBGP neighbors not on directly connected + networks. + - The range is from 1 to 255. + type: int + peer_group: + description: + - Name of the peer group that the neighbor is a member of. + type: str + timers: + description: + - Specifies BGP neighbor timer related configurations. + type: dict + suboptions: + keepalive: + description: + - Frequency (in seconds) with which the device sends keepalive messages + to its peer. + - The range is from 0 to 3600. + type: int + required: true + holdtime: + description: + - Interval (in seconds) after not receiving a keepalive message that + device declares a peer dead. + - The range is from 3 to 7200. + - Setting this value to 0 will not send keep-alives (hold forever). + type: int + required: true + route_reflector_client: + description: + - Specify a neighbor as a route reflector client. + type: int + remove_private_as: + description: + - Remove the private AS number from outbound updates. + type: bool + enabled: + description: + - Administratively shutdown or enable a neighbor. + type: bool + maximum_prefix: + description: + - Maximum number of prefixes to accept from this peer. + - The range is from 0 to 4294967294. + type: int + redistribute: + description: + - Specifies the redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + protocol: + description: + - Specifies the protocol for configuring redistribute information. + required: true + type: str + choices: [ospf, ospfv3, static, connected, rip, isis] + route_map: + description: + - Specifies the route map reference. + type: str + networks: + description: + - Specify Networks to announce via BGP. + - For operation replace, this option is mutually exclusive with networks option + under address_family. + - For operation replace, if the device already has an address family activated, + this option is not allowed. + type: list + elements: dict + suboptions: + prefix: + description: + - Network ID to announce via BGP. + required: true + type: str + masklen: + description: + - Subnet mask length for the Network to announce(e.g, 8, 16, 24, etc.). + type: int + route_map: + description: + - Route map to modify the attributes. + type: str + address_family: + description: + - Specifies BGP address family related configurations. + type: list + elements: dict + suboptions: + afi: + description: + - Type of address family to configure. + type: str + choices: + - ipv4 + - ipv6 + required: true + redistribute: + description: + - Specifies the redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + protocol: + description: + - Specifies the protocol for configuring redistribute information. + required: true + type: str + choices: + - ospfv3 + - ospf + - isis + - static + - connected + - rip + route_map: + description: + - Specifies the route map reference. + type: str + networks: + description: + - Specify Networks to announce via BGP. + - For operation replace, this option is mutually exclusive with root level + networks option. + type: list + elements: dict + suboptions: + prefix: + description: + - Network ID to announce via BGP. + required: true + type: str + masklen: + description: + - Subnet mask length for the Network to announce(e.g, 8, 16, 24, etc.). + type: int + route_map: + description: + - Route map to modify the attributes. + type: str + neighbors: + description: + - Specifies BGP neighbor related configurations in Address Family configuration + mode. + type: list + elements: dict + suboptions: + neighbor: + description: + - Neighbor router address. + required: true + type: str + activate: + description: + - Enable the Address Family for this Neighbor. + type: bool + default_originate: + description: + - Originate default route to this neighbor. + type: bool + graceful_restart: + description: + - Enable/disable graceful restart mode for this neighbor. + type: bool + weight: + description: + - Assign weight for routes learnt from this neighbor. + - The range is from 0 to 65535 + type: int + operation: + description: + - Specifies the operation to be performed on the BGP process configured on the + device. + - In case of merge, the input configuration will be merged with the existing BGP + configuration on the device. + - In case of replace, if there is a diff between the existing configuration and + the input configuration, the existing configuration will be replaced by the + input configuration for every option that has the diff. + - In case of override, all the existing BGP configuration will be removed from + the device and replaced with the input configuration. + - In case of delete the existing BGP configuration will be removed from the device. + type: str + default: merge + choices: + - merge + - replace + - override + - delete +""" + +EXAMPLES = """ +- name: configure global bgp as 64496 + arista.eos.eos_bgp: + config: + bgp_as: 64496 + router_id: 192.0.2.1 + log_neighbor_changes: true + neighbors: + - neighbor: 203.0.113.5 + remote_as: 64511 + timers: + keepalive: 300 + holdtime: 360 + - neighbor: 198.51.100.2 + remote_as: 64498 + networks: + - prefix: 198.51.100.0 + route_map: RMAP_1 + - prefix: 192.0.2.0 + masklen: 23 + address_family: + - afi: ipv4 + safi: unicast + redistribute: + - protocol: isis + route_map: RMAP_1 + operation: merge +- name: Configure BGP neighbors + arista.eos.eos_bgp: + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.10 + remote_as: 64496 + description: IBGP_NBR_1 + ebgp_multihop: 100 + timers: + keepalive: 300 + holdtime: 360 + - neighbor: 192.0.2.15 + remote_as: 64496 + description: IBGP_NBR_2 + ebgp_multihop: 150 + operation: merge +- name: Configure root-level networks for BGP + arista.eos.eos_bgp: + config: + bgp_as: 64496 + networks: + - prefix: 203.0.113.0 + masklen: 27 + route_map: RMAP_1 + - prefix: 203.0.113.32 + masklen: 27 + route_map: RMAP_2 + operation: merge +- name: Configure BGP neighbors under address family mode + arista.eos.eos_bgp: + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + neighbors: + - neighbor: 203.0.113.10 + activate: yes + default_originate: true + - neighbor: 192.0.2.15 + activate: yes + graceful_restart: true + operation: merge +- name: remove bgp as 64496 from config + arista.eos.eos_bgp: + config: + bgp_as: 64496 + operation: delete +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - router bgp 64496 + - bgp router-id 192.0.2.1 + - bgp log-neighbor-changes + - neighbor 203.0.113.5 remote-as 64511 + - neighbor 203.0.113.5 timers 300 360 + - neighbor 198.51.100.2 remote-as 64498 + - network 198.51.100.0 route-map RMAP_1 + - network 192.0.2.0 mask 255.255.254.0 + - address-family ipv4 + - redistribute isis route-map RMAP_1 + - exit-address-family +""" +from ansible.module_utils._text import to_text + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.cli.config.bgp.process import ( + REDISTRIBUTE_PROTOCOLS, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.providers.module import ( + NetworkModule, +) + + +def main(): + """main entry point for module execution""" + network_spec = { + "prefix": dict(required=True), + "masklen": dict(type="int"), + "route_map": dict(), + } + + redistribute_spec = { + "protocol": dict(choices=list(REDISTRIBUTE_PROTOCOLS), required=True), + "route_map": dict(), + } + + timer_spec = { + "keepalive": dict(type="int", required=True), + "holdtime": dict(type="int", required=True), + } + + neighbor_spec = { + "neighbor": dict(required=True), + "remote_as": dict(type="int", required=True), + "update_source": dict(), + "password": dict(no_log=True), + "enabled": dict(type="bool"), + "description": dict(), + "ebgp_multihop": dict(type="int"), + "timers": dict(type="dict", options=timer_spec), + "peer_group": dict(), + "maximum_prefix": dict(type="int"), + "route_reflector_client": dict(type="int"), + "remove_private_as": dict(type="bool"), + } + + af_neighbor_spec = { + "neighbor": dict(required=True), + "activate": dict(type="bool"), + "default_originate": dict(type="bool"), + "graceful_restart": dict(type="bool"), + "weight": dict(type="int"), + } + + address_family_spec = { + "afi": dict(choices=["ipv4", "ipv6"], required=True), + "networks": dict(type="list", elements="dict", options=network_spec), + "redistribute": dict( + type="list", + elements="dict", + options=redistribute_spec, + ), + "neighbors": dict( + type="list", + elements="dict", + options=af_neighbor_spec, + ), + } + + config_spec = { + "bgp_as": dict(type="int", required=True), + "router_id": dict(), + "log_neighbor_changes": dict(type="bool"), + "neighbors": dict(type="list", elements="dict", options=neighbor_spec), + "address_family": dict( + type="list", + elements="dict", + options=address_family_spec, + ), + "redistribute": dict( + type="list", + elements="dict", + options=redistribute_spec, + ), + "networks": dict(type="list", elements="dict", options=network_spec), + } + + argument_spec = { + "config": dict(type="dict", options=config_spec), + "operation": dict( + default="merge", + choices=["merge", "replace", "override", "delete"], + ), + } + + module = NetworkModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + try: + result = module.edit_config(config_filter="| section bgp") + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_bgp_address_family.py b/ansible_collections/arista/eos/plugins/modules/eos_bgp_address_family.py new file mode 100644 index 000000000..67aaabef7 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_bgp_address_family.py @@ -0,0 +1,1351 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 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 module file for eos_bgp_address_family +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: eos_bgp_address_family +short_description: Manages BGP address family resource module +description: This module configures and manages the attributes of BGP AF on Arista + EOS platforms. +version_added: 1.4.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,eos_platform_options). +options: + config: + description: Configurations for BGP address family. + type: dict + suboptions: + as_number: + description: Autonomous system number. + type: str + address_family: &address_family + description: Enable address family and enter its config mode + type: list + elements: dict + suboptions: + afi: + description: address family. + type: str + choices: ['ipv4', 'ipv6', 'evpn'] + safi: + description: Address family type for ipv4. + type: str + choices: ['labeled-unicast', 'multicast'] + bgp_params: + description: BGP parameters. + type: dict + suboptions: + additional_paths: + description: BGP additional-paths commands + type: str + choices: ['install', 'send', 'receive'] + next_hop_address_family: + description: Next-hop address-family configuration + type: str + choices: ['ipv6'] + next_hop_unchanged: + description: Preserve original nexthop while advertising routes to + eBGP peers. + type: bool + redistribute_internal: + description: Redistribute internal BGP routes. + type: bool + route: + description: Configure route-map for route installation. + type: str + graceful_restart: + description: Enable graceful restart mode. + type: bool + neighbor: + description: Configure routing for a network. + type: list + elements: dict + suboptions: + peer: + type: str + description: Neighbor address/ peer group name. + activate: + description: Activate neighbor in the address family. + type: bool + additional_paths: + description: BGP additional-paths commands. + type: str + choices: ['send', 'receive'] + default_originate: + description: Originate default route to this neighbor. + type: dict + suboptions: + route_map: + description: Route map reference. + type: str + always: + description: Always originate default route to this neighbor. + type: bool + graceful_restart: + description: Enable graceful restart mode. + type: bool + next_hop_address_family: + description: Next-hop address-family configuration + type: str + choices: ['ipv6'] + next_hop_unchanged: + description: Preserve original nexthop while advertising routes to + eBGP peers. + type: bool + prefix_list: + description: Prefix list reference. + type: dict + suboptions: + direction: + description: Configure an inbound/outbound prefix-list. + type: str + choices: ['in', 'out'] + name: + description: prefix list name. + type: str + route_map: + description: Route map reference. + type: dict + suboptions: + direction: + description: Configure an inbound/outbound route-map. + type: str + choices: ['in', 'out'] + name: + description: Route map name. + type: str + weight: + description: Weight to assign. + type: int + encapsulation: + description: Default transport encapsulation for neighbor. Applicable for evpn address-family. + type: dict + suboptions: + transport: + description: MPLS/VXLAN transport. + type: str + choices: ['mpls', 'vxlan'] + source_interface: + description: Source interface to update BGP next hop address. Applicable for mpls transport. + type: str + network: + description: configure routing for network. + type: list + elements: dict + suboptions: + route_map: + description: Route map reference. + type: str + address: + description: network address. + type: str + redistribute: + description: Redistribute routes in to BGP. + type: list + elements: dict + suboptions: + protocol: + description: Routes to be redistributed. + type: str + choices: ['isis', 'ospfv3', 'dhcp'] + route_map: + description: Route map reference. + type: str + isis_level: + description: Applicable for isis routes. Specify isis route level. + type: str + choices: ['level-1', 'level-2', 'level-1-2'] + ospf_route: + description: ospf route options. + type: str + choices: ['internal', 'external', 'nssa_external_1', 'nssa_external_2'] + route_target: + description: Route target. + type: dict + suboptions: + action: + description: Route action. + type: str + choices: ['both', 'import', 'export'] + type: + description: Type of address fmaily + type: str + choices: ['evpn', 'vpn-ipv4', 'vpn-ipv6'] + aliases: ['mode'] + route_map: + description: Name of a route map. + type: str + target: + description: Route Target. + type: str + imported_route: + description: Export routes imported from the same Afi/Safi + type: bool + vrf: + description: name of the VRF in which BGP will be configured. + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section bgp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: [deleted, merged, overridden, replaced, gathered, rendered, parsed] + default: merged + +""" + +EXAMPLES = """ + +# Using merged + +# Before state + +# veos(config)#show running-config | section bgp +# veos(config)# + + - name: Merge provided configuration with device configuration + arista.eos.eos_bgp_address_family: + config: + as_number: "10" + address_family: + - afi: "ipv4" + redistribute: + - protocol: "ospfv3" + ospf_route: "external" + network: + - address: "1.1.1.0/24" + - address: "1.5.1.0/24" + route_map: "MAP01" + - afi: "ipv6" + bgp_params: + additional_paths: "receive" + neighbor: + - peer: "peer2" + default_originate: + always: True + - afi: "ipv6" + redistribute: + - protocol: "isis" + isis_level: "level-2" + route_target: + mode: "export" + target: "33:11" + vrf: "vrft" + state: merged + +# After state: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# bgp additional-paths receive +# neighbor peer2 activate +# neighbor peer2 default-originate always +# ! +# vrf vrft +# address-family ipv6 +# route-target export 33:11 +# redistribute isis level-2 +# veos(config-router-bgp)# + +# Module Execution: + +# "after": { +# "address_family": [ +# { +# "afi": "ipv4", +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "redistribute": [ +# { +# "isis_level": "level-2", +# "protocol": "isis" +# } +# ], +# "route_target": { +# "mode": "export", +# "target": "33:11" +# }, +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "before": {}, +# "changed": true, +# "commands": [ +# "router bgp 10", +# "address-family ipv4", +# "redistribute ospfv3 match external", +# "network 1.1.1.0/24", +# "network 1.5.1.0/24 route-map MAP01", +# "exit", +# "address-family ipv6", +# "neighbor peer2 default-originate always", +# "bgp additional-paths receive", +# "exit", +# "vrf vrft", +# "address-family ipv6", +# "redistribute isis level-2", +# "route-target export 33:11", +# "exit", +# "exit" +# ], + +# Using replaced: + +# Before State: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# bgp additional-paths receive +# neighbor peer2 activate +# neighbor peer2 default-originate always +# ! +# vrf vrft +# address-family ipv6 +# route-target export 33:11 +# redistribute isis level-2 +# veos(config-router-bgp)# +# + + - name: Replace + arista.eos.eos_bgp_address_family: + config: + as_number: "10" + address_family: + - afi: "ipv6" + vrf: "vrft" + redistribute: + - protocol: "ospfv3" + ospf_route: "external" + - afi: "ipv6" + redistribute: + - protocol: "isis" + isis_level: "level-2" + state: replaced + +# After State: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# neighbor peer2 default-originate always +# redistribute isis level-2 +# ! +# vrf vrft +# address-family ipv6 +# redistribute ospfv3 match external +# veos(config-router-bgp)# +# +# +# # Module Execution: +# +# "after": { +# "address_family": [ +# { +# "afi": "ipv4", +# "neighbor": [ +# { +# "activate": true, +# "peer": "1.1.1.1" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "redistribute": [ +# { +# "isis_level": "level-2", +# "protocol": "isis" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ], +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "before": { +# "address_family": [ +# { +# "afi": "ipv4", +# "neighbor": [ +# { +# "activate": true, +# "peer": "1.1.1.1" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "activate": true, +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "redistribute": [ +# { +# "isis_level": "level-2", +# "protocol": "isis" +# } +# ], +# "route_target": { +# "mode": "export", +# "target": "33:11" +# }, +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "changed": true, +# "commands": [ +# "router bgp 10", +# "vrf vrft", +# "address-family ipv6", +# "redistribute ospfv3 match external", +# "no redistribute isis level-2", +# "no route-target export 33:11", +# "exit", +# "exit", +# "address-family ipv6", +# "redistribute isis level-2", +# "no neighbor peer2 activate", +# "no bgp additional-paths receive", +# "exit" +# ], + +# Using overridden (overriding af at global context): +# Before state: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# neighbor peer2 default-originate always +# redistribute isis level-2 +# ! +# vrf vrft +# address-family ipv6 +# redistribute ospfv3 match external +# veos(config-router-bgp)# + + - name: Overridden + arista.eos.eos_bgp_address_family: + config: + as_number: "10" + address_family: + - afi: "ipv4" + bgp_params: + additional_paths: "receive" + neighbor: + - peer: "peer2" + default_originate: + always: True + state: overridden + +# After State: +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# ! +# vrf vrft +# address-family ipv6 +# redistribute ospfv3 match external +# veos(config-router-bgp)# +# +# Module Execution: +# +# "after": { +# "address_family": [ +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ], +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "before": { +# "address_family": [ +# { +# "afi": "ipv4", +# "neighbor": [ +# { +# "activate": true, +# "peer": "1.1.1.1" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "redistribute": [ +# { +# "isis_level": "level-2", +# "protocol": "isis" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ], +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "changed": true, +# "commands": [ +# "router bgp 10", +# "address-family ipv4", +# "no redistribute ospfv3 match external", +# "no network 1.1.1.0/24", +# "no network 1.5.1.0/24 route-map MAP01", +# "neighbor peer2 default-originate always", +# "no neighbor 1.1.1.1 activate", +# "bgp additional-paths receive", +# "exit", +# "no address-family ipv6" +# ], + +# using Overridden (overridding af in vrf context): + +# Before State: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# no neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# ! +# vrf vrft +# address-family ipv6 +# route-target export 33:11 +# redistribute isis level-2 +# redistribute ospfv3 match external +# veos(config-router-bgp)# + + + - name: Overridden + arista.eos.eos_bgp_address_family: + config: + as_number: "10" + address_family: + - afi: "ipv4" + bgp_params: + additional_paths: "receive" + neighbor: + - peer: "peer2" + default_originate: + always: True + vrf: vrft + state: overridden + +# After State: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# ! +# vrf vrft +# address-family ipv4 +# bgp additional-paths receive +# veos(config-router-bgp)# +# +# Module Execution: +# +# "after": { +# "address_family": [ +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ] +# }, +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "before": { +# "address_family": [ +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "redistribute": [ +# { +# "isis_level": "level-2", +# "protocol": "isis" +# }, +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ], +# "route_target": { +# "mode": "export", +# "target": "33:11" +# }, +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "changed": true, +# "commands": [ +# "router bgp 10", +# "vrf vrft", +# "address-family ipv4", +# "neighbor peer2 default-originate always", +# "bgp additional-paths receive", +# "exit", +# "exit", +# " vrf vrft", +# "no address-family ipv6" +# ], + +# Using Deleted: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# no neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# ! +# vrf vrft +# address-family ipv4 +# bgp additional-paths receive +# veos(config-router-bgp)# + + - name: Delete + arista.eos.eos_bgp_address_family: + config: + as_number: "10" + address_family: + - afi: "ipv6" + vrf: "vrft" + - afi: "ipv6" + state: deleted + +# After State: + +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# no neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# vrf vrft +# address-family ipv4 +# bgp additional-paths receive +# veos(config-router-bgp)# +# +# Module Execution: +# +# "after": { +# "address_family": [ +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, +# "before": { +# "address_family": [ +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ] +# }, +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, + +# Using parsed: + +# parsed_bgp_address_family.cfg : + +# router bgp 10 +# neighbor n2 peer group +# neighbor n2 next-hop-unchanged +# neighbor n2 maximum-routes 12000 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# ! +# address-family ipv4 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# redistribute ospfv3 match external +# ! +# address-family ipv6 +# no bgp additional-paths receive +# neighbor n2 next-hop-unchanged +# redistribute isis level-2 +# ! +# vrf bgp_10 +# ip access-group acl01 +# ucmp fec threshold trigger 33 clear 22 warning-only +# ! +# address-family ipv4 +# route-target import 20:11 +# ! +# vrf vrft +# address-family ipv4 +# bgp additional-paths receive +# ! +# address-family ipv6 +# redistribute ospfv3 match external + + - name: parse configs + arista.eos.eos_bgp_address_family: + running_config: "{{ lookup('file', './parsed_bgp_address_family.cfg') }}" + state: parsed + +# Module Execution: +# "parsed": { +# "address_family": [ +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "neighbor": [ +# { +# "next_hop_unchanged": true, +# "peer": "n2" +# } +# ], +# "redistribute": [ +# { +# "isis_level": "level-2", +# "protocol": "isis" +# } +# ] +# }, +# { +# "afi": "ipv4", +# "route_target": { +# "mode": "import", +# "target": "20:11" +# }, +# "vrf": "bgp_10" +# }, +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "vrf": "vrft" +# }, +# { +# "afi": "ipv6", +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ], +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# } +# } + +# Using gathered: + +# Device config: +# veos(config-router-bgp)#show running-config | section bgp +# router bgp 10 +# neighbor peer2 peer group +# neighbor peer2 maximum-routes 12000 +# neighbor 1.1.1.1 maximum-routes 12000 +# ! +# address-family ipv4 +# bgp additional-paths receive +# neighbor peer2 default-originate always +# no neighbor 1.1.1.1 activate +# network 1.1.1.0/24 +# network 1.5.1.0/24 route-map MAP01 +# redistribute ospfv3 match external +# ! +# vrf vrft +# address-family ipv4 +# bgp additional-paths receive +# veos(config-router-bgp)# + + - name: gather configs + arista.eos.eos_bgp_address_family: + state: gathered + +# Module Execution: +# "gathered": { +# "address_family": [ +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "neighbor": [ +# { +# "default_originate": { +# "always": true +# }, +# "peer": "peer2" +# } +# ], +# "network": [ +# { +# "address": "1.1.1.0/24" +# }, +# { +# "address": "1.5.1.0/24", +# "route_map": "MAP01" +# } +# ], +# "redistribute": [ +# { +# "ospf_route": "external", +# "protocol": "ospfv3" +# } +# ] +# }, +# { +# "afi": "ipv4", +# "bgp_params": { +# "additional_paths": "receive" +# }, +# "vrf": "vrft" +# } +# ], +# "as_number": "10" +# }, + +# using rendered: + + - name: Render + arista.eos.eos_bgp_address_family: + config: + as_number: "10" + address_family: + - afi: "ipv4" + redistribute: + - protocol: "ospfv3" + ospf_route: "external" + network: + - address: "1.1.1.0/24" + - address: "1.5.1.0/24" + route_map: "MAP01" + - afi: "ipv6" + bgp_params: + additional_paths: "receive" + neighbor: + - peer: "peer2" + default_originate: + always: True + - afi: "ipv6" + redistribute: + - protocol: "isis" + isis_level: "level-2" + route_target: + mode: "export" + target: "33:11" + vrf: "vrft" + + state: rendered + +# Module Execution: + +# "rendered": [ +# "router bgp 10", +# "address-family ipv4", +# "redistribute ospfv3 match external", +# "network 1.1.1.0/24", +# "network 1.5.1.0/24 route-map MAP01", +# "exit", +# "address-family ipv6", +# "neighbor peer2 default-originate always", +# "bgp additional-paths receive", +# "exit", +# "vrf vrft", +# "address-family ipv6", +# "redistribute isis level-2", +# "route-target export 33:11", +# "exit", +# "exit" +# ] +# + +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.bgp_address_family.bgp_address_family import ( + Bgp_afArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.bgp_address_family.bgp_address_family import ( + Bgp_af, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Bgp_afArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Bgp_af(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_bgp_global.py b/ansible_collections/arista/eos/plugins/modules/eos_bgp_global.py new file mode 100644 index 000000000..06168db54 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_bgp_global.py @@ -0,0 +1,2353 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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 module file for eos_bgp_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: eos_bgp_global +short_description: Manages BGP global resource module +description: This module configures and manages the attributes of BGP global on Arista + EOS platforms. +version_added: 1.4.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,eos_platform_options). +options: + config: + description: A list of configurations for BGP global. + type: dict + suboptions: + as_number: + description: Autonomous system number. + type: str + aggregate_address: + description: Configure aggregate address. + type: list + elements: dict + suboptions: + address: + description: ipv4/ipv6 address prefix. + type: str + advertise_only: + description: Advertise without installing the generated blackhole route in + FIB. + type: bool + as_set: + description: Generate autonomous system set path information. + type: bool + attribute_map: + description: Name of the route map used to set the attribute of the + aggregate route. + type: str + match_map: + description: Name of the route map used to filter the contributors of the + aggregate route. + type: str + summary_only: + description: Filters all more-specific routes from updates. + type: bool + bgp_params: + description: BGP parameters. + type: dict + suboptions: + additional_paths: + description: BGP additional-paths commands + type: str + choices: ['install', 'send', 'receive'] + advertise_inactive: + description: Advertise BGP routes even if they are inactive in RIB. + type: bool + allowas_in: + description: Allow local-as in updates. + type: dict + suboptions: + set: + description: When True, it is set. + type: bool + count: + description: Number of local ASNs allowed in a BGP update. + type: int + always_compare_med: + description: BGP Always Compare MED + type: bool + asn: + description: AS Number notation. + type: str + choices: ['asdot', 'asplain'] + auto_local_addr: + description: Automatically determine the local address to be used + for the non-transport AF. + type: bool + bestpath: + description: Select the bestpath selection algorithim for BGP routes. + type: dict + suboptions: + as_path: + description: Select the bestpath selection based on as-path. + type: str + choices: ['ignore', 'multipath_relax'] + ecmp_fast: + description: Tie-break BGP paths in a ECMP group based on the order of arrival. + type: bool + med: + description: MED attribute + type: dict + suboptions: + confed: + description: MED Confed. + type: bool + missing_as_worst: + description: MED missing-as-worst. + type: bool + skip: + description: skip one of the tie breaking rules in the bestpath selection. + type: bool + tie_break: + description: Configure the tie-break option for BGP bestpath selection. + choices: ['cluster_list_length', 'router_id'] + type: str + client_to_client: + description: client to client configuration. + type: bool + cluster_id: + description: Cluster ID of this router acting as a route reflector. + type: str + confederation: + description: confederation. + type: dict + suboptions: + identifier: + description: Confederation identifier. + type: str + peers: + description: Confederation peers. + type: str + control_plane_filter: + description: Control plane filter for BGP. + type: bool + convergence: + description: Bgp convergence parameters. + type: dict + suboptions: + slow_peer: + description: Maximum amount of time to wait for slow peers to estabilsh session. + type: bool + time: + description: time in secs + type: int + default: + description: Default neighbor configuration commands. + type: str + choices: ['ipv4_unicast', 'ipv6_unicast'] + enforce_first_as: + description: Enforce the First AS for EBGP routes(default). + type: bool + host_routes: + description: BGP host routes configuration. + type: bool + labeled_unicast: + description: Labeled Unicast. + type: str + choices: ['ip', 'tunnel'] + listen: + description: BGP listen. + type: dict + suboptions: + limit: + description: Set limit on the number of dynamic BGP peers allowed. + type: int + range: + description: Subnet Range to be associated with the peer group. + type: dict + suboptions: + address: + description: Address prefix + type: str + peer_group: + description: Name of peer group. + type: dict + suboptions: + name: + description: name. + type: str + peer_filter: + description: Name of peer filter. + type: str + remote_as: + description: Neighbor AS number + type: str + log_neighbor_changes: + description: Log neighbor up/down events. + type: bool + missing_policy: + description: Missing policy override configuration commands. + type: dict + suboptions: + direction: + description: Missing policy direction options. + type: str + choices: ['in', 'out'] + action: + description: Missing policy action options. + type: str + choices: ['deny', 'permit', 'deny-in-out'] + monitoring: + description: Enable Bgp monitoring for all/specified stations. + type: bool + next_hop_unchanged: + description: Preserve original nexthop while advertising routes to + eBGP peers. + type: bool + redistribute_internal: + description: Redistribute internal BGP routes. + type: bool + route: + description: Configure route-map for route installation. + type: str + route_reflector: + description: Configure route reflector options + type: dict + suboptions: + set: + description: When True route_reflector is set. + type: bool + preserve: + description: preserve route attributes, overwriting route-map changes + type: bool + transport: + description: Configure transport port for TCP session + type: int + default_metric: + description: Default metric. + type: int + distance: + description: Define an administrative distance. + type: dict + suboptions: + external: + description: distance for external routes. + type: int + internal: + description: distance for internal routes. + type: int + local: + description: distance for local routes. + type: int + graceful_restart: + description: Enable graceful restart mode. + type: dict + suboptions: + set: + description: When True, graceful restart is set. + type: bool + restart_time: + description: Set the max time needed to restart and come back up. + type: int + stalepath_time: + description: Set the max time to hold onto restarting peer stale paths. + type: int + graceful_restart_helper: + description: Enable graceful restart helper mode. + type: bool + access_group: + description: ip/ipv6 access list configuration. + type: list + elements: dict + suboptions: + afi: + description: Specify ip/ipv6. + type: str + choices: ['ipv4', 'ipv6'] + acl_name: + description: access list name. + type: str + direction: + description: direction of packets. + type: str + maximum_paths: + description: Maximum number of equal cost paths. + type: dict + suboptions: + max_equal_cost_paths: + description: Value for maximum number of equal cost paths. + type: int + max_installed_ecmp_paths: + description: Value for maximum number of installed ECMP routes. + type: int + monitoring: + description: BGP monitoring protocol configuration. + type: dict + suboptions: + port: + description: Configure the BGP monitoring protocol port number <1024-65535>. + type: int + received: + description: BGP monitoring protocol received route selection. + type: str + choices: ['post_policy', 'pre_policy'] + station: + description: BGP monitoring station configuration. + type: str + timestamp: + description: BGP monitoring protocol Per-Peer Header timestamp behavior. + type: str + choices: ['none', 'send_time'] + neighbor: + description: Configure routing for a network. + type: list + elements: dict + suboptions: + neighbor_address: + type: str + description: Neighbor address or peer group. + aliases: + - peer + additional_paths: + description: BGP additional-paths commands. + type: str + choices: ['send', 'receive'] + allowas_in: + description: Allow local-as in updates. + type: dict + suboptions: + set: + description: When True, it is set. + type: bool + count: + description: Number of local ASNs allowed in a BGP update. + type: int + auto_local_addr: + description: Automatically determine the local address to be used + for the non-transport AF. + type: bool + bfd: + description: Configure BFD fallover for this peer + type: str + choices: ['enable', 'c_bit'] + default_originate: + description: Originate default route to this neighbor. + type: dict + suboptions: + route_map: + description: Route map reference. + type: str + always: + description: Always originate default route to this neighbor. + type: bool + description: + description: Text describing the neighbor. + type: str + dont_capability_negotiate: + description: Donot perform Capability Negotiation with this + neighbor. + type: bool + ebgp_multihop: + description: Allow BGP connections to indirectly connected + external peers. + type: dict + suboptions: + ttl: + description: Time-to-live in the range 1-255 hops. + type: int + set: + description: If True, ttl is not set. + type: bool + enforce_first_as: + description: Enforce the First AS for EBGP routes(default). + type: bool + export_localpref: + description: Override localpref when exporting to an internal + peer. + type: int + fall_over: + description: Configure BFD protocol options for this peer. + type: bool + graceful_restart: + description: Enable graceful restart mode. + type: bool + graceful_restart_helper: + description: Enable graceful restart helper mode. + type: bool + idle_restart_timer: + description: Neighbor idle restart timer. + type: int + import_localpref: + description: Override localpref when importing from an external + peer. + type: int + link_bandwidth: + description: Enable link bandwidth community for routes to this + peer. + type: dict + suboptions: + set: + description: If True, set link bandwidth + type: bool + auto: + description: Enable link bandwidth auto generation for routes from this peer. + type: bool + default: + description: Enable link bandwidth default generation for routes from this + peer. + type: str + update_delay: + description: Delay outbound route updates. + type: int + local_as: + description: Configure local AS number advertised to peer. + type: dict + suboptions: + as_number: + description: AS number. + type: str + fallback: + description: Prefer router AS Number over local AS Number. + type: bool + local_v6_addr: + description: The local IPv6 address of the neighbor in A:B:C:D:E:F:G:H format. + type: str + maximum_accepted_routes: + description: Maximum number of routes accepted from this peer. + type: dict + suboptions: + count: + description: Maximum number of accepted routes (0 means unlimited). + type: int + warning_limit: + description: Maximum number of accepted routes after which a warning is issued. + (0 means never warn) + type: int + maximum_received_routes: + description: Maximum number of routes received from this peer. + type: dict + suboptions: + count: + description: Maximum number of routes (0 means unlimited). + type: int + warning_limit: + description: Percentage of maximum-routes at which warning is to be issued. + type: dict + suboptions: + limit_count: + description: Number of routes at which to warn. + type: int + limit_percent: + description: Percentage of maximum number of routes at which to warn( 1-100). + type: int + warning_only: + description: Only warn, no restart, if max route limit exceeded. + type: bool + metric_out: + description: MED value to advertise to peer. + type: int + monitoring: + description: Enable BGP Monitoring Protocol for this peer. + type: bool + next_hop_self: + description: Always advertise this router address as the BGP + next hop + type: bool + next_hop_unchanged: + description: Preserve original nexthop while advertising routes to + eBGP peers. + type: bool + next_hop_v6_address: + description: IPv6 next-hop address for the neighbor + type: str + out_delay: + description: Delay outbound route updates. + type: int + encryption_password: + description: Password to use in computation of MD5 hash. + type: dict + suboptions: + type: + description: Encryption type. + type: int + choices: [0, 7] + password: + description: password (up to 80 chars). + type: str + remote_as: + description: Neighbor Autonomous System. + type: str + remove_private_as: + description: Remove private AS number from updates to this peer. + type: dict + suboptions: + set: + description: If True, set remove_private_as. + type: bool + all: + description: Remove private AS number. + type: bool + replace_as: + description: Replace private AS number with local AS number. + type: bool + peer_group: + description: Name of the peer group. + type: str + + prefix_list: + description: Prefix list reference. + type: dict + suboptions: + direction: + description: Configure an inbound/outbound prefix-list. + type: str + choices: ['in', 'out'] + name: + description: prefix list name. + type: str + route_map: + description: Route map reference. + type: dict + suboptions: + direction: + description: Configure an inbound/outbound route-map. + type: str + choices: ['in', 'out'] + name: + description: Route map name. + type: str + route_reflector_client: + description: Configure peer as a route reflector client. + type: bool + route_to_peer: + description: Use routing table information to reach the peer. + type: bool + send_community: + description: Send community attribute to this neighbor. + type: dict + suboptions: + set: + description: Enable send-community + type: bool + community_attribute: + description: Type of community attributes to send to this neighbor. + type: str + sub_attribute: + description: Attribute to be sent to the neighbor. + type: str + choices: ['extended', 'link-bandwidth', 'standard'] + link_bandwidth_attribute: + description: cumulative/aggregate attribute to be sent. + type: str + choices: ['aggregate', 'divide'] + speed: + description: Reference link speed in bits/second + type: str + divide: + description: link-bandwidth divide attribute. + type: str + choices: ['equal', 'ratio'] + shutdown: + description: Administratively shut down this neighbor. + type: bool + soft_recognition: + description: Configure how to handle routes that fail import. + type: str + choices: ['all', 'None'] + timers: + description: Timers. + type: dict + suboptions: + keepalive: + description: Keep Alive Interval in secs. + type: int + holdtime: + description: Hold time in secs. + type: int + transport: + description: Configure transport options for TCP session. + type: dict + suboptions: + connection_mode: + description: Configure connection-mode for TCP session. + type: str + remote_port: + description: Configure BGP peer TCP port to connect to. + type: int + ttl: + description: BGP ttl security check + type: int + update_source: + description: Specify the local source interface for peer BGP + sessions. + type: str + weight: + description: Weight to assign. + type: int + aliases: + - neighbors + network: + description: Configure routing for a network. + type: list + elements: dict + suboptions: + address: + description: address prefix. + type: str + route_map: + description: Name of route map. + type: str + aliases: + - networks + redistribute: + description: Redistribute routes in to BGP. + type: list + elements: dict + suboptions: + protocol: + description: Routes to be redistributed. + type: str + choices: ['isis', 'ospfv3', 'ospf', 'attached-host', 'connected', 'rip', 'static'] + route_map: + description: Route map reference. + type: str + isis_level: + description: Applicable for isis routes. Specify isis route level. + type: str + choices: ['level-1', 'level-2', 'level-1-2'] + ospf_route: + description: ospf route options. + type: str + choices: ['internal', 'external', 'nssa_external_1', 'nssa_external_2'] + router_id: + description: Router id. + type: str + route_target: + description: Route target. + type: dict + suboptions: + action: + description: Route action. + type: str + choices: ['both', 'import', 'export'] + target: + description: Route Target. + type: str + shutdown: + description: When True, shut down BGP. + type: bool + timers: + description: Timers. + type: dict + suboptions: + keepalive: + description: Keep Alive Interval in secs. + type: int + holdtime: + description: Hold time in secs. + type: int + ucmp: + description: Configure unequal cost multipathing. + type: dict + suboptions: + fec: + description: Configure UCMP fec utilization threshold. + type: dict + suboptions: + trigger: + description: UCMP fec utilization too high threshold. + type: int + clear: + description: UCMP FEC utilization Clear thresholds. + type: int + link_bandwidth: + description: Configure link-bandwidth propagation delay. + type: dict + suboptions: + mode: + description: UCMP link bandwidth mode + type: str + choices: ['encoding_weighted', 'recursive'] + update_delay: + description: Link Bandwidth Advertisement delay. + type: int + mode: + description: UCMP mode. + type: dict + suboptions: + set: + description: If True, ucmp mode is set to 1. + type: bool + nexthops: + description: Value for total number UCMP nexthops. + type: int + update: + description: Configure BGP update generation. + type: dict + suboptions: + wait_for: + description: wait for options before converge or synchronize. + type: str + choices: ['wait_for_convergence', 'wait_install'] + batch_size: + description: batch size for FIB route acknowledgements. + type: int + vlan: + description: Configure MAC VRF BGP for single VLAN support. + type: int + vlan_aware_bundle: + description: Configure MAC VRF BGP for multiple VLAN support. + type: str + vrfs: + description: Configure BGP in a VRF. + type: list + elements: dict + suboptions: + vrf: + description: VRF name. + type: str + aggregate_address: + description: Configure aggregate address. + type: list + elements: dict + suboptions: + address: + description: ipv4/ipv6 address prefix. + type: str + advertise_only: + description: Advertise without installing the generated blackhole route in + FIB. + type: bool + as_set: + description: Generate autonomous system set path information. + type: bool + attribute_map: + description: Name of the route map used to set the attribute of the + aggregate route. + type: str + match_map: + description: Name of the route map used to filter the contributors of the + aggregate route. + type: str + summary_only: + description: Filters all more-specific routes from updates. + type: bool + bgp_params: + description: BGP parameters. + type: dict + suboptions: + additional_paths: + description: BGP additional-paths commands + type: str + choices: ['install', 'send', 'receive'] + advertise_inactive: + description: Advertise BGP routes even if they are inactive in RIB. + type: bool + allowas_in: + description: Allow local-as in updates. + type: dict + suboptions: + set: + description: When True, it is set. + type: bool + count: + description: Number of local ASNs allowed in a BGP update. + type: int + always_compare_med: + description: BGP Always Compare MED + type: bool + asn: + description: AS Number notation. + type: str + choices: ['asdot', 'asplain'] + auto_local_addr: + description: Automatically determine the local address to be used + for the non-transport AF. + type: bool + bestpath: + description: Select the bestpath selection algorithim for BGP routes. + type: dict + suboptions: + as_path: + description: Select the bestpath selection based on as-path. + type: str + choices: ['ignore', 'multipath_relax'] + ecmp_fast: + description: Tie-break BGP paths in a ECMP group based on the order of arrival. + type: bool + med: + description: MED attribute + type: dict + suboptions: + confed: + description: MED Confed. + type: bool + missing_as_worst: + description: MED missing-as-worst. + type: bool + skip: + description: skip one of the tie breaking rules in the bestpath selection. + type: bool + tie_break: + description: Configure the tie-break option for BGP bestpath selection. + choices: ['cluster_list_length', 'router_id'] + type: str + client_to_client: + description: client to client configuration. + type: bool + cluster_id: + description: Cluster ID of this router acting as a route reflector. + type: str + confederation: + description: confederation. + type: dict + suboptions: + identifier: + description: Confederation identifier. + type: str + peers: + description: Confederation peers. + type: str + control_plane_filter: + description: Control plane filter for BGP. + type: bool + convergence: + description: Bgp convergence parameters. + type: dict + suboptions: + slow_peer: + description: Maximum amount of time to wait for slow peers to estabilsh session. + type: bool + time: + description: time in secs + type: int + default: + description: Default neighbor configuration commands. + type: str + choices: ['ipv4_unicast', 'ipv6_unicast'] + enforce_first_as: + description: Enforce the First AS for EBGP routes(default). + type: bool + host_routes: + description: BGP host routes configuration. + type: bool + labeled_unicast: + description: Labeled Unicast. + type: str + choices: ['ip', 'tunnel'] + listen: + description: BGP listen. + type: dict + suboptions: + limit: + description: Set limit on the number of dynamic BGP peers allowed. + type: int + range: + description: Subnet Range to be associated with the peer group. + type: dict + suboptions: + address: + description: Address prefix + type: str + peer_group: + description: Name of peer group. + type: dict + suboptions: + name: + description: name. + type: str + peer_filter: + description: Name of peer filter. + type: str + remote_as: + description: Neighbor AS number + type: str + log_neighbor_changes: + description: Log neighbor up/down events. + type: bool + missing_policy: + description: Missing policy override configuration commands. + type: dict + suboptions: + direction: + description: Missing policy direction options. + type: str + choices: ['in', 'out'] + action: + description: Missing policy action options. + type: str + choices: ['deny', 'permit', 'deny-in-out'] + monitoring: + description: Enable Bgp monitoring for all/specified stations. + type: bool + next_hop_unchanged: + description: Preserve original nexthop while advertising routes to + eBGP peers. + type: bool + redistribute_internal: + description: Redistribute internal BGP routes. + type: bool + route: + description: Configure route-map for route installation. + type: str + route_reflector: + description: Configure route reflector options + type: dict + suboptions: + set: + description: When True route_reflector is set. + type: bool + preserve: + description: preserve route attributes, overwriting route-map changes + type: bool + transport: + description: Configure transport port for TCP session + type: int + default_metric: + description: Default metric. + type: int + distance: + description: Define an administrative distance. + type: dict + suboptions: + external: + description: distance for external routes. + type: int + internal: + description: distance for internal routes. + type: int + local: + description: distance for local routes. + type: int + graceful_restart: + description: Enable graceful restart mode. + type: dict + suboptions: + set: + description: When True, graceful restart is set. + type: bool + restart_time: + description: Set the max time needed to restart and come back up. + type: int + stalepath_time: + description: Set the max time to hold onto restarting peer stale paths. + type: int + graceful_restart_helper: + description: Enable graceful restart helper mode. + type: bool + access_group: + description: ip/ipv6 access list configuration. + type: list + elements: dict + suboptions: + afi: + description: Specify ip/ipv6. + type: str + choices: ['ipv4', 'ipv6'] + acl_name: + description: access list name. + type: str + direction: + description: direction of packets. + type: str + maximum_paths: + description: Maximum number of equal cost paths. + type: dict + suboptions: + max_equal_cost_paths: + description: Value for maximum number of equal cost paths. + type: int + max_installed_ecmp_paths: + description: Value for maximum number of installed ECMP routes. + type: int + neighbor: + description: Configure routing for a network. + aliases: + - neighbors + type: list + elements: dict + suboptions: + neighbor_address: + type: str + description: Neighbor address or peer group. + aliases: ["peer"] + additional_paths: + description: BGP additional-paths commands. + type: str + choices: ['send', 'receive'] + allowas_in: + description: Allow local-as in updates. + type: dict + suboptions: + set: + description: When True, it is set. + type: bool + count: + description: Number of local ASNs allowed in a BGP update. + type: int + auto_local_addr: + description: Automatically determine the local address to be used + for the non-transport AF. + type: bool + bfd: + description: Configure BFD fallover for this peer + type: str + choices: ['enable', 'c_bit'] + default_originate: + description: Originate default route to this neighbor. + type: dict + suboptions: + route_map: + description: Route map reference. + type: str + always: + description: Always originate default route to this neighbor. + type: bool + description: + description: Text describing the neighbor. + type: str + dont_capability_negotiate: + description: Donot perform Capability Negotiation with this + neighbor. + type: bool + ebgp_multihop: + description: Allow BGP connections to indirectly connected + external peers. + type: dict + suboptions: + ttl: + description: Time-to-live in the range 1-255 hops. + type: int + set: + description: If True, ttl is not set. + type: bool + enforce_first_as: + description: Enforce the First AS for EBGP routes(default). + type: bool + export_localpref: + description: Override localpref when exporting to an internal + peer. + type: int + fall_over: + description: Configure BFD protocol options for this peer. + type: bool + graceful_restart: + description: Enable graceful restart mode. + type: bool + graceful_restart_helper: + description: Enable graceful restart helper mode. + type: bool + idle_restart_timer: + description: Neighbor idle restart timer. + type: int + import_localpref: + description: Override localpref when importing from an external + peer. + type: int + link_bandwidth: + description: Enable link bandwidth community for routes to this + peer. + type: dict + suboptions: + set: + description: If True, set link bandwidth + type: bool + auto: + description: Enable link bandwidth auto generation for routes from this peer. + type: bool + default: + description: Enable link bandwidth default generation for routes from this + peer. + type: str + update_delay: + description: Delay outbound route updates. + type: int + local_as: + description: Configure local AS number advertised to peer. + type: dict + suboptions: + as_number: + description: AS number. + type: str + fallback: + description: Prefer router AS Number over local AS Number. + type: bool + local_v6_addr: + description: The local IPv6 address of the neighbor in A:B:C:D:E:F:G:H format. + type: str + maximum_accepted_routes: + description: Maximum number of routes accepted from this peer. + type: dict + suboptions: + count: + description: Maximum number of accepted routes (0 means unlimited). + type: int + warning_limit: + description: Maximum number of accepted routes after which a warning is issued. + (0 means never warn) + type: int + maximum_received_routes: + description: Maximum number of routes received from this peer. + type: dict + suboptions: + count: + description: Maximum number of routes (0 means unlimited). + type: int + warning_limit: + description: Percentage of maximum-routes at which warning is to be issued. + type: dict + suboptions: + limit_count: + description: Number of routes at which to warn. + type: int + limit_percent: + description: Percentage of maximum number of routes at which to warn( 1-100). + type: int + warning_only: + description: Only warn, no restart, if max route limit exceeded. + type: bool + metric_out: + description: MED value to advertise to peer. + type: int + monitoring: + description: Enable BGP Monitoring Protocol for this peer. + type: bool + next_hop_self: + description: Always advertise this router address as the BGP + next hop + type: bool + next_hop_unchanged: + description: Preserve original nexthop while advertising routes to + eBGP peers. + type: bool + next_hop_v6_address: + description: IPv6 next-hop address for the neighbor + type: str + out_delay: + description: Delay outbound route updates. + type: int + encryption_password: + description: Password to use in computation of MD5 hash. + type: dict + suboptions: + type: + description: Encryption type. + type: int + choices: [0, 7] + password: + description: password (up to 80 chars). + type: str + remote_as: + description: Neighbor Autonomous System. + type: str + remove_private_as: + description: Remove private AS number from updates to this peer. + type: dict + suboptions: + set: + description: If True, set remove_private_as. + type: bool + all: + description: Remove private AS number. + type: bool + replace_as: + description: Replace private AS number with local AS number. + type: bool + peer_group: + description: Name of the peer group. + type: str + + prefix_list: + description: Prefix list reference. + type: dict + suboptions: + direction: + description: Configure an inbound/outbound prefix-list. + type: str + choices: ['in', 'out'] + name: + description: prefix list name. + type: str + route_map: + description: Route map reference. + type: dict + suboptions: + direction: + description: Configure an inbound/outbound route-map. + type: str + choices: ['in', 'out'] + name: + description: Route map name. + type: str + route_reflector_client: + description: Configure peer as a route reflector client. + type: bool + route_to_peer: + description: Use routing table information to reach the peer. + type: bool + send_community: + description: Send community attribute to this neighbor. + type: dict + suboptions: + community_attribute: + description: Type of community attributes to send to this neighbor. + type: str + sub_attribute: + description: Attribute to be sent to the neighbor. + type: str + choices: ['extended', 'link-bandwidth', 'standard'] + link_bandwidth_attribute: + description: cumulative/aggregate attribute to be sent. + type: str + choices: ['aggregate', 'divide'] + speed: + description: Reference link speed in bits/second + type: str + divide: + description: link-bandwidth divide attribute. + type: str + choices: ['equal', 'ratio'] + shutdown: + description: Administratively shut down this neighbor. + type: bool + soft_recognition: + description: Configure how to handle routes that fail import. + type: str + choices: ['all', 'None'] + timers: + description: Timers. + type: dict + suboptions: + keepalive: + description: Keep Alive Interval in secs. + type: int + holdtime: + description: Hold time in secs. + type: int + transport: + description: Configure transport options for TCP session. + type: dict + suboptions: + connection_mode: + description: Configure connection-mode for TCP session. + type: str + remote_port: + description: Configure BGP peer TCP port to connect to. + type: int + ttl: + description: BGP ttl security check + type: int + update_source: + description: Specify the local source interface for peer BGP + sessions. + type: str + weight: + description: Weight to assign. + type: int + network: + description: Configure routing for a network. + aliases: + - networks + type: list + elements: dict + suboptions: + address: + description: address prefix. + type: str + route_map: + description: Name of route map. + type: str + redistribute: + description: Redistribute routes in to BGP. + type: list + elements: dict + suboptions: + protocol: + description: Routes to be redistributed. + type: str + choices: ['isis', 'ospfv3', 'ospf', 'attached-host', 'connected', 'rip', 'static'] + route_map: + description: Route map reference. + type: str + isis_level: + description: Applicable for isis routes. Specify isis route level. + type: str + choices: ['level-1', 'level-2', 'level-1-2'] + ospf_route: + description: ospf route options. + type: str + choices: ['internal', 'external', 'nssa_external_1', 'nssa_external_2'] + route_target: + description: Route target. + type: dict + suboptions: + action: + description: Route action. + type: str + choices: ['both', 'import', 'export'] + type: + description: Type of address fmaily + type: str + choices: ['evpn', 'vpn-ipv4', 'vpn-ipv6'] + route_map: + description: Name of a route map. + type: str + target: + description: Route Target. + type: str + imported_route: + description: Export routes imported from the same Afi/Safi. + type: bool + router_id: + description: Router id. + type: str + shutdown: + description: When True, shut down BGP. + type: bool + timers: + description: Timers. + type: dict + suboptions: + keepalive: + description: Keep Alive Interval in secs. + type: int + holdtime: + description: Hold time in secs. + type: int + ucmp: + description: Configure unequal cost multipathing. + type: dict + suboptions: + fec: + description: Configure UCMP fec utilization threshold. + type: dict + suboptions: + trigger: + description: UCMP fec utilization too high threshold. + type: int + clear: + description: UCMP FEC utilization Clear thresholds. + type: int + link_bandwidth: + description: Configure link-bandwidth propagation delay. + type: dict + suboptions: + mode: + description: UCMP link bandwidth mode + type: str + choices: ['encoding_weighted', 'recursive', 'update_delay'] + update_delay: + description: Link Bandwidth Advertisement delay. + type: int + mode: + description: UCMP mode. + type: dict + suboptions: + set: + description: If True, ucmp mode is set to 1. + type: bool + nexthops: + description: Value for total number UCMP nexthops. + type: int + update: + description: Configure BGP update generation. + type: dict + suboptions: + wait_for: + description: wait for options before converge or synchronize. + type: str + choices: ['wait_for_convergence', 'wait_install'] + batch_size: + description: batch size for FIB route acknowledgements. + type: int + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section bgp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + - State I(purged) removes all the BGP configurations from the + target device. Use caution with this state.('no router bgp <x>') + - State I(deleted) only removes BGP attributes that this modules + manages and does not negate the BGP process completely. Thereby, preserving + address-family related configurations under BGP context. + - Running states I(deleted) and I(replaced) will result in an error if there + are address-family configuration lines present under vrf context that is + is to be removed. Please use the M(arista.eos.eos_bgp_address_family) + module for prior cleanup. + - Refer to examples for more details. + type: str + choices: [deleted, merged, purged, replaced, gathered, rendered, parsed] + default: merged +""" +EXAMPLES = """ +# Using merged +# Before state + +# veos(config)#show running-config | section bgp +# veos(config)# + + - name: Merge provided configuration with device configuration + arista.eos.eos_bgp_global: + config: + as_number: "100" + bgp_params: + host_routes: True + convergence: + slow_peer: True + time: 6 + additional_paths: "send" + log_neighbor_changes: True + maximum_paths: + max_equal_cost_paths: 55 + aggregate_address: + - address: "1.2.1.0/24" + as_set: true + match_map: "match01" + - address: "5.2.1.0/24" + attribute_map: "attrmatch01" + advertise_only: true + redistribute: + - protocol: "static" + route_map: "map_static" + - protocol: "attached-host" + distance: + internal: 50 + neighbor: + - peer: "10.1.3.2" + allowas_in: + set: true + default_originate: + always: true + dont_capability_negotiate: true + export_localpref: 4000 + maximum_received_routes: + count: 500 + warning_limit: + limit_percent: 5 + next_hop_unchanged: true + prefix_list: + name: "prefix01" + direction: "out" + - neighbor_address: "peer1" + fall_over: true + link_bandwidth: + update_delay: 5 + monitoring: True + send_community: + community_attribute: "extended" + sub_attribute: "link-bandwidth" + link_bandwidth_attribute: "aggregate" + speed: "600" + vlan: 5 + state: merged + +# After State: +# veos(config)#show running-config | section bgp +# router bgp 100 +# bgp convergence slow-peer time 6 +# distance bgp 50 50 50 +# maximum-paths 55 +# bgp additional-paths send any +# neighbor peer1 peer group +# neighbor peer1 link-bandwidth update-delay 5 +# neighbor peer1 fall-over bfd +# neighbor peer1 monitoring +# neighbor peer1 send-community extended link-bandwidth aggregate 600 +# neighbor peer1 maximum-routes 12000 +# neighbor 10.1.3.2 export-localpref 4000 +# neighbor 10.1.3.2 next-hop-unchanged +# neighbor 10.1.3.2 dont-capability-negotiate +# neighbor 10.1.3.2 allowas-in 3 +# neighbor 10.1.3.2 default-originate always +# neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent +# aggregate-address 1.2.1.0/24 as-set match-map match01 +# aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only +# redistribute static route-map map_static +# redistribute attached-host +# ! +# vlan 5 +# ! +# address-family ipv4 +# neighbor 10.1.3.2 prefix-list prefix01 out +# veos(config)# +# +# Module Execution: +# +# "after": { +# "aggregate_address": [ +# { +# "address": "1.2.1.0/24", +# "as_set": true, +# "match_map": "match01" +# }, +# { +# "address": "5.2.1.0/24", +# "advertise_only": true, +# "attribute_map": "attrmatch01" +# } +# ], +# "as_number": "100", +# "bgp_params": { +# "additional_paths": "send", +# "convergence": { +# "slow_peer": true, +# "time": 6 +# } +# }, +# "distance": { +# "external": 50, +# "internal": 50, +# "local": 50 +# }, +# "maximum_paths": { +# "max_equal_cost_paths": 55 +# }, +# "neighbor": [ +# { +# "fall_over": true, +# "link_bandwidth": { +# "set": true, +# "update_delay": 5 +# }, +# "maximum_received_routes": { +# "count": 12000 +# }, +# "monitoring": true, +# "peer": "peer1", +# "peer_group": "peer1", +# "send_community": { +# "community_attribute": "extended", +# "link_bandwidth_attribute": "aggregate", +# "speed": "600", +# "sub_attribute": "link-bandwidth" +# } +# }, +# { +# "allowas_in": { +# "count": 3 +# }, +# "default_originate": { +# "always": true +# }, +# "dont_capability_negotiate": true, +# "export_localpref": 4000, +# "maximum_received_routes": { +# "count": 500, +# "warning_limit": { +# "limit_percent": 5 +# } +# }, +# "next_hop_unchanged": true, +# "peer": "10.1.3.2" +# } +# ], +# "redistribute": [ +# { +# "protocol": "static", +# "route_map": "map_static" +# }, +# { +# "protocol": "attached-host" +# } +# ], +# "vlan": 5 +# }, +# "before": {}, +# "changed": true, +# "commands": [ +# "router bgp 100", +# "neighbor 10.1.3.2 allowas-in", +# "neighbor 10.1.3.2 default-originate always", +# "neighbor 10.1.3.2 dont-capability-negotiate", +# "neighbor 10.1.3.2 export-localpref 4000", +# "neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent", +# "neighbor 10.1.3.2 next-hop-unchanged", +# "neighbor 10.1.3.2 prefix-list prefix01 out", +# "neighbor peer1 fall-over bfd", +# "neighbor peer1 link-bandwidth update-delay 5", +# "neighbor peer1 monitoring", +# "neighbor peer1 send-community extended link-bandwidth aggregate 600", +# "redistribute static route-map map_static", +# "redistribute attached-host", +# "aggregate-address 1.2.1.0/24 as-set match-map match01", +# "aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only", +# "bgp host-routes fib direct-install", +# "bgp convergence slow-peer time 6", +# "bgp additional-paths send any", +# "bgp log-neighbor-changes", +# "maximum-paths 55", +# "distance bgp 50", +# "vlan 5" +# ], + +# Using replaced: + +# Before state: +# veos(config)#show running-config | section bgp +# router bgp 100 +# bgp convergence slow-peer time 6 +# distance bgp 50 50 50 +# maximum-paths 55 +# bgp additional-paths send any +# neighbor peer1 peer group +# neighbor peer1 link-bandwidth update-delay 5 +# neighbor peer1 fall-over bfd +# neighbor peer1 monitoring +# neighbor peer1 send-community extended link-bandwidth aggregate 600 +# neighbor peer1 maximum-routes 12000 +# neighbor 10.1.3.2 export-localpref 4000 +# neighbor 10.1.3.2 next-hop-unchanged +# neighbor 10.1.3.2 dont-capability-negotiate +# neighbor 10.1.3.2 allowas-in 3 +# neighbor 10.1.3.2 default-originate always +# neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent +# aggregate-address 1.2.1.0/24 as-set match-map match01 +# aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only +# redistribute static route-map map_static +# redistribute attached-host +# ! +# vlan 5 +# ! +# address-family ipv4 +# neighbor 10.1.3.2 prefix-list prefix01 out +# ! +# vrf vrf01 +# route-target import 54:11 +# neighbor 12.1.3.2 dont-capability-negotiate +# neighbor 12.1.3.2 allowas-in 3 +# neighbor 12.1.3.2 default-originate always +# neighbor 12.1.3.2 maximum-routes 12000 +# veos(config)# + + - name: replace provided configuration with device configuration + arista.eos.eos_bgp_global: + config: + as_number: "100" + bgp_params: + host_routes: True + convergence: + slow_peer: True + time: 6 + additional_paths: "send" + log_neighbor_changes: True + vrfs: + - vrf: "vrf01" + maximum_paths: + max_equal_cost_paths: 55 + aggregate_address: + - address: "1.2.1.0/24" + as_set: true + match_map: "match01" + - address: "5.2.1.0/24" + attribute_map: "attrmatch01" + advertise_only: true + redistribute: + - protocol: "static" + route_map: "map_static" + - protocol: "attached-host" + distance: + internal: 50 + neighbor: + - neighbor_address: "10.1.3.2" + allowas_in: + set: true + default_originate: + always: true + dont_capability_negotiate: true + export_localpref: 4000 + maximum_received_routes: + count: 500 + warning_limit: + limit_percent: 5 + next_hop_unchanged: true + prefix_list: + name: "prefix01" + direction: "out" + - neighbor_address: "peer1" + fall_over: true + link_bandwidth: + update_delay: 5 + monitoring: True + send_community: + community_attribute: "extended" + sub_attribute: "link-bandwidth" + link_bandwidth_attribute: "aggregate" + speed: "600" + state: replaced + +# After State: + +# veos(config)#show running-config | section bgp +# router bgp 100 +# bgp convergence slow-peer time 6 +# bgp additional-paths send any +# ! +# vrf vrf01 +# distance bgp 50 50 50 +# maximum-paths 55 +# neighbor 10.1.3.2 export-localpref 4000 +# neighbor 10.1.3.2 next-hop-unchanged +# neighbor 10.1.3.2 dont-capability-negotiate +# neighbor 10.1.3.2 allowas-in 3 +# neighbor 10.1.3.2 default-originate always +# neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent +# aggregate-address 1.2.1.0/24 as-set match-map match01 +# aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only +# redistribute static route-map map_static +# redistribute attached-host +# ! +# address-family ipv4 +# neighbor 10.1.3.2 prefix-list prefix01 out +# veos(config)# +# +# +# Module Execution: +# +# "after": { +# "as_number": "100", +# "bgp_params": { +# "additional_paths": "send", +# "convergence": { +# "slow_peer": true, +# "time": 6 +# } +# }, +# "vrfs": [ +# { +# "aggregate_address": [ +# { +# "address": "1.2.1.0/24", +# "as_set": true, +# "match_map": "match01" +# }, +# { +# "address": "5.2.1.0/24", +# "advertise_only": true, +# "attribute_map": "attrmatch01" +# } +# ], +# "distance": { +# "external": 50, +# "internal": 50, +# "local": 50 +# }, +# "maximum_paths": { +# "max_equal_cost_paths": 55 +# }, +# "neighbor": [ +# { +# "allowas_in": { +# "count": 3 +# }, +# "default_originate": { +# "always": true +# }, +# "dont_capability_negotiate": true, +# "export_localpref": 4000, +# "maximum_received_routes": { +# "count": 500, +# "warning_limit": { +# "limit_percent": 5 +# } +# }, +# "next_hop_unchanged": true, +# "peer": "10.1.3.2" +# } +# ], +# "redistribute": [ +# { +# "protocol": "static", +# "route_map": "map_static" +# }, +# { +# "protocol": "attached-host" +# } +# ], +# "vrf": "vrf01" +# } +# ] +# }, +# "before": { +# "aggregate_address": [ +# { +# "address": "1.2.1.0/24", +# "as_set": true, +# "match_map": "match01" +# }, +# { +# "address": "5.2.1.0/24", +# "advertise_only": true, +# "attribute_map": "attrmatch01" +# } +# ], +# "as_number": "100", +# "bgp_params": { +# "additional_paths": "send", +# "convergence": { +# "slow_peer": true, +# "time": 6 +# } +# }, +# "distance": { +# "external": 50, +# "internal": 50, +# "local": 50 +# }, +# "maximum_paths": { +# "max_equal_cost_paths": 55 +# }, +# "neighbor": [ +# { +# "fall_over": true, +# "link_bandwidth": { +# "set": true, +# "update_delay": 5 +# }, +# "maximum_received_routes": { +# "count": 12000 +# }, +# "monitoring": true, +# "peer": "peer1", +# "peer_group": "peer1", +# "send_community": { +# "community_attribute": "extended", +# "link_bandwidth_attribute": "aggregate", +# "speed": "600", +# "sub_attribute": "link-bandwidth" +# } +# }, +# { +# "allowas_in": { +# "count": 3 +# }, +# "default_originate": { +# "always": true +# }, +# "dont_capability_negotiate": true, +# "export_localpref": 4000, +# "maximum_received_routes": { +# "count": 500, +# "warning_limit": { +# "limit_percent": 5 +# } +# }, +# "next_hop_unchanged": true, +# "peer": "10.1.3.2" +# } +# ], +# "redistribute": [ +# { +# "protocol": "static", +# "route_map": "map_static" +# }, +# { +# "protocol": "attached-host" +# } +# ], +# "vlan": 5, +# "vrfs": [ +# { +# "neighbor": [ +# { +# "allowas_in": { +# "count": 3 +# }, +# "default_originate": { +# "always": true +# }, +# "dont_capability_negotiate": true, +# "maximum_received_routes": { +# "count": 12000 +# }, +# "peer": "12.1.3.2" +# } +# ], +# "route_target": { +# "action": "import", +# "target": "54:11" +# }, +# "vrf": "vrf01" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "router bgp 100", +# "vrf vrf01", +# "no route-target import 54:11", +# "neighbor 10.1.3.2 allowas-in", +# "neighbor 10.1.3.2 default-originate always", +# "neighbor 10.1.3.2 dont-capability-negotiate", +# "neighbor 10.1.3.2 export-localpref 4000", +# "neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent", +# "neighbor 10.1.3.2 next-hop-unchanged", +# "neighbor 10.1.3.2 prefix-list prefix01 out", +# "neighbor peer1 fall-over bfd", +# "neighbor peer1 link-bandwidth update-delay 5", +# "neighbor peer1 monitoring", +# "neighbor peer1 send-community extended link-bandwidth aggregate 600", +# "no neighbor 12.1.3.2", +# "redistribute static route-map map_static", +# "redistribute attached-host", +# "aggregate-address 1.2.1.0/24 as-set match-map match01", +# "aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only", +# "maximum-paths 55", +# "distance bgp 50", +# "exit", +# "no neighbor peer1 peer group", +# "no neighbor peer1 link-bandwidth update-delay 5", +# "no neighbor peer1 fall-over bfd", +# "no neighbor peer1 monitoring", +# "no neighbor peer1 send-community extended link-bandwidth aggregate 600", +# "no neighbor peer1 maximum-routes 12000", +# "no neighbor 10.1.3.2", +# "no redistribute static route-map map_static", +# "no redistribute attached-host", +# "no aggregate-address 1.2.1.0/24 as-set match-map match01", +# "no aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only", +# "bgp host-routes fib direct-install", +# "bgp log-neighbor-changes", +# "no distance bgp 50 50 50", +# "no maximum-paths 55", +# "no vlan 5" +# ], +# + +# Using replaced (in presence of address_family under vrf): +# Before State: + +#veos(config)#show running-config | section bgp +# router bgp 100 +# bgp convergence slow-peer time 6 +# bgp additional-paths send any +# ! +# vrf vrf01 +# distance bgp 50 50 50 +# maximum-paths 55 +# neighbor 10.1.3.2 export-localpref 4000 +# neighbor 10.1.3.2 next-hop-unchanged +# neighbor 10.1.3.2 dont-capability-negotiate +# neighbor 10.1.3.2 allowas-in 3 +# neighbor 10.1.3.2 default-originate always +# neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent +# aggregate-address 1.2.1.0/24 as-set match-map match01 +# aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only +# redistribute static route-map map_static +# redistribute attached-host +# ! +# address-family ipv4 +# neighbor 10.1.3.2 prefix-list prefix01 out +# ! +# address-family ipv6 +# redistribute dhcp +# veos(config)# + + - name: Replace + arista.eos.eos_bgp_global: + config: + as_number: "100" + graceful_restart: + set: True + router_id: "1.1.1.1" + timers: + keepalive: 2 + holdtime: 5 + ucmp: + mode: + set: True + vlan_aware_bundle: "bundle1 bundle2 bundle3" + state: replaced + +# Module Execution: + +# fatal: [192.168.122.113]: FAILED! => { +# "changed": false, +# "invocation": { +# "module_args": { +# "config": { +# "access_group": null, +# "aggregate_address": null, +# "as_number": "100", +# "bgp_params": null, +# "default_metric": null, +# "distance": null, +# "graceful_restart": { +# "restart_time": null, +# "set": true, +# "stalepath_time": null +# }, +# "graceful_restart_helper": null, +# "maximum_paths": null, +# "monitoring": null, +# "neighbor": null, +# "network": null, +# "redistribute": null, +# "route_target": null, +# "router_id": "1.1.1.1", +# "shutdown": null, +# "timers": { +# "holdtime": 5, +# "keepalive": 2 +# }, +# "ucmp": { +# "fec": null, +# "link_bandwidth": null, +# "mode": { +# "nexthops": null, +# "set": true +# } +# }, +# "update": null, +# "vlan": null, +# "vlan_aware_bundle": "bundle1 bundle2 bundle3", +# "vrfs": null +# }, +# "running_config": null, +# "state": "replaced" +# } +# }, +# "msg": "Use the _bgp_af module to delete the address_family under vrf, before replacing/deleting the vrf." +# } + +# Using deleted: + +# Before state: + +# veos(config)#show running-config | section bgp +# router bgp 100 +# bgp convergence slow-peer time 6 +# bgp additional-paths send any +# ! +# vrf vrf01 +# distance bgp 50 50 50 +# maximum-paths 55 +# neighbor 10.1.3.2 export-localpref 4000 +# neighbor 10.1.3.2 next-hop-unchanged +# neighbor 10.1.3.2 dont-capability-negotiate +# neighbor 10.1.3.2 allowas-in 3 +# neighbor 10.1.3.2 default-originate always +# neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent +# aggregate-address 1.2.1.0/24 as-set match-map match01 +# aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only +# redistribute static route-map map_static +# redistribute attached-host +# ! + + - name: Delete configuration + arista.eos.eos_bgp_global: + config: + as_number: "100" + state: deleted + +# After State: + +# veos(config)#show running-config | section bgp +# router bgp 100 +# +# +# Module Execution: +# +# "after": { +# "as_number": "100" +# }, +# "before": { +# "as_number": "100", +# "bgp_params": { +# "additional_paths": "send", +# "convergence": { +# "slow_peer": true, +# "time": 6 +# } +# }, +# "vrfs": [ +# { +# "aggregate_address": [ +# { +# "address": "1.2.1.0/24", +# "as_set": true, +# "match_map": "match01" +# }, +# { +# "address": "5.2.1.0/24", +# "advertise_only": true, +# "attribute_map": "attrmatch01" +# } +# ], +# "distance": { +# "external": 50, +# "internal": 50, +# "local": 50 +# }, +# "maximum_paths": { +# "max_equal_cost_paths": 55 +# }, +# "neighbor": [ +# { +# "allowas_in": { +# "count": 3 +# }, +# "default_originate": { +# "always": true +# }, +# "dont_capability_negotiate": true, +# "export_localpref": 4000, +# "maximum_received_routes": { +# "count": 500, +# "warning_limit": { +# "limit_percent": 5 +# } +# }, +# "next_hop_unchanged": true, +# "peer": "10.1.3.2" +# } +# ], +# "redistribute": [ +# { +# "protocol": "static", +# "route_map": "map_static" +# }, +# { +# "protocol": "attached-host" +# } +# ], +# "vrf": "vrf01" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "router bgp 100", +# "no vrf vrf01", +# "no bgp convergence slow-peer time 6", +# "no bgp additional-paths send any" +# ], +# + +# Using purged: + +# Before state: + +# veos(config)#show running-config | section bgp +# router bgp 100 +# bgp convergence slow-peer time 6 +# distance bgp 50 50 50 +# maximum-paths 55 +# bgp additional-paths send any +# neighbor peer1 peer group +# neighbor peer1 link-bandwidth update-delay 5 +# neighbor peer1 fall-over bfd +# neighbor peer1 monitoring +# neighbor peer1 send-community extended link-bandwidth aggregate 600 +# neighbor peer1 maximum-routes 12000 +# neighbor 10.1.3.2 export-localpref 4000 +# neighbor 10.1.3.2 next-hop-unchanged +# neighbor 10.1.3.2 dont-capability-negotiate +# neighbor 10.1.3.2 allowas-in 3 +# neighbor 10.1.3.2 default-originate always +# neighbor 10.1.3.2 maximum-routes 500 warning-limit 5 percent +# aggregate-address 1.2.1.0/24 as-set match-map match01 +# aggregate-address 5.2.1.0/24 attribute-map attrmatch01 advertise-only +# redistribute static route-map map_static +# redistribute attached-host +# ! +# vlan 5 +# ! +# address-family ipv4 +# neighbor 10.1.3.2 prefix-list prefix01 out +# ! +# vrf vrf01 +# route-target import 54:11 +# neighbor 12.1.3.2 dont-capability-negotiate +# neighbor 12.1.3.2 allowas-in 3 +# neighbor 12.1.3.2 default-originate always +# neighbor 12.1.3.2 maximum-routes 12000 +# veos(config)# + + - name: Purge configuration + arista.eos.eos_bgp_global: + config: + as_number: "100" + state: purged + +# After State: + +# veos(config)#show running-config | section bgp +# veos(config)# + +# Module Execution: + +# "after": {}, +# "before": { +# "aggregate_address": [ +# { +# "address": "1.2.1.0/24", +# "as_set": true, +# "match_map": "match01" +# }, +# { +# "address": "5.2.1.0/24", +# "advertise_only": true, +# "attribute_map": "attrmatch01" +# } +# ], +# "as_number": "100", +# "bgp_params": { +# "additional_paths": "send", +# "convergence": { +# "slow_peer": true, +# "time": 6 +# } +# }, +# "distance": { +# "external": 50, +# "internal": 50, +# "local": 50 +# }, +# "maximum_paths": { +# "max_equal_cost_paths": 55 +# }, +# "neighbor": [ +# { +# "fall_over": true, +# "link_bandwidth": { +# "set": true, +# "update_delay": 5 +# }, +# "maximum_received_routes": { +# "count": 12000 +# }, +# "monitoring": true, +# "peer": "peer1", +# "peer_group": "peer1", +# "send_community": { +# "community_attribute": "extended", +# "link_bandwidth_attribute": "aggregate", +# "speed": "600", +# "sub_attribute": "link-bandwidth" +# } +# }, +# { +# "allowas_in": { +# "count": 3 +# }, +# "default_originate": { +# "always": true +# }, +# "dont_capability_negotiate": true, +# "export_localpref": 4000, +# "maximum_received_routes": { +# "count": 500, +# "warning_limit": { +# "limit_percent": 5 +# } +# }, +# "next_hop_unchanged": true, +# "peer": "10.1.3.2" +# } +# ], +# "redistribute": [ +# { +# "protocol": "static", +# "route_map": "map_static" +# }, +# { +# "protocol": "attached-host" +# } +# ], +# "vlan": 5, +# "vrfs": [ +# { +# "neighbor": [ +# { +# "allowas_in": { +# "count": 3 +# }, +# "default_originate": { +# "always": true +# }, +# "dont_capability_negotiate": true, +# "maximum_received_routes": { +# "count": 12000 +# }, +# "peer": "12.1.3.2" +# } +# ], +# "route_target": { +# "action": "import", +# "target": "54:11" +# }, +# "vrf": "vrf01" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "no router bgp 100" +# ], + +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.bgp_global.bgp_global import ( + Bgp_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.bgp_global.bgp_global import ( + Bgp_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Bgp_globalArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Bgp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_command.py b/ansible_collections/arista/eos/plugins/modules/eos_command.py new file mode 100644 index 000000000..437e3bc63 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_command.py @@ -0,0 +1,320 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_command +author: Peter Sprygada (@privateip) +short_description: Run arbitrary commands on an Arista EOS device +description: +- Sends an arbitrary set of commands to an EOS node and returns the results read from + the device. This module includes an argument that will cause the module to wait + for a specific condition before returning or timing out if the condition is not + met. +version_added: 1.0.0 +notes: +- Tested against Arista EOS 4.24.6F +options: + commands: + description: + - The commands to send to the remote EOS device. The + resulting output from the command is returned. If the I(wait_for) argument + is provided, the module is not returned until the condition is satisfied or + the number of I(retries) has been exceeded. + - Commands may be represented either as simple strings or as dictionaries as described below. + Refer to the Examples setion for some common uses. + required: true + type: list + elements: raw + suboptions: + command: + description: + - The command to send to the remote network device. The resulting output from + the command is returned, unless I(sendonly) is set. + required: true + type: str + output: + description: + - How the remote device should format the command response data. + type: str + choices: ["text", "json"] + version: + description: + - Specifies the version of the JSON response returned when I(output=json). + type: str + choices: ["1", "latest"] + default: "latest" + prompt: + description: + - A single regex pattern or a sequence of patterns to evaluate the expected prompt + from I(command). + required: false + type: list + elements: str + answer: + description: + - The answer to reply with if I(prompt) is matched. The value can be a single + answer or a list of answer for multiple prompts. In case the command execution + results in multiple prompts the sequence of the prompt and excepted answer should + be in same order. + required: false + type: list + elements: str + sendonly: + description: + - The boolean value, that when set to true will send I(command) to the device + but not wait for a result. + type: bool + default: false + required: false + newline: + description: + - The boolean value, that when set to false will send I(answer) to the device + without a trailing newline. + type: bool + default: true + required: false + check_all: + description: + - By default if any one of the prompts mentioned in C(prompt) option is matched + it won't check for other prompts. This boolean flag, that when set to I(True) + will check for all the prompts mentioned in C(prompt) option in the given order. + If the option is set to I(True) all the prompts should be received from remote + host if not it will result in timeout. + type: bool + default: false + wait_for: + description: + - Specifies what to evaluate from the output of the command and what conditionals + to apply. This argument will cause the task to wait for a particular conditional + to be true before moving forward. If the conditional is not true by the configured + retries, the task fails. Note - With I(wait_for) the value in C(result['stdout']) + can be accessed using C(result), that is to access C(result['stdout'][0]) use + C(result[0]) See examples. + type: list + elements: str + aliases: + - waitfor + match: + description: + - The I(match) argument is used in conjunction with the I(wait_for) argument to + specify the match policy. Valid values are C(all) or C(any). If the value + is set to C(all) then all conditionals in the I(wait_for) must be satisfied. If + the value is set to C(any) then only one of the values must be satisfied. + type: str + default: all + choices: + - any + - all + retries: + description: + - Specifies the number of retries a command should be tried before it is considered + failed. The command is run on the target device every retry and evaluated against + the I(wait_for) conditionals. + default: 10 + type: int + interval: + description: + - Configures the interval in seconds to wait between retries of the command. If + the command does not pass the specified conditional, the interval indicates + how to long to wait before trying the command again. + default: 1 + type: int +""" + +EXAMPLES = r""" +- name: run show version on remote devices + arista.eos.eos_command: + commands: show version + +- name: run show version and check to see if output contains Arista + arista.eos.eos_command: + commands: show version + wait_for: result[0] contains Arista + +- name: run multiple commands on remote nodes + arista.eos.eos_command: + commands: + - show version + - show interfaces + +- name: run multiple commands and evaluate the output + arista.eos.eos_command: + commands: + - show version + - show interfaces + wait_for: + - result[0] contains Arista + - result[1] contains Loopback0 + +- name: run commands and specify the output format + arista.eos.eos_command: + commands: + - command: show version + output: json + +- name: check whether the switch is in maintenance mode + arista.eos.eos_command: + commands: show maintenance + wait_for: result[0] contains 'Under Maintenance' + +- name: check whether the switch is in maintenance mode using json output + arista.eos.eos_command: + commands: + - command: show maintenance + output: json + wait_for: result[0].units.System.state eq 'underMaintenance' + +- name: check whether the switch is in maintenance, with 8 retries + and 2 second interval between retries + arista.eos.eos_command: + commands: show maintenance + wait_for: result[0]['units']['System']['state'] eq 'underMaintenance' + interval: 2 + retries: 8 + +- name: run a command that requires a confirmation. Note that prompt + takes regexes, and so strings containing characters like brackets + need to be escaped. + arista.eos.eos_command: + commands: + - command: reload power + prompt: \[confirm\] + answer: y + newline: false +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + returned: always apart from low level errors (such as action plugin) + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list + returned: always apart from low level errors (such as action plugin) + type: list + sample: [['...', '...'], ['...'], ['...']] +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] +""" +import time + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import ( + Conditional, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_lines, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + run_commands, + transform_commands, +) + + +def parse_commands(module, warnings): + commands = transform_commands(module) + + if module.check_mode: + for item in list(commands): + if not item["command"].startswith("show"): + warnings.append( + "Only show commands are supported when using check mode, not " + "executing %s" % item["command"], + ) + commands.remove(item) + + return commands + + +def to_cli(obj): + cmd = obj["command"] + if obj.get("output") == "json": + cmd += " | json" + return cmd + + +def main(): + """entry point for module execution""" + argument_spec = dict( + commands=dict(type="list", required=True, elements="raw"), + wait_for=dict(type="list", aliases=["waitfor"], elements="str"), + match=dict(default="all", choices=["all", "any"]), + retries=dict(default=10, type="int"), + interval=dict(default=1, type="int"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + warnings = list() + result = {"changed": False, "warnings": warnings} + commands = parse_commands(module, warnings) + wait_for = module.params["wait_for"] or list() + + try: + conditionals = [Conditional(c) for c in wait_for] + except AttributeError as exc: + module.fail_json(msg=to_text(exc)) + + retries = module.params["retries"] + interval = module.params["interval"] + match = module.params["match"] + + while retries > 0: + responses = run_commands(module, commands) + + for item in list(conditionals): + if item(responses): + if match == "any": + conditionals = list() + break + conditionals.remove(item) + + if not conditionals: + break + + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = "One or more conditional statements have not been satisfied" + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + result.update( + {"stdout": responses, "stdout_lines": list(to_lines(responses))}, + ) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_config.py b/ansible_collections/arista/eos/plugins/modules/eos_config.py new file mode 100644 index 000000000..0c8271adc --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_config.py @@ -0,0 +1,630 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_config +author: Peter Sprygada (@privateip) +short_description: Manage Arista EOS configuration sections +description: +- Arista EOS configurations use a simple block indent file syntax for segmenting configuration + into sections. This module provides an implementation for working with EOS configuration + sections in a deterministic way. This module works with either CLI or eAPI transports. +version_added: 1.0.0 +notes: +- Tested against Arista EOS 4.24.6F +- Abbreviated commands are NOT idempotent, see + L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands). +- To ensure idempotency and correct diff the configuration lines in the relevant module options should be similar to how they + appear if present in the running configuration on device including the indentation. +options: + lines: + description: + - The ordered set of commands that should be configured in the section. The commands + must be the exact same commands as found in the device running-config as found in the + device running-config to ensure idempotency and correct diff. Be sure + to note the configuration command syntax as some commands are automatically + modified by the device config parser. + aliases: + - commands + type: list + elements: str + parents: + description: + - The ordered set of parents that uniquely identify the section or hierarchy the + commands should be checked against. If the parents argument is omitted, the + commands are checked against the set of top level or global commands. + type: list + elements: str + src: + description: + - The I(src) argument provides a path to the configuration file to load into the + remote system. The path can either be a full system path to the configuration + file if the value starts with / or relative to the root of the implemented role + or playbook. This argument is mutually exclusive with the I(lines) and I(parents) + arguments. It can be a Jinja2 template as well. The configuration lines in the source + file should be similar to how it will appear if present in the running-configuration + (live switch config) of the device i ncluding the indentation to ensure idempotency + and correct diff. Arista EOS device config has 3 spaces indentation. + type: path + before: + description: + - The ordered set of commands to push on to the command stack if a change needs + to be made. This allows the playbook designer the opportunity to perform configuration + commands prior to pushing any changes without affecting how the set of commands + are matched against the system. + type: list + elements: str + after: + description: + - The ordered set of commands to append to the end of the command stack if a change + needs to be made. Just like with I(before) this allows the playbook designer + to append a set of commands to be executed after the command set. + type: list + elements: str + match: + description: + - Instructs the module on the way to perform the matching of the set of commands + against the current device config. If match is set to I(line), commands are + matched line by line. If match is set to I(strict), command lines are matched + with respect to position. If match is set to I(exact), command lines must be + an equal match. Finally, if match is set to I(none), the module will not attempt + to compare the source configuration with the running configuration on the remote + device. + type: str + default: line + choices: + - line + - strict + - exact + - none + replace: + description: + - Instructs the module on the way to perform the configuration on the device. If + the replace argument is set to I(line) then the modified lines are pushed to + the device in configuration mode. If the replace argument is set to I(block) + then the entire command block is pushed to the device in configuration mode + if any line is not correct. + type: str + default: line + choices: + - line + - block + - config + backup: + description: + - This argument will cause the module to create a full backup of the current C(running-config) + from the remote device before any changes are made. If the C(backup_options) + value is not given, the backup file is written to the C(backup) folder in the + playbook root directory or role root directory, if playbook is part of an ansible + role. If the directory does not exist, it is created. + type: bool + default: no + running_config: + description: + - The module, by default, will connect to the remote device and retrieve the current + running-config to use as a base for comparing against the contents of source. There + are times when it is not desirable to have the task get the current running-config + for every task in a playbook. The I(running_config) argument allows the implementer + to pass in the configuration to use as the base config for this module. + The configuration lines for this option should be similar to how it will appear if present + in the running-configuration of the device including the indentation to ensure idempotency + and correct diff. + type: str + aliases: + - config + defaults: + description: + - The I(defaults) argument will influence how the running-config is collected + from the device. When the value is set to true, the command used to collect + the running-config is append with the all keyword. When the value is set to + false, the command is issued without the all keyword + type: bool + default: no + save_when: + description: + - When changes are made to the device running-configuration, the changes are not + copied to non-volatile storage by default. Using this argument will change + that before. If the argument is set to I(always), then the running-config will + always be copied to the startup-config and the I(modified) flag will always + be set to True. If the argument is set to I(modified), then the running-config + will only be copied to the startup-config if it has changed since the last save + to startup-config. If the argument is set to I(never), the running-config will + never be copied to the startup-config. If the argument is set to I(changed), + then the running-config will only be copied to the startup-config if the task + has made a change. I(changed) was added in Ansible 2.5. + default: never + type: str + choices: + - always + - never + - modified + - changed + diff_against: + description: + - When using the C(ansible-playbook --diff) command line argument the module can + generate diffs against different sources. + - When this option is configure as I(startup), the module will return the diff + of the running-config against the startup-config. + - When this option is configured as I(intended), the module will return the diff + of the running-config against the configuration provided in the C(intended_config) + argument. + - When this option is configured as I(running), the module will return the before + and after diff of the running-config with respect to any changes made to the + device configuration. + - When this option is configured as C(session), the diff returned will be based + on the configuration session. + - When this option is configured as C(validate_config), the module will return before + with the running-config before applying the intended config and after with the session + config after applying the intended config to the session. + default: session + type: str + choices: + - startup + - running + - intended + - session + - validate_config + diff_ignore_lines: + description: + - Use this argument to specify one or more lines that should be ignored during + the diff. This is used for lines in the configuration that are automatically + updated by the system. This argument takes a list of regular expressions or + exact line matches. + type: list + elements: str + intended_config: + description: + - The C(intended_config) provides the master configuration that the node should + conform to and is used to check the final running-config against. This argument + will not modify any settings on the remote device and is strictly used to check + the compliance of the current device's configuration against. When specifying + this argument, the task should also modify the C(diff_against) value and set + it to I(intended). The configuration lines for this value should be similar to how it + will appear if present in the running-configuration of the device including the indentation + to ensure correct diff. + type: str + backup_options: + description: + - This is a dict object containing configurable options related to backup file + path. The value of this option is read only when C(backup) is set to I(yes), + if C(backup) is set to I(no) this option will be silently ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by <hostname>_config.<current-date>@<current-time> + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be first created and the filename is either the value of C(filename) or + default filename as described in C(filename) options description. If the + path value is not given in that case a I(backup) directory will be created + in the current working directory and backup configuration will be copied + in C(filename) within I(backup) directory. + type: path + type: dict +""" +# noqa: E501 + +EXAMPLES = """ +- name: configure top level settings + arista.eos.eos_config: + lines: hostname {{ inventory_hostname }} + +- name: load an acl into the device + arista.eos.eos_config: + lines: + - 10 permit ip host 192.0.2.1 any log + - 20 permit ip host 192.0.2.2 any log + - 30 permit ip host 192.0.2.3 any log + - 40 permit ip host 192.0.2.4 any log + parents: ip access-list test + before: no ip access-list test + replace: block + +- name: load configuration from file + arista.eos.eos_config: + src: eos.cfg + +- name: render a Jinja2 template onto an Arista switch + arista.eos.eos_config: + backup: yes + src: eos_template.j2 + +- name: diff the running config against a master config + arista.eos.eos_config: + diff_against: intended + intended_config: "{{ lookup('file', 'master.cfg') }}" + +- name: for idempotency, use full-form commands + arista.eos.eos_config: + lines: + # - shut + - shutdown + # parents: int eth1 + parents: interface Ethernet1 + +- name: configurable backup path + arista.eos.eos_config: + src: eos_template.j2 + backup: yes + backup_options: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +commands: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['hostname switch01', 'interface Ethernet1', 'no shutdown'] +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['hostname switch01', 'interface Ethernet1', 'no shutdown'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/eos_config.2016-07-16@22:28:34 +filename: + description: The name of the backup file + returned: when backup is yes and filename is not specified in backup options + type: str + sample: eos_config.2016-07-16@22:28:34 +shortname: + description: The full path to the backup file excluding the timestamp + returned: when backup is yes and filename is not specified in backup options + type: str + sample: /playbooks/ansible/backup/eos_config +date: + description: The date extracted from the backup file name + returned: when backup is yes + type: str + sample: "2016-07-16" +time: + description: The time extracted from the backup file name + returned: when backup is yes + type: str + sample: "22:28:34" +""" +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import ConnectionError +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, + dumps, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + get_config, + get_connection, + get_session_config, + load_config, + run_commands, +) + + +def get_candidate(module): + candidate = "" + if module.params["src"]: + candidate = module.params["src"] + elif module.params["lines"]: + candidate_obj = NetworkConfig(indent=3) + parents = module.params["parents"] or list() + candidate_obj.add(module.params["lines"], parents=parents) + candidate = dumps(candidate_obj, "raw") + return candidate + + +def get_running_config(module, config=None, flags=None): + contents = module.params["running_config"] + if not contents: + if config: + contents = config + else: + contents = get_config(module, flags=flags) + return contents + + +def save_config(module, result): + result["changed"] = True + if not module.check_mode: + cmd = { + "command": "copy running-config startup-config", + "output": "text", + } + run_commands(module, [cmd]) + else: + module.warn( + "Skipping command `copy running-config startup-config` " + "due to check_mode. Configuration not copied to " + "non-volatile storage", + ) + + +def main(): + """main entry point for module execution""" + backup_spec = dict(filename=dict(), dir_path=dict(type="path")) + argument_spec = dict( + src=dict(type="path"), + lines=dict(aliases=["commands"], type="list", elements="str"), + parents=dict(type="list", elements="str"), + before=dict(type="list", elements="str"), + after=dict(type="list", elements="str"), + match=dict( + default="line", + choices=["line", "strict", "exact", "none"], + ), + replace=dict(default="line", choices=["line", "block", "config"]), + defaults=dict(type="bool", default=False), + backup=dict(type="bool", default=False), + backup_options=dict(type="dict", options=backup_spec), + save_when=dict( + choices=["always", "never", "modified", "changed"], + default="never", + ), + diff_against=dict( + choices=[ + "startup", + "session", + "intended", + "running", + "validate_config", + ], + default="session", + ), + diff_ignore_lines=dict(type="list", elements="str"), + running_config=dict(aliases=["config"]), + intended_config=dict(), + ) + + mutually_exclusive = [("lines", "src"), ("parents", "src")] + + required_if = [ + ("match", "strict", ["lines"]), + ("match", "exact", ["lines"]), + ("replace", "block", ["lines"]), + ("replace", "config", ["src"]), + ("diff_against", "intended", ["intended_config"]), + ("diff_against", "validate_config", ["intended_config"]), + ] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_if=required_if, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + if warnings: + result["warnings"] = warnings + + diff_ignore_lines = module.params["diff_ignore_lines"] + config = None + contents = None + flags = ["all"] if module.params["defaults"] else [] + connection = get_connection(module) + # Refuse to diff_against: session if sessions are disabled + if ( + module.params["diff_against"] in ["session", "validate_config"] + and not connection.supports_sessions + ): + module.fail_json( + msg="Cannot diff against sessions when sessions are disabled. Please change diff_against to another value", + ) + + if module.params["backup"] or ( + module._diff and module.params["diff_against"] == "running" + ): + contents = get_config(module, flags=flags) + config = NetworkConfig(indent=1, contents=contents) + if module.params["backup"]: + result["__backup__"] = contents + + if any((module.params["src"], module.params["lines"])): + match = module.params["match"] + replace = module.params["replace"] + path = module.params["parents"] + + candidate = get_candidate(module) + running = get_running_config(module, contents, flags=flags) + + try: + response = connection.get_diff( + candidate=candidate, + running=running, + diff_match=match, + diff_ignore_lines=diff_ignore_lines, + path=path, + diff_replace=replace, + ) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + + config_diff = response["config_diff"] + if config_diff: + commands = config_diff.split("\n") + if module.params["before"]: + commands[:0] = module.params["before"] + + if module.params["after"]: + commands.extend(module.params["after"]) + + result["commands"] = commands + result["updates"] = commands + + replace = module.params["replace"] == "config" + commit = not module.check_mode + + response = load_config( + module, + commands, + replace=replace, + commit=commit, + ) + + result["changed"] = True + + if module.params["diff_against"] == "session": + if "diff" in response: + result["diff"] = {"prepared": response["diff"]} + else: + result["changed"] = False + + if "session" in response: + result["session"] = response["session"] + + running_config = module.params["running_config"] + startup_config = None + if module.params["save_when"] == "always": + save_config(module, result) + elif module.params["save_when"] == "modified": + output = run_commands( + module, + [ + {"command": "show running-config", "output": "text"}, + {"command": "show startup-config", "output": "text"}, + ], + ) + + running_config = NetworkConfig( + indent=3, + contents=output[0], + ignore_lines=diff_ignore_lines, + ) + startup_config = NetworkConfig( + indent=3, + contents=output[1], + ignore_lines=diff_ignore_lines, + ) + + if running_config.sha1 != startup_config.sha1: + save_config(module, result) + + elif module.params["save_when"] == "changed" and result["changed"]: + save_config(module, result) + if module._diff: + if not running_config: + output = run_commands( + module, + {"command": "show running-config", "output": "text"}, + ) + contents = output[0] + else: + contents = running_config + + # recreate the object in order to process diff_ignore_lines + running_config = NetworkConfig( + indent=3, + contents=contents, + ignore_lines=diff_ignore_lines, + ) + + if module.params["diff_against"] == "running": + if module.check_mode: + module.warn( + "unable to perform diff against running-config due to check mode", + ) + contents = None + else: + contents = config.config_text + + elif module.params["diff_against"] == "startup": + if not startup_config: + output = run_commands( + module, + {"command": "show startup-config", "output": "text"}, + ) + contents = output[0] + else: + contents = startup_config.config_text + + elif module.params["diff_against"] in ["intended", "validate_config"]: + contents = module.params["intended_config"] + + if contents is not None: + base_config = NetworkConfig( + indent=3, + contents=contents, + ignore_lines=diff_ignore_lines, + ) + + if running_config.sha1 != base_config.sha1: + before = "" + after = "" + if module.params["diff_against"] == "intended": + before = running_config + after = base_config + elif module.params["diff_against"] in ("startup", "running"): + before = base_config + after = running_config + elif module.params["diff_against"] == "validate_config": + before = running = get_running_config( + module, + None, + flags=flags, + ) + replace = module.params["replace"] == "config" + after = get_session_config( + module, + contents.split("\n"), + replace=replace, + commit=False, + ) + + result.update( + { + "changed": False, + "diff": {"before": str(before), "after": str(after)}, + }, + ) + + if result.get("changed") and any( + (module.params["src"], module.params["lines"]), + ): + msg = ( + "To ensure idempotency and correct diff the input configuration lines should be" + " similar to how they appear if present in" + " the running configuration on device" + ) + if module.params["src"]: + msg += " including the indentation" + if "warnings" in result: + result["warnings"].append(msg) + else: + result["warnings"] = msg + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_eapi.py b/ansible_collections/arista/eos/plugins/modules/eos_eapi.py new file mode 100644 index 000000000..2b3add753 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_eapi.py @@ -0,0 +1,437 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_eapi +author: Peter Sprygada (@privateip) +short_description: Manage and configure Arista EOS eAPI. +requirements: +- EOS v4.12 or greater +description: +- Use to enable or disable eAPI access, and set the port and state of http, https, + local_http and unix-socket servers. +- When enabling eAPI access the default is to enable HTTP on port 80, enable HTTPS + on port 443, disable local HTTP, and disable Unix socket server. Use the options + listed below to override the default configuration. +- Requires EOS v4.12 or greater. +version_added: 1.0.0 +options: + http: + description: + - The C(http) argument controls the operating state of the HTTP transport protocol + when eAPI is present in the running-config. When the value is set to True, the + HTTP protocol is enabled and when the value is set to False, the HTTP protocol + is disabled. By default, when eAPI is first configured, the HTTP protocol is + disabled. + type: bool + aliases: + - enable_http + http_port: + description: + - Configures the HTTP port that will listen for connections when the HTTP transport + protocol is enabled. This argument accepts integer values in the valid range + of 1 to 65535. + type: int + https: + description: + - The C(https) argument controls the operating state of the HTTPS transport protocol + when eAPI is present in the running-config. When the value is set to True, the + HTTPS protocol is enabled and when the value is set to False, the HTTPS protocol + is disabled. By default, when eAPI is first configured, the HTTPS protocol is + enabled. + type: bool + aliases: + - enable_https + https_port: + description: + - Configures the HTTP port that will listen for connections when the HTTP transport + protocol is enabled. This argument accepts integer values in the valid range + of 1 to 65535. + type: int + local_http: + description: + - The C(local_http) argument controls the operating state of the local HTTP transport + protocol when eAPI is present in the running-config. When the value is set + to True, the HTTP protocol is enabled and restricted to connections from localhost + only. When the value is set to False, the HTTP local protocol is disabled. + - Note is value is independent of the C(http) argument + type: bool + aliases: + - enable_local_http + local_http_port: + description: + - Configures the HTTP port that will listen for connections when the HTTP transport + protocol is enabled. This argument accepts integer values in the valid range + of 1 to 65535. + type: int + socket: + description: + - The C(socket) argument controls the operating state of the UNIX Domain Socket + used to receive eAPI requests. When the value of this argument is set to True, + the UDS will listen for eAPI requests. When the value is set to False, the + UDS will not be available to handle requests. By default when eAPI is first + configured, the UDS is disabled. + type: bool + aliases: + - enable_socket + timeout: + description: + - The time (in seconds) to wait for the eAPI configuration to be reflected in + the running-config. + type: int + default: 30 + vrf: + description: + - The C(vrf) argument will configure eAPI to listen for connections in the specified + VRF. By default, eAPI transports will listen for connections in the global + table. This value requires the VRF to already be created otherwise the task + will fail. + default: default + type: str + config: + description: + - The module, by default, will connect to the remote device and retrieve the current + running-config to use as a base for comparing against the contents of source. There + are times when it is not desirable to have the task get the current running-config + for every task in a playbook. The I(config) argument allows the implementer + to pass in the configuration to use as the base config for comparison. + type: str + state: + description: + - The C(state) argument controls the operational state of eAPI on the remote device. When + this argument is set to C(started), eAPI is enabled to receive requests and + when this argument is C(stopped), eAPI is disabled and will not receive requests. + type: str + default: started + choices: + - started + - stopped +""" + +EXAMPLES = """ +- name: Enable eAPI access with default configuration + arista.eos.eos_eapi: + state: started + +- name: Enable eAPI with no HTTP, HTTPS at port 9443, local HTTP at port 80, and socket + enabled + arista.eos.eos_eapi: + state: started + http: false + https_port: 9443 + local_http: yes + local_http_port: 80 + socket: yes + +- name: Shutdown eAPI access + arista.eos.eos_eapi: + state: stopped +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - management api http-commands + - protocol http port 81 + - no protocol https +urls: + description: Hash of URL endpoints eAPI is listening on per interface + returned: when eAPI is started + type: dict + sample: {'Management1': ['http://172.26.10.1:80']} +session_name: + description: The EOS config session name used to load the configuration + returned: when changed is True + type: str + sample: ansible_1479315771 +""" +import re +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + load_config, + run_commands, +) + + +def validate_http_port(value, module): + if not 1 <= value <= 65535: + module.fail_json(msg="http_port must be between 1 and 65535") + + +def validate_https_port(value, module): + if not 1 <= value <= 65535: + module.fail_json(msg="http_port must be between 1 and 65535") + + +def validate_local_http_port(value, module): + if not 1 <= value <= 65535: + module.fail_json(msg="http_port must be between 1 and 65535") + + +def validate_vrf(value, module): + out = run_commands(module, ["show vrf"]) + configured_vrfs = [] + lines = out[0].strip().splitlines()[3:] + for line in lines: + if not line: + continue + splitted_line = re.split(r"\s{2,}", line.strip()) + if len(splitted_line) > 2: + configured_vrfs.append(splitted_line[0]) + + configured_vrfs.append("default") + if value not in configured_vrfs: + module.fail_json( + msg="vrf `%s` is not configured on the system" % value, + ) + + +def map_obj_to_commands(updates, module, warnings): + commands = list() + want, have = updates + + def needs_update(x): + return want.get(x) is not None and (want.get(x) != have.get(x)) + + def add(cmd): + if "management api http-commands" not in commands: + commands.insert(0, "management api http-commands") + commands.append(cmd) + + if any((needs_update("http"), needs_update("http_port"))): + if want["http"] is False: + add("no protocol http") + else: + if have["http"] is False and want["http"] in (False, None): + warnings.append( + "protocol http is not enabled, not configuring http port value", + ) + else: + port = want["http_port"] or 80 + add("protocol http port %s" % port) + + if any((needs_update("https"), needs_update("https_port"))): + if want["https"] is False: + add("no protocol https") + else: + if have["https"] is False and want["https"] in (False, None): + warnings.append( + "protocol https is not enabled, not configuring https port value", + ) + else: + port = want["https_port"] or 443 + add("protocol https port %s" % port) + + if any((needs_update("local_http"), needs_update("local_http_port"))): + if want["local_http"] is False: + add("no protocol http localhost") + else: + if have["local_http"] is False and want["local_http"] in ( + False, + None, + ): + warnings.append( + "protocol local_http is not enabled, not configuring local_http port value", + ) + else: + port = want["local_http_port"] or 8080 + add("protocol http localhost port %s" % port) + + if any((needs_update("socket"), needs_update("socket"))): + if want["socket"] is False: + add("no protocol unix-socket") + else: + add("protocol unix-socket") + if needs_update("state"): + if want["state"] == "stopped": + add("shutdown") + elif want["state"] == "started": + add("no shutdown") + + if needs_update("vrf"): + add("vrf %s" % want["vrf"]) + # switching operational vrfs here + # need to add the desired state as well + if want["state"] == "stopped": + add("shutdown") + elif want["state"] == "started": + add("no shutdown") + + return commands + + +def parse_state(data): + if data[0]["enabled"]: + return "started" + else: + return "stopped" + + +def map_config_to_obj(module): + out = run_commands(module, ["show management api http-commands | json"]) + return { + "http": out[0]["httpServer"]["configured"], + "http_port": out[0]["httpServer"]["port"], + "https": out[0]["httpsServer"]["configured"], + "https_port": out[0]["httpsServer"]["port"], + "local_http": out[0]["localHttpServer"]["configured"], + "local_http_port": out[0]["localHttpServer"]["port"], + "socket": out[0]["unixSocketServer"]["configured"], + "vrf": out[0]["vrf"] or "default", + "state": parse_state(out), + } + + +def map_params_to_obj(module): + obj = { + "http": module.params["http"], + "http_port": module.params["http_port"], + "https": module.params["https"], + "https_port": module.params["https_port"], + "local_http": module.params["local_http"], + "local_http_port": module.params["local_http_port"], + "socket": module.params["socket"], + "vrf": module.params["vrf"], + "state": module.params["state"], + } + + for key, value in iteritems(obj): + if value: + validator = globals().get("validate_%s" % key) + if validator: + validator(value, module) + + return obj + + +def verify_state(updates, module): + want, have = updates + + invalid_state = [ + ("http", "httpServer"), + ("https", "httpsServer"), + ("local_http", "localHttpServer"), + ("socket", "unixSocketServer"), + ] + + timeout = module.params["timeout"] + state = module.params["state"] + + while invalid_state: + out = run_commands( + module, + ["show management api http-commands | json"], + ) + for index, item in enumerate(invalid_state): + want_key, eapi_key = item + if want[want_key] is not None: + if want[want_key] == out[0][eapi_key]["running"]: + del invalid_state[index] + elif state == "stopped": + if not out[0][eapi_key]["running"]: + del invalid_state[index] + else: + del invalid_state[index] + time.sleep(1) + timeout -= 1 + if timeout == 0: + module.fail_json( + msg="timeout expired before eapi running state changed", + ) + + +def collect_facts(module, result): + out = run_commands(module, ["show management api http-commands | json"]) + facts = dict(eos_eapi_urls=dict()) + for each in out[0]["urls"]: + intf, url = each.split(":", 1) + key = str(intf).strip() + if key not in facts["eos_eapi_urls"]: + facts["eos_eapi_urls"][key] = list() + facts["eos_eapi_urls"][key].append(str(url).strip()) + result["ansible_facts"] = facts + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + http=dict(aliases=["enable_http"], type="bool"), + http_port=dict(type="int"), + https=dict(aliases=["enable_https"], type="bool"), + https_port=dict(type="int"), + local_http=dict(aliases=["enable_local_http"], type="bool"), + local_http_port=dict(type="int"), + socket=dict(aliases=["enable_socket"], type="bool"), + timeout=dict(type="int", default=30), + vrf=dict(default="default"), + config=dict(), + state=dict(default="started", choices=["stopped", "started"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + result = {"changed": False} + + warnings = list() + if module.params["config"]: + warnings.append( + "config parameter is no longer necessary and will be ignored", + ) + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module, warnings) + result["commands"] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get("diff") and module._diff: + result["diff"] = {"prepared": response.get("diff")} + result["session_name"] = response.get("session") + result["changed"] = True + + if result["changed"]: + verify_state((want, have), module) + + collect_facts(module, result) + + if warnings: + result["warnings"] = warnings + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_facts.py b/ansible_collections/arista/eos/plugins/modules/eos_facts.py new file mode 100644 index 000000000..f404d54bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_facts.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_facts +author: +- Peter Sprygada (@privateip) +- Nathaniel Case (@Qalthos) +short_description: Collect facts from remote devices running Arista EOS +description: +- Collects facts from Arista devices running the EOS operating system. This module + places the facts gathered in the fact tree keyed by the respective resource name. The + facts module will always collect a base set of facts from the device and can enable + or disable collection of additional facts. +version_added: 1.0.0 +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected to a given subset. Possible + values for this argument include C(all), C(hardware), C(config), C(legacy), C(interfaces), and C(min). + Can specify a list of values to include a larger subset. Values can also be used + with an initial C(!) to specify that a specific subset should not be collected. + required: false + type: list + elements: str + default: 'min' + gather_network_resources: + description: + - When supplied, this argument will restrict the facts collected to a given subset. + Possible values for this argument include all and the resources like interfaces, + vlans etc. Can specify a list of values to include a larger subset. Values can + also be used with an initial C(!) to specify that a specific subset should + not be collected. Values can also be used with an initial C(!) to specify + that a specific subset should not be collected. Valid subsets are 'all', 'interfaces', + 'l2_interfaces', 'l3_interfaces', 'lacp', 'lacp_interfaces', 'lag_interfaces', + 'lldp_global', 'lldp_interfaces', 'vlans', 'acls'. + required: false + type: list + elements: str + available_network_resources: + description: When 'True' a list of network resources for which resource modules are available will be provided. + type: bool + default: false +""" + +EXAMPLES = """ +- name: Gather all legacy facts +- arista.eos.eos_facts: + gather_subset: all + +- name: Gather only the config and default facts + arista.eos.eos_facts: + gather_subset: + - config + +- name: Do not gather hardware facts + arista.eos.eos_facts: + gather_subset: + - '!hardware' + +- name: Gather legacy and resource facts + arista.eos.eos_facts: + gather_subset: all + gather_network_resources: all + +- name: Gather only the interfaces resource facts and no legacy facts +- arista.eos.eos_facts: + gather_subset: + - '!all' + - '!min' + gather_network_resources: + - interfaces + +- name: Gather all resource facts and minimal legacy facts + arista.eos.eos_facts: + gather_subset: min + gather_network_resources: all +""" + +RETURN = """ +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list + +ansible_net_gather_network_resources: + description: The list of fact for network resource subsets collected from the device + returned: when the resource is configured + type: list + +# default +ansible_net_model: + description: The model name returned from the device + returned: always + type: str +ansible_net_serialnum: + description: The serial number of the remote device + returned: always + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: str +ansible_net_image: + description: The image file the device is running + returned: always + type: str +ansible_net_fqdn: + description: The fully qualified domain name of the device + returned: always + type: str +ansible_net_api: + description: The name of the transport + returned: always + type: str +ansible_net_python_version: + description: The Python version Ansible controller is using + returned: always + type: str + +# hardware +ansible_net_filesystems: + description: All file system names available on the device + returned: when hardware is configured + type: list +ansible_net_memfree_mb: + description: The available free memory on the remote device in Mb + returned: when hardware is configured + type: int +ansible_net_memtotal_mb: + description: The total memory on the remote device in Mb + returned: when hardware is configured + type: int + +# config +ansible_net_config: + description: The current active config from the device + returned: when config is configured + type: str + +# interfaces +ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict +ansible_net_neighbors: + description: The list of LLDP neighbors from the remote device + returned: when interfaces is configured + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.facts.facts import ( + FactsArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( + FACT_RESOURCE_SUBSETS, + Facts, +) + + +def main(): + """ + Main entry point for module execution + + :returns: ansible_facts + """ + argument_spec = FactsArgs.argument_spec + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + warnings = [] + ansible_facts = {} + if module.params.get("available_network_resources"): + ansible_facts["available_network_resources"] = sorted( + FACT_RESOURCE_SUBSETS.keys(), + ) + result = Facts(module).get_facts() + additional_facts, additional_warnings = result + ansible_facts.update(additional_facts) + warnings.extend(additional_warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_hostname.py b/ansible_collections/arista/eos/plugins/modules/eos_hostname.py new file mode 100644 index 000000000..dddadb71a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_hostname.py @@ -0,0 +1,333 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for eos_hostname +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: eos_hostname +short_description: Manages hostname resource module +description: This module configures and manages the attribute of hostname on Arista + EOS platforms. +version_added: 4.1.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.60M +- This module works with connection C(network_cli). See the L(EOS Platform Options,eos_platform_options). +options: + config: + description: A dictionary of hostname options + type: dict + suboptions: + hostname: + description: + - The system's hostname + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section hostname). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + - The states I(rendered), I(gathered) and I(parsed) does not perform any change + on the device. + - The state I(rendered) will transform the configuration in C(config) option to + platform specific CLI commands which will be returned in the I(rendered) key + within the result. For state I(rendered) active connection to remote host is + not required. + - The states I(merged), I(replaced) and I(overridden) have identical + behaviour for this module. + - The state I(gathered) will fetch the running configuration from device and transform + it into structured data in the format as per the resource module argspec and + the value is returned in the I(gathered) key within the result. + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into JSON format as per the resource module parameters and the + value is returned in the I(parsed) key within the result. The value of C(running_config) + option should be the same format as the output of command + I(show running-config | section ^hostname) executed on device. For state I(parsed) active + connection to remote host is not required. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged +""" + +EXAMPLES = """ + +# Using state: merged +# Before state: +# ------------- +# test#show running-config | section ^hostname +# hostname eos +# Merged play: +# ------------ +- name: Apply the provided configuration + arista.eos.eos_hostname: + config: + hostname: eos + state: merged +# Commands Fired: +# --------------- +# "commands": [ +# "hostname eos", +# ], +# After state: +# ------------ +# test#show running-config | section ^hostname +# hostname eos + + +# Using state: deleted +# Before state: +# ------------- +# test#show running-config | section ^hostname +# hostname eosTest +# Deleted play: +# ------------- +- name: Remove all existing configuration + arista.eos.eos_hostname: + state: deleted +# Commands Fired: +# --------------- +# "commands": [ +# "no hostname eosTest", +# ], +# After state: +# ------------ +# test#show running-config | section ^hostname +# hostname eos + + +# Using state: overridden +# Before state: +# ------------- +# test#show running-config | section ^hostname +# hostname eos +# Overridden play: +# ---------------- +- name: Override commands with provided configuration + arista.eos.eos_hostname: + config: + hostname: eosTest + state: overridden +# Commands Fired: +# --------------- +# "commands": [ +# "hostname eosTest", +# ], +# After state: +# ------------ +# test#show running-config | section ^hostname +# hostname eosTest + + +# Using state: replaced +# Before state: +# ------------- +# test#show running-config | section ^hostname +# hostname eosTest +# Replaced play: +# -------------- +- name: Replace commands with provided configuration + arista.eos.eos_hostname: + config: + hostname: eosTest + state: replaced +# Commands Fired: +# --------------- +# "commands": [], +# After state: +# ------------ +# test#show running-config | section ^hostname +# hostname eosTest + +# Using state: gathered +# Before state: +# ------------- +#test#show running-config | section ^hostname +# hostname eosTest +# Gathered play: +# -------------- +- name: Gather listed hostname config + arista.eos.eos_hostname: + state: gathered +# Module Execution Result: +# ------------------------ +# "gathered": { +# "hostname": "eosTest" +# }, + +# Using state: rendered +# Rendered play: +# -------------- +- name: Render the commands for provided configuration + arista.eos.eos_hostname: + config: + hostname: eosTest + state: rendered +# Module Execution Result: +# ------------------------ +# "rendered": [ +# "hostname eosTest", +# ] + +# Using state: parsed +# File: parsed.cfg +# ---------------- +# hostname eosTest +# Parsed play: +# ------------ +- name: Parse the provided configuration with the existing running configuration + arista.eos.eos_hostname: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed +# Module Execution Result: +# ------------------------ +# "parsed": { +# "hostname": "eosTest" +# } +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - hostname eos +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - hostname eos +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - hostname eost_test +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - hostname eost_test +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.hostname.hostname import ( + HostnameArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.hostname.hostname import ( + Hostname, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=HostnameArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Hostname(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_interfaces.py new file mode 100644 index 000000000..c7d0ae021 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_interfaces.py @@ -0,0 +1,415 @@ +#!/usr/bin/python +# -*- 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 module file for eos_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_interfaces +short_description: Interfaces resource module +description: +- This module manages the interface attributes of Arista EOS interfaces. +version_added: 1.0.0 +author: +- Nathaniel Case (@qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: The provided configuration + type: list + elements: dict + suboptions: + name: + description: + - Full name of the interface, e.g. GigabitEthernet1. + type: str + required: True + description: + description: + - Interface description + type: str + duplex: + description: + - Interface link status. Applicable for Ethernet interfaces only. + - Values other than C(auto) must also set I(speed). + - Ignored when I(speed) is set above C(1000). + type: str + enabled: + default: true + description: + - Administrative state of the interface. + - Set the value to C(true) to administratively enable the interface or C(false) + to disable it. + type: bool + mtu: + description: + - MTU for a specific interface. Must be an even number between 576 and 9216. + Applicable for Ethernet interfaces only. + type: int + speed: + description: + - Interface link speed. Applicable for Ethernet interfaces only. + type: str + mode: + description: + - Manage Layer2 or Layer3 state of the interface. Applicable for Ethernet + and port channel interfaces only. + choices: + - layer2 + - layer3 + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - rendered + - gathered + default: merged + description: + - The state of the configuration after module completion. + type: str + +""" + +EXAMPLES = """ + +# Using merged + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Merge provided configuration with device configuration + arista.eos.eos_interfaces: + config: + - name: Ethernet1 + enabled: true + mode: layer3 + - name: Ethernet2 + description: Configured by Ansible + enabled: false + state: merged + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# no switchport +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +# Using replaced + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Replaces device configuration of listed interfaces with provided configuration + arista.eos.eos_interfaces: + config: + - name: Ethernet1 + enabled: true + - name: Ethernet2 + description: Configured by Ansible + enabled: false + state: replaced + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +# Using overridden + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Overrides all device configuration with provided configuration + arista.eos.eos_interfaces: + config: + - name: Ethernet1 + enabled: true + - name: Ethernet2 + description: Configured by Ansible + enabled: false + state: overridden + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! +# interface Management1 +# ip address dhcp +# ! + +# Using deleted + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# no switchport +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Delete or return interface parameters to default settings + arista.eos.eos_interfaces: + config: + - name: Ethernet1 + state: deleted + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +# Using rendered + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_interfaces: + config: + - name: Ethernet1 + enabled: true + mode: layer3 + - name: Ethernet2 + description: Configured by Ansible + enabled: false + state: merged + +# Output: +# ------------ + +# - "interface Ethernet1" +# - "description "Interface 1"" +# - "no swithcport" +# - "interface Ethernet2" +# - "description "Configured by Ansible"" +# - "shutdown" +# - "interface Management1" +# - "description "Management interface"" +# - "ip address dhcp" + +# Using parsed +# parsed.cfg + +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! + +- name: Use parsed to convert native configs to structured data + arista.eos.interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Output +# parsed: +# - name: Ethernet1 +# enabled: True +# mode: layer2 +# - name: Ethernet2 +# description: 'Configured by Ansible' +# enabled: False +# mode: layer2 + +# Using gathered: + +# Existing config on the device +# ----------------------------- +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! + +- name: Gather interfaces facts from the device + arista.eos.interfaces: + state: gathered + +# output +# gathered: +# - name: Ethernet1 +# enabled: True +# mode: layer2 +# - name: Ethernet2 +# description: 'Configured by Ansible' +# enabled: False +# mode: layer2 +""" + +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: dict + sample: The configuration returned will always be in the same format of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: dict + sample: The configuration returned will always be in the same format of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet2', 'shutdown', 'speed 10full'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.interfaces.interfaces import ( + InterfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.interfaces.interfaces import ( + Interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=InterfacesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_l2_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_l2_interfaces.py new file mode 100644 index 000000000..863a2cf2a --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_l2_interfaces.py @@ -0,0 +1,430 @@ +#!/usr/bin/python +# -*- 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 module file for eos_l2_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_l2_interfaces +short_description: L2 interfaces resource module +description: This module provides declarative management of Layer-2 interface on Arista + EOS devices. +version_added: 1.0.0 +author: Nathaniel Case (@qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: A dictionary of Layer-2 interface options + type: list + elements: dict + suboptions: + name: + description: + - Full name of interface, e.g. Ethernet1. + type: str + required: true + access: + description: + - Switchport mode access command to configure the interface as a layer 2 access. + type: dict + suboptions: + vlan: + description: + - Configure given VLAN in access port. It's used as the access VLAN ID. + type: int + trunk: + description: + - Switchport mode trunk command to configure the interface as a Layer 2 trunk. + type: dict + suboptions: + native_vlan: + description: + - Native VLAN to be configured in trunk port. It is used as the trunk + native VLAN ID. + type: int + trunk_allowed_vlans: + description: + - List of allowed VLANs in a given trunk port. These are the only VLANs + that will be configured on the trunk. + type: list + elements: str + mode: + description: + - Mode in which interface needs to be configured. + - Access mode is not shown in interface facts, so idempotency will not be + maintained for switchport mode access and every time the output will come + as changed=True. + type: str + choices: + - access + - trunk + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - rendered + - gathered + default: merged + description: + - The state of the configuration after module completion + type: str + +""" + +EXAMPLES = """ + +# Using merged + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# switchport access vlan 20 +# ! +# interface Ethernet2 +# switchport trunk native vlan 20 +# switchport mode trunk +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config +# ! + +- name: Merge provided configuration with device configuration. + arista.eos.eos_l2_interfaces: + config: + - name: Ethernet1 + mode: trunk + trunk: + native_vlan: 10 + - name: Ethernet2 + mode: access + access: + vlan: 30 + state: merged + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# switchport trunk native vlan 10 +# switchport mode trunk +# ! +# interface Ethernet2 +# switchport access vlan 30 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config +# ! + +# Using replaced + +# Before state: +# ------------- +# +# veos2#show running-config | s int +# interface Ethernet1 +# switchport access vlan 20 +# ! +# interface Ethernet2 +# switchport trunk native vlan 20 +# switchport mode trunk +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config +# ! + +- name: Replace device configuration of specified L2 interfaces with provided configuration. + arista.eos.eos_l2_interfaces: + config: + - name: Ethernet1 + mode: trunk + trunk: + native_vlan: 20 + trunk_allowed_vlans: 5-10, 15 + state: replaced + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# switchport trunk native vlan 20 +# switchport trunk allowed vlan 5-10,15 +# switchport mode trunk +# ! +# interface Ethernet2 +# switchport trunk native vlan 20 +# switchport mode trunk +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config +# ! + +# Using overridden + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# switchport access vlan 20 +# ! +# interface Ethernet2 +# switchport trunk native vlan 20 +# switchport mode trunk +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config +# ! + +- name: Override device configuration of all L2 interfaces on device with provided + configuration. + arista.eos.eos_l2_interfaces: + config: + - name: Ethernet2 + mode: access + access: + vlan: 30 + state: overridden + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# switchport access vlan 30 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config +# ! + +# Using deleted + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# switchport access vlan 20 +# ! +# interface Ethernet2 +# switchport trunk native vlan 20 +# switchport mode trunk +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config +# ! + +- name: Delete EOS L2 interfaces as in given arguments. + arista.eos.eos_l2_interfaces: + config: + - name: Ethernet1 + - name: Ethernet2 + state: deleted + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + +# using rendered + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_l2_interfaces: + config: + - name: Ethernet1 + mode: trunk + trunk: + native_vlan: 10 + - name: Ethernet2 + mode: access + access: + vlan: 30 + state: merged + +# Output : +# ------------ +# +# - "interface Ethernet1" +# - "switchport trunk native vlan 10" +# - "switchport mode trunk" +# - "interface Ethernet2" +# - "switchport access vlan 30" +# - "interface Management1" +# - "ip address dhcp" +# - "ipv6 address auto-config" + + +# using parsed + +# parsed.cfg + +# interface Ethernet1 +# switchport trunk native vlan 10 +# switchport mode trunk +# ! +# interface Ethernet2 +# switchport access vlan 30 +# ! + +- name: Use parsed to convert native configs to structured data + arista.eos.l2_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Output: +# parsed: +# - name: Ethernet1 +# mode: trunk +# trunk: +# native_vlan: 10 +# - name: Ethernet2 +# mode: access +# access: +# vlan: 30 + + +# Using gathered: +# Existing config on the device: +# +# veos#show running-config | section interface +# interface Ethernet1 +# switchport trunk native vlan 10 +# switchport mode trunk +# ! +# interface Ethernet2 +# switchport access vlan 30 +# ! + +- name: Gather interfaces facts from the device + arista.eos.l2_interfaces: + state: gathered +# output: +# gathered: +# - name: Ethernet1 +# mode: trunk +# trunk: +# native_vlan: 10 +# - name: Ethernet2 +# mode: access +# access: +# vlan: 30 + +""" + +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: The configuration returned will always be in the same format of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: The configuration returned will always be in the same format of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet2', 'switchport access vlan 20'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.l2_interfaces.l2_interfaces import ( + L2_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.l2_interfaces.l2_interfaces import ( + L2_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=L2_interfacesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = L2_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_l3_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_l3_interfaces.py new file mode 100644 index 000000000..93da7612c --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_l3_interfaces.py @@ -0,0 +1,412 @@ +#!/usr/bin/python +# -*- 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 module file for eos_l3_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_l3_interfaces +short_description: L3 interfaces resource module +description: This module provides declarative management of Layer 3 interfaces on + Arista EOS devices. +version_added: 1.0.0 +author: Nathaniel Case (@qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). + 'eos_l2_interfaces/eos_interfaces' should be used for preparing the interfaces , before applying L3 configurations using + this module (eos_l3_interfaces). +options: + config: + description: A dictionary of Layer 3 interface options + type: list + elements: dict + suboptions: + name: + description: + - Full name of the interface, i.e. Ethernet1. + type: str + required: true + ipv4: + description: + - List of IPv4 addresses to be set for the Layer 3 interface mentioned in + I(name) option. + type: list + elements: dict + suboptions: + address: + description: + - IPv4 address to be set in the format <ipv4 address>/<mask> eg. 192.0.2.1/24, + or C(dhcp) to query DHCP for an IP address. + type: str + secondary: + description: + - Whether or not this address is a secondary address. + type: bool + virtual: + description: + - Whether or not this address is a virtual address. + type: bool + ipv6: + description: + - List of IPv6 addresses to be set for the Layer 3 interface mentioned in + I(name) option. + type: list + elements: dict + suboptions: + address: + description: + - IPv6 address to be set in the address format is <ipv6 address>/<mask> + eg. 2001:db8:2201:1::1/64 or C(auto-config) to use SLAAC to chose an + address. + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state of the configuration after module completion + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged + +""" + +EXAMPLES = """ + +# Using deleted + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 192.0.2.12/24 +# ! +# interface Ethernet2 +# ipv6 address 2001:db8::1/64 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + +- name: Delete L3 attributes of given interfaces. + arista.eos.eos_l3_interfaces: + config: + - name: Ethernet1 + - name: Ethernet2 + state: deleted + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + + +# Using merged + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 192.0.2.12/24 +# ! +# interface Ethernet2 +# ipv6 address 2001:db8::1/64 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + +- name: Merge provided configuration with device configuration. + arista.eos.eos_l3_interfaces: + config: + - name: Ethernet1 + ipv4: + - address: 198.51.100.14/24 + - name: Ethernet2 + ipv4: + - address: 203.0.113.27/24 + state: merged + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 198.51.100.14/24 +# ! +# interface Ethernet2 +# ip address 203.0.113.27/24 +# ipv6 address 2001:db8::1/64 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + + +# Using overridden + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 192.0.2.12/24 +# ! +# interface Ethernet2 +# ipv6 address 2001:db8::1/64 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + +- name: Override device configuration of all L2 interfaces on device with provided + configuration. + arista.eos.eos_l3_interfaces: + config: + - name: Ethernet1 + ipv6: + - address: 2001:db8:feed::1/96 + - name: Management1 + ipv4: + - address: dhcp + ipv6: auto-config + state: overridden + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ipv6 address 2001:db8:feed::1/96 +# ! +# interface Ethernet2 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + + +# Using replaced + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 192.0.2.12/24 +# ! +# interface Ethernet2 +# ipv6 address 2001:db8::1/64 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + +- name: Replace device configuration of specified L2 interfaces with provided configuration. + arista.eos.eos_l3_interfaces: + config: + - name: Ethernet2 + ipv4: + - address: 203.0.113.27/24 + state: replaced + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 192.0.2.12/24 +# ! +# interface Ethernet2 +# ip address 203.0.113.27/24 +# ! +# interface Management1 +# ip address dhcp +# ipv6 address auto-config + +# Using parsed: + +# parsed.cfg +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 198.51.100.14/24 +# ! +# interface Ethernet2 +# ip address 203.0.113.27/24 +# ! + +- name: Use parsed to convert native configs to structured data + arista.eos.interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Output: + +# parsed: +# - name: Ethernet1 +# ipv4: +# - address: 198.51.100.14/24 +# - name: Ethernet2 +# ipv4: +# - address: 203.0.113.27/24 + +# Using rendered: + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_l3_interfaces: + config: + - name: Ethernet1 + ipv4: + - address: 198.51.100.14/24 + - name: Ethernet2 + ipv4: + - address: 203.0.113.27/24 + state: rendered + +# Output +# ------------ +#rendered: +# - "interface Ethernet1" +# - "ip address 198.51.100.14/24" +# - "interface Ethernet2" +# - "ip address 203.0.113.27/24" + +# using gathered: + +# Native COnfig: +# veos#show running-config | section interface +# interface Ethernet1 +# ip address 198.51.100.14/24 +# ! +# interface Ethernet2 +# ip address 203.0.113.27/24 +# ! + +- name: Gather l3 interfaces facts from the device + arista.eos.l3_interfaces: + state: gathered + +# gathered: +# - name: Ethernet1 +# ipv4: +# - address: 198.51.100.14/24 +# - name: Ethernet2 +# ipv4: +# - address: 203.0.113.27/24 + + +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet2', 'ip address 192.0.2.12/24'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.l3_interfaces.l3_interfaces import ( + L3_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.l3_interfaces.l3_interfaces import ( + L3_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + module = AnsibleModule( + argument_spec=L3_interfacesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = L3_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_lacp.py b/ansible_collections/arista/eos/plugins/modules/eos_lacp.py new file mode 100644 index 000000000..d79ca8805 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_lacp.py @@ -0,0 +1,247 @@ +#!/usr/bin/python +# -*- 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 module file for eos_lacp +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_lacp +short_description: LACP resource module +description: +- This module manages Global Link Aggregation Control Protocol (LACP) on Arista EOS + devices. +version_added: 1.0.0 +author: Nathaniel Case (@Qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: LACP global options. + type: dict + suboptions: + system: + description: LACP system options. + type: dict + suboptions: + priority: + description: + - The system priority to use in LACP negotiations. + - Lower value is higher priority. + - Refer to vendor documentation for valid values. + type: int + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ^lacp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - deleted + - parsed + - rendered + - gathered + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# veos# show running-config | include lacp +# lacp system-priority 10 + +- name: Merge provided global LACP attributes with device attributes + arista.eos.eos_lacp: + config: + system: + priority: 20 + state: merged + +# After state: +# ------------ +# veos# show running-config | include lacp +# lacp system-priority 20 +# + + +# Using replaced + +# Before state: +# ------------- +# veos# show running-config | include lacp +# lacp system-priority 10 + +- name: Replace device global LACP attributes with provided attributes + arista.eos.eos_lacp: + config: + system: + priority: 20 + state: replaced + +# After state: +# ------------ +# veos# show running-config | include lacp +# lacp system-priority 20 +# + + +# Using deleted + +# Before state: +# ------------- +# veos# show running-config | include lacp +# lacp system-priority 10 + +- name: Delete global LACP attributes + arista.eos.eos_lacp: + state: deleted + +# After state: +# ------------ +# veos# show running-config | include lacp +# + +#Using rendered: + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_lacp: + config: + system: + priority: 20 + state: rendered + +# Output: +# ------------ +# rendered: +# - "lacp system-priority 20" +# + +# Using parsed: + +# parsed.cfg +# lacp system-priority 20 + +- name: Use parsed to convert native configs to structured data + arista.eos.eos_lacp: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Output: +# parsed: +# system: +# priority: 20 + +# Using gathered: +# nathive config: +# ------------- +# lacp system-priority 10 + +- name: Gather lacp facts from the device + arista.eos.eos_lacp: + state: gathered + +# Output: +# gathered: +# system: +# priority: 10 +# + +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['lacp system-priority 10'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lacp.lacp import ( + LacpArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.lacp.lacp import ( + Lacp, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + module = AnsibleModule( + argument_spec=LacpArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Lacp(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_lacp_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_lacp_interfaces.py new file mode 100644 index 000000000..47a3662e8 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_lacp_interfaces.py @@ -0,0 +1,340 @@ +#!/usr/bin/python +# -*- 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 module file for eos_lacp_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_lacp_interfaces +short_description: LACP interfaces resource module +description: +- This module manages Link Aggregation Control Protocol (LACP) attributes of interfaces + on Arista EOS devices. +version_added: 1.0.0 +author: Nathaniel Case (@Qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: A dictionary of LACP interfaces options. + type: list + elements: dict + suboptions: + name: + description: + - Full name of the interface (i.e. Ethernet1). + type: str + port_priority: + description: + - LACP port priority for the interface. Range 1-65535. + type: int + timer: + description: + - Rate at which PDUs are sent by LACP. At fast rate LACP is transmitted once + every 1 second. At normal rate LACP is transmitted every 30 seconds after + the link is bundled. + type: str + choices: + - fast + - normal + aliases: + - rate + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ^interfaces). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - rendered + - gathered + default: merged + +""" +EXAMPLES = """ +# Using merged +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# lacp port-priority 30 +# interface Ethernet2 +# lacp rate fast + +- name: Merge provided configuration with device configuration + arista.eos.eos_lacp_interfaces: + config: + - name: Ethernet1 + rate: fast + - name: Ethernet2 + rate: normal + state: merged + +# +# ----------- +# After state +# ----------- +# +# veos#show run | section ^interface +# interface Ethernet1 +# lacp port-priority 30 +# lacp rate fast +# interface Ethernet2 + + +# Using replaced +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# lacp port-priority 30 +# interface Ethernet2 +# lacp rate fast + +- name: Replace existing LACP configuration of specified interfaces with provided + configuration + arista.eos.eos_lacp_interfaces: + config: + - name: Ethernet1 + rate: fast + state: replaced + +# +# ----------- +# After state +# ----------- +# +# veos#show run | section ^interface +# interface Ethernet1 +# lacp rate fast +# interface Ethernet2 +# lacp rate fast + + +# Using overridden +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# lacp port-priority 30 +# interface Ethernet2 +# lacp rate fast + +- name: Override the LACP configuration of all the interfaces with provided configuration + arista.eos.eos_lacp_interfaces: + config: + - name: Ethernet1 + rate: fast + state: overridden + +# +# ----------- +# After state +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# lacp rate fast +# interface Ethernet2 + + +# Using deleted +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# lacp port-priority 30 +# interface Ethernet2 +# lacp rate fast + +- name: Delete LACP attributes of given interfaces (or all interfaces if none specified). + arista.eos.eos_lacp_interfaces: + state: deleted + +# +# ----------- +# After state +# ----------- +# +# veos#show run | section ^interface +# interface Ethernet1 +# interface Ethernet2 + +# using rendered: + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_lacp_interfaces: + config: + - name: Ethernet1 + rate: fast + - name: Ethernet2 + rate: normal + state: rendered + +# +# ----------- +# Output +# ----------- +# rendered: +# - "interface Ethernet1" +# - "lacp rate fast" + +# Using parsed: + +# parsed.cfg: +# "interface Ethernet1" +# "lacp rate fast" +# "interface Ethernet2" + +- name: Use parsed to convert native configs to structured data + arista.eos.eos_lacp_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Output: +# parsed: +# - name: Ethernet1 +# rate: fast +# - name: Ethernet2 +# rate: normal + +# Using gathered: +# native config: +# veos#show run | section ^interface +# interface Ethernet1 +# lacp port-priority 30 +# interface Ethernet2 +# lacp rate fast + +- name: Gather LACP facts from the device + arista.eos.eos_lacp_interfaces: + state: gathered + +# Output: +# gathered: +# - name: Ethernet1 +# - name: Ethernet2 +# rate: fast + +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1', 'lacp rate fast'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lacp_interfaces.lacp_interfaces import ( + Lacp_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.lacp_interfaces.lacp_interfaces import ( + Lacp_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + module = AnsibleModule( + argument_spec=Lacp_interfacesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Lacp_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_lag_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_lag_interfaces.py new file mode 100644 index 000000000..471652772 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_lag_interfaces.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +# -*- 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 module file for eos_lag_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_lag_interfaces +short_description: LAG interfaces resource module +description: This module manages attributes of link aggregation groups on Arista EOS + devices. +version_added: 1.0.0 +author: Nathaniel Case (@Qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: A list of link aggregation group configurations. + type: list + elements: dict + suboptions: + name: + description: + - Name of the port-channel interface of the link aggregation group (LAG) e.g., + Port-Channel5. + type: str + required: true + members: + description: + - Ethernet interfaces that are part of the group. + type: list + elements: dict + suboptions: + member: + description: + - Name of ethernet interface that is a member of the LAG. + type: str + mode: + description: + - LAG mode for this interface. + type: str + choices: + - 'active' + - 'on' + - 'passive' + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section interfaces). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - rendered + - gathered + - parsed + default: merged + +""" + +EXAMPLES = """ + +# Using merged + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 + +- name: Merge provided LAG attributes with existing device configuration + arista.eos.eos_lag_interfaces: + config: + - name: 5 + members: + - member: Ethernet2 + mode: on + state: merged + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 +# channel-group 5 mode on + + +# Using replaced + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 + +- name: Replace all device configuration of specified LAGs with provided configuration + arista.eos.eos_lag_interfaces: + config: + - name: 5 + members: + - member: Ethernet2 + mode: on + state: replaced + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# interface Ethernet2 +# channel-group 5 mode on + + +# Using overridden + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 + +- name: Override all device configuration of all LAG attributes with provided configuration + arista.eos.eos_lag_interfaces: + config: + - name: 10 + members: + - member: Ethernet2 + mode: on + state: overridden + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# interface Ethernet2 +# channel-group 10 mode on + + +# Using deleted + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 +# channel-group 5 mode on + +- name: Delete LAG attributes of the given interfaces. + arista.eos.eos_lag_interfaces: + config: + - name: 5 + members: + - member: Ethernet1 + state: deleted + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# interface Ethernet2 +# channel-group 5 mode on + +# Using parsed: + +# parsed.cfg +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 +# channel-group 5 mode on + +- name: Use parsed to convert native configs to structured data + arista.eos.lag_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Output: +# parsed: +# - name: 5 +# members: +# - member: Ethernet2 +# mode: on +# - member: Ethernet1 +# mode: on + +# using rendered: + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_lag_interfaces: + config: + - name: 5 + members: + - member: Ethernet2 + mode: on + - member: Ethernet1 + mode: on + state: rendered +# ----------- +# Output +# ----------- +# +# rendered: + +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 +# channel-group 5 mode on + + +# Using gathered: + +# native config: +# interface Ethernet1 +# channel-group 5 mode on +# interface Ethernet2 +# channel-group 5 mode on + +- name: Gather lldp_global facts from the device + arista.eos.lldp_global: + state: gathered + +# Output: +# gathered: +# - name: 5 +# members: +# - member: Ethernet2 +# mode: on +# - member: Ethernet1 +# mode: on + +""" + +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['command 1', 'command 2', 'command 3'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lag_interfaces.lag_interfaces import ( + Lag_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.lag_interfaces.lag_interfaces import ( + Lag_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + module = AnsibleModule( + argument_spec=Lag_interfacesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Lag_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_lldp.py b/ansible_collections/arista/eos/plugins/modules/eos_lldp.py new file mode 100644 index 000000000..86a3cfba8 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_lldp.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_lldp +author: Ganesh Nalawade (@ganeshrn) +short_description: Manage LLDP configuration on Arista EOS network devices +description: +- This module provides declarative management of LLDP service on Arista EOS network + devices. +version_added: 1.0.0 +notes: +- Tested against Arista EOS 4.24.6F +options: + state: + description: + - State of the LLDP configuration. If value is I(present) lldp will be enabled + else if it is I(absent) it will be disabled. + default: present + type: str + choices: + - present + - absent + - enabled + - disabled +""" + +EXAMPLES = """ +- name: Enable LLDP service + arista.eos.eos_lldp: + state: present + +- name: Disable LLDP service + arista.eos.eos_lldp: + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - lldp run +""" +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + get_config, + load_config, +) + + +def has_lldp(module): + config = get_config(module, flags=["| section lldp"]) + + is_lldp_enable = False + if "no lldp run" not in config: + is_lldp_enable = True + + return is_lldp_enable + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + state=dict( + default="present", + choices=["present", "absent", "enabled", "disabled"], + ), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + + if warnings: + result["warnings"] = warnings + + HAS_LLDP = has_lldp(module) + + commands = [] + + if module.params["state"] == "absent" and HAS_LLDP: + commands.append("no lldp run") + elif module.params["state"] == "present" and not HAS_LLDP: + commands.append("lldp run") + + result["commands"] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get("diff") and module._diff: + result["diff"] = {"prepared": response.get("diff")} + result["session_name"] = response.get("session") + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_lldp_global.py b/ansible_collections/arista/eos/plugins/modules/eos_lldp_global.py new file mode 100644 index 000000000..a98535c66 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_lldp_global.py @@ -0,0 +1,347 @@ +#!/usr/bin/python +# -*- 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 module file for eos_lldp_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_lldp_global +short_description: LLDP resource module +description: +- This module manages Global Link Layer Discovery Protocol (LLDP) settings on Arista + EOS devices. +version_added: 1.0.0 +author: Nathaniel Case (@Qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: The provided global LLDP configuration. + type: dict + suboptions: + holdtime: + description: + - Specifies the holdtime (in sec) to be sent in packets. + type: int + reinit: + description: + - Specifies the delay (in sec) for LLDP initialization on any interface. + type: int + timer: + description: + - Specifies the rate at which LLDP packets are sent (in sec). + type: int + tlv_select: + description: + - Specifies the LLDP TLVs to enable or disable. + type: dict + suboptions: + link_aggregation: + description: + - Enable or disable link aggregation TLV. + type: bool + management_address: + description: + - Enable or disable management address TLV. + type: bool + max_frame_size: + description: + - Enable or disable maximum frame size TLV. + type: bool + port_description: + description: + - Enable or disable port description TLV. + type: bool + system_capabilities: + description: + - Enable or disable system capabilities TLV. + type: bool + system_description: + description: + - Enable or disable system description TLV. + type: bool + system_name: + description: + - Enable or disable system name TLV. + type: bool + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section lldp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - deleted + - rendered + - gathered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged +# +# ------------ +# Before State +# ------------ +# +# veos# show run | section lldp +# lldp timer 3000 +# lldp holdtime 100 +# lldp reinit 5 +# no lldp tlv-select management-address +# no lldp tlv-select system-description + +- name: Merge provided LLDP configuration with the existing configuration + arista.eos.eos_lldp_global: + config: + holdtime: 100 + tlv_select: + management_address: false + port_description: false + system_description: true + state: merged + +# ----------- +# After state +# ----------- +# +# veos# show run | section lldp +# lldp timer 3000 +# lldp holdtime 100 +# lldp reinit 5 +# no lldp tlv-select management-address +# no lldp tlv-select port-description + + +# Using replaced +# +# ------------ +# Before State +# ------------ +# +# veos# show run | section lldp +# lldp timer 3000 +# lldp holdtime 100 +# lldp reinit 5 +# no lldp tlv-select management-address +# no lldp tlv-select system-description + +- name: Replace existing LLDP device configuration with provided configuration + arista.eos.eos_lldp_global: + config: + holdtime: 100 + tlv_select: + management_address: false + port_description: false + system_description: true + state: replaced + +# ----------- +# After state +# ----------- +# +# veos# show run | section lldp +# lldp holdtime 100 +# no lldp tlv-select management-address +# no lldp tlv-select port-description + + +# Using deleted +# +# ------------ +# Before State +# ------------ +# +# veos# show run | section lldp +# lldp timer 3000 +# lldp holdtime 100 +# lldp reinit 5 +# no lldp tlv-select management-address +# no lldp tlv-select system-description + +- name: Delete existing LLDP configurations from the device + arista.eos.eos_lldp_global: + state: deleted + +# ----------- +# After state +# ----------- +# +# veos# show run | section ^lldp + +# Using rendered: + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_lldp_global: + config: + holdtime: 100 + tlv_select: + management_address: false + port_description: false + system_description: true + state: rendered + +# ----------- +# Output +# ----------- +# +# rendered: +# - "lldp holdtime 100" +# - "no lldp tlv-select management-address" +# - "no lldp tlv-select port-description" + +# Using parsed + +# parsed.cfg + +# lldp timer 3000 +# lldp holdtime 100 +# lldp reinit 5 +# no lldp tlv-select management-address +# no lldp tlv-select system-description + +- name: Use parsed to convert native configs to structured data + arista.eos.lldp_global: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# ----------- +# Output +# ----------- + +# parsed: +# holdtime: 100 +# timer 3000 +# reinit 5 +# tlv_select: +# management_address: False +# port_description: False +# system_description: True + +# Using gathered: +# native config: +# lldp timer 3000 +# lldp holdtime 100 +# lldp reinit 5 +# no lldp tlv-select management-address +# no lldp tlv-select system-description + + +- name: Gather lldp_global facts from the device + arista.eos.lldp_global: + state: gathered + +# ----------- +# Output +# ----------- + +# gathered: +# holdtime: 100 +# timer 3000 +# reinit 5 +# tlv_select: +# management_address: False +# port_description: False +# system_description: True + +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['lldp holdtime 100', 'no lldp timer', 'lldp tlv-select system-description'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lldp_global.lldp_global import ( + Lldp_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.lldp_global.lldp_global import ( + Lldp_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + module = AnsibleModule( + argument_spec=Lldp_globalArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Lldp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_lldp_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_lldp_interfaces.py new file mode 100644 index 000000000..6b5aae118 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_lldp_interfaces.py @@ -0,0 +1,346 @@ +#!/usr/bin/python +# -*- 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 module file for eos_lldp_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_lldp_interfaces +short_description: LLDP interfaces resource module +description: +- This module manages Link Layer Discovery Protocol (LLDP) attributes of interfaces + on Arista EOS devices. +version_added: 1.0.0 +author: Nathaniel Case (@Qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: A dictionary of LLDP interfaces options. + type: list + elements: dict + suboptions: + name: + description: + - Full name of the interface (i.e. Ethernet1). + type: str + receive: + description: + - Enable/disable LLDP RX on an interface. + type: bool + transmit: + description: + - Enable/disable LLDP TX on an interface. + type: bool + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged + +""" +EXAMPLES = """ +# Using merged +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# no lldp receive +# interface Ethernet2 +# no lldp transmit + +- name: Merge provided configuration with running configuration + arista.eos.eos_lldp_interfaces: + config: + - name: Ethernet1 + transmit: false + - name: Ethernet2 + transmit: false + state: merged + +# +# ------------ +# After state +# ------------ +# +# veos#show run | section ^interface +# interface Ethernet1 +# no lldp transmit +# no lldp receive +# interface Ethernet2 +# no lldp transmit + + +# Using replaced +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# no lldp receive +# interface Ethernet2 +# no lldp transmit + +- name: Replace existing LLDP configuration of specified interfaces with provided + configuration + arista.eos.eos_lldp_interfaces: + config: + - name: Ethernet1 + transmit: false + state: replaced + +# +# ------------ +# After state +# ------------ +# +# veos#show run | section ^interface +# interface Ethernet1 +# no lldp transmit +# interface Ethernet2 +# no lldp transmit + + +# Using overridden +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# no lldp receive +# interface Ethernet2 +# no lldp transmit + +- name: Override the LLDP configuration of all the interfaces with provided configuration + arista.eos.eos_lldp_interfaces: + config: + - name: Ethernet1 + transmit: false + state: overridden + +# +# ------------ +# After state +# ------------ +# +# veos#show run | section ^interface +# interface Ethernet1 +# no lldp transmit +# interface Ethernet2 + + +# Using deleted +# +# +# ------------ +# Before state +# ------------ +# +# +# veos#show run | section ^interface +# interface Ethernet1 +# no lldp receive +# interface Ethernet2 +# no lldp transmit + +- name: Delete LLDP configuration of specified interfaces (or all interfaces if none + are specified) + arista.eos.eos_lldp_interfaces: + state: deleted + +# +# ------------ +# After state +# ------------ +# +# veos#show run | section ^interface +# interface Ethernet1 +# interface Ethernet2 + +# using rendered: + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_lldp_interfaces: + config: + - name: Ethernet1 + transmit: false + - name: Ethernet2 + transmit: false + state: rendered + +# +# ------------ +# Output +# ------------ +# +# interface Ethernet1 +# no lldp transmit +# interface Ethernet2 +# no lldp transmit + +# Using parsed +# parsed.cfg + +# interface Ethernet1 +# no lldp transmit +# interface Ethernet2 +# no lldp transmit + + +- name: Use parsed to convert native configs to structured data + arista.eos.lldp_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# ------------ +# Output +# ------------ + +# parsed: +# - name: Ethernet1 +# transmit: False +# - name: Ethernet2 +# transmit: False + +# Using gathered: + +# native config: +# interface Ethernet1 +# no lldp transmit +# interface Ethernet2 +# no lldp transmit + +- name: Gather lldp interfaces facts from the device + arista.eos.lldp_interfaces: + state: gathered + +# ------------ +# Output +# ------------ + +# gathered: +# - name: Ethernet1 +# transmit: False +# - name: Ethernet2 +# transmit: False + +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1', 'no lldp transmit'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.lldp_interfaces.lldp_interfaces import ( + Lldp_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.lldp_interfaces.lldp_interfaces import ( + Lldp_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + module = AnsibleModule( + argument_spec=Lldp_interfacesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Lldp_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_logging.py b/ansible_collections/arista/eos/plugins/modules/eos_logging.py new file mode 100644 index 000000000..b2b9106f6 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_logging.py @@ -0,0 +1,505 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# Copyright: (c) 2017, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = """ +module: eos_logging +author: Trishna Guha (@trishnaguha) +short_description: Manage logging on network devices +description: +- This module provides declarative management of logging on Arista Eos devices. +version_added: 1.0.0 +deprecated: + alternative: eos_logging_global + why: Updated module released with more functionality. + removed_at_date: '2024-01-01' +notes: +- Tested against Arista EOS 4.24.6F +options: + dest: + description: + - Destination of the logs. + choices: + - "on" + - host + - console + - monitor + - buffered + type: str + name: + description: + - The hostname or IP address of the destination. + - Required when I(dest=host). + type: str + size: + description: + - Size of buffer. The acceptable value is in range from 10 to 2147483647 bytes. + type: int + facility: + description: + - Set logging facility. + type: str + level: + description: + - Set logging severity levels. + choices: + - emergencies + - alerts + - critical + - errors + - warnings + - notifications + - informational + - debugging + type: str + aggregate: + description: List of logging definitions. + type: list + elements: dict + suboptions: + dest: + description: + - Destination of the logs. + choices: + - "on" + - host + - console + - monitor + - buffered + type: str + name: + description: + - The hostname or IP address of the destination. + - Required when I(dest=host). + type: str + size: + description: + - Size of buffer. The acceptable value is in range from 10 to 2147483647 bytes. + type: int + facility: + description: + - Set logging facility. + type: str + level: + description: + - Set logging severity levels. + choices: + - emergencies + - alerts + - critical + - errors + - warnings + - notifications + - informational + - debugging + type: str + state: + description: + - State of the logging configuration. + default: present + type: str + choices: + - present + - absent + state: + description: + - State of the logging configuration. + default: present + type: str + choices: + - present + - absent +""" + +EXAMPLES = """ +- name: configure host logging + arista.eos.eos_logging: + dest: host + name: 172.16.0.1 + state: present + +- name: remove host logging configuration + arista.eos.eos_logging: + dest: host + name: 172.16.0.1 + state: absent + +- name: configure console logging level and facility + arista.eos.eos_logging: + dest: console + facility: local7 + level: debugging + state: present + +- name: enable logging to all + arista.eos.eos_logging: + dest: on + +- name: configure buffer size + arista.eos.eos_logging: + dest: buffered + size: 5000 + +- name: Configure logging using aggregate + arista.eos.eos_logging: + aggregate: + - {dest: console, level: warnings} + - {dest: buffered, size: 480000} + state: present +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - logging facility local7 + - logging host 172.16.0.1 +""" + +import re + +from copy import deepcopy + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.validation import check_required_if +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_default_spec, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + get_config, + load_config, +) + + +DEST_GROUP = ["on", "host", "console", "monitor", "buffered"] +LEVEL_GROUP = [ + "emergencies", + "alerts", + "critical", + "errors", + "warnings", + "notifications", + "informational", + "debugging", +] + + +def validate_size(value, module): + if value: + if not int(10) <= value <= int(2147483647): + module.fail_json(msg="size must be between 10 and 2147483647") + else: + return value + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + + for w in want: + dest = w["dest"] + name = w["name"] + size = w["size"] + facility = w["facility"] + level = w["level"] + state = w["state"] + del w["state"] + + if state == "absent" and w in have: + if dest: + if dest == "host": + commands.append("no logging host {0}".format(name)) + + elif dest in DEST_GROUP: + commands.append("no logging {0}".format(dest)) + + else: + module.fail_json( + msg="dest must be among console, monitor, buffered, host, on", + ) + + if facility: + commands.append("no logging facility {0}".format(facility)) + + if state == "present" and w not in have: + if facility: + present = False + + # Iterate over every dictionary in the 'have' list to check if + # similar configuration for facility exists or not + + for entry in have: + if not entry["dest"] and entry["facility"] == facility: + present = True + + if not present: + commands.append("logging facility {0}".format(facility)) + + if dest == "host": + commands.append("logging host {0}".format(name)) + + elif dest == "on": + commands.append("logging on") + + elif dest == "buffered" and size: + present = False + + # Deals with the following two cases: + # Case 1: logging buffered <size> <level> + # logging buffered <same-size> + # + # Case 2: Same buffered logging configuration + # already exists (i.e., both size & + # level are same) + + for entry in have: + if entry["dest"] == "buffered" and entry["size"] == size: + if not level or entry["level"] == level: + present = True + + if not present: + if size and level: + commands.append( + "logging buffered {0} {1}".format(size, level), + ) + else: + commands.append("logging buffered {0}".format(size)) + + else: + if dest: + dest_cmd = "logging {0}".format(dest) + if level: + dest_cmd += " {0}".format(level) + + commands.append(dest_cmd) + return commands + + +def parse_facility(line): + facility = None + match = re.search(r"logging facility (\S+)", line, re.M) + if match: + facility = match.group(1) + + return facility + + +def parse_size(line, dest): + size = None + + if dest == "buffered": + match = re.search(r"logging buffered (\S+)", line, re.M) + if match: + try: + int_size = int(match.group(1)) + except ValueError: + int_size = None + + if int_size: + if isinstance(int_size, int): + size = str(match.group(1)) + else: + size = str(10) + + return size + + +def parse_name(line, dest): + name = None + if dest == "host": + match = re.search(r"logging host (\S+)", line, re.M) + if match: + name = match.group(1) + + return name + + +def parse_level(line, dest): + level = None + + if dest != "host": + # Line for buffer logging entry in running-config is of the form: + # logging buffered <size> <level> + + if dest == "buffered": + match = re.search(r"logging buffered (?:\d+) (\S+)", line, re.M) + + else: + match = re.search(r"logging {0} (\S+)".format(dest), line, re.M) + + if match: + if match.group(1) in LEVEL_GROUP: + level = match.group(1) + + return level + + +def map_config_to_obj(module): + obj = [] + + data = get_config(module, flags=["section logging"]) + + for line in data.split("\n"): + match = re.search(r"logging (\S+)", line, re.M) + + if match: + if match.group(1) in DEST_GROUP: + dest = match.group(1) + + else: + dest = None + + obj.append( + { + "dest": dest, + "name": parse_name(line, dest), + "size": parse_size(line, dest), + "facility": parse_facility(line), + "level": parse_level(line, dest), + }, + ) + + return obj + + +def parse_obj(obj, module): + if module.params["size"] is None: + obj.append( + { + "dest": module.params["dest"], + "name": module.params["name"], + "size": module.params["size"], + "facility": module.params["facility"], + "level": module.params["level"], + "state": module.params["state"], + }, + ) + + else: + obj.append( + { + "dest": module.params["dest"], + "name": module.params["name"], + "size": str(validate_size(module.params["size"], module)), + "facility": module.params["facility"], + "level": module.params["level"], + "state": module.params["state"], + }, + ) + + return obj + + +def map_params_to_obj(module, required_if=None): + obj = [] + aggregate = module.params.get("aggregate") + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + try: + check_required_if(required_if, item) + except TypeError as exc: + module.fail_json(to_text(exc)) + d = item.copy() + + if d["dest"] != "host": + d["name"] = None + + if d["dest"] == "buffered": + if "size" in d: + d["size"] = str(validate_size(d["size"], module)) + elif "size" not in d: + d["size"] = str(10) + else: + pass + + if d["dest"] != "buffered": + d["size"] = None + + obj.append(d) + + else: + if module.params["dest"] != "host": + module.params["name"] = None + + if module.params["dest"] == "buffered": + if not module.params["size"]: + module.params["size"] = str(10) + else: + module.params["size"] = None + + parse_obj(obj, module) + + return obj + + +def main(): + """main entry point for module execution""" + element_spec = dict( + dest=dict(choices=DEST_GROUP), + name=dict(), + size=dict(type="int"), + facility=dict(), + level=dict(choices=LEVEL_GROUP), + state=dict(default="present", choices=["present", "absent"]), + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + aggregate_spec["state"].update(default="present") + argument_spec = dict( + aggregate=dict(type="list", elements="dict", options=aggregate_spec), + ) + + argument_spec.update(element_spec) + + required_if = [("dest", "host", ["name"])] + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + if warnings: + result["warnings"] = warnings + + have = map_config_to_obj(module) + want = map_params_to_obj(module, required_if=required_if) + + commands = map_obj_to_commands((want, have), module) + result["commands"] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get("diff") and module._diff: + result["diff"] = {"prepared": response.get("diff")} + result["session_name"] = response.get("session") + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_logging_global.py b/ansible_collections/arista/eos/plugins/modules/eos_logging_global.py new file mode 100644 index 000000000..a614dcb8e --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_logging_global.py @@ -0,0 +1,942 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for eos_logging_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: eos_logging_global +short_description: Manages logging resource module +description: This module configures and manages the attributes of logging on Arista + EOS platforms. +version_added: 3.0.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6M +- This module works with connection C(network_cli). See the L(EOS Platform Options,eos_platform_options). +options: + config: + description: A dictionary of logging options + type: dict + suboptions: + buffered: + description: + - Set buffered logging parameters. + type: dict + suboptions: &message_options + severity: &sev + description: Severity level . + type: str + choices: + - emergencies + - alerts + - critical + - errors + - warnings + - notifications + - informational + - debugging + buffer_size: + description: Logging buffer size + type: int + console: + description: + - Set console logging parameters. + type: dict + suboptions: + severity: *sev + event: + description: Global events + type: str + choices: ["link-status", "port-channel", "spanning-tree"] + facility: + description: Set logging facility. + type: str + "choices": [ + "auth", + "cron", + "daemon", + "kern", + "local0", + "local1", + "local2", + "local3", + "local4", + "local5", + "local6", + "local7", + "lpr", + "mail", + "news", + "sys10", + "sys11", + "sys12", + "sys13", + "sys14", + "sys9", + "syslog", + "user", + "uucp", + ] + format: + description: Set logging format parameters + type: dict + suboptions: + hostname: + description: Specify hostname logging format. + type: str + timestamp: + description: Set timestamp logging parameters. + type: dict + suboptions: + high_resolution: + description: RFC3339 timestamps. + type: bool + traditional: + description: Traditional syslog timestamp format as specified in RFC3164. + type: dict + suboptions: + state: + description: When enabled traditional timestamp format is set. + type: str + choices: ["enabled", "disabled"] + timezone: + description: Show timezone in traditional format timestamp + type: bool + year: + description: Show year in traditional format timestamp + type: bool + sequence_numbers: + description: No. of log messages. + type: bool + hosts: &host + description: Set syslog server IP address and parameters. + type: list + elements: dict + suboptions: + name: + description: Hostname or IP address of the syslog server. + type: str + add: + description: Configure ports on the given host. + type: bool + remove: + description: Remove configured ports from the given host + type: bool + protocol: + description: Set syslog server transport protocol + type: str + choices: ["tcp", "udp"] + port: + description: Port of the syslog server. + type: int + level: + description: Configure logging severity + type: dict + suboptions: + facility: + description: Facility level + type: str + severity: *sev + monitor: + description: Set terminal monitor severity + type: str + turn_on: + description: Turn on logging. + type: bool + persistent: + description: Save logging messages to the flash disk. + type: dict + suboptions: + set: + description: Save logging messages to the flash dis. + type: bool + size: + description: The maximum size (in bytes) of logging file stored on flash disk. + type: int + policy: + description: Configure logging policies. + type: dict + suboptions: + invert_result: + description: Invert the match of match-list. + type: bool + match_list: + description: Configure logging message filtering. + type: str + qos: + description: Set DSCP value in IP header. + type: int + relogging_interval: + description: Configure relogging-interval for critical log messages + type: int + repeat_messages: + description: Repeat messages instead of summarizing number of repeats + type: bool + source_interface: &srcint + description: Use IP Address of interface as source IP of log messages. + type: str + synchronous: + description: Set synchronizing unsolicited with solicited messages + type: dict + suboptions: + set: + description: Set synchronizing unsolicited with solicited messages. + type: bool + level: + description: Configure logging severity + type: str + trap: + description: Severity of messages sent to the syslog server. + type: dict + suboptions: + set: + description: Severity of messages sent to the syslog server. + type: bool + severity: *sev + vrfs: + description: Specify vrf + type: list + elements: dict + suboptions: + name: + description: vrf name. + type: str + hosts: *host + source_interface: *srcint + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section access-list). + - The states I(replaced) and I(overridden) have identical + behaviour for this module. + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged +""" +EXAMPLES = """ + +# Using merged + +# Before state + +# test(config)#show running-config | section logging +# test(config)# + + - name: Merge provided configuration with device configuration + arista.eos.eos_logging_global: + config: + hosts: + - name: "host01" + protocol: "tcp" + - name: "11.11.11.1" + port: 25 + vrfs: + - name: "vrf01" + source_interface: "Ethernet1" + - name: "vrf02" + hosts: + - name: "hostvrf1" + protocol: "tcp" + - name: "24.1.1.1" + port: "33" + +# After State: + +# test(config)#show running-config | section logging +# logging host 11.11.11.1 25 +# logging host host01 514 protocol tcp +# logging vrf vrf02 host 24.1.1.1 33 +# logging vrf vrf02 host hostvrf1 514 protocol tcp +# logging vrf vrf01 source-interface Ethernet1 +# test(config)# +# +# +# Module Execution: +# "after": { +# "hosts": [ +# { +# "name": "11.11.11.1", +# "port": 25 +# }, +# { +# "name": "host01", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "vrfs": [ +# { +# "name": "vrf01", +# "source_interface": "Ethernet1" +# }, +# { +# "hosts": [ +# { +# "name": "24.1.1.1", +# "port": 33 +# }, +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf02" +# } +# ] +# }, +# "before": {}, +# "changed": true, +# "commands": [ +# "logging host host01 protocol tcp", +# "logging host 11.11.11.1 25", +# "logging vrf vrf01 source-interface Ethernet1", +# "logging vrf vrf02 host hostvrf1 protocol tcp", +# "logging vrf vrf02 host 24.1.1.1 33" +# ], +# + +# Using replaced: +# Before State: + +# test(config)#show running-config | section logging +# logging host 11.11.11.1 25 +# logging host host01 514 protocol tcp +# logging vrf vrf02 host 24.1.1.1 33 +# logging vrf vrf02 host hostvrf1 514 protocol tcp +# logging format timestamp traditional timezone +# logging vrf vrf01 source-interface Ethernet1 +# logging policy match inverse-result match-list list01 discard +# logging persistent 4096 +# ! +# logging level AAA alerts +# test(config)# + + - name: Repalce + arista.eos.eos_logging_global: + config: + synchronous: + set: True + trap: + severity: "critical" + hosts: + - name: "host02" + protocol: "tcp" + vrfs: + - name: "vrf03" + source_interface: "Vlan100" + - name: "vrf04" + hosts: + - name: "hostvrf1" + protocol: "tcp" + + state: replaced + +# After State: +# test(config)#show running-config | section logging +# logging synchronous +# logging trap critical +# logging host host02 514 protocol tcp +# logging vrf vrf04 host hostvrf1 514 protocol tcp +# logging vrf vrf03 source-interface Vlan100 +# test(config)# +# +# Module Execution: +# "after": { +# "hosts": [ +# { +# "name": "host02", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "synchronous": { +# "set": True +# }, +# "trap": { +# "severity": "critical" +# }, +# "vrfs": [ +# { +# "name": "vrf03", +# "source_interface": "Vlan100" +# }, +# { +# "hosts": [ +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf04" +# } +# ] +# }, +# "before": { +# "format": { +# "timestamp": { +# "traditional": { +# "timezone": true +# } +# } +# }, +# "hosts": [ +# { +# "name": "11.11.11.1", +# "port": 25 +# }, +# { +# "name": "host01", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "level": { +# "facility": "AAA", +# "severity": "alerts" +# }, +# "persistent": { +# "size": 4096 +# }, +# "policy": { +# "invert_result": true, +# "match_list": "list01" +# }, +# "vrfs": [ +# { +# "name": "vrf01", +# "source_interface": "Ethernet1" +# }, +# { +# "hosts": [ +# { +# "name": "24.1.1.1", +# "port": 33 +# }, +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf02" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "logging host host02 protocol tcp", +# "no logging host 11.11.11.1 25", +# "no logging host host01 514 protocol tcp", +# "logging vrf vrf03 source-interface Vlan100", +# "logging vrf vrf04 host hostvrf1 protocol tcp", +# "no logging vrf vrf01 source-interface Ethernet1", +# "no logging vrf vrf02 host 24.1.1.1 33", +# "no logging vrf vrf02 host hostvrf1 514 protocol tcp", +# "no logging format timestamp traditional timezone", +# "no logging level AAA alerts", +# "no logging persistent 4096", +# "no logging policy match invert-result match-list list01 discard", +# "logging synchronous", +# "logging trap critical" +# ], +# +# + + +# Using overridden: +# Before State: + +# test(config)#show running-config | section logging +# logging host 11.11.11.1 25 +# logging host host01 514 protocol tcp +# logging vrf vrf02 host 24.1.1.1 33 +# logging vrf vrf02 host hostvrf1 514 protocol tcp +# logging format timestamp traditional timezone +# logging vrf vrf01 source-interface Ethernet1 +# logging policy match inverse-result match-list list01 discard +# logging persistent 4096 +# ! +# logging level AAA alerts +# test(config)# + + - name: Repalce + arista.eos.eos_logging_global: + config: + synchronous: + set: True + trap: + severity: "critical" + hosts: + - name: "host02" + protocol: "tcp" + vrfs: + - name: "vrf03" + source_interface: "Vlan100" + - name: "vrf04" + hosts: + - name: "hostvrf1" + protocol: "tcp" + + state: overridden + +# After State: +# test(config)#show running-config | section logging +# logging synchronous +# logging trap critical +# logging host host02 514 protocol tcp +# logging vrf vrf04 host hostvrf1 514 protocol tcp +# logging vrf vrf03 source-interface Vlan100 +# test(config)# +# +# Module Execution: +# "after": { +# "hosts": [ +# { +# "name": "host02", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "synchronous": { +# "set": True +# }, +# "trap": { +# "severity": "critical" +# }, +# "vrfs": [ +# { +# "name": "vrf03", +# "source_interface": "Vlan100" +# }, +# { +# "hosts": [ +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf04" +# } +# ] +# }, +# "before": { +# "format": { +# "timestamp": { +# "traditional": { +# "timezone": true +# } +# } +# }, +# "hosts": [ +# { +# "name": "11.11.11.1", +# "port": 25 +# }, +# { +# "name": "host01", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "level": { +# "facility": "AAA", +# "severity": "alerts" +# }, +# "persistent": { +# "size": 4096 +# }, +# "policy": { +# "invert_result": true, +# "match_list": "list01" +# }, +# "vrfs": [ +# { +# "name": "vrf01", +# "source_interface": "Ethernet1" +# }, +# { +# "hosts": [ +# { +# "name": "24.1.1.1", +# "port": 33 +# }, +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf02" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "logging host host02 protocol tcp", +# "no logging host 11.11.11.1 25", +# "no logging host host01 514 protocol tcp", +# "logging vrf vrf03 source-interface Vlan100", +# "logging vrf vrf04 host hostvrf1 protocol tcp", +# "no logging vrf vrf01 source-interface Ethernet1", +# "no logging vrf vrf02 host 24.1.1.1 33", +# "no logging vrf vrf02 host hostvrf1 514 protocol tcp", +# "no logging format timestamp traditional timezone", +# "no logging level AAA alerts", +# "no logging persistent 4096", +# "no logging policy match invert-result match-list list01 discard", +# "logging synchronous", +# "logging trap critical" +# ], +# +# + +# Using deleted: + +# Before State: +# test(config)#show running-config | section logging +# logging synchronous level critical +# logging host 11.11.11.1 25 +# logging host host01 514 protocol tcp +# logging host host02 514 protocol tcp +# logging vrf vrf02 host 24.1.1.1 33 +# logging vrf vrf02 host hostvrf1 514 protocol tcp +# logging vrf vrf04 host hostvrf1 514 protocol tcp +# logging vrf vrf01 source-interface Ethernet1 +# logging vrf vrf03 source-interface Vlan100 +# test(config)# + + - name: Delete all logging configs + arista.eos.eos_logging_global: + state: deleted + become: yes + +# After state: +# test(config)#show running-config | section logging +# test(config)# +# +# "after": {}, +# "before": { +# "hosts": [ +# { +# "name": "11.11.11.1", +# "port": 25 +# }, +# { +# "name": "host01", +# "port": 514, +# "protocol": "tcp" +# }, +# { +# "name": "host02", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "synchronous": { +# "level": "critical" +# }, +# "vrfs": [ +# { +# "name": "vrf01", +# "source_interface": "Ethernet1" +# }, +# { +# "hosts": [ +# { +# "name": "24.1.1.1", +# "port": 33 +# }, +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf02" +# }, +# { +# "name": "vrf03", +# "source_interface": "Vlan100" +# }, +# { +# "hosts": [ +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf04" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "no logging host 11.11.11.1 25", +# "no logging host host01 514 protocol tcp", +# "no logging host host02 514 protocol tcp", +# "no logging vrf vrf01 source-interface Ethernet1", +# "no logging vrf vrf02 host 24.1.1.1 33", +# "no logging vrf vrf02 host hostvrf1 514 protocol tcp", +# "no logging vrf vrf03 source-interface Vlan100", +# "no logging vrf vrf04 host hostvrf1 514 protocol tcp", +# "no logging synchronous level critical" +# ], + +# Using parsed: +# parsed.cfg + +# logging host 11.11.11.1 25 +# logging host host01 514 protocol tcp +# logging vrf vrf02 host 24.1.1.1 33 +# logging vrf vrf02 host hostvrf1 514 protocol tcp +# logging format timestamp traditional timezone +# logging vrf vrf01 source-interface Ethernet1 +# logging policy match inverse-result match-list list01 discard +# logging persistent 4096 +# ! +# logging level AAA alerts + + - name: parse configs + arista.eos.eos_logging_global: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + +# Module Execution +# "parsed": { +# "format": { +# "timestamp": { +# "traditional": { +# "timezone": true +# } +# } +# }, +# "hosts": [ +# { +# "name": "11.11.11.1", +# "port": 25 +# }, +# { +# "name": "host01", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "level": { +# "facility": "AAA", +# "severity": "alerts" +# }, +# "persistent": { +# "size": 4096 +# }, +# "policy": { +# "invert_result": true, +# "match_list": "list01" +# }, +# "vrfs": [ +# { +# "name": "vrf01", +# "source_interface": "Ethernet1" +# }, +# { +# "hosts": [ +# { +# "name": "24.1.1.1", +# "port": 33 +# }, +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf02" +# } +# ] +# } +# + +# Using gathered: +# Before State: +# test(config)#show running-config | section logging +# logging host 11.11.11.1 25 +# logging host host01 514 protocol tcp +# logging vrf vrf02 host 24.1.1.1 33 +# logging vrf vrf02 host hostvrf1 514 protocol tcp +# logging format timestamp traditional timezone +# logging vrf vrf01 source-interface Ethernet1 +# logging policy match inverse-result match-list list01 discard +# logging persistent 4096 +# ! +# logging level AAA alerts +# test(config)# + + - name: gather configs + arista.eos.eos_logging_global: + state: gathered + +# Module Execution: +# "gathered": { +# "format": { +# "timestamp": { +# "traditional": { +# "timezone": true +# } +# } +# }, +# "hosts": [ +# { +# "name": "11.11.11.1", +# "port": 25 +# }, +# { +# "name": "host01", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "level": { +# "facility": "AAA", +# "severity": "alerts" +# }, +# "persistent": { +# "size": 4096 +# }, +# "policy": { +# "invert_result": true, +# "match_list": "list01" +# }, +# "vrfs": [ +# { +# "name": "vrf01", +# "source_interface": "Ethernet1" +# }, +# { +# "hosts": [ +# { +# "name": "24.1.1.1", +# "port": 33 +# }, +# { +# "name": "hostvrf1", +# "port": 514, +# "protocol": "tcp" +# } +# ], +# "name": "vrf02" +# } +# ] +# }, +# + +# Using rendered: + - name: Render provided configuration + arista.eos.eos_logging_global: + config: + format: + timestamp: + traditional: + timezone: True + level: + facility: "AAA" + severity: "alerts" + persistent: + size: 4096 + policy: + invert_result: True + match_list: "list01" + hosts: + - name: "host01" + protocol: "tcp" + - name: "11.11.11.1" + port: 25 + vrfs: + - name: "vrf01" + source_interface: "Ethernet1" + - name: "vrf02" + hosts: + - name: "hostvrf1" + protocol: "tcp" + - name: "24.1.1.1" + port: "33" +# Module Execution: + +# "rendered": [ +# "logging host host01 protocol tcp", +# "logging host 11.11.11.1 25", +# "logging vrf vrf01 source-interface Ethernet1", +# "logging vrf vrf02 host hostvrf1 protocol tcp", +# "logging vrf vrf02 host 24.1.1.1 33", +# "logging format timestamp traditional timezone", +# "logging level AAA alerts", +# "logging persistent 4096", +# "logging policy match invert-result match-list list01 discard" +# ] +# + +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.logging_global.logging_global import ( + Logging_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.logging_global.logging_global import ( + Logging_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Logging_globalArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Logging_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_ntp_global.py b/ansible_collections/arista/eos/plugins/modules/eos_ntp_global.py new file mode 100644 index 000000000..cb89f3a9d --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_ntp_global.py @@ -0,0 +1,1053 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for eos_ntp_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: eos_ntp_global +short_description: Manages ntp resource module +description: This module configures and manages the attributes of ntp on Arista + EOS platforms. +version_added: 3.1.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.60M +- This module works with connection C(network_cli). See the U(https://docs.ansible.com/ansible/latest/network/user_guide/platform_eos.html). +options: + config: + description: A dictionary of ntp options + type: dict + suboptions: + authenticate: + description: + - Require authentication for NTP synchronization. + type: dict + suboptions: + enable: + description: Enable authentication for NTP synchronization. + type: bool + servers: + description: Authentication required only for incoming NTP server responses. + type: bool + authentication_keys: + description: + - Define a key to use for authentication. + type: list + elements: dict + suboptions: + id: + description: key identifier. + type: int + algorithm: + description: hash algorithm, + type: str + choices: ["md5", "sha1"] + encryption: + description: key type + type: int + choices: [0, 7] + key: + description: Unobfuscated key string. + type: str + local_interface: + description: Configure the interface from which the IP source address is taken. + type: str + qos_dscp: + description: Set DSCP value in IP header + type: int + serve: + description: Configure the switch as an NTP server. + type: dict + suboptions: + all: + description: Service NTP requests received on any interface. + type: bool + access_lists: + description: Configure access control list. + type: list + elements: dict + suboptions: + afi: + description: ip/ipv6 config commands. + type: str + acls: + description: Access lists to be configured under the afi + type: list + elements: dict + suboptions: + acl_name: + description: Name of the access list. + type: str + direction: + description: direction for the packets. + type: str + choices: ["in", "out"] + vrf: + description: VRF in which to apply the access control list. + type: str + servers: + description: Configure NTP server to synchronize to. + type: list + elements: dict + suboptions: + vrf: + description: vrf name. + type: str + server: + description: Hostname or A.B.C.D or A:B:C:D:E:F:G:H. + type: str + required: True + burst: + description: Send a burst of packets instead of the usual one. + type: bool + iburst: + description: Send bursts of packets until the server is reached + type: bool + key_id: + description: Set a key to use for authentication. + type: int + local_interface: + description: Configure the interface from which the IP source address is taken. + type: str + source: + description: Configure the interface from which the IP source address is taken. + type: str + maxpoll: + description: Maximum poll interval. + type: int + minpoll: + description: Minimum poll interval. + type: int + prefer: + description: Mark this server as preferred. + type: bool + version: + description: NTP version. + type: int + trusted_key: + description: Configure the set of keys that are accepted for incoming messages + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ntp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + - The states I(replaced) and I(overridden) have identical + behaviour for this module. + - Please refer to examples for more details. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged +""" +EXAMPLES = """ + +# Using merged + +# Before state + +# localhost(config)#show running-config | section ntp +# localhost(config)# + + - name: Merge provided configuration with device configuration + arista.eos.eos_ntp_global: + config: + authenticate: + enable: true + authentication_keys: + - id: 2 + algorithm: "sha1" + encryption: 7 + key: "123456" + - id: 23 + algorithm: "md5" + encryption: 7 + key: "123456" + local_interface: "Ethernet1" + qos_dscp: 10 + trusted_key: 23 + servers: + - server: "10.1.1.1" + vrf: "vrf01" + burst: True + prefer: True + - server: "25.1.1.1" + vrf: "vrf01" + maxpoll: 15 + key_id: 2 + serve: + access_lists: + - afi: "ip" + acls: + - acl_name: "acl01" + direction: "in" + - afi: "ipv6" + acls: + - acl_name: "acl02" + direction: "in" + +# After State + +# localhost(config)#show running-config | section ntp +# ntp authentication-key 2 sha1 7 123456 +# ntp authentication-key 23 md5 7 123456 +# ntp trusted-key 23 +# ntp authenticate +# ntp local-interface Ethernet1 +# ntp qos dscp 10 +# ntp server vrf vrf01 10.1.1.1 prefer burst +# ntp server vrf vrf01 25.1.1.1 maxpoll 15 key 2 +# ntp serve ip access-group acl01 in +# ntp serve ipv6 access-group acl02 in +# localhost(config)# +# +# +# Module Execution: +# "after": { +# "authenticate": { +# "enable": true +# }, +# "authentication_keys": [ +# { +# "algorithm": "sha1", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# }, +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 23, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "local_interface": "Ethernet1", +# "qos_dscp": 10, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl01", +# "direction": "in" +# } +# ], +# "afi": "ip" +# }, +# { +# "acls": [ +# { +# "acl_name": "acl02", +# "direction": "in" +# } +# ], +# "afi": "ipv6" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "prefer": true, +# "server": "10.1.1.1", +# "vrf": "vrf01" +# }, +# { +# "key_id": 2, +# "maxpoll": 15, +# "server": "25.1.1.1", +# "vrf": "vrf01" +# } +# ], +# "trusted_key": "23" +# }, +# "before": {}, +# "changed": true, +# "commands": [ +# "ntp serve ip access-group acl01 in", +# "ntp serve ipv6 access-group acl02 in", +# "ntp authentication-key 2 sha1 7 ********", +# "ntp authentication-key 23 md5 7 ********", +# "ntp server vrf vrf01 10.1.1.1 burst prefer", +# "ntp server vrf vrf01 25.1.1.1 key 2 maxpoll 15", +# "ntp authenticate", +# "ntp local-interface Ethernet1", +# "ntp qos dscp 10", +# "ntp trusted-key 23" +# ], + +# Using Replaced + +# Before State + +# localhost(config)#show running-config | section ntp +# ntp authentication-key 2 sha1 7 123456 +# ntp authentication-key 23 md5 7 123456 +# ntp trusted-key 23 +# ntp authenticate +# ntp local-interface Ethernet1 +# ntp qos dscp 10 +# ntp server vrf vrf01 10.1.1.1 prefer burst +# ntp server vrf vrf01 25.1.1.1 maxpoll 15 key 2 +# ntp serve ip access-group acl01 in +# ntp serve ipv6 access-group acl02 in +# localhost(config)# + + - name: Replace + arista.eos.eos_ntp_global: + config: + qos_dscp: 15 + authentication_keys: + - id: 2 + algorithm: "md5" + encryption: 7 + key: "123456" + servers: + - server: "11.21.1.1" + vrf: "vrf01" + burst: True + prefer: True + minpoll: 13 + serve: + access_lists: + - afi: "ip" + acls: + - acl_name: "acl03" + direction: "in" + state: replaced +# After State: +# localhost(config)#show running-config | section ntp +# ntp authentication-key 2 md5 7 123456 +# ntp qos dscp 15 +# ntp server vrf vrf01 11.21.1.1 prefer burst minpoll 13 +# ntp serve ip access-group acl03 in +# localhost(config)# +# +# +# Module Execution: +# "after": { +# "authentication_keys": [ +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "qos_dscp": 15, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl03", +# "direction": "in" +# } +# ], +# "afi": "ip" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "minpoll": 13, +# "prefer": true, +# "server": "11.21.1.1", +# "vrf": "vrf01" +# } +# ] +# }, +# "before": { +# "authenticate": { +# "enable": true +# }, +# "authentication_keys": [ +# { +# "algorithm": "sha1", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# }, +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 23, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "local_interface": "Ethernet1", +# "qos_dscp": 10, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl01", +# "direction": "in" +# } +# ], +# "afi": "ip" +# }, +# { +# "acls": [ +# { +# "acl_name": "acl02", +# "direction": "in" +# } +# ], +# "afi": "ipv6" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "prefer": true, +# "server": "10.1.1.1", +# "vrf": "vrf01" +# }, +# { +# "key_id": 2, +# "maxpoll": 15, +# "server": "25.1.1.1", +# "vrf": "vrf01" +# } +# ], +# "trusted_key": "23" +# }, +# "changed": true, +# "commands": [ +# "no ntp serve ip access-group acl01 in", +# "no ntp serve ipv6 access-group acl02 in", +# "no ntp authentication-key 23 md5 7 ********", +# "no ntp server vrf vrf01 10.1.1.1 burst prefer", +# "no ntp server vrf vrf01 25.1.1.1 key 2 maxpoll 15", +# "no ntp authenticate", +# "no ntp local-interface Ethernet1", +# "no ntp trusted-key 23", +# "ntp serve ip access-group acl03 in", +# "ntp authentication-key 2 md5 7 ********", +# "ntp server vrf vrf01 11.21.1.1 burst minpoll 13 prefer", +# "ntp qos dscp 15" +# ], +# +# Using Overridden + +# Before State + +# localhost(config)#show running-config | section ntp +# ntp authentication-key 2 sha1 7 123456 +# ntp authentication-key 23 md5 7 123456 +# ntp trusted-key 23 +# ntp authenticate +# ntp local-interface Ethernet1 +# ntp qos dscp 10 +# ntp server vrf vrf01 10.1.1.1 prefer burst +# ntp server vrf vrf01 25.1.1.1 maxpoll 15 key 2 +# ntp serve ip access-group acl01 in +# ntp serve ipv6 access-group acl02 in +# localhost(config)# + + - name: Replace + arista.eos.eos_ntp_global: + config: + qos_dscp: 15 + authentication_keys: + - id: 2 + algorithm: "md5" + encryption: 7 + key: "123456" + servers: + - server: "11.21.1.1" + vrf: "vrf01" + burst: True + prefer: True + minpoll: 13 + serve: + access_lists: + - afi: "ip" + acls: + - acl_name: "acl03" + direction: "in" + state: overridden +# After State: +# localhost(config)#show running-config | section ntp +# ntp authentication-key 2 md5 7 123456 +# ntp qos dscp 15 +# ntp server vrf vrf01 11.21.1.1 prefer burst minpoll 13 +# ntp serve ip access-group acl03 in +# localhost(config)# +# +# +# Module Execution: +# "after": { +# "authentication_keys": [ +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "qos_dscp": 15, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl03", +# "direction": "in" +# } +# ], +# "afi": "ip" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "minpoll": 13, +# "prefer": true, +# "server": "11.21.1.1", +# "vrf": "vrf01" +# } +# ] +# }, +# "before": { +# "authenticate": { +# "enable": true +# }, +# "authentication_keys": [ +# { +# "algorithm": "sha1", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# }, +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 23, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "local_interface": "Ethernet1", +# "qos_dscp": 10, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl01", +# "direction": "in" +# } +# ], +# "afi": "ip" +# }, +# { +# "acls": [ +# { +# "acl_name": "acl02", +# "direction": "in" +# } +# ], +# "afi": "ipv6" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "prefer": true, +# "server": "10.1.1.1", +# "vrf": "vrf01" +# }, +# { +# "key_id": 2, +# "maxpoll": 15, +# "server": "25.1.1.1", +# "vrf": "vrf01" +# } +# ], +# "trusted_key": "23" +# }, +# "changed": true, +# "commands": [ +# "no ntp serve ip access-group acl01 in", +# "no ntp serve ipv6 access-group acl02 in", +# "no ntp authentication-key 23 md5 7 ********", +# "no ntp server vrf vrf01 10.1.1.1 burst prefer", +# "no ntp server vrf vrf01 25.1.1.1 key 2 maxpoll 15", +# "no ntp authenticate", +# "no ntp local-interface Ethernet1", +# "no ntp trusted-key 23", +# "ntp serve ip access-group acl03 in", +# "ntp authentication-key 2 md5 7 ********", +# "ntp server vrf vrf01 11.21.1.1 burst minpoll 13 prefer", +# "ntp qos dscp 15" +# ], +# + +# using deleted: +# Before State + +# localhost(config)#show running-config | section ntp +# ntp authentication-key 2 sha1 7 123456 +# ntp authentication-key 23 md5 7 123456 +# ntp trusted-key 23 +# ntp authenticate +# ntp local-interface Ethernet1 +# ntp qos dscp 10 +# ntp server vrf vrf01 10.1.1.1 prefer burst +# ntp server vrf vrf01 11.21.1.1 prefer burst minpoll 13 +# ntp server vrf vrf01 25.1.1.1 maxpoll 15 key 2 +# ntp serve ip access-group acl01 in +# ntp serve ipv6 access-group acl02 in +# localhost(config)# + + - name: Delete ntp-global + arista.eos.eos_ntp_global: + state: deleted + +# After State: +# localhost(config)#show running-config | section ntp +# localhost(config)# +# +# +# # Module Execution +# "after": {}, +# "before": { +# "authenticate": { +# "enable": true +# }, +# "authentication_keys": [ +# { +# "algorithm": "sha1", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# }, +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 23, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "local_interface": "Ethernet1", +# "qos_dscp": 10, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl01", +# "direction": "in" +# } +# ], +# "afi": "ip" +# }, +# { +# "acls": [ +# { +# "acl_name": "acl02", +# "direction": "in" +# } +# ], +# "afi": "ipv6" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "prefer": true, +# "server": "10.1.1.1", +# "vrf": "vrf01" +# }, +# { +# "burst": true, +# "minpoll": 13, +# "prefer": true, +# "server": "11.21.1.1", +# "vrf": "vrf01" +# }, +# { +# "key": 2, +# "maxpoll": 15, +# "server": "25.1.1.1", +# "vrf": "vrf01" +# } +# ], +# "trusted_key": "23" +# }, +# "changed": true, +# "commands": [ +# "no ntp serve ip access-group acl01 in", +# "no ntp serve ipv6 access-group acl02 in", +# "no ntp authentication-key 2 sha1 7 ********", +# "no ntp authentication-key 23 md5 7 ********", +# "no ntp server vrf vrf01 10.1.1.1 burst prefer", +# "no ntp server vrf vrf01 11.21.1.1 burst minpoll 13 prefer", +# "no ntp server vrf vrf01 25.1.1.1 key 2 maxpoll 15", +# "no ntp authenticate", +# "no ntp local-interface Ethernet1", +# "no ntp qos dscp 10", +# "no ntp trusted-key 23" +# ], +# + +# Using parsed: +# parsed.cfg +# ntp authentication-key 2 sha1 7 123456 +# ntp authentication-key 23 md5 7 123456 +# ntp trusted-key 23 +# ntp authenticate +# ntp local-interface Ethernet1 +# ntp qos dscp 10 +# ntp server vrf vrf01 10.1.1.1 prefer burst +# ntp server vrf vrf01 11.21.1.1 prefer burst minpoll 13 +# ntp server vrf vrf01 25.1.1.1 maxpoll 15 key 2 +# ntp serve ip access-group acl01 in +# ntp serve ipv6 access-group acl02 in + + - name: parse configs + arista.eos.eos_ntp_global: + running_config: "{{ lookup('file', './parsed_ntp_global.cfg') }}" + state: parsed + tags: + - parsed +# Module Execution +# "parsed": { +# "authenticate": { +# "enable": true +# }, +# "authentication_keys": [ +# { +# "algorithm": "sha1", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# }, +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 23, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "local_interface": "Ethernet1", +# "qos_dscp": 10, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl01", +# "direction": "in" +# } +# ], +# "afi": "ip" +# }, +# { +# "acls": [ +# { +# "acl_name": "acl02", +# "direction": "in" +# } +# ], +# "afi": "ipv6" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "prefer": true, +# "server": "10.1.1.1", +# "vrf": "vrf01" +# }, +# { +# "burst": true, +# "minpoll": 13, +# "prefer": true, +# "server": "11.21.1.1", +# "vrf": "vrf01" +# }, +# { +# "key": 2, +# "maxpoll": 15, +# "server": "25.1.1.1", +# "vrf": "vrf01" +# } +# ], +# "trusted_key": "23" +# } +# } + +# using Gathered +# Device config: +# localhost(config)#show running-config | section ntp +# ntp authentication-key 2 sha1 7 123456 +# ntp authentication-key 23 md5 7 123456 +# ntp trusted-key 23 +# ntp authenticate +# ntp local-interface Ethernet1 +# ntp qos dscp 10 +# ntp server vrf vrf01 10.1.1.1 prefer burst +# ntp server vrf vrf01 25.1.1.1 maxpoll 15 key 2 +# ntp serve ip access-group acl01 in +# ntp serve ipv6 access-group acl02 in +# localhost(config)# + + + - name: gather configs + arista.eos.eos_ntp_global: + state: gathered + tags: + - gathered +# Module Execution +# "gathered": { +# "authenticate": { +# "enable": true +# }, +# "authentication_keys": [ +# { +# "algorithm": "sha1", +# "encryption": 7, +# "id": 2, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# }, +# { +# "algorithm": "md5", +# "encryption": 7, +# "id": 23, +# "key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" +# } +# ], +# "local_interface": "Ethernet1", +# "qos_dscp": 10, +# "serve": { +# "access_lists": [ +# { +# "acls": [ +# { +# "acl_name": "acl01", +# "direction": "in" +# } +# ], +# "afi": "ip" +# }, +# { +# "acls": [ +# { +# "acl_name": "acl02", +# "direction": "in" +# } +# ], +# "afi": "ipv6" +# } +# ] +# }, +# "servers": [ +# { +# "burst": true, +# "prefer": true, +# "server": "10.1.1.1", +# "vrf": "vrf01" +# }, +# { +# "key_id": 2, +# "maxpoll": 15, +# "server": "25.1.1.1", +# "vrf": "vrf01" +# } +# ], +# "trusted_key": "23" +# }, +# "invocation": { +# "module_args": { +# "config": null, +# "running_config": null, +# "state": "gathered" +# } +# } +# } + + +# using rendered: + + - name: Render provided configuration + arista.eos.eos_ntp_global: + config: + authenticate: + enable: true + authentication_keys: + - id: 2 + algorithm: "sha1" + encryption: 7 + key: "123456" + - id: 23 + algorithm: "md5" + encryption: 7 + key: "123456" + local_interface: "Ethernet1" + qos_dscp: 10 + trusted_key: 23 + servers: + - server: "10.1.1.1" + vrf: "vrf01" + burst: True + prefer: True + - server: "25.1.1.1" + vrf: "vrf01" + maxpoll: 15 + key_id: 2 + serve: + access_lists: + - afi: "ip" + acls: + - acl_name: "acl01" + direction: "in" + - afi: "ipv6" + acls: + - acl_name: "acl02" + direction: "in" + state: rendered + become: yes + +# Module Execution: +# "rendered": [ +# "ntp serve ip access-group acl01 in", +# "ntp serve ipv6 access-group acl02 in", +# "ntp authentication-key 2 sha1 7 ********", +# "ntp authentication-key 23 md5 7 ********", +# "ntp server vrf vrf01 10.1.1.1 burst prefer", +# "ntp server vrf vrf01 25.1.1.1 key 2 maxpoll 15", +# "ntp authenticate", +# "ntp local-interface Ethernet1", +# "ntp qos dscp 10", +# "ntp trusted-key 23" +# ] +# +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - ntp master stratum 2 + - ntp peer 198.51.100.1 use-vrf test maxpoll 7 + - ntp authentication-key 10 md5 wawyhanx2 7 + - ntp access-group peer PeerAcl1 + - ntp access-group peer PeerAcl2 + - ntp access-group query-only QueryAcl1 +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + + - ntp authentication-key 2 sha1 7 123456 + - ntp authentication-key 23 md5 7 123456 + - ntp trusted-key 23 + - ntp authenticate + - ntp local-interface Ethernet1 + - ntp qos dscp 10 + - ntp server vrf vrf01 10.1.1.1 prefer burst + - ntp server vrf vrf01 25.1.1.1 maxpoll 15 key 2 + - ntp serve ip access-group acl01 in + - ntp serve ipv6 access-group acl02 in + +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ntp_global.ntp_global import ( + Ntp_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.ntp_global.ntp_global import ( + Ntp_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Ntp_globalArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Ntp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_ospf_interfaces.py b/ansible_collections/arista/eos/plugins/modules/eos_ospf_interfaces.py new file mode 100644 index 000000000..29ad037f7 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_ospf_interfaces.py @@ -0,0 +1,1230 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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 module file for eos_ospf_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: eos_ospf_interfaces +version_added: 1.1.0 +short_description: OSPF Interfaces Resource Module. +description: +- This module manages OSPF configuration of interfaces on devices running Arista EOS. +author: Gomathi Selvi Srinivasan (@GomathiselviS) +options: + config: + description: A list of OSPF configuration for interfaces. + type: list + elements: dict + suboptions: + name: + description: + - Name/Identifier of the interface. + type: str + address_family: + description: + - OSPF settings on the interfaces in address-family context. + type: list + elements: dict + suboptions: + afi: + description: + - Address Family Identifier (AFI) for OSPF settings on the interfaces. + type: str + choices: ['ipv4', 'ipv6'] + required: True + area: + description: + - Area associated with interface. + - Valid only when afi = ipv4. + type: dict + suboptions: + area_id: + description: + - Area ID as a decimal or IP address format. + type: str + required: True + authentication_v2: + description: + - Authentication settings on the interface. + - Valid only when afi = ipv4. + type: dict + suboptions: + message_digest: + description: + - Use message-digest authentication. + type: bool + set: + description: + - Enable authentication on the interface. + type: bool + authentication_v3: + description: + - Authentication settings on the interface. + - Valid only when afi = ipv6. + type: dict + suboptions: + spi: + description: IPsec Security Parameter Index. + type: int + algorithm: + description: Encryption alsgorithm. + type: str + choices: ["md5", "sha1"] + keytype: + description: + - Specifies if an unencrypted/hidden follows. + - 0 denotes unencrypted key. + - 7 denotes hidden key. + type: str + passphrase: + description: Passphrase String for deriving keys for authentication and encryption. + type: str + key: + description: 128 bit MD5 key or 140 bit SHA1 key. + type: str + authentication_key: + description: + - Configure the authentication key for the interface. + - Valid only when afi = ipv4. + type: dict + suboptions: + encryption: + description: + - 0 Specifies an UNENCRYPTED authentication key will follow. + - 7 Specifies a proprietry encryption type.` + type: str + key: + description: + - password (up to 8 chars). + type: str + bfd: + description: Enable BFD. + type: bool + cost: + description: + - metric associated with interface. + type: int + dead_interval: + description: + - Time interval to detect a dead router. + type: int + encryption_v3: + description: + - Authentication settings on the interface. + - Valid only when afi = ipv6. + type: dict + suboptions: + spi: + description: IPsec Security Parameter Index. + type: int + encryption: + description: encryption type. + choices: ["3des-cbc", "aes-128-cbc", "aes-192-cbc", "aes-256-cbc", "null"] + type: str + algorithm: + description: algorithm. + type: str + choices: ["md5", "sha1"] + keytype: + description: + - Specifies if an unencrypted/hidden follows. + - 0 denotes unencrypted key. + - 7 denotes hidden key. + type: str + passphrase: + description: Passphrase String for deriving keys for authentication and encryption. + type: str + key: + description: key + type: str + hello_interval: + description: + - Timer interval between transmission of hello packets. + type: int + ip_params: + description: + - Specify parameters for IPv4/IPv6. + - Valid only when afi = ipv6. + type: list + elements: dict + suboptions: + afi: + description: + - Address Family Identifier (AFI) for OSPF settings on the interfaces. + type: str + choices: ['ipv4', 'ipv6'] + required: True + area: + description: + - Area associated with interface. + - Valid only when afi = ipv4. + type: dict + suboptions: + area_id: + description: + - Area ID as a decimal or IP address format. + type: str + required: True + bfd: + description: Enable BFD. + type: bool + cost: + description: + - metric associated with interface. + type: int + dead_interval: + description: + - Time interval to detect a dead router. + type: int + hello_interval: + description: + - Timer interval between transmission of hello packets. + type: int + mtu_ignore: + description: + - if True, Disable MTU check for Database Description packets. + type: bool + network: + description: + - Interface type. + type: str + priority: + description: + - Interface priority. + type: int + retransmit_interval: + description: + - LSA retransmission interval. + type: int + passive_interface: + description: + - Suppress routing updates in an interface. + type: bool + transmit_delay: + description: + - LSA transmission delay. + type: int + message_digest_key: + description: + - Message digest authentication password (key) settings. + type: dict + suboptions: + key_id: + description: + - Key ID. + type: int + encryption: + description: + - 0 Specifies an UNENCRYPTED ospf password (key) will follow. + - 7 Specifies a proprietry encryption type. + type: str + key: + description: + - Authentication key (upto 16 chars). + type: str + mtu_ignore: + description: + - if True, Disable MTU check for Database Description packets. + type: bool + network: + description: + - Interface type. + type: str + passive_interface: + description: + - Suppress routing updates in an interface. + - Valid only when afi = ipv6. + type: bool + priority: + description: + - Interface priority. + type: int + retransmit_interval: + description: + - LSA retransmission interval. + type: int + shutdown: + description: + - Shutdown OSPF on this interface. + type: bool + transmit_delay: + description: + - LSA transmission delay. + type: int + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - parsed + - rendered + default: merged +""" + +EXAMPLES = """ + +# Using merged + +# Before state + +# veos(config)#show running-config | section interface | ospf +# veos(config)# + + - name: Merge provided configuration with device configuration + arista.eos.eos_ospf_interfaces: + config: + - name: "Vlan1" + address_family: + - afi: "ipv4" + area: + area_id: "0.0.0.50" + cost: 500 + mtu_ignore: True + - afi: "ipv6" + dead_interval: 44 + ip_params: + - afi: "ipv6" + mtu_ignore: True + network: "point-to-point" + state: merged + +# After State + +# veos(config)#show running-config | section interface | ospf +# interface Vlan1 +# ip ospf cost 500 +# ip ospf mtu-ignore +# ip ospf area 0.0.0.50 +# ospfv3 dead-interval 44 +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# veos(config)# +# +# +# Module Execution: +# +# "after": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.50" +# }, +# "cost": 500, +# "mtu_ignore": True +# }, +# { +# "afi": "ipv6", +# "dead_interval": 44, +# "ip_params": [ +# { +# "afi": "ipv6", +# "mtu_ignore": True, +# "network": "point-to-point" +# } +# ] +# } +# ], +# "name": "Vlan1" +# } +# ], +# "before": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# } +# ], +# "changed": True, +# "commands": [ +# "interface Vlan1", +# "ip ospf area 0.0.0.50", +# "ip ospf cost 500", +# "ip ospf mtu-ignore", +# "ospfv3 dead-interval 44", +# "ospfv3 ipv6 mtu-ignore", +# "ospfv3 ipv6 network point-to-point" +# ], +# + +# Using replaced +#--------------- + +# Before State: + +# veos(config)#show running-config | section interface | ospf +# interface Vlan1 +# ip ospf cost 500 +# ip ospf dead-interval 29 +# ip ospf hello-interval 66 +# ip ospf mtu-ignore +# ip ospf area 0.0.0.50 +# ospfv3 cost 106 +# ospfv3 hello-interval 77 +# ospfv3 dead-interval 44 +# ospfv3 transmit-delay 100 +# ospfv3 ipv4 priority 45 +# ospfv3 ipv4 area 0.0.0.5 +# ospfv3 ipv6 passive-interface +# ospfv3 ipv6 retransmit-interval 115 +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# ! +# interface Vlan2 +# ospfv3 ipv4 hello-interval 45 +# ospfv3 ipv4 retransmit-interval 100 +# ospfv3 ipv4 area 0.0.0.6 +# veos(config)# + + + - name: Replace device configuration with provided configuration + arista.eos.eos_ospf_interfaces: + config: + - name: "Vlan1" + address_family: + - afi: "ipv6" + cost: 44 + bfd: True + ip_params: + - afi: "ipv6" + mtu_ignore: True + network: "point-to-point" + dead_interval: 56 + state: replaced + +# After State: + +# veos(config)#show running-config | section interface | ospf +# interface Vlan1 +# ospfv3 bfd +# ospfv3 cost 44 +# no ospfv3 ipv6 passive-interface +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# ! +# interface Vlan2 +# ospfv3 ipv4 hello-interval 45 +# ospfv3 ipv4 retransmit-interval 100 +# ospfv3 ipv4 area 0.0.0.6 +# veos(config)# +# +# Module Execution: +# +# "after": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "bfd": True, +# "cost": 44, +# "ip_params": [ +# { +# "afi": "ipv6", +# "mtu_ignore": True, +# "network": "point-to-point" +# } +# ] +# } +# ], +# "name": "Vlan1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.6" +# }, +# "hello_interval": 45, +# "retransmit_interval": 100 +# } +# ] +# } +# ], +# "name": "Vlan2" +# } +# ], +# "before": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.50" +# }, +# "cost": 500, +# "dead_interval": 29, +# "hello_interval": 66, +# "mtu_ignore": True +# }, +# { +# "afi": "ipv6", +# "cost": 106, +# "dead_interval": 44, +# "hello_interval": 77, +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.5" +# }, +# "priority": 45 +# }, +# { +# "afi": "ipv6", +# "mtu_ignore": True, +# "network": "point-to-point", +# "passive_interface": True, +# "retransmit_interval": 115 +# } +# ], +# "transmit_delay": 100 +# } +# ], +# "name": "Vlan1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.6" +# }, +# "hello_interval": 45, +# "retransmit_interval": 100 +# } +# ] +# } +# ], +# "name": "Vlan2" +# } +# ], +# "changed": True, +# "commands": [ +# "interface Vlan1", +# "no ip ospf cost 500", +# "no ip ospf dead-interval 29", +# "no ip ospf hello-interval 66", +# "no ip ospf mtu-ignore", +# "no ip ospf area 0.0.0.50", +# "ospfv3 cost 44", +# "ospfv3 bfd", +# "ospfv3 authentication ipsec spi 30 md5 passphrase 7 7hl8FV3lZ6H1mAKpjL47hQ==", +# "no ospfv3 ipv4 priority 45", +# "no ospfv3 ipv4 area 0.0.0.5", +# "ospfv3 ipv6 dead-interval 56", +# "no ospfv3 ipv6 passive-interface", +# "no ospfv3 ipv6 retransmit-interval 115", +# "no ospfv3 hello-interval 77", +# "no ospfv3 dead-interval 44", +# "no ospfv3 transmit-delay 100" +# ], +# + +# Using overidden: +# ---------------- + +# Before State: +# veos(config)#show running-config | section interface | ospf +# interface Vlan1 +# ip ospf dead-interval 29 +# ip ospf hello-interval 66 +# ip ospf mtu-ignore +# ospfv3 bfd +# ospfv3 cost 106 +# ospfv3 hello-interval 77 +# ospfv3 transmit-delay 100 +# ospfv3 ipv4 priority 45 +# ospfv3 ipv4 area 0.0.0.5 +# ospfv3 ipv6 passive-interface +# ospfv3 ipv6 dead-interval 56 +# ospfv3 ipv6 retransmit-interval 115 +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# ! +# interface Vlan2 +# ospfv3 ipv4 hello-interval 45 +# ospfv3 ipv4 retransmit-interval 100 +# ospfv3 ipv4 area 0.0.0.6 +# veos(config)# + + - name: Override device configuration with provided configuration + arista.eos.eos_ospf_interfaces: + config: + - name: "Vlan1" + address_family: + - afi: "ipv6" + cost: 44 + bfd: True + ip_params: + - afi: "ipv6" + mtu_ignore: True + network: "point-to-point" + dead_interval: 56 + state: overridden + +# After State: + +# veos(config)#show running-config | section interface | ospf +# interface Vlan1 +# ospfv3 bfd +# ospfv3 cost 44 +# no ospfv3 ipv6 passive-interface +# ospfv3 ipv6 dead-interval 56 +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# veos(config)# +# +# +# Module Execution: +# +# "after": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "bfd": True, +# "cost": 44, +# "ip_params": [ +# { +# "afi": "ipv6", +# "dead_interval": 56, +# "mtu_ignore": True, +# "network": "point-to-point" +# } +# ] +# } +# ], +# "name": "Vlan1" +# }, +# { +# "name": "Vlan2" +# } +# ], +# "before": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "dead_interval": 29, +# "hello_interval": 66, +# "mtu_ignore": True +# }, +# { +# "afi": "ipv6", +# "bfd": True, +# "cost": 106, +# "hello_interval": 77, +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.5" +# }, +# "priority": 45 +# }, +# { +# "afi": "ipv6", +# "dead_interval": 56, +# "mtu_ignore": True, +# "network": "point-to-point", +# "passive_interface": True, +# "retransmit_interval": 115 +# } +# ], +# "transmit_delay": 100 +# } +# ], +# "name": "Vlan1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.6" +# }, +# "hello_interval": 45, +# "retransmit_interval": 100 +# } +# ] +# } +# ], +# "name": "Vlan2" +# } +# ], +# "changed": True, +# "commands": [ +# "interface Vlan2", +# "no ospfv3 ipv4 hello-interval 45", +# "no ospfv3 ipv4 retransmit-interval 100", +# "no ospfv3 ipv4 area 0.0.0.6", +# "interface Vlan1", +# "no ip ospf dead-interval 29", +# "no ip ospf hello-interval 66", +# "no ip ospf mtu-ignore", +# "ospfv3 cost 44", +# "ospfv3 authentication ipsec spi 30 md5 passphrase 7 7hl8FV3lZ6H1mAKpjL47hQ==", +# "no ospfv3 ipv4 priority 45", +# "no ospfv3 ipv4 area 0.0.0.5", +# "no ospfv3 ipv6 passive-interface", +# "no ospfv3 ipv6 retransmit-interval 115", +# "no ospfv3 hello-interval 77", +# "no ospfv3 transmit-delay 100" +# ], +# + +# Using deleted: +#-------------- + +# before State: + +# veos(config)#show running-config | section interface | ospf +# interface Vlan1 +# ip ospf dead-interval 29 +# ip ospf hello-interval 66 +# ip ospf mtu-ignore +# ospfv3 bfd +# ospfv3 cost 106 +# ospfv3 hello-interval 77 +# ospfv3 transmit-delay 100 +# ospfv3 ipv4 priority 45 +# ospfv3 ipv4 area 0.0.0.5 +# ospfv3 ipv6 passive-interface +# ospfv3 ipv6 dead-interval 56 +# ospfv3 ipv6 retransmit-interval 115 +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# ! +# interface Vlan2 +# ospfv3 ipv4 hello-interval 45 +# ospfv3 ipv4 retransmit-interval 100 +# ospfv3 ipv4 area 0.0.0.6 +# veos(config)# + + - name: Delete device configuration + arista.eos.eos_ospf_interfaces: + config: + - name: "Vlan1" + state: deleted + +# After State: + +# veos#show running-config | section interface | ospf +# interface Vlan2 +# ospfv3 ipv4 hello-interval 45 +# ospfv3 ipv4 retransmit-interval 100 +# ospfv3 ipv4 area 0.0.0.6 +# +# Module Execution: +# +# "after": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "name": "Vlan1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.6" +# }, +# "hello_interval": 45, +# "retransmit_interval": 100 +# } +# ] +# } +# ], +# "name": "Vlan2" +# } +# ], +# "before": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "dead_interval": 29, +# "hello_interval": 66, +# "mtu_ignore": True +# }, +# { +# "afi": "ipv6", +# "bfd": True, +# "cost": 106, +# "hello_interval": 77, +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.5" +# }, +# "priority": 45 +# }, +# { +# "afi": "ipv6", +# "dead_interval": 56, +# "mtu_ignore": True, +# "network": "point-to-point", +# "passive_interface": True, +# "retransmit_interval": 115 +# } +# ], +# "transmit_delay": 100 +# } +# ], +# "name": "Vlan1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.6" +# }, +# "hello_interval": 45, +# "retransmit_interval": 100 +# } +# ] +# } +# ], +# "name": "Vlan2" +# } +# ], +# "changed": True, +# "commands": [ +# "interface Vlan1", +# "no ip ospf dead-interval 29", +# "no ip ospf hello-interval 66", +# "no ip ospf mtu-ignore", +# "no ospfv3 bfd", +# "no ospfv3 cost 106", +# "no ospfv3 hello-interval 77", +# "no ospfv3 transmit-delay 100", +# "no ospfv3 ipv4 priority 45", +# "no ospfv3 ipv4 area 0.0.0.5", +# "no ospfv3 ipv6 passive-interface", +# "no ospfv3 ipv6 dead-interval 56", +# "no ospfv3 ipv6 retransmit-interval 115", +# "no ospfv3 ipv6 network point-to-point", +# "no ospfv3 ipv6 mtu-ignore" +# ], +# + +# Using parsed: +# ------------ + +# parsed.cfg: +# ---------- + +# interface Vlan1 +# ip ospf dead-interval 29 +# ip ospf hello-interval 66 +# ip ospf mtu-ignore +# ip ospf cost 500 +# ospfv3 bfd +# ospfv3 cost 106 +# ospfv3 hello-interval 77 +# ospfv3 transmit-delay 100 +# ospfv3 ipv4 priority 45 +# ospfv3 ipv4 area 0.0.0.5 +# ospfv3 ipv6 passive-interface +# ospfv3 ipv6 dead-interval 56 +# ospfv3 ipv6 retransmit-interval 115 +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# ! +# interface Vlan2 +# ospfv3 ipv4 hello-interval 45 +# ospfv3 ipv4 retransmit-interval 100 +# ospfv3 ipv4 area 0.0.0.6 +# + + - name: parse configs + arista.eos.eos_ospf_interfaces: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + +# Module Execution: +# "parsed": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "cost": 500, +# "dead_interval": 29, +# "hello_interval": 66, +# "mtu_ignore": True +# }, +# { +# "afi": "ipv6", +# "bfd": True, +# "cost": 106, +# "hello_interval": 77, +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.5" +# }, +# "priority": 45 +# }, +# { +# "afi": "ipv6", +# "dead_interval": 56, +# "mtu_ignore": True, +# "network": "point-to-point", +# "passive_interface": True, +# "retransmit_interval": 115 +# } +# ], +# "transmit_delay": 100 +# } +# ], +# "name": "Vlan1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.6" +# }, +# "hello_interval": 45, +# "retransmit_interval": 100 +# } +# ] +# } +# ], +# "name": "Vlan2" +# } +# ] + +# Using gathered: + +# Device COnfig: + +# veos#show running-config | section interface | ospf +# interface Vlan1 +# ip ospf cost 500 +# ip ospf dead-interval 29 +# ip ospf hello-interval 66 +# ip ospf mtu-ignore +# ip ospf area 0.0.0.50 +# ospfv3 cost 106 +# ospfv3 hello-interval 77 +# ospfv3 transmit-delay 100 +# ospfv3 ipv4 priority 45 +# ospfv3 ipv4 area 0.0.0.5 +# ospfv3 ipv6 passive-interface +# ospfv3 ipv6 dead-interval 56 +# ospfv3 ipv6 retransmit-interval 115 +# ospfv3 ipv6 network point-to-point +# ospfv3 ipv6 mtu-ignore +# ! +# interface Vlan2 +# ospfv3 ipv4 hello-interval 45 +# ospfv3 ipv4 retransmit-interval 100 +# ospfv3 ipv4 area 0.0.0.6 +# veos# + + - name: gather configs + arista.eos.eos_ospf_interfaces: + state: gathered + +# Module Execution: +# +# "gathered": [ +# { +# "name": "Ethernet1" +# }, +# { +# "name": "Ethernet2" +# }, +# { +# "name": "Management1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.50" +# }, +# "cost": 500, +# "dead_interval": 29, +# "hello_interval": 66, +# "mtu_ignore": True +# }, +# { +# "afi": "ipv6", +# "cost": 106, +# "hello_interval": 77, +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.5" +# }, +# "priority": 45 +# }, +# { +# "afi": "ipv6", +# "dead_interval": 56, +# "mtu_ignore": True, +# "network": "point-to-point", +# "passive_interface": True, +# "retransmit_interval": 115 +# } +# ], +# "transmit_delay": 100 +# } +# ], +# "name": "Vlan1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "ip_params": [ +# { +# "afi": "ipv4", +# "area": { +# "area_id": "0.0.0.6" +# }, +# "hello_interval": 45, +# "retransmit_interval": 100 +# } +# ] +# } +# ], +# "name": "Vlan2" +# } +# ], +# + + +# Using rendered: +# -------------- + + - name: Render provided configuration + arista.eos.eos_ospf_interfaces: + config: + - name: "Vlan1" + address_family: + - afi: "ipv4" + dead_interval: 29 + mtu_ignore: True + hello_interval: 66 + - afi: "ipv6" + hello_interval: 77 + cost : 106 + transmit_delay: 100 + ip_params: + - afi: "ipv6" + retransmit_interval: 115 + dead_interval: 56 + passive_interface: True + - afi: "ipv4" + area: + area_id: "0.0.0.5" + priority: 45 + - name: "Vlan2" + address_family: + - afi: "ipv6" + ip_params: + - afi: "ipv4" + area: + area_id: "0.0.0.6" + hello_interval: 45 + retransmit_interval: 100 + - afi: "ipv4" + message_digest_key: + key_id: 200 + encryption: 7 + key: "hkdfhtu==" + + state: rendered + +# Module Execution: +# +# "rendered": [ +# "interface Vlan1", +# "ip ospf dead-interval 29", +# "ip ospf mtu-ignore", +# "ip ospf hello-interval 66", +# "ospfv3 hello-interval 77", +# "ospfv3 cost 106", +# "ospfv3 transmit-delay 100", +# "ospfv3 ipv4 area 0.0.0.5", +# "ospfv3 ipv4 priority 45", +# "ospfv3 ipv6 retransmit-interval 115", +# "ospfv3 ipv6 dead-interval 56", +# "ospfv3 ipv6 passive-interface", +# "interface Vlan2", +# "ip ospf message-digest-key 200 md5 7 hkdfhtu==", +# "ospfv3 ipv4 area 0.0.0.6", +# "ospfv3 ipv4 hello-interval 45", +# "ospfv3 ipv4 retransmit-interval 100" +# ] +# + +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ospf_interfaces.ospf_interfaces import ( + Ospf_interfacesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.ospf_interfaces.ospf_interfaces import ( + Ospf_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Ospf_interfacesArgs.argument_spec, + mutually_exclusive=[], + required_if=[], + supports_check_mode=False, + ) + + result = Ospf_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_ospfv2.py b/ansible_collections/arista/eos/plugins/modules/eos_ospfv2.py new file mode 100644 index 000000000..470efb023 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_ospfv2.py @@ -0,0 +1,1563 @@ +#!/usr/bin/python +# -*- 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 module file for eos_ospfv2 +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_ospfv2 +short_description: OSPFv2 resource module +description: This module configures and manages the attributes of ospfv2 on Arista + EOS platforms. +version_added: 1.0.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: A list of configurations for ospfv2. + type: dict + suboptions: + processes: + description: A list of dictionary specifying the ospfv2 processes. + type: list + elements: dict + suboptions: + process_id: + description: ID of OSPFV2 process. + type: int + vrf: + description: VRF name . + type: str + traffic_engineering: + description: Enter traffic engineering config mode + type: bool + adjacency: + description: Configure adjacency options for OSPF instance. + type: dict + suboptions: + exchange_start: + description: Configure exchange-start options for OSPF instance. + type: dict + suboptions: + threshold: + description: Number of peers to bring up simultaneously. + type: int + router_id: + description: 32-bit number assigned to a router running OSPFv2. + type: str + max_lsa: + description: Specifies the switch behavior on reaching max lsa count. + type: dict + suboptions: + count: + description: maximum count of lsas. + type: int + threshold: + description: percentage of <count> , when a warning should be raised. + type: int + ignore_time: + description: time in minutes, for which the switch shoud be shutdown + on max-lsa warning + type: int + ignore_count: + description: No. of times the switch can shut down temporarily on + warning + type: int + reset_time: + description: Time in minutes, after which the shutdown counter resets. + type: int + warning: + description: Only give warning message when limit is exceeded + type: bool + max_metric: + description: Set maximum metric. + type: dict + suboptions: + router_lsa: + description: Maximum metric in self-originated router-LSAs. + type: dict + suboptions: + set: + description: + - Set router-lsa attribute. + type: bool + external_lsa: + description: Override external-lsa metric with max-metric value. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + include_stub: + description: Set maximum metric for stub links in router-LSAs. + type: bool + on_startup: + description: Set maximum metric temporarily after reboot. + type: dict + suboptions: + wait_period: + description: + - Wait period in seconds after startup. + type: int + summary_lsa: + description: Override summary-lsa metric with max-metric value. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + log_adjacency_changes: + description: To configure link-state changes and transitions of OSPFv2 + neighbors. + type: dict + suboptions: + detail: + description: If true , configures the switch to log all link-state + changes. + type: bool + maximum_paths: + description: Maximum number of next-hops in an ECMP route. + type: int + mpls_ldp: + description: mpls ldp sync configuration. + type: bool + networks: + description: Configure routing for a network. + type: list + elements: dict + suboptions: + network_address: + description: Network Address. + type: str + prefix: + description: Prefix. + type: str + mask: + description: Network Wildcard Mask. + type: str + area: + description: Configure OSPF area. + type: str + passive_interface: + description: Include interface but without actively running OSPF. + type: dict + suboptions: + interface_list: + description: Interface range. + type: str + default: + description: If True, Set all interfaces to passive by default + type: bool + point_to_point: + description: Configure Point-to-point specific features. + type: bool + rfc1583compatibility: + description: Specifies different methods for calculating summary route + metrics. + type: bool + distance: + description: Specifies the administrative distance for routes. + type: dict + suboptions: + external: + description: Routes external to the area + type: int + inter_area: + description: Routes from other areas + type: int + intra_area: + description: Routes with in an area + type: int + redistribute: + description: Specifies the routes to be redistributed + type: list + elements: dict + suboptions: + routes: + description: Route types (BGP,isis,connected etc) + type: str + route_map: + description: Specify which route map to use. + type: str + isis_level: + description: ISIS levels. + type: str + retransmission_threshold: + description: Configure threshold for retransmission. + type: int + distribute_list: + description: Specifies the list of routes to be filtered. + type: dict + suboptions: + route_map: + description: route map to be filtered + type: str + prefix_list: + description: prefix list to be filtered + type: str + areas: + description: Specifies the configuration for OSPF areas + type: list + elements: dict + suboptions: + area_id: + description: Specifies a 32 bit number expressed in decimal or dotted-decimal + notation. + type: str + default_cost: + description: Specify the cost for default summary route in stub/NSSA + area. + type: int + filter: + description: Specify the filter for incoming summary LSAs. + type: dict + suboptions: + address: + description: IP address. + type: str + subnet_address: + description: IP address with mask length + type: str + subnet_mask: + description: IP subnet mask + type: str + prefix_list: + description: Specify list to filter for incoming LSAs. + type: str + nssa: + description: Configures NSSA parameters. + type: dict + suboptions: + default_information_originate: + description: Originate default Type 7 LSA. + type: dict + suboptions: + metric: + description: Metric for default route. + type: int + metric_type: + description: Metric type for default route. + type: int + nssa_only: + description: Limit default advertisement to this NSSA area. + type: bool + no_summary: + description: Filter all type-3 LSAs in the nssa area. + type: bool + nssa_only: + description: Disable Type-7 LSA p-bit setting + type: bool + set: + description: Set config up to nssa + type: bool + not_so_stubby: + description: Configures NSSA parameters. + type: dict + suboptions: + default_information_originate: + description: Originate default Type 7 LSA. + type: dict + suboptions: + metric: + description: Metric for default route. + type: int + metric_type: + description: Metric type for default route. + type: int + nssa_only: + description: Limit default advertisement to this NSSA area. + type: bool + lsa: + description: lsa parameters + type: bool + no_summary: + description: Filter all type-3 LSAs in the nssa area. + type: bool + nssa_only: + description: Disable Type-7 LSA p-bit setting + type: bool + set: + description: Set config up to not-so-stubby + type: bool + range: + description: Configure route summarization. + type: dict + suboptions: + address: + description: IP address. + type: str + subnet_address: + description: IP address with mask length + type: str + subnet_mask: + description: IP subnet mask + type: str + advertise: + description: Enable Advertisement of the range. + type: bool + cost: + description: Configures the metric. + type: int + stub: + description: Stub area. + type: dict + suboptions: + no_summary: + description: If False , Filter all type-3 LSAs in the stub area. + type: bool + set: + description: When true sets the stub config alone. + type: bool + auto_cost: + description: Set auto-cost. + type: dict + suboptions: + reference_bandwidth: + description: reference bandwidth in megabits per sec. + type: int + bfd: + description: Enable BFD. + type: dict + suboptions: + all_interfaces: + description: Enable BFD on all interfaces. + type: bool + default_information: + description: Control distribution of default information. + type: dict + suboptions: + originate: + description: Distribute a default route. + type: bool + always: + description: Always advertise default route. + type: bool + metric: + description: Metric for default route. + type: int + metric_type: + description: Metric type for default route. + type: int + route_map: + description: Specify which route-map to use. + type: str + default_metric: + description: Configure the default metric for redistributed routes + type: int + dn_bit_ignore: + description: If True, Disable dn-bit check for Type-3 LSAs in non-default + VRFs. + type: bool + graceful_restart: + description: Enable graceful restart mode. + type: dict + suboptions: + grace_period: + description: Specify maximum time to wait for graceful-restart to + complete. + type: int + set: + description: When true sets the grace_fulrestart config alone. + type: bool + graceful_restart_helper: + description: If True, Enable graceful restart helper. + type: bool + shutdown: + description: Disable the OSPF instance. + type: bool + summary_address: + description: Summary route configuration. + type: dict + suboptions: + address: + description: IP summary address. + type: str + prefix: + description: Prefix. + type: str + mask: + description: Summary Mask. + type: str + attribute_map: + description: Set attributes of summary route. + type: str + not_advertise: + description: Do not advertise summary route. + type: bool + tag: + description: Set tag. + type: int + timers: + description: Configure OSPF timers. + type: list + elements: dict + suboptions: + lsa: + description: Configure OSPF LSA timers. + type: dict + suboptions: + rx: + description: Configure OSPF LSA receiving timers + type: dict + suboptions: + min_interval: + description: Configure OSPF LSA arrival timer. + type: int + tx: + description: Configure OSPF LSA transmission timers. + type: dict + suboptions: + delay: + description: Configure OSPF LSA transmission delay. + type: dict + suboptions: + initial: + description: Delay to generate first occurrence of LSA + in msecs. + type: int + min: + description: Min delay between originating the same LSA + in msecs. + type: int + max: + description: Maximum delay between originating the same + LSA in msecs. + type: int + out_delay: + description: Configure out-delay timer. + type: int + pacing: + description: Configure OSPF packet pacing. + type: int + spf: + description: Configure SPF timers + type: dict + suboptions: + seconds: + description: Seconds. + type: int + initial: + description: Initial SPF schedule delay in msecs. + type: int + min: + description: Min Hold time between two SPFs in msecs + type: int + max: + description: Max wait time between two SPFs in msecs. + type: int + throttle: + description: Configure throttle timers(valid only for eos version < 4.23). + type: dict + suboptions: + attr: + description: throttle attribute. + type: str + initial: + description: Initial schedule delay in msecs. + type: int + min: + description: Min Hold time + type: int + max: + description: Max wait time + type: int + fips_restrictions: + description: Use FIPS compliant algorithms + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ospf). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: [deleted, merged, overridden, replaced, gathered, rendered, parsed] + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------ +# localhost#show running-config | section ospf +# localhost# + + - name: replace Ospf configs + arista.eos.eos_ospfv2: + config: + - processes: + - process_id: 1 + adjacency: + exchange_start: + threshold: 20045623 + areas: + - filter: + address: "10.1.1.0/24" + id: "0.0.0.2" + - id: "0.0.0.50" + range: + address: "172.20.0.0/16" + cost: 34 + default_information: + metric: 100 + metric_type: 1 + originate: True + distance: + intra_area: 85 + max_lsa: + count: 8000 + ignore_count: 3 + ignore_time: 6 + reset_time: 20 + threshold: 40 + networks: + - area: "0.0.0.0" + prefix: 10.10.2.0/24 + - area: "0.0.0.0" + prefix: "10.10.3.0/24" + redistribute: + - routes: "static" + router_id: "170.21.0.4" + - process_id: 2 + vrf: "vrf01" + areas: + - id: "0.0.0.9" + default_cost: 20 + max_lsa: + count: 8000 + ignore_count: 3 + ignore_time: 6 + reset_time: 20 + threshold: 40 + networks: + - area: "0.0.0.0" + prefix: 10.10.2.0/24 + - area: "0.0.0.0" + prefix: "10.10.3.0/24" + redistribute: + - routes: "static" + router_id: "170.21.0.4" + - process_id: 2 + vrf: "vrf01" + areas: + - id: "0.0.0.9" + default_cost: 20 + max_lsa: + count: 8000 + ignore_count: 3 + ignore_time: 6 + reset_time: 20 + threshold: 40 + - process_id: 3 + vrf: "vrf02" + redistribute: + - routes: "connected" + +# After state: +# localhost#show running-config | section ospf +# router ospf 1 +# router-id 170.21.0.4 +# distance ospf intra-area 85 +# redistribute static +# area 0.0.0.2 filter 10.1.1.0/24 +# area 0.0.0.50 range 172.20.0.0/16 cost 34 +# network 10.10.2.0/24 area 0.0.0.0 +# network 10.10.3.0/24 area 0.0.0.0 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# adjacency exchange-start threshold 20045623 +# default-information originate metric 100 metric-type 1 +# +# router ospf 2 vrf vrf01 +# area 0.0.0.9 default-cost 20 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# ! +# router ospf 3 vrf vrf02 +# redistribute connected +# max-lsa 12000 +# localhost# +# +# "processes": [ +# { +# "adjacency": { +# "exchange_start": { +# "threshold": 20045623 +# } +# }, +# "areas": [ +# { +# "filter": { +# "address": "10.1.1.0/24" +# }, +# "id": "0.0.0.2" +# }, +# { +# "id": "0.0.0.50", +# "range": { +# "address": "172.20.0.0/16", +# "cost": 34 +# } +# } +# ], +# "default_information": { +# "metric": 100, +# "metric_type": 1, +# "originate": true +# }, +# "distance": { +# "intra_area": 85 +# }, +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "networks": [ +# { +# "area": "0.0.0.0", +# "prefix": "10.10.2.0/24" +# }, +# { +# "area": "0.0.0.0", +# "prefix": "10.10.3.0/24" +# } +# ], +# "process_id": 1, +# "redistribute": [ +# { +# "routes": "static" +# } +# ], +# "router_id": "170.21.0.4" +# }, +# { +# "areas": [ +# { +# "default_cost": 20, +# "id": "0.0.0.9" +# } +# ], +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "process_id": 2, +# "vrf": "vrf01" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] +# + + +# Using replaced: +# -------------- + +# Before State: + +# localhost#show running-config | section ospf +# router ospf 1 +# router-id 170.21.0.4 +# distance ospf intra-area 85 +# redistribute static +# area 0.0.0.2 filter 10.1.1.0/24 +# area 0.0.0.50 range 172.20.0.0/16 cost 34 +# network 10.10.2.0/24 area 0.0.0.0 +# network 10.10.3.0/24 area 0.0.0.0 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# adjacency exchange-start threshold 20045623 +# default-information originate metric 100 metric-type 1 +# ! +# router ospf 2 vrf vrf01 +# area 0.0.0.9 default-cost 20 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# ! +# router ospf 3 vrf vrf02 +# redistribute connected +# max-lsa 12000 +# localhost# +# +# "before": [ +# { +# "processes": [ +# { +# "adjacency": { +# "exchange_start": { +# "threshold": 20045623 +# } +# }, +# "areas": [ +# { +# "filter": { +# "address": "10.1.1.0/24" +# }, +# "id": "0.0.0.2" +# }, +# { +# "id": "0.0.0.50", +# "range": { +# "address": "172.20.0.0/16", +# "cost": 34 +# } +# } +# ], +# "default_information": { +# "metric": 100, +# "metric_type": 1, +# "originate": true +# }, +# "distance": { +# "intra_area": 85 +# }, +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "networks": [ +# { +# "area": "0.0.0.0", +# "prefix": "10.10.2.0/24" +# }, +# { +# "area": "0.0.0.0", +# "prefix": "10.10.3.0/24" +# } +# ], +# "process_id": 1, +# "redistribute": [ +# { +# "routes": "static" +# } +# ], +# "router_id": "170.21.0.4" +# }, +# { +# "areas": [ +# { +# "default_cost": 20, +# "id": "0.0.0.9" +# } +# ], +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "process_id": 2, +# "vrf": "vrf01" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] +# + - name: replace Ospf configs + arista.eos.eos_ospfv2: + config: + - processes: + - process_id: 2 + vrf: "vrf01" + point_to_point: True + redistribute: + - routes: "isis" + isis_level: "level-1" + + state: replaced + +# After State: +# ----------- +# "router ospf 2 vrf vrf01", +# "no area 0.0.0.9 default-cost 20", +# "no max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20", +# "point-to-point routes", +# "redistribute isis level-1" +# +# "after": [ +# { +# "processes": [ +# { +# "adjacency": { +# "exchange_start": { +# "threshold": 20045623 +# } +# }, +# "areas": [ +# { +# "filter": { +# "address": "10.1.1.0/24" +# }, +# "id": "0.0.0.2" +# }, +# { +# "id": "0.0.0.50", +# "range": { +# "address": "172.20.0.0/16", +# "cost": 34 +# } +# } +# ], +# "default_information": { +# "metric": 100, +# "metric_type": 1, +# "originate": true +# }, +# "distance": { +# "intra_area": 85 +# }, +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "networks": [ +# { +# "area": "0.0.0.0", +# "prefix": "10.10.2.0/24" +# }, +# { +# "area": "0.0.0.0", +# "prefix": "10.10.3.0/24" +# } +# ], +# "process_id": 1, +# "redistribute": [ +# { +# "routes": "static" +# } +# ], +# "router_id": "170.21.0.4" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 2, +# "redistribute": [ +# { +# "isis_level": "level-1", +# "routes": "isis" +# } +# ], +# "vrf": "vrf01" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] +# + +# Using overridden: +# ---------------- + +# Before State: +# localhost#show running-config | section ospf +# router ospf 1 +# router-id 170.21.0.4 +# distance ospf intra-area 85 +# redistribute static +# area 0.0.0.2 filter 10.1.1.0/24 +# area 0.0.0.50 range 172.20.0.0/16 cost 34 +# network 10.10.2.0/24 area 0.0.0.0 +# network 10.10.3.0/24 area 0.0.0.0 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# adjacency exchange-start threshold 20045623 +# default-information originate metric 100 metric-type 1 +# ! +# router ospf 2 vrf vrf01 +# redistribute isis level-1 +# max-lsa 12000 +# ! +# router ospf 3 vrf vrf02 +# redistribute connected +# max-lsa 12000 +# localhost# +# +# "before": [ +# { +# "processes": [ +# { +# "adjacency": { +# "exchange_start": { +# "threshold": 20045623 +# } +# }, +# "areas": [ +# { +# "filter": { +# "address": "10.1.1.0/24" +# }, +# "id": "0.0.0.2" +# }, +# { +# "id": "0.0.0.50", +# "range": { +# "address": "172.20.0.0/16", +# "cost": 34 +# } +# } +# ], +# "default_information": { +# "metric": 100, +# "metric_type": 1, +# "originate": true +# }, +# "distance": { +# "intra_area": 85 +# }, +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "networks": [ +# { +# "area": "0.0.0.0", +# "prefix": "10.10.2.0/24" +# }, +# { +# "area": "0.0.0.0", +# "prefix": "10.10.3.0/24" +# } +# ], +# "process_id": 1, +# "redistribute": [ +# { +# "routes": "static" +# } +# ], +# "router_id": "170.21.0.4" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 2, +# "redistribute": [ +# { +# "isis_level": "level-1", +# "routes": "isis" +# } +# ], +# "vrf": "vrf01" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] + + - name: override Ospf configs + arista.eos.eos_ospfv2: + config: + - processes: + - process_id: 2 + vrf: "vrf01" + redistribute: + - routes: "connected" + + state: override + +# After State: + +# "no router ospf 1", +# "no router ospf 3", +# "router ospf 2 vrf vrf01", +# "no max-lsa 12000", +# "no redistribute isis level-1", +# "redistribute connected" +# +# "after": [ +# { +# "processes": [ +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 2, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf01" +# } +# ] +# } +# ] + +# Using Deleted: + +# localhost#show running-config | section ospf +# router ospf 1 +# router-id 170.21.0.4 +# distance ospf intra-area 85 +# redistribute static +# area 0.0.0.2 filter 10.1.1.0/24 +# area 0.0.0.50 range 172.20.0.0/16 cost 34 +# network 10.10.2.0/24 area 0.0.0.0 +# network 10.10.3.0/24 area 0.0.0.0 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# adjacency exchange-start threshold 20045623 +# default-information originate metric 100 metric-type 1 +# ! +# router ospf 2 vrf vrf01 +# redistribute connected +# area 0.0.0.9 default-cost 20 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# ! +# router ospf 3 vrf vrf02 +# redistribute connected +# max-lsa 12000 +# localhost# +# +# "before": [ +# { +# "processes": [ +# { +# "adjacency": { +# "exchange_start": { +# "threshold": 20045623 +# } +# }, +# "areas": [ +# { +# "filter": { +# "address": "10.1.1.0/24" +# }, +# "id": "0.0.0.2" +# }, +# { +# "id": "0.0.0.50", +# "range": { +# "address": "172.20.0.0/16", +# "cost": 34 +# } +# } +# ], +# "default_information": { +# "metric": 100, +# "metric_type": 1, +# "originate": true +# }, +# "distance": { +# "intra_area": 85 +# }, +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "networks": [ +# { +# "area": "0.0.0.0", +# "prefix": "10.10.2.0/24" +# }, +# { +# "area": "0.0.0.0", +# "prefix": "10.10.3.0/24" +# } +# ], +# "process_id": 1, +# "redistribute": [ +# { +# "routes": "static" +# } +# ], +# "router_id": "170.21.0.4" +# }, +# { +# "areas": [ +# { +# "default_cost": 20, +# "id": "0.0.0.9" +# } +# ], +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "process_id": 2, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf01" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] + + - name: Delete Ospf configs + arista.eos.eos_ospfv2: + config: + - processes: + - process_id: 1 + + state: deleted + +# After State: +# Commands: +# "no router ospf 1" + +# "after": [ +# { +# "processes": [ +# { +# "areas": [ +# { +# "default_cost": 20, +# "id": "0.0.0.9" +# } +# ], +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "process_id": 2, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf01" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] + +# Using gathered: +# localhost#show running-config | section ospf +# router ospf 2 vrf vrf01 +# redistribute connected +# area 0.0.0.9 default-cost 20 +# max-lsa 8000 40 ignore-time 6 ignore-count 3 reset-time 20 +# ! +# router ospf 3 vrf vrf02 +# redistribute connected +# max-lsa 12000 +# localhost# + + - name: replace Ospf configs + arista.eos.eos_ospfv2: + state: gathered + +# "gathered": [ +# { +# "processes": [ +# { +# "areas": [ +# { +# "default_cost": 20, +# "id": "0.0.0.9" +# } +# ], +# "max_lsa": { +# "count": 8000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "process_id": 2, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf01" +# }, +# { +# "max_lsa": { +# "count": 12000 +# }, +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] + +# Using parsed: +# ------------ + +# parsed.cfg +# router ospf 1 +# adjacency exchange-start threshold 20045623 +# area 0.0.0.2 filter 10.1.1.0/24 +# area 0.0.0.50 range 172.20.0.0/16 cost 34 +# default-information originate metric 100 metric-type 1 +# distance ospf intra-area 85 +# max-lsa 80000 40 ignore-count 3 ignore-time 6 reset-time 20 +# network 10.10.2.0/24 area 0.0.0.0 +# network 10.10.3.0/24 area 0.0.0.0 +# redistribute static +# router-id 170.21.0.4 +# router ospf 2 vrf vrf01, +# area 0.0.0.9 default-cost 20 +# max-lsa 80000 40 ignore-count 3 ignore-time 6 reset-time 20 +# router ospf 3 vrf vrf02 +# redistribute static + + - name: Parse Ospf configs + arista.eos.eos_ospfv2: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + +# "parsed": [ +# { +# "processes": [ +# { +# "adjacency": { +# "exchange_start": { +# "threshold": 20045623 +# } +# }, +# "areas": [ +# { +# "filter": { +# "address": "10.1.1.0/24" +# }, +# "id": "0.0.0.2" +# }, +# { +# "id": "0.0.0.50", +# "range": { +# "address": "172.20.0.0/16", +# "cost": 34 +# } +# } +# ], +# "default_information": { +# "metric": 100, +# "metric_type": 1, +# "originate": true +# }, +# "distance": { +# "intra_area": 85 +# }, +# "max_lsa": { +# "count": 80000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "networks": [ +# { +# "area": "0.0.0.0", +# "prefix": "10.10.2.0/24" +# }, +# { +# "area": "0.0.0.0", +# "prefix": "10.10.3.0/24" +# } +# ], +# "process_id": 1, +# "redistribute": [ +# { +# "routes": "static" +# } +# ], +# "router_id": "170.21.0.4" +# }, +# { +# "areas": [ +# { +# "default_cost": 20, +# "id": "0.0.0.9" +# } +# ], +# "max_lsa": { +# "count": 80000, +# "ignore_count": 3, +# "ignore_time": 6, +# "reset_time": 20, +# "threshold": 40 +# }, +# "process_id": 2, +# "vrf": "vrf01," +# }, +# { +# "process_id": 3, +# "redistribute": [ +# { +# "routes": "static" +# } +# ], +# "vrf": "vrf02" +# } +# ] +# } +# ] + +# Using rendered: +# -------------- + + - name: replace Ospf configs + arista.eos.eos_ospfv2: + config: + - processes: + - process_id: 1 + adjacency: + exchange_start: + threshold: 20045623 + areas: + - filter: + address: 10.1.1.0/24 + id: 0.0.0.2 + - id: 0.0.0.50 + range: + address: 172.20.0.0/16 + cost: 34 + default_information: + metric: 100 + metric_type: 1 + originate: true + distance: + intra_area: 85 + max_lsa: + count: 8000 + ignore_count: 3 + ignore_time: 6 + reset_time: 20 + threshold: 40 + networks: + - area: 0.0.0.0 + prefix: 10.10.2.0/24 + - area: 0.0.0.0 + prefix: 10.10.3.0/24 + redistribute: + - routes: static + router_id: 170.21.0.4 + state: rendered + +# "rendered": [ +# "router ospf 1", +# "adjacency exchange-start threshold 20045623", +# "area 0.0.0.2 filter 10.1.1.0/24", +# "area 0.0.0.50 range 172.20.0.0/16 cost 34", +# "default-information originate metric 100 metric-type 1", +# "distance ospf intra-area 85", +# "max-lsa 8000 40 ignore-count 3 ignore-time 6 reset-time 20", +# "network 10.10.2.0/24 area 0.0.0.0", +# "network 10.10.3.0/24 area 0.0.0.0", +# "redistribute static", +# "router-id 170.21.0.4" +# ] +# + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + sample: > + The configuration returned will always be in the same format + of the parameters above. + type: list +after: + description: The resulting configuration model invocation. + returned: when changed + sample: > + The configuration returned will always be in the same format + of the parameters above. + type: list +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ["router ospf 1", + "adjacency exchange-start threshold 20045623", + "area 0.0.0.2 filter 10.1.1.0/24", + "area 0.0.0.50 range 172.20.0.0/16 cost 34", + "default-information originate metric 100 metric-type 1", + "distance ospf intra-area 85", + "max-lsa 8000 40 ignore-count 3 ignore-time 6 reset-time 20", + "network 10.10.2.0/24 area 0.0.0.0", + "network 10.10.3.0/24 area 0.0.0.0", + "redistribute static", + "router-id 170.21.0.4"] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ospfv2.ospfv2 import ( + Ospfv2Args, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.ospfv2.ospfv2 import ( + Ospfv2, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=Ospfv2Args.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Ospfv2(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_ospfv3.py b/ansible_collections/arista/eos/plugins/modules/eos_ospfv3.py new file mode 100644 index 000000000..8c1845262 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_ospfv3.py @@ -0,0 +1,1583 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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 module file for eos_ospfv3 +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: eos_ospfv3 +short_description: OSPFv3 resource module +description: This module configures and manages the attributes of ospfv3 on Arista + EOS platforms. +version_added: 1.1.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: A list of configurations for ospfv3. + type: dict + suboptions: + processes: + description: A list of dictionary specifying the ospfv3 processes. + type: list + elements: dict + suboptions: + vrf: + description: VRF name . + type: str + adjacency: + description: Configure adjacency options for OSPF instance. + type: dict + suboptions: + exchange_start: + description: Configure exchange-start options for OSPF instance. + type: dict + suboptions: + threshold: + description: Number of peers to bring up simultaneously. + type: int + auto_cost: + description: Set auto-cost. + type: dict + suboptions: + reference_bandwidth: + description: reference bandwidth in megabits per sec. + type: int + areas: + description: Specifies the configuration for OSPF areas + type: list + elements: dict + suboptions: + area_id: + description: Specifies a 32 bit number expressed in decimal or dotted-decimal + notation. + type: str + default_cost: + description: Specify the cost for default summary route in stub/NSSA + area. + type: int + authentication: + description: Configure authentication for the area incase of ospfv3. + type: dict + suboptions: + spi: + description: Specify the SPI value + type: int + algorithm: + description: Name of algorithm to be used. + type: str + choices: ['md5', 'sha1'] + encrypt_key: + description: If False, key string is not encrypted + type: bool + hidden_key: + description: If True, Specifies that a HIDDEN key will follow. + type: bool + key: + description: 128 bit MD5 key or 140 bit SHA1 key. + type: str + passphrase: + description: Passphrase String for deriving keys for authentication and encryption. + type: str + encryption: + description: Configure encryption for the area + type: dict + suboptions: + spi: + description: Specify the SPI value + type: int + encryption: + description: name of encryption to be used. + type: str + choices: ['3des-cbc', 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', 'null'] + algorithm: + description: name of the algorithm to be used. + type: str + choices: ['sha1', 'md5'] + encrypt_key: + description: If False, key string is not encrypted + type: bool + hidden_key: + description: If True, Specifies that a HIDDEN key will follow. + type: bool + key: + description: 128 bit MD5 key or 140 bit SHA1 key. + type: str + passphrase: + description: Passphrase String for deriving keys for authentication and encryption. + type: str + nssa: + description: Configures NSSA parameters. + type: dict + suboptions: + default_information_originate: + description: Originate default Type 7 LSA. + type: dict + suboptions: + metric: + description: Metric for default route. + type: int + metric_type: + description: Metric type for default route. + type: int + nssa_only: + description: Limit default advertisement to this NSSA area. + type: bool + set: + description: True if only default information orignate is set + type: bool + no_summary: + description: Filter all type-3 LSAs in the nssa area. + type: bool + nssa_only: + description: Disable Type-7 LSA p-bit setting + type: bool + translate: + description: Enable LSA translation. + type: bool + set: + description: True if only nssa is set + type: bool + stub: + description: Stub area. + type: dict + suboptions: + set: + description: True if only stub is set. + type: bool + summary_lsa: + description: If False , Filter all type-3 LSAs in the stub area. + type: bool + + bfd: + description: Enable BFD. + type: dict + suboptions: + all_interfaces: + description: Enable BFD on all interfaces. + type: bool + fips_restrictions: + description: Use FIPS compliant algorithms + type: bool + graceful_restart: + description: Enable graceful restart mode. + type: dict + suboptions: + grace_period: + description: Specify maximum time to wait for graceful-restart to + complete. + type: int + set: + description: When true sets the grace_fulrestart config alone. + type: bool + graceful_restart_helper: + description: If True, Enable graceful restart helper. + type: bool + log_adjacency_changes: + description: To configure link-state changes and transitions of OSPFv3 + neighbors. + type: dict + suboptions: + detail: + description: If true , configures the switch to log all link-state + changes. + type: bool + set: + description: When true sets the log_adjacency_changes config alone. + type: bool + max_metric: + description: Set maximum metric. + type: dict + suboptions: + router_lsa: + description: Maximum metric in self-originated router-LSAs. + type: dict + suboptions: + set: + description: + - Set router-lsa attribute. + type: bool + external_lsa: + description: Override external-lsa metric with max-metric value. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + include_stub: + description: Set maximum metric for stub links in router-LSAs. + type: bool + on_startup: + description: Set maximum metric temporarily after reboot. + type: dict + suboptions: + wait_period: + description: + - Wait period in seconds after startup. + type: int + wait_for_bgp: + description: + - Let BGP decide when to originate router-LSA with normal metric + type: bool + summary_lsa: + description: Override summary-lsa metric with max-metric value. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + passive_interface: + description: Include interface but without actively running OSPF. + type: bool + router_id: + description: 32-bit number assigned to a router running OSPFv3. + type: str + shutdown: + description: Disable the OSPF instance. + type: bool + timers: + description: Configure OSPF timers. + type: dict + suboptions: + out_delay: + description: Configure out-delay timer. + type: int + pacing: + description: Configure OSPF packet pacing. + type: int + throttle: + description: This command is deprecated by 'timers lsa' or 'timers spf'. + type: dict + suboptions: + initial: + description: Initial SPF schedule delay in msecs. + type: int + min: + description: Min Hold time between two SPFs in msecs + type: int + max: + description: Max wait time between two SPFs in msecs. + type: int + lsa: + description: Configure threshold for retransmission of lsa + type: bool + spf: + description: Configure time between SPF calculations + type: bool + spf: + description: Configure OSPFv3 spf timers. + type: dict + suboptions: + initial: + description: Initial SPF schedule delay in msecs. + type: int + min: + description: Min Hold time between two SPFs in msecs + type: int + max: + description: Max wait time between two SPFs in msecs. + type: int + lsa: + description: Configure OSPFv3 LSA timers. + type: raw + suboptions: + direction: + description: Configure OSPFv3 LSA receiving/transmission timers. + type: str + choices: ["rx", "tx"] + initial: + description: Initial SPF schedule delay in msecs. + type: int + min: + description: Min Hold time between two SPFs in msecs + type: int + max: + description: Max wait time between two SPFs in msecs. + type: int + address_family: + description: Enable address family and enter its config mode + type: list + elements: dict + suboptions: + afi: + description: address family . + type: str + choices: + - ipv4 + - ipv6 + adjacency: + description: Configure adjacency options for OSPF instance. + type: dict + suboptions: + exchange_start: + description: Configure exchange-start options for OSPF instance. + type: dict + suboptions: + threshold: + description: Number of peers to bring up simultaneously. + type: int + auto_cost: + description: Set auto-cost. + type: dict + suboptions: + reference_bandwidth: + description: reference bandwidth in megabits per sec. + type: int + areas: + description: Specifies the configuration for OSPF areas + type: list + elements: dict + suboptions: + area_id: + description: Specifies a 32 bit number expressed in decimal or dotted-decimal + notation. + type: str + default_cost: + description: Specify the cost for default summary route in stub/NSSA + area. + type: int + authentication: + description: Configure authentication for the area incase of ospfv3. + type: dict + suboptions: + spi: + description: Specify the SPI value + type: int + algorithm: + description: Name of algorithm to be used. + type: str + choices: ['md5', 'sha1'] + encrypt_key: + description: If False, key string is not encrypted + type: bool + hidden_key: + description: If True, Specifies that a HIDDEN key will follow. + type: bool + key: + description: 128 bit MD5 key or 140 bit SHA1 key. + type: str + passphrase: + description: Passphrase String for deriving keys for authentication and encryption. + type: str + encryption: + description: Configure encryption for the area + type: dict + suboptions: + spi: + description: Specify the SPI value + type: int + encryption: + description: name of encryption to be used. + type: str + choices: ['3des-cbc', 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', 'null'] + algorithm: + description: name of the algorithm to be used. + type: str + choices: ['sha1', 'md5'] + encrypt_key: + description: If False, key string is not encrypted + type: bool + hidden_key: + description: If True, Specifies that a HIDDEN key will follow. + type: bool + key: + description: 128 bit MD5 key or 140 bit SHA1 key. + type: str + passphrase: + description: Passphrase String for deriving keys for authentication and encryption. + type: str + nssa: + description: Configures NSSA parameters. + type: dict + suboptions: + default_information_originate: + description: Originate default Type 7 LSA. + type: dict + suboptions: + metric: + description: Metric for default route. + type: int + metric_type: + description: Metric type for default route. + type: int + nssa_only: + description: Limit default advertisement to this NSSA area. + type: bool + set: + description: True if only default information orignate is set + type: bool + no_summary: + description: Filter all type-3 LSAs in the nssa area. + type: bool + nssa_only: + description: Disable Type-7 LSA p-bit setting + type: bool + translate: + description: Enable LSA translation. + type: bool + set: + description: True if only nssa is set + type: bool + ranges: + description: Configure route summarization. + type: list + elements: dict + suboptions: + address: + description: IP address. + type: str + subnet_address: + description: IP address with mask length + type: str + subnet_mask: + description: IP subnet mask + type: str + advertise: + description: Enable Advertisement of the range. + type: bool + cost: + description: Configures the metric. + type: int + stub: + description: Stub area. + type: dict + suboptions: + set: + description: True if only stub is set + type: bool + summary_lsa: + description: If False , Filter all type-3 LSAs in the stub area. + type: bool + + bfd: + description: Enable BFD. + type: dict + suboptions: + all_interfaces: + description: Enable BFD on all interfaces. + type: bool + default_information: + description: Control distribution of default information. + type: dict + suboptions: + originate: + description: Distribute a default route. + type: bool + always: + description: Always advertise default route. + type: bool + metric: + description: Metric for default route. + type: int + metric_type: + description: Metric type for default route. + type: int + route_map: + description: Specify which route-map to use. + type: str + default_metric: + description: Configure the default metric for redistributed routes. + type: int + distance: + description: Specifies the administrative distance for routes. + type: int + fips_restrictions: + description: Use FIPS compliant algorithms + type: bool + graceful_restart: + description: Enable graceful restart mode. + type: dict + suboptions: + grace_period: + description: Specify maximum time to wait for graceful-restart to complete. + type: int + set: + description: When true sets the grace_fulrestart config alone. + type: bool + graceful_restart_helper: + description: If True, Enable graceful restart helper. + type: bool + log_adjacency_changes: + description: To configure link-state changes and transitions of OSPFv3 + neighbors. + type: dict + suboptions: + detail: + description: If true , configures the switch to log all link-state + changes. + type: bool + set: + description: When true sets the log_adjacency_changes config alone. + type: bool + max_metric: + description: Set maximum metric. + type: dict + suboptions: + router_lsa: + description: Maximum metric in self-originated router-LSAs. + type: dict + suboptions: + set: + description: + - Set router-lsa attribute. + type: bool + external_lsa: + description: Override external-lsa metric with max-metric value. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + include_stub: + description: Set maximum metric for stub links in router-LSAs. + type: bool + on_startup: + description: Set maximum metric temporarily after reboot. + type: dict + suboptions: + wait_period: + description: + - Wait period in seconds after startup. + type: int + wait_for_bgp: + description: + - Let BGP decide when to originate router-LSA with normal metric + type: bool + summary_lsa: + description: Override summary-lsa metric with max-metric value. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + maximum_paths: + description: Maximum number of next-hops in an ECMP route. + type: int + passive_interface: + description: Include interface but without actively running OSPF. + type: bool + redistribute: + description: Specifies the routes to be redistributed. + type: list + elements: dict + suboptions: + routes: + description: Route types (BGP,static,connected) + type: str + choices: ['bgp', 'connected', 'static'] + route_map: + description: Specify which route map to use. + type: str + router_id: + description: 32-bit number assigned to a router running OSPFv3. + type: str + shutdown: + description: Disable the OSPF instance. + type: bool + timers: + description: Configure OSPF timers. + type: dict + suboptions: + throttle: + description: This command is deprecated by 'timers lsa' or 'timers spf'. + type: dict + suboptions: + initial: + description: Initial SPF schedule delay in msecs. + type: int + min: + description: Min Hold time between two SPFs in msecs + type: int + max: + description: Max wait time between two SPFs in msecs. + type: int + lsa: + description: Configure threshold for retransmission of lsa + type: bool + spf: + description: Configure time between SPF calculations + type: bool + out_delay: + description: Configure out-delay timer. + type: int + pacing: + description: Configure OSPF packet pacing. + type: int + spf: + description: Configure OSPFv3 spf timers. + type: dict + suboptions: + initial: + description: Initial SPF schedule delay in msecs. + type: int + min: + description: Min Hold time between two SPFs in msecs + type: int + max: + description: Max wait time between two SPFs in msecs. + type: int + lsa: + description: Configure OSPFv3 LSA timers. + type: raw + suboptions: + direction: + description: Configure OSPFv3 LSA receiving/transmission timers. + type: str + choices: ["rx", "tx"] + initial: + description: Initial SPF schedule delay in msecs. + type: int + min: + description: Min Hold time between two SPFs in msecs + type: int + max: + description: Max wait time between two SPFs in msecs. + type: int + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section ospfv3). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: [deleted, merged, overridden, replaced, gathered, rendered, parsed] + default: merged +""" + +EXAMPLES = """ + +# Using merged + +# Before state + +# veos#show running-config | section ospfv3 +# veos# + + + - arista.eos.eos_ospfv3: + config: + processes: + - address_family: + - timers: + lsa: 22 + graceful_restart: + grace_period: 35 + afi: "ipv6" + timers: + pacing: 55 + fips_restrictions: True + router_id: "2.2.2.2" + vrf: "vrfmerge" + + +# After state + +# veos#show running-config | section ospfv3 +# router ospfv3 vrf vrfmerge +# router-id 2.2.2.2 +# test +# fips restrictions +# timers pacing flood 55 +# ! +# address-family ipv6 +# fips restrictions +# timers lsa arrival 22 +# graceful-restart grace-period 35 +# veos# + +# Module Execution +# "after": { +# "processes": [ +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "fips_restrictions": true, +# "graceful_restart": { +# "grace_period": 35 +# }, +# "timers": { +# "lsa": 22 +# } +# } +# ], +# "fips_restrictions": true, +# "router_id": "2.2.2.2", +# "timers": { +# "pacing": 55 +# }, +# "vrf": "vrfmerge" +# } +# ] +# }, +# "before": {}, +# "changed": true, +# "commands": [ +# "router ospfv3 vrf vrfmerge", +# "address-family ipv6", +# "graceful-restart grace-period 35", +# "timers lsa arrival 22", +# "exit", +# "timers pacing flood 55", +# "fips restrictions", +# "router-id 2.2.2.2", +# "exit" +# ], + + +# using replaced + +# before state + +# veos#show running-config | section ospfv3 +# router ospfv3 +# fips restrictions +# area 0.0.0.0 encryption ipsec spi 43 esp null md5 passphrase 7 h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4= +# ! +# router ospfv3 vrf vrfmerge +# router-id 2.2.2.2 +# fips restrictions +# timers pacing flood 55 +# ! +# address-family ipv6 +# fips restrictions +# timers lsa arrival 22 +# graceful-restart grace-period 35 +# veos# + + + - arista.eos.eos_ospfv3: + config: + processes: + - areas: + - area_id: "0.0.0.0" + encryption: + spi: 43 + encryption: "null" + algorithm: "md5" + encrypt_key: False + passphrase: "7hl8FV3lZ6H1mAKpjL47hQ==" + vrf: "default" + address_family: + - afi: "ipv4" + router_id: "7.1.1.1" + state: replaced + +# After state +# veos#show running-config | section ospfv3 +# router ospfv3 +# area 0.0.0.0 encryption ipsec spi 43 esp null md5 passphrase 7 h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4= +# ! +# router ospfv3 vrf vrfmerge +# passive-interface default +# ! +# address-family ipv6 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# veos# + +# Module execution + +# "after": { +# "processes": [ +# { +# "areas": [ +# { +# "area_id": "0.0.0.0", +# "encryption": { +# "algorithm": "md5", +# "encryption": "null", +# "hidden_key": true, +# "passphrase": "h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4=" +# } +# } +# ], +# "vrf": "default" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ] +# } +# ], +# "passive_interface": true, +# "vrf": "vrfmerge" +# } +# ] +# }, +# "before": { +# "processes": [ +# { +# "areas": [ +# { +# "area_id": "0.0.0.0", +# "encryption": { +# "algorithm": "md5", +# "encryption": "null", +# "hidden_key": true, +# "passphrase": "h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4=" +# } +# } +# ], +# "fips_restrictions": true, +# "vrf": "default" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "fips_restrictions": true, +# "graceful_restart": { +# "grace_period": 35 +# }, +# "timers": { +# "lsa": 22 +# } +# } +# ], +# "fips_restrictions": true, +# "router_id": "2.2.2.2", +# "timers": { +# "pacing": 55 +# }, +# "vrf": "vrfmerge" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "router ospfv3 vrf vrfmerge", +# "address-family ipv6", +# "no fips restrictions", +# "no graceful-restart", +# "no timers lsa arrival 22", +# "area 0.0.0.3 range 10.1.2.2/24 advertise", +# "area 0.0.0.3 range 60.1.1.1 255.255.0.0 cost 30", +# "exit", +# "passive-interface default", +# "no router-id", +# "no fips restrictions", +# "no timers pacing flood 55", +# "exit" +# ], + + +# using overridden + +# before state + +# veos#show running-config | section ospfv3 +# router ospfv3 +# area 0.0.0.0 encryption ipsec spi 43 esp null md5 passphrase 7 h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4= +# ! +# router ospfv3 vrf vrfmerge +# passive-interface default +# ! +# address-family ipv6 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# veos# + + + - arista.eos.eos_ospfv3: + config: + processes: + - address_family: + - areas: + - area_id: "0.0.0.3" + ranges: + - address: 10.1.2.2/24 + advertise: True + - address: 60.1.1.1 + subnet_mask: 255.255.0.0 + cost: 30 + afi: "ipv6" + passive_interface: True + vrf: "vrfmerge" + state: overridden + +# After state + +# veos#show running-config | section ospfv3 +# router ospfv3 vrf vrfmerge +# passive-interface default +# ! +# address-family ipv6 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# veos# + + + +# Module execution + +# "after": { +# "processes": [ +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ] +# } +# ], +# "passive_interface": true, +# "vrf": "vrfmerge" +# } +# ] +# }, +# "before": { +# "processes": [ +# { +# "areas": [ +# { +# "area_id": "0.0.0.0", +# "encryption": { +# "algorithm": "md5", +# "encryption": "null", +# "hidden_key": true, +# "passphrase": "h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4=" +# } +# } +# ], +# "vrf": "default" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ] +# } +# ], +# "passive_interface": true, +# "vrf": "vrfmerge" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "no router ospfv3", +# "router ospfv3 vrf vrfmerge", +# "address-family ipv6", +# "no area 0.0.0.3 range 10.1.2.0/24", +# "no area 0.0.0.3 range 60.1.0.0/16 cost 30", +# "area 0.0.0.3 range 10.1.2.2/24 advertise", +# "area 0.0.0.3 range 60.1.1.1 255.255.0.0 cost 30", +# "exit", +# "exit" +# ], + +# using deleted + +# Before state + +# veos#show running-config | section ospfv3 +# router ospfv3 +# area 0.0.0.0 encryption ipsec spi 43 esp null md5 passphrase 7 h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4= +# ! +# router ospfv3 vrf vrfmerge +# passive-interface default +# ! +# address-family ipv4 +# redistribute connected +# redistribute static route-map MAP01 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# ! +# address-family ipv6 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# veos# + + + - arista.eos.eos_ospfv3: + config: + processes: + - vrf: "default" + state: deleted + +# After state + +# veos#show running-config | section ospfv3 +# router ospfv3 vrf vrfmerge +# passive-interface default +# ! +# address-family ipv4 +# redistribute connected +# redistribute static route-map MAP01 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# ! +# address-family ipv6 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# veos# + + +# Module execution +# "after": { +# "processes": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ], +# "redistribute": [ +# { +# "routes": "connected" +# }, +# { +# "route_map": "MAP01", +# "routes": "static" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ] +# } +# ], +# "passive_interface": true, +# "vrf": "vrfmerge" +# } +# ] +# }, +# "before": { +# "processes": [ +# { +# "areas": [ +# { +# "area_id": "0.0.0.0", +# "encryption": { +# "algorithm": "md5", +# "encryption": "null", +# "hidden_key": true, +# "passphrase": "h8pZp9eprTYjjoY/NKFFe0Ei7x03Y7dyLotRhI0a5t4=" +# } +# } +# ], +# "vrf": "default" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ], +# "redistribute": [ +# { +# "routes": "connected" +# }, +# { +# "route_map": "MAP01", +# "routes": "static" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ] +# } +# ], +# "passive_interface": true, +# "vrf": "vrfmerge" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "no router ospfv3" +# ], + +# using parsed + +# parsed_ospfv3.cfg + +# router ospfv3 +# fips restrictions +# area 0.0.0.20 stub +# area 0.0.0.20 authentication ipsec spi 33 sha1 passphrase 7 4O8T3zo4xBdRWXBnsnK934o9SEb+jEhHUN6+xzZgCo2j9EnQBUvtwNxxLEmYmm6w +# area 0.0.0.40 default-cost 45 +# area 0.0.0.40 stub +# timers pacing flood 7 +# adjacency exchange-start threshold 11 +# ! +# address-family ipv4 +# fips restrictions +# redistribute connected +# ! +# address-family ipv6 +# router-id 10.1.1.1 +# fips restrictions +# ! +# router ospfv3 vrf vrf01 +# bfd all-interfaces +# fips restrictions +# area 0.0.0.0 encryption ipsec spi 256 esp null sha1 passphrase 7 7hl8FV3lZ6H1mAKpjL47hQ== +# log-adjacency-changes detail +# ! +# address-family ipv4 +# passive-interface default +# fips restrictions +# redistribute connected route-map MAP01 +# maximum-paths 100 +# ! +# address-family ipv6 +# fips restrictions +# area 0.0.0.10 nssa no-summary +# default-information originate route-map DefaultRouteFilter +# max-metric router-lsa external-lsa 25 summary-lsa +# ! +# router ospfv3 vrf vrf02 +# fips restrictions +# ! +# address-family ipv6 +# router-id 10.17.0.3 +# distance ospf intra-area 200 +# fips restrictions +# area 0.0.0.1 stub +# timers spf delay initial 56 56 56 +# timers out-delay 10 + + + - arista.eos.eos_ospfv3: + running_config: "{{ lookup('file', './parsed_ospfv3.cfg') }}" + state: parsed + +# Module execution + +# "parsed": { +# "processes": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "fips_restrictions": true, +# "redistribute": [ +# { +# "routes": "connected" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "fips_restrictions": true, +# "router_id": "10.1.1.1" +# } +# ], +# "adjacency": { +# "exchange_start": { +# "threshold": 11 +# } +# }, +# "areas": [ +# { +# "area_id": "0.0.0.20", +# "authentication": { +# "algorithm": "sha1", +# "hidden_key": true, +# "passphrase": "4O8T3zo4xBdRWXBnsnK934o9SEb+jEhHUN6+xzZgCo2j9EnQBUvtwNxxLEmYmm6w", +# "spi": 33 +# }, +# "stub": { +# "set": true +# } +# }, +# { +# "area_id": "0.0.0.40", +# "default_cost": 45, +# "stub": { +# "set": true +# } +# } +# ], +# "fips_restrictions": true, +# "timers": { +# "pacing": 7 +# }, +# "vrf": "default" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "fips_restrictions": true, +# "maximum_paths": 100, +# "passive_interface": true, +# "redistribute": [ +# { +# "route_map": "MAP01", +# "routes": "connected" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.10", +# "nssa": { +# "no_summary": true +# } +# } +# ], +# "default_information": { +# "originate": true, +# "route_map": "DefaultRouteFilter" +# }, +# "fips_restrictions": true, +# "max_metric": { +# "router_lsa": { +# "external_lsa": { +# "max_metric_value": 25 +# }, +# "summary_lsa": { +# "set": true +# } +# } +# } +# } +# ], +# "areas": [ +# { +# "area_id": "0.0.0.0", +# "encryption": { +# "algorithm": "sha1", +# "encryption": "null", +# "hidden_key": true, +# "passphrase": "7hl8FV3lZ6H1mAKpjL47hQ==" +# } +# } +# ], +# "bfd": { +# "all_interfaces": true +# }, +# "fips_restrictions": true, +# "log_adjacency_changes": { +# "detail": true +# }, +# "vrf": "vrf01" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.1", +# "stub": { +# "set": true +# } +# } +# ], +# "distance": 200, +# "fips_restrictions": true, +# "router_id": "10.17.0.3", +# "timers": { +# "out_delay": 10, +# "spf": { +# "initial": 56, +# "max": 56, +# "min": 56, +# } +# } +# } +# ], +# "fips_restrictions": true, +# "vrf": "vrf02" +# } +# ] + +# using gathered + +# native config + +# veos#show running-config | section ospfv3 +# router ospfv3 vrf vrfmerge +# passive-interface default +# ! +# address-family ipv4 +# redistribute connected +# redistribute static route-map MAP01 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# ! +# address-family ipv6 +# area 0.0.0.3 range 10.1.2.0/24 +# area 0.0.0.3 range 60.1.0.0/16 cost 30 +# veos# + + + - arista.eos.eos_ospfv3: + state: gathered + +# module execution + +# "gathered": { +# "processes": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ], +# "redistribute": [ +# { +# "routes": "connected" +# }, +# { +# "route_map": "MAP01", +# "routes": "static" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "areas": [ +# { +# "area_id": "0.0.0.3", +# "ranges": [ +# { +# "address": "10.1.2.0/24" +# }, +# { +# "address": "60.1.0.0/16", +# "cost": 30 +# } +# ] +# } +# ] +# } +# ], +# "passive_interface": true, +# "vrf": "vrfmerge" +# } +# ] + +# using rendered + + - arista.eos.eos_ospfv3: + config: + processes: + - address_family: + - timers: + lsa: 22 + graceful_restart: + grace_period: 35 + afi: "ipv6" + timers: + pacing: 55 + fips_restrictions: True + router_id: "2.2.2.2" + vrf: "vrfmerge" + state: rendered + +# module execution + +# "rendered": [ +# "router ospfv3 vrf vrfmerge", +# "address-family ipv6", +# "graceful-restart grace-period 35", +# "timers lsa arrival 22", +# "exit", +# "timers pacing flood 55", +# "fips restrictions", +# "router-id 2.2.2.2", +# "exit" +# ] + + +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.ospfv3.ospfv3 import ( + Ospfv3Args, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.ospfv3.ospfv3 import ( + Ospfv3, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Ospfv3Args.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=False, + ) + + result = Ospfv3(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_prefix_lists.py b/ansible_collections/arista/eos/plugins/modules/eos_prefix_lists.py new file mode 100644 index 000000000..dffd65527 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_prefix_lists.py @@ -0,0 +1,1197 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for eos_prefix_lists +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: eos_prefix_lists +short_description: Manages Prefix lists resource module +description: This module configures and manages the attributes of Prefix lists on Arista + EOS platforms. +version_added: 2.2.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,eos_platform_options). +options: + config: + description: A list of dictionary of prefix-list options + type: list + elements: dict + suboptions: + afi: + description: + - The Address Family Indicator (AFI) for the prefix list. + type: str + required: true + choices: + - ipv4 + - ipv6 + prefix_lists: + description: + - A list of prefix-lists. + type: list + elements: dict + suboptions: + name: + description: Name of the prefix-list + type: str + required: true + entries: + description: List of prefix-lists + type: list + elements: dict + suboptions: + action: + description: action to be performed on the specified path + type: str + choices: ['deny', 'permit'] + address: + description: ipv4/v6 address in prefix-mask or address-masklen format + type: str + match: + description: match masklen + type: dict + suboptions: + operator: + description: equalto/greater than/lesser than + type: str + choices: ['eq', 'le', 'ge'] + masklen: + description: Mask Length. + type: int + sequence: + description: sequence number + type: int + resequence: + description: Resequence the list. + type: dict + suboptions: + default: + description: Resequence with default values (10). + type: bool + start_seq: + description: Starting sequence number. + type: int + step: + description: Step to increment the sequence number. + type: int + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section access-list). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged +""" +EXAMPLES = """ +# Using merged +# Before state +# veos#show running-config | section prefix-lists +# veos# + + - name: Merge provided configuration with device configuration + arista.eos.eos_prefix_lists: + config: + - afi: "ipv4" + prefix_lists: + - name: "v401" + entries: + - sequence: 25 + action: "deny" + address: "45.55.4.0/24" + - sequence: 100 + action: "permit" + address: "11.11.2.0/24" + match: + masklen: 32 + operator: "ge" + - name: "v402" + entries: + - action: "deny" + address: "10.1.1.0/24" + sequence: 10 + match: + masklen: 32 + operator: "ge" + - afi: "ipv6" + prefix_lists: + - name: "v601" + entries: + - sequence: 125 + action: "deny" + address: "5000:1::/64" + +# After State +# veos# +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 +# seq 100 permit 11.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# veos# +# +# Module Execution: +# "after": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 10 +# } +# ], +# "name": "v402" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ], +# "before": {}, +# "changed": true, +# "commands": [ +# "ipv6 prefix-list v601", +# "seq 125 deny 5000:1::/64", +# "ip prefix-list v401", +# "seq 25 deny 45.55.4.0/24", +# "seq 100 permit 11.11.2.0/24 ge 32", +# "ip prefix-list v402", +# "seq 10 deny 10.1.1.0/24 ge 32" +# ], +# + +# using merged: +# Failure scenario : 'merged' should not be used when an existing prefix-list (sequence number) +# is to be modified. + +# Before State: +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 +# seq 100 permit 11.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# veos# + + - name: Merge provided configuration with device configuration + arista.eos.eos_prefix_lists: + config: + - afi: "ipv4" + prefix_lists: + - name: "v401" + entries: + - sequence: 25 + action: "deny" + address: "45.55.4.0/24" + match: + masklen: 32 + operator: "ge" + - sequence: 100 + action: "permit" + address: "11.11.2.0/24" + match: + masklen: 32 + operator: "ge" + - name: "v402" + entries: + - action: "deny" + address: "10.1.1.0/24" + sequence: 10 + match: + masklen: 32 + operator: "ge" + - afi: "ipv6" + prefix_lists: + - name: "v601" + entries: + - sequence: 125 + action: "deny" + address: "5000:1::/64" + state: merged + +# Module Execution: +# fatal: [192.168.122.113]: FAILED! => { +# "changed": false, +# "invocation": { +# "module_args": { +# "config": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "resequence": null, +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "resequence": null, +# "sequence": 100 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "resequence": null, +# "sequence": 10 +# } +# ], +# "name": "v402" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "match": null, +# "resequence": null, +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ], +# "running_config": null, +# "state": "merged" +# } +# }, +# "msg": "Sequence number 25 is already present. Use replaced/overridden operation to change the configuration" +# } +# + +# Using Replaced: + +# Before state: +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 +# seq 100 permit 11.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# veos# + - name: Replace + arista.eos.eos_prefix_lists: + config: + - afi: "ipv4" + prefix_lists: + - name: "v401" + entries: + - sequence: 25 + action: "deny" + address: "45.55.4.0/24" + match: + masklen: 32 + operator: "ge" + - sequence: 200 + action: "permit" + address: "200.11.2.0/24" + match: + masklen: 32 + operator: "ge" + state: replaced +# After State: +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 ge 32 +# seq 200 permit 200.11.2.0/24 ge 32 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# veos# +# +# +# Module Execution: +# +# "after": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "200.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 200 +# } +# ], +# "name": "v401" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ], +# "before": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 10 +# } +# ], +# "name": "v402" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ], +# "changed": true, +# "commands": [ +# "ip prefix-list v401", +# "no seq 25", +# "seq 25 deny 45.55.4.0/24 ge 32", +# "seq 200 permit 200.11.2.0/24 ge 32", +# "no seq 100", +# "no ip prefix-list v402" +# ], + +# Using overridden: +# Before State: + +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 ge 32 +# seq 100 permit 11.11.2.0/24 ge 32 +# seq 200 permit 200.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# veos# + + + - name: Override + arista.eos.eos_prefix_lists: + config: + - afi: "ipv4" + prefix_lists: + - name: "v401" + entries: + - sequence: 25 + action: "deny" + address: "45.55.4.0/24" + - sequence: 300 + action: "permit" + address: "30.11.2.0/24" + match: + masklen: 32 + operator: "ge" + - name: "v403" + entries: + - action: "deny" + address: "10.1.1.0/24" + sequence: 10 + state: overridden + +# After State +# veos# +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 ge 32 +# seq 300 permit 30.11.2.0/24 ge 32 +# ! +# ip prefix-list v403 +# seq 10 deny 10.1.1.0/24 +# veos# +# +# +# Module Execution: +# "after": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "30.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 300 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "sequence": 10 +# } +# ], +# "name": "v403" +# } +# ] +# } +# ], +# "before": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# }, +# { +# "action": "permit", +# "address": "200.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 200 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 10 +# } +# ], +# "name": "v402" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ], +# "changed": true, +# "commands": [ +# "no ipv6 prefix-list v601", +# "ip prefix-list v401", +# "seq 25 deny 45.55.4.0/24", +# "seq 300 permit 30.11.2.0/24 ge 32", +# "no seq 100", +# "no seq 200", +# "ip prefix-list v403", +# "seq 10 deny 10.1.1.0/24", +# "no ip prefix-list v402" +# ], +# + +# Using deleted: +# Before State: + +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 ge 32 +# seq 100 permit 11.11.2.0/24 ge 32 +# seq 300 permit 30.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ip prefix-list v403 +# seq 10 deny 10.1.1.0/24 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# veos# + + - name: Delete device configuration + arista.eos.eos_prefix_lists: + config: + - afi: "ipv6" + state: deleted + + +# after State: +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 ge 32 +# seq 100 permit 11.11.2.0/24 ge 32 +# seq 300 permit 30.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ip prefix-list v403 +# seq 10 deny 10.1.1.0/24 +# +# +# Module Execution: +# "after": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# }, +# { +# "action": "permit", +# "address": "30.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 300 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 10 +# } +# ], +# "name": "v402" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "sequence": 10 +# } +# ], +# "name": "v403" +# } +# ] +# } +# ], +# "before": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# }, +# { +# "action": "permit", +# "address": "30.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 300 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 10 +# } +# ], +# "name": "v402" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "sequence": 10 +# } +# ], +# "name": "v403" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ], +# "changed": true, +# "commands": [ +# "no ipv6 prefix-list v601" +# ], +# + +# Using deleted +# Before state: + +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 ge 32 +# seq 100 permit 11.11.2.0/24 ge 32 +# seq 300 permit 30.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ip prefix-list v403 +# seq 10 deny 10.1.1.0/24 +# veos# + + - name: Delete device configuration + arista.eos.eos_prefix_lists: + state: deleted + +# After State: +# veos#show running-config | section prefix-list +# veos# +# +# Module Execution: +# "after": {}, +# "before": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# }, +# { +# "action": "permit", +# "address": "30.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 300 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 10 +# } +# ], +# "name": "v402" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "sequence": 10 +# } +# ], +# "name": "v403" +# } +# ] +# } +# ], +# "changed": true, +# "commands": [ +# "no ip prefix-list v401", +# "no ip prefix-list v402", +# "no ip prefix-list v403" +# ], +# + +# Using parsed: +# parse_prefix_lists.cfg +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 +# seq 100 permit 11.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# + - name: parse configs + arista.eos.eos_prefix_lists: + running_config: "{{ lookup('file', './parsed_prefix_lists.cfg') }}" + state: parsed + +# Module Execution: +# "parsed": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "sequence": 10 +# } +# ], +# "name": "v402" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ] + +# Using rendered: + - name: Render provided configuration + arista.eos.eos_prefix_lists: + config: + - afi: "ipv4" + prefix_lists: + - name: "v401" + entries: + - sequence: 25 + action: "deny" + address: "45.55.4.0/24" + - sequence: 200 + action: "permit" + address: "200.11.2.0/24" + match: + masklen: 32 + operator: "ge" + - name: "v403" + entries: + - action: "deny" + address: "10.1.1.0/24" + sequence: 10 + state: rendered + +# Module Execution: +# "rendered": [ +# "ip prefix-list v401", +# "seq 25 deny 45.55.4.0/24", +# "seq 200 permit 200.11.2.0/24 ge 32", +# "ip prefix-list v403", +# "seq 10 deny 10.1.1.0/24" +# ] +# + +# using gathered: +# Device config: +# veos#show running-config | section prefix-list +# ip prefix-list v401 +# seq 25 deny 45.55.4.0/24 +# seq 100 permit 11.11.2.0/24 ge 32 +# ! +# ip prefix-list v402 +# seq 10 deny 10.1.1.0/24 ge 32 +# ! +# ipv6 prefix-list v601 +# seq 125 deny 5000:1::/64 +# veos# + + - name: gather configs + arista.eos.eos_prefix_lists: + state: gathered + +# Module Execution: +# +# "gathered": [ +# { +# "afi": "ipv4", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "45.55.4.0/24", +# "sequence": 25 +# }, +# { +# "action": "permit", +# "address": "11.11.2.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 100 +# } +# ], +# "name": "v401" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "address": "10.1.1.0/24", +# "match": { +# "masklen": 32, +# "operator": "ge" +# }, +# "sequence": 10 +# } +# ], +# "name": "v402" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "prefix_lists": [ +# { +# "entries": [ +# { +# "action": "deny", +# "address": "5000:1::/64", +# "sequence": 125 +# } +# ], +# "name": "v601" +# } +# ] +# } +# ], + +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.prefix_lists.prefix_lists import ( + Prefix_listsArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.prefix_lists.prefix_lists import ( + Prefix_lists, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Prefix_listsArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Prefix_lists(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_route_maps.py b/ansible_collections/arista/eos/plugins/modules/eos_route_maps.py new file mode 100644 index 000000000..3b1869547 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_route_maps.py @@ -0,0 +1,1406 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 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 module file for eos_route_maps +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: eos_route_maps +short_description: Manages Route Maps resource module +description: This module configures and manages the attributes of Route Mapd on Arista + EOS platforms. +version_added: 2.1.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,eos_platform_options). +options: + config: + description: A list of route-map options + type: list + elements: dict + suboptions: + route_map: + description: Route map name. + type: str + entries: + description: Route Map entries. + type: list + elements: dict + suboptions: + statement: + description: statement name + type: str + source: + description: Rename/Copy configuration + type: dict + suboptions: + action: + description: rename or copy configuration + type: str + choices: ["rename", "copy"] + source_map_name: + description: Source route map name. + type: str + overwrite: + description: if True, overwrite existing config. + type: bool + action: + description: Action for matching routes + type: str + choices: ["deny", "permit"] + sequence: + description: Index in the sequence. + type: int + continue_sequence: + description: Route map entry sequence number. + type: int + description: + description: Description for the route map. + type: str + sub_route_map: + description: Sub route map + type: dict + suboptions: + name: + description: sub route map name + type: str + invert_result: + description: Invert sub route map result + type: bool + set: + description: set route attributes. + type: dict + suboptions: + as_path: + description: Set as-path. + type: dict + suboptions: + match: + description: Match the entire as-path. + type: dict + suboptions: + as_number: + description: as number to use (includes auto;in csv format) + type: str + none: + description: Remove matching AS numbers + type: bool + prepend: + description: Prepend to the as-path. + type: dict + suboptions: + as_number: + description: as number to prepend (includes auto;in csv format) + type: str + last_as: + description: The number of times to prepend the last AS number. + type: int + bgp: + description: BGP AS path multipath weight. + type: int + community_attributes: + description: BGP community attribute. + type: dict + suboptions: + graceful_shutdown: + description: Graceful shutdown + type: bool + community: + description: community attributes. + type: dict + suboptions: + number: + description: community number (in csv format). + type: str + list: + description: community list name. + type: str + graceful_shutdown: + description: Gracefully shutdown. + type: bool + additive: + description: Add to existing community. + type: bool + delete: + description: Delete matching communities. + type: bool + internet: + description: Internet community + type: bool + local_as: + description: Do not send outside local AS. + type: bool + no_advertise: + description: Do not advertise to any peer. + type: bool + no_export: + description: Do not export to next AS. + type: bool + none: + description: No community attribute. + type: bool + distance: + description: Set protocol independent distance. + type: int + evpn: + description: Keep the next hop when advertising to eBGP peers. + type: bool + extcommunity: + description: BGP extended community attribute. + type: dict + suboptions: + lbw: + description: Link bandwith values. + type: dict + suboptions: + value: + description: Link Bandwidth extended community value. + type: str + aggregate: + description: Aggregate Link Bandwidth. + type: bool + divide: + description: Divide Link Bandwidth. + type: str + choices: ["equal", "ration"] + none: + description: No attribute. + type: bool + rt: + description: Route target extended community + type: dict + suboptions: ¶ms01 + vpn: + description: VPN extended community. + type: str + additive: + description: Add to the existing community. + type: bool + delete: + description: Delete matching communities. + type: bool + soo: + description: Site-of-Origin extended community. + type: dict + suboptions: *params01 + ip: + description: Set IP specific information. + type: dict + suboptions: ¶ms02 + address: + description: next hop address. + type: str + unchanged: + description: Keep the next hop when advertising to eBGP peer + type: bool + peer_address: + description: Use BGP peering addr as next-hop. + type: bool + ipv6: + description: Set IPv6 specific information. + type: dict + suboptions: *params02 + isis_level: + description: IS-IS level. + type: str + local_preference: + description: BGP local preference. + type: int + metric: + description: Route metric. + type: dict + suboptions: + igp_param: + description: IGP parameter + type: str + choices: ['igp-metric', 'igp-nexthop-cost'] + add: + description: Add igp-metric / igp-nexthop-cost + type: str + choices: ['igp-metric', 'igp-nexthop-cost'] + value: + description: metric value to add or subtract(with +/- sign). + type: str + metric_type: + description: Route metric type. + type: str + choices: ["type-1", "type-2"] + nexthop: + description: Route next hop. + type: dict + suboptions: + value: + description: IGP metric value. + type: int + max_metric: + description: Set IGP max metric value. + type: bool + origin: + description: Set bgp origin. + type: str + choices: ["egp", "igp", "incomplete"] + segment_index: + description: MPLS Segment-routing Segment Index. + type: int + tag: + description: Route tag + type: int + weight: + description: BGP weight. + type: int + match: + description: Route map match rules. + type: dict + suboptions: + aggregate_role: ¶ms04 + description: Role in BGP contributor-aggregate relation. + type: dict + suboptions: + contributor: + description: BGP aggregate's contributor. + type: bool + route_map: + description: Route map to apply against the aggregate route. + type: str + as: + description: BGP AS number. + type: int + as_path: ¶ms05 + description: Set as-path. + type: dict + suboptions: + path_list: + description: AS path list name. + type: str + length: + description: Specify as-path length ( with comparison operators like <= 60 and >= 40 ). + type: str + community: ¶ms06 + description: BGP community attribute. + type: dict + suboptions: + community_list: + description: list of community names (in csv format). + type: str + exact_match: + description: Do exact matching of communities. + type: bool + instances: + description: Match number of community instances ( with comparison operators like <= 60 and >= 40 ). + type: str + extcommunity: ¶ms07 + description: extended community list name. + type: dict + suboptions: + community_list: + description: list of community names (in csv format). + type: str + exact_match: + description: Do exact matching of communities. + type: bool + interface: + description: interface name. + type: str + invert_result: + description: Invert match result. + type: dict + suboptions: + aggregate_role: *params04 + as_path: *params05 + community: *params06 + extcommunity: *params07 + large_community: *params07 + ip: + description: Set IP specific information. + type: dict + suboptions: ¶ms08 + address: + description: next hop destination. + type: dict + suboptions: + access_list: + description: ip access-list. + type: str + dynamic: + description: Configure dynamic prefix-list. + type: bool + prefix_list: + description: Prefix list. + type: str + next_hop: + description: next hop prefix list. + type: str + resolved_next_hop: + description: Route resolved prefix list. + type: str + ipv6: + description: Set IPv6 specific information. + type: dict + suboptions: *params08 + large_community: *params07 + isis_level: + description: IS-IS level. + type: str + local_preference: + description: BGP local preference. + type: int + metric: + description: Route metric. + type: int + metric_type: + description: Route metric type. + type: str + choices: ["type-1", "type-2"] + route_type: + description: Route type + type: str + router_id: + description: Router ID. + type: str + source_protocol: + description: Source routing protocol, + type: str + tag: + description: Route tag + type: int + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | section route-map). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged +""" +EXAMPLES = """ +# Using merged +# Before state +# veos#show running-config | section route-map +# veos# + + - name: Merge provided configuration with device configuration + arista.eos.eos_route_maps: + config: + - route_map: "mapmerge" + entries: + - description: "merged_map" + action: "permit" + sequence: 10 + match: + router_id: 22 + - description: "newmap" + action: "deny" + sequence: 25 + continue_sequence: 45 + match: + interface: "Ethernet1" + - route_map: "mapmerge2" + entries: + - sub_route_map: + name: "mapmerge" + action: "deny" + sequence: 45 + set: + metric: + value: 25 + add: "igp-metric" + as_path: + prepend: + last_as: 2 + match: + ipv6: + resolved_next_hop: "list1" + state: merged + +# After State: + +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# description merged_map +# match router-id prefix-list 22 +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# ! +# route-map test permit 10 +# veos# + + +# Module Execution: + +# "after": [ +# { +# "entries": [ +# { +# "action": "permit", +# "description": "merged_map", +# "match": { +# "router_id": "22" +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "continue_sequence": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# } +# ], +# "before": {}, +# "changed": true, +# "commands": [ +# "route-map mapmerge permit 10", +# "match router-id prefix-list 22", +# "description merged_map", +# "route-map mapmerge deny 25", +# "match interface Ethernet1", +# "description newmap", +# "continue 45", +# "route-map mapmerge2 deny 45", +# "match ipv6 resolved-next-hop prefix-list list1", +# "set metric 25 +igp-metric", +# "set as-path prepend last-as 2", +# "sub-route-map mapmerge" +# ], +# + +# Using replaced: + +# Before State: + +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# description merged_map +# match router-id prefix-list 22 +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# ! +# veos# + + - name: Replace + arista.eos.eos_route_maps: + config: + - route_map: "mapmerge" + entries: + - action: "permit" + sequence: 10 + match: + ipv6: + resolved_next_hop: "listr" + - action: "deny" + sequence: 90 + set: + extcommunity: + rt: + vpn: "22:11" + delete: True + ip: + unchanged: True + state: replaced + +# After State: + +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# match ipv6 resolved-next-hop prefix-list listr +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge deny 90 +# set ip next-hop unchanged +# set extcommunity rt 22:11 delete +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# ! +# +# Module Execution: +# +# "after": [ +# { +# "entries": [ +# { +# "action": "permit", +# "match": { +# "ipv6": { +# "resolved_next_hop": "listr" +# } +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "continue_sequence": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# }, +# { +# "action": "deny", +# "sequence": 90, +# "set": { +# "extcommunity": { +# "rt": { +# "delete": true, +# "vpn": "22:11" +# } +# }, +# "ip": { +# "unchanged": true +# } +# } +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# }, +# { +# "entries": [ +# { +# "action": "permit", +# "sequence": 10 +# } +# ], +# "route_map": "test" +# } +# ], +# "before": [ +# { +# "entries": [ +# { +# "action": "permit", +# "description": "merged_map", +# "match": { +# "router_id": "22" +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "continue_sequence": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# } +# ], +# "changed": true, +# "commands": [ +# "route-map mapmerge permit 10", +# "match ipv6 resolved-next-hop prefix-list listr", +# "no match router-id prefix-list 22", +# "no description", +# "route-map mapmerge deny 90", +# "set extcommunity rt 22:11 delete", +# "set ip next-hop unchanged" +# ], +# +# +# Using Overridden: + +# Before state: +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# match ipv6 resolved-next-hop prefix-list listr +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge deny 90 +# set ip next-hop unchanged +# set extcommunity rt 22:11 delete +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# ! +# route-map test permit 10 +# veos# + + - name: Override + arista.eos.eos_route_maps: + config: + - route_map: "mapmerge" + entries: + - action: "permit" + sequence: 10 + match: + ipv6: + resolved_next_hop: "listr" + - action: "deny" + sequence: 90 + set: + metric: + igp_param: "igp-nexthop-cost" + state: overridden + +# After State: + +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# match ipv6 resolved-next-hop prefix-list listr +# ! +# route-map mapmerge deny 90 +# set metric igp-nexthop-cost +# veos# +# +# +# "after": [ +# { +# "entries": [ +# { +# "action": "permit", +# "match": { +# "ipv6": { +# "resolved_next_hop": "listr" +# } +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "sequence": 90, +# "set": { +# "metric": { +# "igp_param": "igp-nexthop-cost" +# } +# } +# } +# ], +# "route_map": "mapmerge" +# } +# ], +# "before": [ +# { +# "entries": [ +# { +# "action": "permit", +# "match": { +# "ipv6": { +# "resolved_next_hop": "listr" +# } +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "continue_sequence": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# }, +# { +# "action": "deny", +# "sequence": 90, +# "set": { +# "extcommunity": { +# "rt": { +# "delete": true, +# "vpn": "22:11" +# } +# }, +# "ip": { +# "unchanged": true +# } +# } +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# }, +# { +# "entries": [ +# { +# "action": "permit", +# "sequence": 10 +# } +# ], +# "route_map": "test" +# } +# ], +# "changed": true, +# "commands": [ +# "no route-map mapmerge deny 25", +# "no route-map mapmerge2 deny 45", +# "no route-map test permit 10", +# "route-map mapmerge deny 90", +# "set metric igp-nexthop-cost", +# "no set ip next-hop unchanged", +# "no set extcommunity rt 22:11 delete" +# ], +# +# Using deleted: +# Before State: + +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# description merged_map +# match router-id prefix-list 22 +# match ipv6 resolved-next-hop prefix-list listr +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge deny 90 +# set metric igp-nexthop-cost +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# veos# + + - name: Delete route-map + arista.eos.eos_route_maps: + config: + - route_map: "mapmerge" + state: deleted + become: yes + tags: + - deleted1 + +# After State: + +# veos#show running-config | section route-map +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# veos# +# +# Module Execution: +# +# "after": [ +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# } +# ], +# "before": [ +# { +# "entries": [ +# { +# "action": "permit", +# "description": "merged_map", +# "match": { +# "ipv6": { +# "resolved_next_hop": "listr" +# }, +# "router_id": "22" +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "continue": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# }, +# { +# "action": "deny", +# "sequence": 90, +# "set": { +# "metric": { +# "igp_param": "igp-nexthop-cost" +# } +# } +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# } +# ], +# "changed": true, +# "commands": [ +# "no route-map mapmerge" +# ], + +# Using deleted to delete all route-maps: + +# Before State: + +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# description merged_map +# match router-id prefix-list 22 +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# veos# + + - name: Delete all route-maps + arista.eos.eos_route_maps: + state: deleted + +# After State: +# veos#show running-config | section route-map +# veos# +# +# Module Execution: +# +# "after": {}, +# "before": [ +# { +# "entries": [ +# { +# "action": "permit", +# "description": "merged_map", +# "match": { +# "router_id": "22" +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "continue": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# } +# ], +# "changed": true, +# "commands": [ +# "no route-map mapmerge", +# "no route-map mapmerge2" +# ], + +# Using gathered: + +# Device configs: + +# veos#show running-config | section route-map +# route-map mapmerge permit 10 +# description merged_map +# match router-id prefix-list 22 +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 +# veos# + + - name: gather configs + arista.eos.eos_route_maps: + state: gathered + +# Module Execution: +# "gathered": [ +# { +# "entries": [ +# { +# "action": "permit", +# "description": "merged_map", +# "match": { +# "router_id": "22" +# }, +# "sequence": 10 +# }, +# { +# "action": "deny", +# "continue_sequence": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# } +# ], + +# Using rendered: + + - name: Render provided configuration + arista.eos.eos_route_maps: + config: + - route_map: "mapmerge" + entries: + - description: "merged_map" + action: "permit" + sequence: 10 + match: + router_id: 22 + set: + bgp: 20 + - description: "newmap" + action: "deny" + sequence: 25 + continue_sequence: 45 + match: + interface: "Ethernet1" + - route_map: "mapmerge2" + entries: + - sub_route_map: + name: "mapmerge" + action: "deny" + sequence: 45 + set: + metric: + value: 25 + add: "igp-metric" + as_path: + prepend: + last_as: 2 + match: + ipv6: + resolved_next_hop: "list1" + state: rendered + +# Module Execution: +# "rendered": [ +# "route-map mapmerge permit 10", +# "match router-id prefix-list 22", +# "set bgp bestpath as-path weight 20", +# "description merged_map", +# "route-map mapmerge deny 25", +# "match interface Ethernet1", +# "description newmap", +# "continue 45", +# "route-map mapmerge2 deny 45", +# "match ipv6 resolved-next-hop prefix-list list1", +# "set metric 25 +igp-metric", +# "set as-path prepend last-as 2", +# "sub-route-map mapmerge" +# ] + +# Using parsed: + +# parsed.cfg +# route-map mapmerge permit 10 +# description merged_map +# match router-id prefix-list 22 +# set bgp bestpath as-path weight 20 +# ! +# route-map mapmerge deny 25 +# description newmap +# match interface Ethernet1 +# continue 45 +# ! +# route-map mapmerge2 deny 45 +# match ipv6 resolved-next-hop prefix-list list1 +# sub-route-map mapmerge +# set metric 25 +igp-metric +# set as-path prepend last-as 2 + + - name: parse configs + arista.eos.eos_route_maps: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + +# Module Execution: +# "parsed": [ +# { +# "entries": [ +# { +# "action": "permit", +# "description": "merged_map", +# "match": { +# "router_id": "22" +# }, +# "sequence": 10, +# "set": { +# "bgp": 20 +# } +# }, +# { +# "action": "deny", +# "continue_sequence": 45, +# "description": "newmap", +# "match": { +# "interface": "Ethernet1" +# }, +# "sequence": 25 +# } +# ], +# "route_map": "mapmerge" +# }, +# { +# "entries": [ +# { +# "action": "deny", +# "match": { +# "ipv6": { +# "resolved_next_hop": "list1" +# } +# }, +# "sequence": 45, +# "set": { +# "as_path": { +# "prepend": { +# "last_as": 2 +# } +# }, +# "metric": { +# "add": "igp-metric", +# "value": "25" +# } +# }, +# "sub_route_map": { +# "name": "mapmerge" +# } +# } +# ], +# "route_map": "mapmerge2" +# } +# ] + + + +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.route_maps.route_maps import ( + Route_mapsArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.route_maps.route_maps import ( + Route_maps, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Route_mapsArgs.argument_spec, + mutually_exclusive=[], + required_if=[], + supports_check_mode=False, + ) + + result = Route_maps(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_snmp_server.py b/ansible_collections/arista/eos/plugins/modules/eos_snmp_server.py new file mode 100644 index 000000000..b702c8a46 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_snmp_server.py @@ -0,0 +1,1522 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for eos_snmp_server +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: eos_snmp_server +short_description: Manages snmp_server resource module +description: This module configures and manages the attributes of snmp_server on Arista + EOS platforms. +version_added: 3.2.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli) and C(httpapi). +options: + config: + description: SNMP server configuration. + type: dict + suboptions: + chassis_id: + description: SNMP chassis identifier. + type: str + communities: + description: Community name configuration. + type: list + elements: dict + suboptions: + name: + description: Community name + type: str + acl_v4: + description: standard access_list name + type: str + acl_v6: + description: IPv6 access list name. + type: str + ro: + description: Only reads are permitted. + type: bool + rw: + description: Read_write access + type: bool + view: + description: MIB view name + type: str + contact: + description: Person to contact about the syste,. + type: str + traps: + description: Enable traps to all configured recipients. + type: dict + suboptions: + bgp: + description: Enable Bgp traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_backward_transition: + description: arista_backward_transition + type: bool + arista_established: + description: arista_established + type: bool + backward_transition: + description: backward_transition + type: bool + established: + description: established. + type: bool + enabled: + description: All traps are set. + type: bool + bridge: + description: Enable Bridge traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_mac_age: + description: arista_mac_age + type: bool + arista_mac_learn: + description: arista_mac_learn + type: bool + arista_mac_move: + description: arista_mac_move + type: bool + enabled: + description: All traps are set. + type: bool + capacity: + description: Enable Capacity traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_hardware_utilization_alert: + description: arista_hardware_utilization_alert + type: bool + enabled: + description: All traps are set. + type: bool + entity: + description: Enable Entity traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_ent_sensor_alarm: + description: arista_ent_sensor_alarm + type: bool + ent_config_change: + description: ent_config_change + type: bool + ent_state_oper: + description: ent_state_oper + type: bool + ent_state_oper_disabled: + description: ent_state_oper_disabled. + type: bool + ent_state_oper_enabled: + description: ent_state_oper_enabled. + type: bool + enabled: + description: All traps are set. + type: bool + external_alarm: + description: Enable external alarm traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_external_alarm_asserted_notif: + description: arista_external_alarm_asserted_notif + type: bool + arista_external_alarm_deasserted_notif: + description: arista_external_alarm_deasserted_notif + type: bool + enabled: + description: All traps are set. + type: bool + isis: + description: Enable isis traps. If set to enabled , all the traps are set. + type: dict + suboptions: + adjacency_change: + description: adjacency_change + type: bool + area_mismatch: + description: area_mismatch + type: bool + attempt_to_exceed_max_sequence: + description: attempt_to_exceed_max_sequence + type: bool + authentication_type_failure: + description: authentication_type_failure. + type: bool + database_overload: + description: database_overload + type: bool + own_lsp_purge: + description: own_lsp_purge + type: bool + rejected_adjacency: + description: rejected_adjacency + type: bool + sequence_number_skip: + description: sequence_number_skip. + type: bool + enabled: + description: All traps are set. + type: bool + lldp: + description: Enable Lldp traps. If set to enabled , all the traps are set. + type: dict + suboptions: + rem_tables_change: + description: rem_tables_change + type: bool + enabled: + description: All traps are set. + type: bool + mpls_ldp: + description: Enable mpls_ldp traps. If set to enabled , all the traps are set. + type: dict + suboptions: + mpls_ldp_session_down: + description: mpls_ldp_session_down + type: bool + mpls_ldp_session_up: + description: mpls_ldp_session_up + type: bool + enabled: + description: All traps are set. + type: bool + msdp: + description: Enable msdp traps. If set to enabled , all the traps are set. + type: dict + suboptions: + backward_transition: + description: backward_transition. + type: bool + established: + description: established. + type: bool + enabled: + description: All traps are set. + type: bool + ospf: + description: Enable Ospf traps. If set to enabled , all the traps are set. + type: dict + suboptions: + if_config_error: + description: if_config_error + type: bool + if_auth_failure: + description: if_auth_failure + type: bool + if_state_change: + description: if_state_change + type: bool + nbr_state_change: + description: nbr_state_change. + type: bool + enabled: + description: All traps are set. + type: bool + ospfv3: + description: Enable Ospfv3 traps. If set to enabled , all the traps are set. + type: dict + suboptions: + if_config_error: + description: if_config_error + type: bool + if_rx_bad_packet: + description: if_rx_bad_packet + type: bool + if_state_change: + description: if_state_change + type: bool + nbr_state_change: + description: nbr_state_change. + type: bool + nbr_restart_helper_status_change: + description: Enable ospfv3NbrRestartHelperStatusChange trap + type: bool + nssa_translator_status_change: + description: Enable ospfv3NssaTranslatorStatusChange trap + type: bool + restart_status_change: + description: restart_status_change + type: bool + enabled: + description: All traps are set. + type: bool + pim: + description: Enable Pim traps. If set to enabled , all the traps are set. + type: dict + suboptions: + neighbor_loss: + description: neighbor_loss + type: bool + enabled: + description: All traps are set. + type: bool + snmp: + description: Enable snmp traps. If set to enabled , all the traps are set. + type: dict + suboptions: + authentication: + description: authentication + type: bool + link_down: + description: link_down + type: bool + link_up: + description: link_up + type: bool + enabled: + description: All traps are set. + type: bool + snmpConfigManEvent: + description: Enable snmpConfigManEvent traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_config_man_event: + description: arista_config_man_event + type: bool + enabled: + description: All traps are set. + type: bool + switchover: + description: Enable switchover traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_redundancy_switch_over_notif: + description: arista_redundancy_switch_over_notif + type: bool + enabled: + description: All traps are set. + type: bool + test: + description: Enable test traps. If set to enabled , all the traps are set. + type: dict + suboptions: + arista_test_notification: + description: arista_test_notification + type: bool + enabled: + description: All traps are set. + type: bool + vrrp: + description: Enable vrrp traps. If set to enabled , all the traps are set. + type: dict + suboptions: + trap_new_master: + description: vrrp + type: bool + enabled: + description: All traps are set. + type: bool + engineid: + description: SNMPv3 engine ID configuration. + type: dict + suboptions: + local: + description: Local SNMP agent + type: str + remote: + description: Remote SNMP agent + type: dict + suboptions: + host: + description: Hostname or IP address of remote SNMP notification host + type: str + udp_port: + description: The remote SNMP notification host's UDP port number. + type: int + id: + description: engine ID octet string + type: str + extension: + description: Configure extension script to serve an OID range + type: dict + suboptions: + root_oid: + description: Extension root oid + type: str + script_location: + description: script location + type: str + oneshot: + description: Use inefficient one_shot interface + type: bool + groups: + description: SNMP USM group + type: list + elements: dict + suboptions: + group: + description: SNMP group for the user + type: str + version: + description: snmp security group version + type: str + choices: ['v1', 'v3', 'v2c'] + auth_privacy: + description: auth and privacy config. Valid when version = v3. + type: str + choices: ['auth', 'noauth', 'priv'] + context: + description: Specify a context to associate with the group + type: str + notify: + description: View to restrict notifications + type: str + read: + description: View to restrict read access + type: str + write: + description: View to restrict write access + type: str + hosts: + description: Notification destinations + type: list + elements: dict + suboptions: + host: + description: Hostname or IP address of SNMP notification host. + type: str + user: + description: Community or user name. + type: str + udp_port: + description: UDP destination port for notification messages. + type: int + informs: + description: Use SNMP inform messages. + type: bool + traps: + description: Use SNMP trap messages + type: bool + version: + description: Notification message SNMP version. + type: str + choices: ['1', '2c', '3 auth', '3 noauth', '3 priv'] + vrf: + description: Specify the VRF in which the host is configured + type: str + acls: + description: ipv4/ipv6 access_lists + type: list + elements: dict + suboptions: + afi: + description: ipv4/ipv6 + type: str + choices: ['ipv4', 'ipv6'] + acl: + description: acl name + type: str + vrf: + description: vrf name + type: str + local_interface: + description: Configure the source interface for SNMP notifications. + type: str + location: + description: The sysLocation string. + type: str + notification: + description: Maximum number of notifications in the log + type: int + objects: + description: when True Disable implementation of a group of objects + type: dict + suboptions: + mac_address_tables: + description: dot1dTpFdbTable, dot1qTpFdbTable + type: bool + route_tables: + description: ipCidrRouteTable, ipCidrRouteNumber, aristaFIBStats* + type: bool + qos: + description: Configure QoS parameters. + type: int + qosmib: + description: Set QOS_MIB counter update interval + type: int + transmit: + description: Maximum number of bytes in SNMP message (UDP/TCP payload) + type: int + transport: + description: Enable snmpd transport layer protocol + type: str + users: + description: SNMP user configuration. + type: list + elements: dict + suboptions: + user: + description: SNMP user name + type: str + group: + description: SNMP group for the user. + type: str + remote: + description: System where an SNMPv3 user is hosted + type: str + udp_port: + description: UDP port used by the remote SNMP system + type: int + version: + description: snmp security version + type: str + choices: ['v1', 'v2c', 'v3'] + auth: + description: User authentication settings + type: dict + suboptions: + algorithm: + description: algorithm for authentication + type: str + auth_passphrase: + description: authentication passphrase hex string + type: str + encryption: + description: algorithm for encryption. + type: str + priv_passphrase: + description: privacy passphrase hexstring + type: str + localized: + description: localized auth and privacy passphrases. + type: dict + suboptions: + engineid: + description: Engine id + type: str + algorithm: + description: algorithm for authentication + type: str + auth_passphrase: + description: authentication passphrase hex string + type: str + encryption: + description: algorithm for encryption. + type: str + priv_passphrase: + description: privacy passphrase hexstring + type: str + views: + description: SNMPv2 MIB view configuration + type: list + elements: dict + suboptions: + view: + description: SNMP view name + type: str + mib: + description: SNMP MIB name + type: str + action: + description: Action to be performed. + type: str + choices: ['excluded', 'included'] + vrfs: + description: Specify the VRF in which the source address is used + type: list + elements: dict + suboptions: + vrf: + description: vrf name. + type: str + local_interface: + description: Configure the source interface for SNMP notifications + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running_config | section snmp_server). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + - The states I(replaced) and I(overridden) have identical + behaviour for this module. + - Please refer to examples for more details. + type: str + choices: [deleted, merged, overridden, replaced, gathered, rendered, parsed] + default: merged +""" +EXAMPLES = """ +# Using merged: +# Before State +# eos#show running-config | section snmp-server +# eos# + + - name: merge given snmp_server configuration + arista.eos.eos_snmp_server: + config: + communities: + - name: "comm3" + acl_v6: "list1" + view: "view1" + - name: "comm4" + acl_v4: "list3" + view: "view1" + - name: "comm5" + acl_v4: "list4" + ro: True + contact: "admin" + engineid: + remote: + host: 1.1.1.1 + id: "1234567" + groups: + - group: "group1" + version: "v1" + read: "view1" + - group: "group2" + version: "v3" + auth_privacy: "priv" + notify: "view1" + write: "view2" + hosts: + - host: "host02" + user: "user01" + udp_port: 23 + version: "2c" + - host: "host01" + user: "user01" + udp_port: 23 + version: "3 priv" + traps: + capacity: + arista_hardware_utilization_alert: True + bgp: + enabled: True + external_alarm: + arista_external_alarm_deasserted_notif: True + arista_external_alarm_asserted_notif: True + vrfs: + - vrf: "vrf01" + local_interface: "Ethernet1" + +# After state +# eos#show running-config | section snmp-server +# snmp-server community comm3 view view1 ipv6 list1 +# snmp-server community comm4 view view1 list3 +# snmp-server community comm5 ro list4 +# snmp-server group group1 v1 read view1 +# snmp-server group group2 v3 priv write view2 notify view1 +# snmp-server host host02 version 2c user01 udp-port 23 +# snmp-server host host01 version 3 priv user01 udp-port 23 +# snmp-server vrf vrf01 local-interface Ethernet1 +# snmp-server contact admin +# snmp-server enable traps bgp +# snmp-server enable traps capacity arista-hardware-utilization-alert +# snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif +# +# Module Execution +# +# "after": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list3", +# "name": "comm4", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "comm5", +# "ro": true +# } +# ], +# "contact": "admin", +# "groups": [ +# { +# "group": "group1", +# "read": "view1", +# "version": "v1" +# }, +# { +# "auth_privacy": "priv", +# "group": "group2", +# "notify": "view1", +# "version": "v3", +# "write": "view2" +# } +# ], +# "hosts": [ +# { +# "host": "host01", +# "udp_port": 23, +# "user": "user01", +# "version": "3 priv" +# }, +# { +# "host": "host02", +# "udp_port": 23, +# "user": "user01", +# "version": "2c" +# } +# ], +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "capacity": { +# "arista_hardware_utilization_alert": true +# }, +# "external_alarm": { +# "arista_external_alarm_asserted_notif": true, +# "arista_external_alarm_deasserted_notif": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf01" +# } +# ] +# }, +# "before": {}, +# "changed": true, +# "commands": [ +# "snmp-server community comm3 view view1 ipv6 list1", +# "snmp-server community comm4 view view1 list3", +# "snmp-server community comm5 ro list4", +# "snmp-server group group1 v1 read view1", +# "snmp-server group group2 v3 priv write view2 notify view1", +# "snmp-server host host02 version 2c user01 udp-port 23", +# "snmp-server host host01 version 3 priv user01 udp-port 23", +# "snmp-server vrf vrf01 local-interface Ethernet1", +# "snmp-server contact admin", +# "snmp-server engineID remote 1.1.1.1 1234567", +# "snmp-server enable traps bgp", +# "snmp-server enable traps capacity arista-hardware-utilization-alert", +# "snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif" +# ], +# + +# Using replaced: +# Before State: +# eos#show running-config | section snmp-server +# snmp-server community comm3 view view1 ipv6 list1 +# snmp-server community comm4 view view1 list3 +# snmp-server community comm5 ro list4 +# snmp-server group group1 v1 read view1 +# snmp-server group group2 v3 priv write view2 notify view1 +# snmp-server host host02 version 2c user01 udp-port 23 +# snmp-server host host01 version 3 priv user01 udp-port 23 +# snmp-server vrf vrf01 local-interface Ethernet1 +# snmp-server contact admin +# snmp-server enable traps bgp +# snmp-server enable traps capacity arista-hardware-utilization-alert +# snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif + + - name: Replace given snmp_server configuration + become: true + register: result + arista.eos.eos_snmp_server: &replaced + state: replaced + config: + communities: + - name: "comm3" + acl_v6: "list1" + view: "view1" + - name: "replacecomm" + acl_v4: "list4" + extension: + root_oid: "123456" + script_location: "flash:" + traps: + test: + arista_test_notification: True + bgp: + enabled: True + vrfs: + - vrf: "vrf_replace" + local_interface: "Ethernet1" + +# After State: + +# eos#show running-config | section snmp-server +# snmp-server community comm3 view view1 ipv6 list1 +# snmp-server community replacecomm list4 +# snmp-server vrf vrf_replace local-interface Ethernet1 +# snmp-server extension 123456 flash: +# snmp-server enable traps test arista-test-notification +# snmp-server enable traps bgp + +# Module Execution: +# "after": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "replacecomm", +# "ro": true +# } +# ], +# "extension": { +# "root_oid": "0.123456", +# "script_location": "flash:" +# }, +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "test": { +# "arista_test_notification": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf_replace" +# } +# ] +# }, +# "before": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list3", +# "name": "comm4", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "comm5", +# "ro": true +# } +# ], +# "contact": "admin", +# "groups": [ +# { +# "group": "group1", +# "read": "view1", +# "version": "v1" +# }, +# { +# "auth_privacy": "priv", +# "group": "group2", +# "notify": "view1", +# "version": "v3", +# "write": "view2" +# } +# ], +# "hosts": [ +# { +# "host": "host01", +# "udp_port": 23, +# "user": "user01", +# "version": "3 priv" +# }, +# { +# "host": "host02", +# "udp_port": 23, +# "user": "user01", +# "version": "2c" +# } +# ], +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "capacity": { +# "arista_hardware_utilization_alert": true +# }, +# "external_alarm": { +# "arista_external_alarm_asserted_notif": true, +# "arista_external_alarm_deasserted_notif": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf01" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "snmp-server community comm3 view view1 ipv6 list1", +# "snmp-server community replacecomm list4", +# "no snmp-server community comm4 view view1 ro list3", +# "no snmp-server community comm5 ro list4", +# "no snmp-server group group1 v1 read view1", +# "no snmp-server group group2 v3 priv write view2 notify view1", +# "no snmp-server host host01 version 3 priv user01 udp-port 23", +# "no snmp-server host host02 version 2c user01 udp-port 23", +# "snmp-server vrf vrf_replace local-interface Ethernet1", +# "no snmp-server vrf vrf01 local-interface Ethernet1", +# "snmp-server extension 123456 flash:", +# "default snmp-server enable traps capacity arista-hardware-utilization-alert", +# "default snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif", +# "snmp-server enable traps test arista-test-notification", +# "no snmp-server contact admin" +# ], + +# Using overridden: +# Before State: +# eos#show running-config | section snmp-server +# snmp-server community comm3 view view1 ipv6 list1 +# snmp-server community comm4 view view1 list3 +# snmp-server community comm5 ro list4 +# snmp-server group group1 v1 read view1 +# snmp-server group group2 v3 priv write view2 notify view1 +# snmp-server host host02 version 2c user01 udp-port 23 +# snmp-server host host01 version 3 priv user01 udp-port 23 +# snmp-server vrf vrf01 local-interface Ethernet1 +# snmp-server contact admin +# snmp-server enable traps bgp +# snmp-server enable traps capacity arista-hardware-utilization-alert +# snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif + + - name: Override given snmp_server configuration + arista.eos.eos_snmp_server: + state: overridden + config: + communities: + - name: "comm3" + acl_v6: "list1" + view: "view1" + - name: "replacecomm" + acl_v4: "list4" + extension: + root_oid: "123456" + script_location: "flash:" + traps: + test: + arista_test_notification: True + bgp: + enabled: True + vrfs: + - vrf: "vrf_replace" + local_interface: "Ethernet1" + +# After State: + +# eos#show running-config | section snmp-server +# snmp-server community comm3 view view1 ipv6 list1 +# snmp-server community replacecomm list4 +# snmp-server vrf vrf_replace local-interface Ethernet1 +# snmp-server extension 123456 flash: +# snmp-server enable traps test arista-test-notification +# snmp-server enable traps bgp + +# Module Execution: +# "after": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "replacecomm", +# "ro": true +# } +# ], +# "extension": { +# "root_oid": "0.123456", +# "script_location": "flash:" +# }, +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "test": { +# "arista_test_notification": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf_replace" +# } +# ] +# }, +# "before": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list3", +# "name": "comm4", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "comm5", +# "ro": true +# } +# ], +# "contact": "admin", +# "groups": [ +# { +# "group": "group1", +# "read": "view1", +# "version": "v1" +# }, +# { +# "auth_privacy": "priv", +# "group": "group2", +# "notify": "view1", +# "version": "v3", +# "write": "view2" +# } +# ], +# "hosts": [ +# { +# "host": "host01", +# "udp_port": 23, +# "user": "user01", +# "version": "3 priv" +# }, +# { +# "host": "host02", +# "udp_port": 23, +# "user": "user01", +# "version": "2c" +# } +# ], +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "capacity": { +# "arista_hardware_utilization_alert": true +# }, +# "external_alarm": { +# "arista_external_alarm_asserted_notif": true, +# "arista_external_alarm_deasserted_notif": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf01" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "snmp-server community comm3 view view1 ipv6 list1", +# "snmp-server community replacecomm list4", +# "no snmp-server community comm4 view view1 ro list3", +# "no snmp-server community comm5 ro list4", +# "no snmp-server group group1 v1 read view1", +# "no snmp-server group group2 v3 priv write view2 notify view1", +# "no snmp-server host host01 version 3 priv user01 udp-port 23", +# "no snmp-server host host02 version 2c user01 udp-port 23", +# "snmp-server vrf vrf_replace local-interface Ethernet1", +# "no snmp-server vrf vrf01 local-interface Ethernet1", +# "snmp-server extension 123456 flash:", +# "default snmp-server enable traps capacity arista-hardware-utilization-alert", +# "default snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif", +# "snmp-server enable traps test arista-test-notification", +# "no snmp-server contact admin" +# ], + +# Using deleted: +# Before State: +# eos#show running-config | section snmp-server +# snmp-server community comm3 view view1 ipv6 list1 +# snmp-server community comm4 view view1 list3 +# snmp-server community comm5 ro list4 +# snmp-server group group1 v1 read view1 +# snmp-server group group2 v3 priv write view2 notify view1 +# snmp-server host host02 version 2c user01 udp-port 23 +# snmp-server host host01 version 3 priv user01 udp-port 23 +# snmp-server vrf vrf01 local-interface Ethernet1 +# snmp-server contact admin +# snmp-server enable traps bgp +# snmp-server enable traps capacity arista-hardware-utilization-alert +# snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif + + - name: Delete given snmp_server configuration + arista.eos.eos_snmp_server: + state: deleted + +# After State: +# eos#show running-config | section snmp-server +# + +# Module Execution: +# "after": {}, +# "before": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list3", +# "name": "comm4", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "comm5", +# "ro": true +# } +# ], +# "contact": "admin", +# "groups": [ +# { +# "group": "group1", +# "read": "view1", +# "version": "v1" +# }, +# { +# "auth_privacy": "priv", +# "group": "group2", +# "notify": "view1", +# "version": "v3", +# "write": "view2" +# } +# ], +# "hosts": [ +# { +# "host": "host01", +# "udp_port": 23, +# "user": "user01", +# "version": "3 priv" +# }, +# { +# "host": "host02", +# "udp_port": 23, +# "user": "user01", +# "version": "2c" +# } +# ], +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "capacity": { +# "arista_hardware_utilization_alert": true +# }, +# "external_alarm": { +# "arista_external_alarm_asserted_notif": true, +# "arista_external_alarm_deasserted_notif": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf01" +# } +# ] +# }, +# "changed": true, +# "commands": [ +# "no snmp-server community comm3 view view1 ro ipv6 list1", +# "no snmp-server community comm4 view view1 ro list3", +# "no snmp-server community comm5 ro list4", +# "no snmp-server group group1 v1 read view1", +# "no snmp-server group group2 v3 priv write view2 notify view1", +# "no snmp-server host host01 version 3 priv user01 udp-port 23", +# "no snmp-server host host02 version 2c user01 udp-port 23", +# "no snmp-server vrf vrf01 local-interface Ethernet1", +# "no snmp-server contact admin", +# "default snmp-server enable traps bgp", +# "default snmp-server enable traps capacity arista-hardware-utilization-alert", +# "default snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif" +# ], +# + +# Using parsed: + +# _parsed.cfg +# snmp-server contact admin +# snmp-server vrf vrf01 local-interface Ethernet1 +# snmp-server community comm3 view view1 ro ipv6 list1 +# snmp-server community comm4 view view1 ro list3 +# snmp-server community comm5 ro list4 +# snmp-server group group1 v1 read view1 +# snmp-server group group2 v3 priv write view2 notify view1 +# snmp-server host host01 version 3 priv user01 udp-port 23 +# snmp-server host host02 version 2c user01 udp-port 23 +# snmp-server enable traps bgp +# snmp-server enable traps capacity arista-hardware-utilization-alert +# snmp-server enable traps external-alarm arista-external-alarm-asserted-notif +# snmp-server enable traps external-alarm arista-external-alarm-deasserted-notif + + - name: Provide the running configuration for parsing (config to be parsed) + arista.eos.eos_snmp_server: + running_config: "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + +# Module Execution: +# "parsed": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list3", +# "name": "comm4", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "comm5", +# "ro": true +# } +# ], +# "contact": "admin", +# "groups": [ +# { +# "group": "group1", +# "read": "view1", +# "version": "v1" +# }, +# { +# "auth_privacy": "priv", +# "group": "group2", +# "notify": "view1", +# "version": "v3", +# "write": "view2" +# } +# ], +# "hosts": [ +# { +# "host": "host01", +# "udp_port": 23, +# "user": "user01", +# "version": "3 priv" +# }, +# { +# "host": "host02", +# "udp_port": 23, +# "user": "user01", +# "version": "2c" +# } +# ], +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "capacity": { +# "arista_hardware_utilization_alert": true +# }, +# "external_alarm": { +# "arista_external_alarm_asserted_notif": true, +# "arista_external_alarm_deasserted_notif": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf01" +# } +# ] +# } + +# Using rendered: + - name: Render given snmp_server configuration + arista.eos.eos_snmp_server: + state: "rendered" + config: + communities: + - name: "comm3" + acl_v6: "list1" + view: "view1" + - name: "comm4" + acl_v4: "list3" + view: "view1" + - name: "comm5" + acl_v4: "list4" + ro: True + contact: "admin" + engineid: + remote: + host: 1.1.1.1 + id: "1234567" + groups: + - group: "group1" + version: "v1" + read: "view1" + - group: "group2" + version: "v3" + auth_privacy: "priv" + notify: "view1" + write: "view2" + hosts: + - host: "host02" + user: "user01" + udp_port: 23 + version: "2c" + - host: "host01" + user: "user01" + udp_port: 23 + version: "3 priv" + traps: + capacity: + arista_hardware_utilization_alert: True + bgp: + enabled: True + external_alarm: + arista_external_alarm_deasserted_notif: True + arista_external_alarm_asserted_notif: True + vrfs: + - vrf: "vrf01" + local_interface: "Ethernet1" + +# Module Execution: +# "rendered": [ +# "snmp-server community comm3 view view1 ipv6 list1", +# "snmp-server community comm4 view view1 list3", +# "snmp-server community comm5 ro list4", +# "snmp-server group group1 v1 read view1", +# "snmp-server group group2 v3 priv write view2 notify view1", +# "snmp-server host host02 version 2c user01 udp-port 23", +# "snmp-server host host01 version 3 priv user01 udp-port 23", +# "snmp-server vrf vrf01 local-interface Ethernet1", +# "snmp-server contact admin", +# "snmp-server engineID remote 1.1.1.1 1234567", +# "snmp-server enable traps bgp", +# "snmp-server enable traps capacity arista-hardware-utilization-alert", +# "snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif" +# ] + +# using gathered: + +# eos#show running-config | section snmp-server +# snmp-server community comm3 view view1 ipv6 list1 +# snmp-server community comm4 view view1 list3 +# snmp-server community comm5 ro list4 +# snmp-server group group1 v1 read view1 +# snmp-server group group2 v3 priv write view2 notify view1 +# snmp-server host host02 version 2c user01 udp-port 23 +# snmp-server host host01 version 3 priv user01 udp-port 23 +# snmp-server vrf vrf01 local-interface Ethernet1 +# snmp-server contact admin +# snmp-server enable traps bgp +# snmp-server enable traps capacity arista-hardware-utilization-alert +# snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif + + - name: Gathered the provided configuration with the exisiting running configuration + arista.eos.eos_snmp_server: + config: + state: gathered + +# Module Execution: +# "gathered": { +# "communities": [ +# { +# "acl_v6": "list1", +# "name": "comm3", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list3", +# "name": "comm4", +# "ro": true, +# "view": "view1" +# }, +# { +# "acl_v4": "list4", +# "name": "comm5", +# "ro": true +# } +# ], +# "contact": "admin", +# "groups": [ +# { +# "group": "group1", +# "read": "view1", +# "version": "v1" +# }, +# { +# "auth_privacy": "priv", +# "group": "group2", +# "notify": "view1", +# "version": "v3", +# "write": "view2" +# } +# ], +# "hosts": [ +# { +# "host": "host01", +# "udp_port": 23, +# "user": "user01", +# "version": "3 priv" +# }, +# { +# "host": "host02", +# "udp_port": 23, +# "user": "user01", +# "version": "2c" +# } +# ], +# "traps": { +# "bgp": { +# "enabled": true +# }, +# "capacity": { +# "arista_hardware_utilization_alert": true +# }, +# "external_alarm": { +# "arista_external_alarm_asserted_notif": true, +# "arista_external_alarm_deasserted_notif": true +# } +# }, +# "vrfs": [ +# { +# "local_interface": "Ethernet1", +# "vrf": "vrf01" +# } +# ] +# }, + +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - "snmp-server community comm3 view view1 ipv6 list1" + - "snmp-server community comm4 view view1 list3" + - "snmp-server community comm5 ro list4" + - "snmp-server group group1 v1 read view1" + - "snmp-server group group2 v3 priv write view2 notify view1" + - "snmp-server host host02 version 2c user01 udp-port 23" + - "snmp-server host host01 version 3 priv user01 udp-port 23" + - "snmp-server vrf vrf01 local-interface Ethernet1" + - "snmp-server contact admin" + - "snmp-server engineID remote 1.1.1.1 1234567" + - "snmp-server enable traps bgp" + - "snmp-server enable traps capacity arista-hardware-utilization-alert" + - "snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif" + +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - "snmp-server community comm3 view view1 ipv6 list1" + - "snmp-server community comm4 view view1 list3" + - "snmp-server community comm5 ro list4" + - "snmp-server group group1 v1 read view1" + - "snmp-server group group2 v3 priv write view2 notify view1" + - "snmp-server host host02 version 2c user01 udp-port 23" + - "snmp-server host host01 version 3 priv user01 udp-port 23" + - "snmp-server vrf vrf01 local-interface Ethernet1" + - "snmp-server contact admin" + - "snmp-server engineID remote 1.1.1.1 1234567" + - "snmp-server enable traps bgp" + - "snmp-server enable traps capacity arista-hardware-utilization-alert" + - "snmp-server enable traps external-alarm arista-external-alarm-asserted-notif arista-external-alarm-deasserted-notif" +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.snmp_server.snmp_server import ( + Snmp_serverArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.snmp_server.snmp_server import ( + Snmp_server, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Snmp_serverArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Snmp_server(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_static_routes.py b/ansible_collections/arista/eos/plugins/modules/eos_static_routes.py new file mode 100644 index 000000000..5b291b12d --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_static_routes.py @@ -0,0 +1,969 @@ +#!/usr/bin/python +# -*- 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 module file for eos_static_routes +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_static_routes +short_description: Static routes resource module +description: This module configures and manages the attributes of static routes on + Arista EOS platforms. +version_added: 1.0.0 +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: + - A list of configurations for static routes. + type: list + elements: dict + suboptions: + vrf: + description: + - The VRF to which the static route(s) belong. + type: str + address_families: + description: A dictionary specifying the address family to which the static + route(s) belong. + type: list + elements: dict + suboptions: + afi: + description: + - Specifies the top level address family indicator. + type: str + choices: + - ipv4 + - ipv6 + required: true + routes: + description: A dictionary that specifies the static route configurations. + elements: dict + type: list + suboptions: + dest: + description: + - Destination IPv4 subnet (CIDR or address-mask notation). + - The address format is <v4/v6 address>/<mask> or <v4/v6 address> + <mask>. + - The mask is number in range 0-32 for IPv4 and in range 0-128 for + IPv6. + type: str + required: true + next_hops: + description: + - Details of route to be taken. + type: list + elements: dict + suboptions: + forward_router_address: + description: + - Forwarding router's address on destination interface. + type: str + interface: + description: + - Outgoing interface to take. For anything except 'null0', then + next hop IP address should also be configured. + - IP address of the next hop router or + - null0 Null0 interface or + - ethernet e_num Ethernet interface or + - loopback l_num Loopback interface or + - management m_num Management interface or + - port-channel p_num + - vlan v_num + - vxlan vx_num + - Nexthop-Group Specify nexthop group name + - Tunnel Tunnel interface + - vtep Configure VXLAN Tunnel End Points + type: str + nexthop_grp: + description: + - Nexthop group + type: str + admin_distance: + description: + - Preference or administrative distance of route (range 1-255). + type: int + description: + description: + - Name of the static route. + type: str + tag: + description: + - Route tag value (ranges from 0 to 4294967295). + type: int + track: + description: + - Track value (range 1 - 512). Track must already be configured + on the device before adding the route. + type: str + mpls_label: + description: + - MPLS label + type: int + vrf: + description: + - VRF of the destination. + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device by + executing the command B(show running-config | grep routes). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using deleted + +# Before State: +# ------------ + +# veos(config)#show running-config | grep route +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 +# veos(config)# + +- name: Delete afi + arista.eos.eos_static_routes: + config: + - vrf: testvrf + address_families: + - afi: ipv4 + state: deleted + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "before": [ +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "changed": true, +# "commands": [ +# "no ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute" +# ], + +# After State +# ___________ + +# veos(config)#show running-config | grep route +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 + +# +# Using merged + +# Before : [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "165.10.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 100, +# "interface": "Ethernet1" +# } +# ] +# }, +# { +# "dest": "172.17.252.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgroup" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5001::/64", +# "next_hops": [ +# { +# "admin_distance": 50, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "130.1.122.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1", +# "tag": 50 +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] +# +# Before State +# ------------- +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 100 +# ip route 172.17.252.0/24 Nexthop-Group testgroup +# ip route vrf testvrf 130.1.122.0/24 Ethernet1 tag 50 +# ipv6 route 5001::/64 Ethernet1 50 +# veos(config)# + +- name: Merge new static route configuration + arista.eos.eos_static_routes: + config: + - vrf: testvrf + address_families: + - afi: ipv6 + routes: + - dest: 2211::0/64 + next_hop: + - forward_router_address: 100:1::2 + interface: Ethernet1 + state: merged + +# After State +# ----------- + +#After [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "165.10.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 100, +# "interface": "Ethernet1" +# } +# ] +# }, +# { +# "dest": "172.17.252.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgroup" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5001::/64", +# "next_hops": [ +# { +# "admin_distance": 50, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "130.1.122.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1", +# "tag": 50 +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2211::0/64", +# "next_hops": [ +# { +# "aforward_router_address": 100:1::2 +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } + +# ], +# "vrf": "testvrf" +# } +# ] +# +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 100 +# ip route 172.17.252.0/24 Nexthop-Group testgroup +# ip route vrf testvrf 130.1.122.0/24 Ethernet1 tag 50 +# ipv6 route 2211::/64 Ethernet1 100:1::2 +# ipv6 route 5001::/64 Ethernet1 50 +# veos(config)# + + +# Using overridden + + +# Before State +# ------------- + +# "before": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "165.10.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 100, +# "interface": "Ethernet1" +# } +# ] +# }, +# { +# "dest": "172.17.252.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgroup" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5001::/64", +# "next_hops": [ +# { +# "admin_distance": 50, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "130.1.122.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1", +# "tag": 50 +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 100 +# ip route 172.17.252.0/24 Nexthop-Group testgroup +# ip route vrf testvrf 130.1.122.0/24 Ethernet1 tag 50 +# ipv6 route 5001::/64 Ethernet1 50 +# veos(config)# + +- name: Overridden static route configuration + arista.eos.eos_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.2.2.0/24 + next_hop: + - interface: Ethernet1 + state: replaced + +# After State +# ----------- + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# } +# ] +# veos(config)#show running-config | grep "route" +# ip route 10.2.2.0/24 Ethernet1 +# veos(config)# + + +# Using replaced + +# Before State +# ------------- + +# ip route 10.2.2.0/24 Ethernet1 +# ip route 10.2.2.0/24 64.1.1.1 label 17 33 +# ip route 33.33.33.0/24 Nexthop-Group testgrp +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 + +# [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 33, +# "interface": "64.1.1.1", +# "mpls_label": 17 +# } +# ] +# }, +# { +# "dest": "33.33.33.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgrp" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] + +- name: Replace nexthop + arista.eos.eos_static_routes: + config: + - vrf: testvrf + address_families: + - afi: ipv6 + routes: + - dest: 2222:6::/64 + next_hops: + - admin_distance: 55 + interface: Ethernet1 + state: replaced + +# After State +# ----------- + +# veos(config)#show running-config | grep route +# ip route 10.2.2.0/24 Ethernet1 +# ip route 10.2.2.0/24 64.1.1.1 label 17 33 +# ip route 33.33.33.0/24 Nexthop-Group testgrp +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 33, +# "interface": "64.1.1.1", +# "mpls_label": 17 +# } +# ] +# }, +# { +# "dest": "33.33.33.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgrp" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] + +# Before State +# ------------- +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 10.1.1.2 100 +# ipv6 route 5001::/64 Ethernet1 +# veos(config)# + + +- name: Gather the exisitng condiguration + arista.eos.eos_static_routes: + state: gathered + +# returns : +# arista.eos.eos_static_routes: +# config: +# - address_families: +# - afi: ipv4 +# routes: +# - dest: 165.10.1.0/24 +# next_hop: +# - forward_router_address: 10.1.1.2 +# interface: "Ethernet1" +# admin_distance: 100 +# - afi: ipv6 +# routes: +# - dest: 5001::/64 +# next_hop: +# - interface: "Ethernet1" + + +# Using rendered + +# arista.eos.eos_static_routes: +# config: +# - address_families: +# - afi: ipv4 +# routes: +# - dest: 165.10.1.0/24 +# next_hop: +# - forward_router_address: 10.1.1.2 +# interface: "Ethernet1" +# admin_distance: 100 +# - afi: ipv6 +# routes: +# - dest: 5001::/64 +# next_hop: +# - interface: "Ethernet1" + +# returns: + +# ip route 165.10.1.0/24 Ethernet1 10.1.1.2 100 +# ipv6 route 5001::/64 Ethernet1 + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - ip route vrf vrf1 192.2.2.0/24 125.2.3.1 93 +rendered: + description: The set of CLI commands generated from the value in C(config) option + returned: When C(state) is I(rendered) + type: list + sample: > + "address_families": [ + { + "afi": "ipv4", + "routes": [ + { + "dest": "192.2.2.0/24", + "next_hops": [ + { + "admin_distance": 93, + "description": null, + "forward_router_address": null, + "interface": "125.2.3.1", + "mpls_label": null, + "nexthop_grp": null, + "tag": null, + "track": null, + "vrf": null + } + ] + } + ] + } + ], + "vrf": "vrf1" + } + ], + "running_config": null, + "state": "rendered" + } +gathered: + description: The configuration as structured data transformed for the running configuration + fetched from remote host + returned: When C(state) is I(gathered) + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +parsed: + description: The configuration as structured data transformed for the value of + C(running_config) option + returned: When C(state) is I(parsed) + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.static_routes.static_routes import ( + Static_routesArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.static_routes.static_routes import ( + Static_routes, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=Static_routesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Static_routes(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_system.py b/ansible_collections/arista/eos/plugins/modules/eos_system.py new file mode 100644 index 000000000..9fb1aca9f --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_system.py @@ -0,0 +1,378 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_system +author: Peter Sprygada (@privateip) +short_description: Manage the system attributes on Arista EOS devices +description: +- This module provides declarative management of node system attributes on Arista + EOS devices. It provides an option to configure host system parameters or remove + those parameters from the device active configuration. +version_added: 1.0.0 +notes: +- Tested against Arista EOS 4.24.6F +options: + hostname: + description: + - Configure the device hostname parameter. This option takes an ASCII string value. + type: str + domain_name: + description: + - Configure the IP domain name on the remote device to the provided value. Value + should be in the dotted name form and will be appended to the C(hostname) to + create a fully-qualified domain name. + type: str + domain_list: + description: + - Provides the list of domain suffixes to append to the hostname for the purpose + of doing name resolution. This argument accepts a list of names and will be + reconciled with the current active configuration on the running node. + aliases: + - domain_search + type: list + elements: str + lookup_source: + description: + - Provides one or more source interfaces to use for performing DNS lookups. The + interface provided in C(lookup_source) can only exist in a single VRF. This + argument accepts either a list of interface names or a list of hashes that configure + the interface name and VRF name. See examples. + elements: raw + type: list + name_servers: + description: + - List of DNS name servers by IP address to use to perform name resolution lookups. This + argument accepts either a list of DNS servers or a list of hashes that configure + the name server and VRF name. See examples. + type: list + elements: str + state: + description: + - State of the configuration values in the device's current active configuration. When + set to I(present), the values should be configured in the device active configuration + and when set to I(absent) the values should not be in the device active configuration + default: present + type: str + choices: + - present + - absent +""" + +EXAMPLES = """ +- name: configure hostname and domain-name + arista.eos.eos_system: + hostname: eos01 + domain_name: test.example.com + +- name: remove configuration + arista.eos.eos_system: + state: absent + +- name: configure DNS lookup sources + arista.eos.eos_system: + lookup_source: Management1 + +- name: configure DNS lookup sources with VRF support + arista.eos.eos_system: + lookup_source: + - interface: Management1 + vrf: mgmt + - interface: Ethernet1 + vrf: myvrf + +- name: configure name servers + arista.eos.eos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + +- name: configure name servers with VRF support + arista.eos.eos_system: + name_servers: + - {server: 8.8.8.8, vrf: mgmt} + - {server: 8.8.4.4, vrf: mgmt} +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - hostname eos01 + - dns domain test.example.com +session_name: + description: The EOS config session name used to load the configuration + returned: changed + type: str + sample: ansible_1479315771 +""" +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + ComplexList, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + get_config, + load_config, +) + + +_CONFIGURED_VRFS = None + + +def has_vrf(module, vrf): + global _CONFIGURED_VRFS + if _CONFIGURED_VRFS is not None: + return vrf in _CONFIGURED_VRFS + config = get_config(module) + _CONFIGURED_VRFS = re.findall(r"vrf instance (\S+)", config) + _CONFIGURED_VRFS.append("default") + return vrf in _CONFIGURED_VRFS + + +def map_obj_to_commands(want, have, module): + commands = list() + state = module.params["state"] + + def needs_update(x): + return want.get(x) and (want.get(x) != have.get(x)) + + if state == "absent": + if have["domain_name"]: + commands.append("no dns domain") + + if have["hostname"] != "localhost": + commands.append("no hostname") + + if state == "present": + if needs_update("hostname"): + commands.append("hostname %s" % want["hostname"]) + + if needs_update("domain_name"): + commands.append("dns domain %s" % want["domain_name"]) + + if want["domain_list"]: + # handle domain_list items to be removed + for item in set(have["domain_list"]).difference( + want["domain_list"], + ): + commands.append("no ip domain-list %s" % item) + + # handle domain_list items to be added + for item in set(want["domain_list"]).difference( + have["domain_list"], + ): + commands.append("ip domain-list %s" % item) + + if want["lookup_source"]: + # handle lookup_source items to be removed + for item in have["lookup_source"]: + if item not in want["lookup_source"]: + if item["vrf"]: + if not has_vrf(module, item["vrf"]): + module.fail_json( + msg="vrf %s is not configured" % item["vrf"], + ) + values = (item["vrf"], item["interface"]) + commands.append( + "no ip domain lookup vrf %s source-interface %s" + % values, + ) + else: + commands.append( + "no ip domain lookup source-interface %s" + % item["interface"], + ) + + # handle lookup_source items to be added + for item in want["lookup_source"]: + if item not in have["lookup_source"]: + if item["vrf"]: + if not has_vrf(module, item["vrf"]): + module.fail_json( + msg="vrf %s is not configured" % item["vrf"], + ) + values = (item["vrf"], item["interface"]) + commands.append( + "ip domain lookup vrf %s source-interface %s" + % values, + ) + else: + commands.append( + "ip domain lookup source-interface %s" + % item["interface"], + ) + + if want["name_servers"]: + # handle name_servers items to be removed. Order does matter here + # since name servers can only be in one vrf at a time + for item in have["name_servers"]: + if item not in want["name_servers"]: + if not has_vrf(module, item["vrf"]): + module.fail_json( + msg="vrf %s is not configured" % item["vrf"], + ) + if item["vrf"] not in ("default", None): + values = (item["vrf"], item["server"]) + commands.append("no ip name-server vrf %s %s" % values) + else: + commands.append( + "no ip name-server %s" % item["server"], + ) + + # handle name_servers items to be added + for item in want["name_servers"]: + if item not in have["name_servers"]: + if not has_vrf(module, item["vrf"]): + module.fail_json( + msg="vrf %s is not configured" % item["vrf"], + ) + if item["vrf"] not in ("default", None): + values = (item["vrf"], item["server"]) + commands.append("ip name-server vrf %s %s" % values) + else: + commands.append("ip name-server %s" % item["server"]) + + return commands + + +def parse_hostname(config): + match = re.search(r"^hostname (\S+)", config, re.M) + if match: + return match.group(1) + + +def parse_domain_name(config): + match = re.search(r"^dns domain (\S+)", config, re.M) + if match: + return match.group(1) + + +def parse_lookup_source(config): + objects = list() + regex = r"ip domain lookup (?:vrf (\S+) )*source-interface (\S+)" + for vrf, intf in re.findall(regex, config, re.M): + if len(vrf) == 0: + vrf = None + objects.append({"interface": intf, "vrf": vrf}) + return objects + + +def parse_name_servers(config): + objects = list() + for vrf, addr in re.findall( + r"ip name-server vrf (\S+) (\S+)", + config, + re.M, + ): + objects.append({"server": addr, "vrf": vrf}) + return objects + + +def map_config_to_obj(module): + config = get_config(module) + return { + "hostname": parse_hostname(config), + "domain_name": parse_domain_name(config), + "domain_list": re.findall(r"^ip domain-list (\S+)", config, re.M), + "lookup_source": parse_lookup_source(config), + "name_servers": parse_name_servers(config), + } + + +def map_params_to_obj(module): + obj = { + "hostname": module.params["hostname"], + "domain_name": module.params["domain_name"], + "domain_list": module.params["domain_list"], + } + + lookup_source = ComplexList( + dict(interface=dict(key=True), vrf=dict()), + module, + ) + + name_servers = ComplexList( + dict(server=dict(key=True), vrf=dict(default="default")), + module, + ) + + for arg, cast in [ + ("lookup_source", lookup_source), + ("name_servers", name_servers), + ]: + if module.params[arg] is not None: + obj[arg] = cast(module.params[arg]) + else: + obj[arg] = None + + return obj + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + hostname=dict(), + domain_name=dict(), + domain_list=dict( + type="list", + aliases=["domain_search"], + elements="str", + ), + # { interface: <str>, vrf: <str> } + lookup_source=dict(type="list", elements="raw"), + # { server: <str>; vrf: <str> } + name_servers=dict(type="list", elements="str"), + state=dict(default="present", choices=["present", "absent"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + result = {"changed": False} + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(want, have, module) + result["commands"] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get("diff") and module._diff: + result["diff"] = {"prepared": response.get("diff")} + result["session_name"] = response.get("session") + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_user.py b/ansible_collections/arista/eos/plugins/modules/eos_user.py new file mode 100644 index 000000000..519892daf --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_user.py @@ -0,0 +1,491 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_user +author: Peter Sprygada (@privateip) +short_description: Manage the collection of local users on EOS devices +description: +- This module provides declarative management of the local usernames configured on + Arista EOS devices. It allows playbooks to manage either individual usernames or + the collection of usernames in the current running config. It also supports purging + usernames from the configuration that are not explicitly defined. +version_added: 1.0.0 +notes: +- Tested against Arista EOS 4.24.6F +options: + aggregate: + description: + - The set of username objects to be configured on the remote Arista EOS device. The + list entries can either be the username or a hash of username and properties. This + argument is mutually exclusive with the C(username) argument. + aliases: + - users + - collection + type: list + elements: dict + suboptions: + name: + description: + - The username to be configured on the remote Arista EOS device. This argument + accepts a stringv value and is mutually exclusive with the C(aggregate) argument. + type: str + configured_password: + description: + - The password to be configured on the remote Arista EOS device. The password + needs to be provided in clear and it will be encrypted on the device. + type: str + update_password: + description: + - Since passwords are encrypted in the device running config, this argument will + instruct the module when to change the password. When set to C(always), the + password will always be updated in the device and when set to C(on_create) the + password will be updated only if the username is created. + type: str + choices: + - on_create + - always + privilege: + description: + - The C(privilege) argument configures the privilege level of the user when logged + into the system. This argument accepts integer values in the range of 1 to + 15. + type: int + role: + description: + - Configures the role for the username in the device running configuration. The + argument accepts a string value defining the role name. This argument does + not check if the role has been configured on the device. + type: str + sshkey: + description: + - Specifies the SSH public key to configure for the given username. This argument + accepts a valid SSH key value. + type: str + nopassword: + description: + - Defines the username without assigning a password. This will allow the user + to login to the system without being authenticated by a password. + type: bool + state: + description: + - Configures the state of the username definition as it relates to the device + operational configuration. When set to I(present), the username(s) should be + configured in the device active configuration and when set to I(absent) the + username(s) should not be in the device active configuration + type: str + choices: + - present + - absent + name: + description: + - The username to be configured on the remote Arista EOS device. This argument + accepts a stringv value and is mutually exclusive with the C(aggregate) argument. + type: str + configured_password: + description: + - The password to be configured on the remote Arista EOS device. The password + needs to be provided in clear and it will be encrypted on the device. + type: str + update_password: + description: + - Since passwords are encrypted in the device running config, this argument will + instruct the module when to change the password. When set to C(always), the + password will always be updated in the device and when set to C(on_create) the + password will be updated only if the username is created. + default: always + type: str + choices: + - on_create + - always + privilege: + description: + - The C(privilege) argument configures the privilege level of the user when logged + into the system. This argument accepts integer values in the range of 1 to + 15. + type: int + role: + description: + - Configures the role for the username in the device running configuration. The + argument accepts a string value defining the role name. This argument does + not check if the role has been configured on the device. + type: str + sshkey: + description: + - Specifies the SSH public key to configure for the given username. This argument + accepts a valid SSH key value. + type: str + nopassword: + description: + - Defines the username without assigning a password. This will allow the user + to login to the system without being authenticated by a password. + type: bool + purge: + description: + - Instructs the module to consider the resource definition absolute. It will + remove any previously configured usernames on the device with the exception + of the `admin` user which cannot be deleted per EOS constraints. + type: bool + default: false + state: + description: + - Configures the state of the username definition as it relates to the device + operational configuration. When set to I(present), the username(s) should be + configured in the device active configuration and when set to I(absent) the + username(s) should not be in the device active configuration + type: str + default: present + choices: + - present + - absent +""" + +EXAMPLES = """ +- name: create a new user + arista.eos.eos_user: + name: ansible + sshkey: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + state: present + +- name: remove all users except admin + arista.eos.eos_user: + purge: yes + +- name: set multiple users to privilege level 15 + arista.eos.eos_user: + aggregate: + - name: netop + - name: netend + privilege: 15 + state: present + +- name: Change Password for User netop + arista.eos.eos_user: + username: netop + configured_password: '{{ new_password }}' + update_password: always + state: present +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - name ansible secret password + - name admin secret admin +session_name: + description: The EOS config session name used to load the configuration + returned: when changed is True + type: str + sample: ansible_1479315771 +""" + +import re + +from copy import deepcopy +from functools import partial + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_default_spec, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + get_config, + load_config, + run_commands, +) + + +def validate_privilege(value, module): + if not 1 <= value <= 15: + module.fail_json( + msg="privilege must be between 1 and 15, got %s" % value, + ) + + +def get_os_version(module): + os_version = "4.20.10" + response = run_commands( + module, + 'show version | grep "Software image version"', + ) + version_match = re.search( + r"Software image version:\s+([\d\.]+)", + response[0], + re.M, + ) + if version_match: + v = version_match.group(1).split(".") + os_version = tuple(int(digit) for digit in v) + return os_version + + +def map_obj_to_commands(updates, module): + commands = list() + update_password = module.params["update_password"] + + for update in updates: + want, have = update + + def needs_update(x): + return want.get(x) and (want.get(x) != have.get(x)) + + def add(x): + return commands.append("username %s %s" % (want["name"], x)) + + if want["state"] == "absent": + commands.append("no username %s" % want["name"]) + continue + + if needs_update("configured_password"): + if update_password == "always" or not have: + add("secret %s" % want["configured_password"]) + + if needs_update("role"): + add("role %s" % want["role"]) + + if needs_update("privilege"): + add("privilege %s" % want["privilege"]) + + if needs_update("sshkey"): + ver = get_os_version(module) + # compare against image version 4.20.10 + if ver > (4, 20, 10): + add("ssh-key %s" % want["sshkey"]) + else: + add("sshkey %s" % want["sshkey"]) + + if needs_update("nopassword"): + if want["nopassword"]: + add("nopassword") + else: + add("no username %s nopassword" % want["name"]) + + if want.get("state") == "present" and want.get("name"): + value = [ + want.get("configured_password"), + want.get("nopassword"), + want.get("sshkey"), + ] + if all(v is None for v in value) is True: + module.fail_json( + msg="configured_password, sshkey or nopassword should be provided", + ) + return commands + + +def parse_role(data): + match = re.search(r"role (\S+)", data, re.M) + if match: + return match.group(1) + + +def parse_sshkey(data): + match = re.search(r"sshkey|ssh-key (.+)$", data, re.M) + if match: + return match.group(1) + + +def parse_privilege(data): + match = re.search(r"privilege (\S+)", data, re.M) + if match: + return int(match.group(1)) + + +def map_config_to_obj(module): + data = get_config(module, flags=["section username"]) + + match = re.findall(r"^username (\S+)", data, re.M) + if not match: + return list() + + instances = list() + + for user in set(match): + regex = r"username %s .+$" % user + cfg = re.findall(regex, data, re.M) + cfg = "\n".join(cfg) + obj = { + "name": user, + "state": "present", + "nopassword": "nopassword" in cfg, + "configured_password": None, + "sshkey": parse_sshkey(cfg), + "privilege": parse_privilege(cfg), + "role": parse_role(cfg), + } + instances.append(obj) + + return instances + + +def get_param_value(key, item, module): + # if key doesn't exist in the item, get it from module.params + if not item.get(key): + value = module.params[key] + + # if key does exist, do a type check on it to validate it + else: + value_type = module.argument_spec[key].get("type", "str") + type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type] + type_checker(item[key]) + value = item[key] + + # validate the param value (if validator func exists) + validator = globals().get("validate_%s" % key) + if all((value, validator)): + validator(value, module) + + return value + + +def map_params_to_obj(module): + aggregate = module.params["aggregate"] + if not aggregate: + if not module.params["name"] and module.params["purge"]: + return list() + elif not module.params["name"]: + module.fail_json(msg="name is required") + else: + collection = [{"name": module.params["name"]}] + else: + collection = list() + for item in aggregate: + if not isinstance(item, dict): + collection.append({"name": item}) + elif "name" not in item: + module.fail_json(msg="name is required") + else: + collection.append(item) + + objects = list() + + for item in collection: + get_value = partial(get_param_value, item=item, module=module) + item["configured_password"] = get_value("configured_password") + item["nopassword"] = get_value("nopassword") + item["privilege"] = get_value("privilege") + item["role"] = get_value("role") + item["sshkey"] = get_value("sshkey") + item["state"] = get_value("state") + objects.append(item) + + return objects + + +def update_objects(want, have): + updates = list() + for entry in want: + if "name" in entry: + item = next((i for i in have if i["name"] == entry["name"]), None) + if all((item is None, entry["state"] == "present")): + updates.append((entry, {})) + elif item: + for key, value in iteritems(entry): + if value and value != item[key]: + updates.append((entry, item)) + return updates + + +def main(): + """main entry point for module execution""" + element_spec = dict( + name=dict(), + configured_password=dict(no_log=True), + nopassword=dict(type="bool"), + update_password=dict( + default="always", + choices=["on_create", "always"], + ), + privilege=dict(type="int"), + role=dict(), + sshkey=dict(no_log=True), + state=dict(default="present", choices=["present", "absent"]), + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict( + type="list", + elements="dict", + options=aggregate_spec, + aliases=["collection", "users"], + ), + purge=dict(type="bool", default=False), + ) + + argument_spec.update(element_spec) + mutually_exclusive = [("name", "aggregate")] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + if warnings: + result["warnings"] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(update_objects(want, have), module) + + if module.params["purge"]: + want_users = [x["name"] for x in want] + have_users = [x["name"] for x in have] + for item in set(have_users).difference(want_users): + if item != "admin": + commands.append("no username %s" % item) + + result["commands"] = commands + + # the eos cli prevents this by rule so capture it and display + # a nice failure message + if "no username admin" in commands: + module.fail_json(msg="cannot delete the `admin` account") + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get("diff") and module._diff: + result["diff"] = {"prepared": response.get("diff")} + result["session_name"] = response.get("session") + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_vlans.py b/ansible_collections/arista/eos/plugins/modules/eos_vlans.py new file mode 100644 index 000000000..7c834a3ac --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_vlans.py @@ -0,0 +1,329 @@ +#!/usr/bin/python +# -*- 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 module file for eos_vlans +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: eos_vlans +short_description: VLANs resource module +description: This module provides declarative management of VLANs on Arista EOS network + devices. +version_added: 1.0.0 +author: Nathaniel Case (@qalthos) +notes: +- Tested against Arista EOS 4.24.6F +- This module works with connection C(network_cli). See the L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: A dictionary of VLANs options + type: list + elements: dict + suboptions: + name: + description: + - Name of the VLAN. + type: str + vlan_id: + description: + - ID of the VLAN. Range 1-4094 + type: int + required: true + state: + description: + - Operational state of the VLAN + type: str + choices: + - active + - suspend + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the EOS device + by executing the command B(show running-config | section vlan). + - The state I(parsed) reads the configuration from C(running_config) option + and transforms it into Ansible structured data as per the resource module's + argspec and the value + type: str + state: + description: + - The state of the configuration after module completion + type: str + choices: + - merged + - replaced + - overridden + - deleted + - rendered + - gathered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using deleted + +# Before state: +# ------------- +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 10 +# name ten +# ! +# vlan 20 +# name twenty + +- name: Delete attributes of the given VLANs. + arista.eos.eos_vlans: + config: + - vlan_id: 20 + state: deleted + +# After state: +# ------------ +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 10 +# name ten + + +# Using merged + +# Before state: +# ------------- +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 10 +# name ten +# ! +# vlan 20 +# name twenty + +- name: Merge given VLAN attributes with device configuration + arista.eos.eos_vlans: + config: + - vlan_id: 20 + state: suspend + state: merged + +# After state: +# ------------ +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 10 +# name ten +# ! +# vlan 20 +# name twenty +# state suspend + + +# Using overridden + +# Before state: +# ------------- +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 10 +# name ten +# ! +# vlan 20 +# name twenty + +- name: Override device configuration of all VLANs with provided configuration + arista.eos.eos_vlans: + config: + - vlan_id: 20 + state: suspend + state: overridden + +# After state: +# ------------ +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 20 +# state suspend + + +# Using replaced + +# Before state: +# ------------- +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 10 +# name ten +# ! +# vlan 20 +# name twenty + +- name: Replace all attributes of specified VLANs with provided configuration + arista.eos.eos_vlans: + config: + - vlan_id: 20 + state: suspend + state: replaced + +# After state: +# ------------ +# +# veos(config-vlan-20)#show running-config | section vlan +# vlan 10 +# name ten +# ! +# vlan 20 +# state suspend + +# using parsed + +# parsed.cfg +# vlan 10 +# name ten +# ! +# vlan 20 +# name twenty +# state suspend + +- name: Use parsed to convert native configs to structured data + arista.eos.eos_vlans: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Output: +# ------- +# parsed: +# - vlan_id: 10 +# name: ten +# - vlan_id: 20 +# state: suspend + +# Using rendered: + +- name: Use Rendered to convert the structured data to native config + arista.eos.eos_vlans: + config: + - vlan_id: 10 + name: ten + - vlan_id: 20 + state: suspend + state: rendered + +# Output: +# ------ +# rendered: +# - "vlan 10" +# - "name ten" +# - "vlan 20" +# - "state suspend" + +# Using gathered: +# native_config: +# vlan 10 +# name ten +# ! +# vlan 20 +# name twenty +# state suspend + +- name: Gather vlans facts from the device + arista.eos.eos_vlans: + state: gathered + +# Output: +# ------ + +# gathered: +# - vlan_id: 10 +# name: ten +# - vlan_id: 20 +# state: suspend + +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['vlan 10', 'no name', 'vlan 11', 'name Eleven'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.argspec.vlans.vlans import ( + VlansArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.vlans.vlans import ( + Vlans, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + module = AnsibleModule( + argument_spec=VlansArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + ) + + result = Vlans(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/modules/eos_vrf.py b/ansible_collections/arista/eos/plugins/modules/eos_vrf.py new file mode 100644 index 000000000..705796512 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/modules/eos_vrf.py @@ -0,0 +1,427 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + + +DOCUMENTATION = """ +module: eos_vrf +author: Ricardo Carrillo Cruz (@rcarrillocruz) +short_description: Manage VRFs on Arista EOS network devices +description: +- This module provides declarative management of VRFs on Arista EOS network devices. +version_added: 1.0.0 +notes: +- Tested against Arista EOS 4.24.6F +options: + name: + description: + - Name of the VRF. + type: str + rd: + description: + - Route distinguisher of the VRF + type: str + interfaces: + description: + - Identifies the set of interfaces that should be configured in the VRF. Interfaces + must be routed interfaces in order to be placed into a VRF. The name of interface + should be in expanded format and not abbreviated. + type: list + elements: str + associated_interfaces: + description: + - This is a intent option and checks the operational state of the for given vrf + C(name) for associated interfaces. If the value in the C(associated_interfaces) + does not match with the operational state of vrf interfaces on device it will + result in failure. + type: list + elements: str + aggregate: + description: List of VRFs instances + type: list + elements: dict + suboptions: + name: + description: + - Name of the VRF. + required: true + type: str + rd: + description: + - Route distinguisher of the VRF + type: str + interfaces: + description: + - Identifies the set of interfaces that should be configured in the VRF. Interfaces + must be routed interfaces in order to be placed into a VRF. The name of interface + should be in expanded format and not abbreviated. + type: list + elements: str + associated_interfaces: + description: + - This is a intent option and checks the operational state of the for given vrf + C(name) for associated interfaces. If the value in the C(associated_interfaces) + does not match with the operational state of vrf interfaces on device it will + result in failure. + type: list + elements: str + delay: + description: + - Time in seconds to wait before checking for the operational state on remote + device. This wait is applicable for operational state arguments. + default: 10 + type: int + state: + description: + - State of the VRF configuration. + default: present + type: str + choices: + - present + - absent + purge: + description: + - Purge VRFs not defined in the I(aggregate) parameter. + default: false + type: bool + delay: + description: + - Time in seconds to wait before checking for the operational state on remote + device. This wait is applicable for operational state arguments. + default: 10 + type: int + state: + description: + - State of the VRF configuration. + default: present + type: str + choices: + - present + - absent +""" + +EXAMPLES = """ +- name: Create vrf + arista.eos.eos_vrf: + name: test + rd: 1:200 + interfaces: + - Ethernet2 + state: present + +- name: Delete VRFs + arista.eos.eos_vrf: + name: test + state: absent + +- name: Create aggregate of VRFs with purge + arista.eos.eos_vrf: + aggregate: + - name: test4 + rd: 1:204 + - name: test5 + rd: 1:205 + state: present + purge: yes + +- name: Delete aggregate of VRFs + arista.eos.eos_vrf: + aggregate: + - name: test2 + - name: test3 + - name: test4 + - name: test5 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - vrf instance test + - rd 1:100 + - interface Ethernet1 + - vrf test +""" +import re +import time + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_default_spec, +) + +from ansible_collections.arista.eos.plugins.module_utils.network.eos.eos import ( + load_config, + run_commands, +) + + +def search_obj_in_list(name, lst): + for o in lst: + if o["name"] == name: + return o + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + state = module.params["state"] + purge = module.params["purge"] + + for w in want: + name = w["name"] + rd = w["rd"] + + obj_in_have = search_obj_in_list(name, have) + + if state == "absent": + if obj_in_have: + commands.append("no vrf instance %s" % name) + elif state == "present": + if not obj_in_have: + commands.append("vrf instance %s" % name) + + if rd is not None: + commands.append("rd %s" % rd) + + if w["interfaces"]: + for i in w["interfaces"]: + commands.append("interface %s" % i) + commands.append("vrf %s" % w["name"]) + else: + if w["rd"] is not None and w["rd"] != obj_in_have["rd"]: + commands.append("vrf instance %s" % w["name"]) + commands.append("rd %s" % w["rd"]) + + if w["interfaces"]: + if not obj_in_have["interfaces"]: + for i in w["interfaces"]: + commands.append("interface %s" % i) + commands.append("vrf %s" % w["name"]) + elif set(w["interfaces"]) != obj_in_have["interfaces"]: + missing_interfaces = list( + set(w["interfaces"]) + - set(obj_in_have["interfaces"]), + ) + + for i in missing_interfaces: + commands.append("interface %s" % i) + commands.append("vrf %s" % w["name"]) + + if purge: + for h in have: + obj_in_want = search_obj_in_list(h["name"], want) + if not obj_in_want: + commands.append("no vrf instance %s" % h["name"]) + + return commands + + +def map_config_to_obj(module): + objs = [] + output = run_commands(module, {"command": "show vrf", "output": "text"}) + + lines = output[0].strip().splitlines()[3:] + + out_len = len(lines) + index = 0 + while out_len > index: + line = lines[index] + if not line: + continue + + splitted_line = re.split(r"\s{2,}", line.strip()) + + if len(splitted_line) == 1: + index += 1 + continue + obj = dict() + obj["name"] = splitted_line[0] + obj["rd"] = splitted_line[1] + obj["interfaces"] = [] + + if len(splitted_line) > 4: + obj["interfaces"] = [] + interfaces = splitted_line[4] + if interfaces.endswith(","): + while interfaces.endswith(","): + # gather all comma separated interfaces + if out_len <= index: + break + index += 1 + line = lines[index] + vrf_line = re.split(r"\s{2,}", line.strip()) + interfaces += vrf_line[-1] + + for i in interfaces.split(","): + obj["interfaces"].append(i.strip().lower()) + index += 1 + objs.append(obj) + + return objs + + +def map_params_to_obj(module): + obj = [] + aggregate = module.params.get("aggregate") + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + if item.get("interfaces"): + item["interfaces"] = [ + intf.replace(" ", "").lower() + for intf in item.get("interfaces") + if intf + ] + + if item.get("associated_interfaces"): + item["associated_interfaces"] = [ + intf.replace(" ", "").lower() + for intf in item.get("associated_interfaces") + if intf + ] + + obj.append(item.copy()) + else: + obj.append( + { + "name": module.params["name"], + "state": module.params["state"], + "rd": module.params["rd"], + "interfaces": [ + intf.replace(" ", "").lower() + for intf in module.params["interfaces"] + ] + if module.params["interfaces"] + else [], + "associated_interfaces": [ + intf.replace(" ", "").lower() + for intf in module.params["associated_interfaces"] + ] + if module.params["associated_interfaces"] + else [], + }, + ) + + return obj + + +def check_declarative_intent_params(want, module, result): + have = None + is_delay = False + + for w in want: + if w.get("associated_interfaces") is None: + continue + + if result["changed"] and not is_delay: + time.sleep(module.params["delay"]) + is_delay = True + + if have is None: + have = map_config_to_obj(module) + + for i in w["associated_interfaces"]: + obj_in_have = search_obj_in_list(w["name"], have) + + if obj_in_have: + interfaces = obj_in_have.get("interfaces") + if interfaces is not None and i not in interfaces: + module.fail_json( + msg="Interface %s not configured on vrf %s" + % (i, w["name"]), + ) + + +def main(): + """main entry point for module execution""" + element_spec = dict( + name=dict(), + interfaces=dict(type="list", elements="str"), + associated_interfaces=dict(type="list", elements="str"), + delay=dict(default=10, type="int"), + rd=dict(), + state=dict(default="present", choices=["present", "absent"]), + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec["name"] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + aggregate_spec["state"].update(default="present") + aggregate_spec["delay"].update(default=10) + + argument_spec = dict( + aggregate=dict(type="list", elements="dict", options=aggregate_spec), + purge=dict(default=False, type="bool"), + ) + + argument_spec.update(element_spec) + + required_one_of = [["name", "aggregate"]] + mutually_exclusive = [["name", "aggregate"]] + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + + if warnings: + result["warnings"] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result["commands"] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get("diff") and module._diff: + result["diff"] = {"prepared": response.get("diff")} + result["session_name"] = response.get("session") + result["changed"] = True + + check_declarative_intent_params(want, module, result) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/arista/eos/plugins/terminal/__init__.py b/ansible_collections/arista/eos/plugins/terminal/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/arista/eos/plugins/terminal/__init__.py diff --git a/ansible_collections/arista/eos/plugins/terminal/eos.py b/ansible_collections/arista/eos/plugins/terminal/eos.py new file mode 100644 index 000000000..8ea27e296 --- /dev/null +++ b/ansible_collections/arista/eos/plugins/terminal/eos.py @@ -0,0 +1,113 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import re + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_bytes, to_text +from ansible_collections.ansible.netcommon.plugins.plugin_utils.terminal_base import ( + TerminalBase, +) + + +class TerminalModule(TerminalBase): + terminal_stdout_re = [ + re.compile(rb"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), + re.compile(rb"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$"), + ] + + terminal_stderr_re = [ + re.compile(rb"% ?Error"), + # re.compile(br"^% \w+", re.M), + re.compile(rb"% User not present"), + re.compile(rb"% ?Bad secret"), + re.compile(rb"invalid input", re.I), + re.compile(rb"(?:incomplete|ambiguous) command", re.I), + re.compile(rb"connection timed out", re.I), + # Strings like this regarding VLANs are not errors + re.compile(rb"[^\r\n]+ not found(?! in current VLAN)", re.I), + re.compile(rb"'[^']' +returned error code: ?\d+"), + re.compile(rb"[^\r\n](?<! shell )\/bin\/(?:ba)?sh"), + re.compile(rb"% More than \d+ OSPF instance", re.I), + re.compile(rb"% Subnet [0-9a-f.:/]+ overlaps", re.I), + re.compile(rb"Maximum number of pending sessions has been reached"), + re.compile(rb"% Prefix length must be less than"), + # returned in response to 'channel-group <name> mode <mode>' + re.compile( + rb"% Cannot change mode; remove all members and try again.", + ), + ] + + terminal_config_prompt = re.compile(r"^.+\(config(-.*)?\)#$") + + def on_open_shell(self): + try: + for cmd in (b"terminal length 0", b"terminal width 512"): + self._exec_cli_command(cmd) + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure("unable to set terminal parameters") + + def on_become(self, passwd=None): + if self._get_prompt().endswith(b"#"): + return + + cmd = {"command": "enable"} + if passwd: + cmd["prompt"] = to_text( + r"[\r\n]?[Pp]assword: $", + errors="surrogate_or_strict", + ) + cmd["answer"] = passwd + cmd["prompt_retry_check"] = True + + try: + self._exec_cli_command( + to_bytes(json.dumps(cmd), errors="surrogate_or_strict"), + ) + prompt = self._get_prompt() + if prompt is None or not prompt.endswith(b"#"): + raise AnsibleConnectionFailure( + "failed to elevate privilege to enable mode still at prompt [%s]" + % prompt, + ) + except AnsibleConnectionFailure as e: + prompt = self._get_prompt() + raise AnsibleConnectionFailure( + "unable to elevate privilege to enable mode, at prompt [%s] with error: %s" + % (prompt, e.message), + ) + + def on_unbecome(self): + prompt = self._get_prompt() + if prompt is None: + # if prompt is None most likely the terminal is hung up at a prompt + return + + if b"(config" in prompt: + self._exec_cli_command(b"end") + self._exec_cli_command(b"disable") + + elif prompt.endswith(b"#"): + self._exec_cli_command(b"disable") |