diff --git a/.github/workflows/collection-continuous-integration.yml b/.github/workflows/collection-continuous-integration.yml new file mode 100644 index 0000000..efd1eb9 --- /dev/null +++ b/.github/workflows/collection-continuous-integration.yml @@ -0,0 +1,266 @@ +name: Collection test suite + +on: + push: + pull_request: + schedule: + - cron: 3 0 * * * # Run daily at 0:03 UTC + +jobs: + build-collection-artifact: + name: Build collection + runs-on: ${{ matrix.runner-os }} + strategy: + matrix: + runner-os: + - ubuntu-latest + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + runner-python-version: + - 3.8 + steps: + - name: Set up Python ${{ matrix.runner-python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.runner-python-version }} + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Build a collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + build + --output-path + "${GITHUB_WORKSPACE}/.cache/collection-tarballs" + - name: Store migrated collection artifacts + uses: actions/upload-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + + sanity-test-collection-via-vms: + name: Sanity in VM ${{ matrix.os.vm || 'ubuntu-latest' }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.os.vm || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + os: + - vm: ubuntu-latest + - vm: ubuntu-16.04 + - vm: macos-latest + python-version: + - 3.8 + - 3.7 + - 3.6 + - 3.5 + - 2.7 + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection sanity tests + run: >- + ~/.local/bin/ansible-test + sanity + --color + --requirements + --venv + --python + "${{ matrix.python-version }}" + -vvv + working-directory: >- + /${{ runner.os == 'Linux' && 'home' || 'Users' }}/runner/.ansible/collections/ansible_collections/huawei/cloudengine + + sanity-test-collection-via-containers: + name: Sanity in container via Python ${{ matrix.python-version }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.runner-os }} + strategy: + fail-fast: false + matrix: + runner-os: + - ubuntu-latest + runner-python-version: + - 3.8 + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + python-version: + - 3.8 + - 2.7 + - 3.7 + - 3.6 + - 3.5 + - 2.6 + steps: + - name: Set up Python ${{ matrix.runner-python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.runner-python-version }} + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection sanity tests + run: >- + ~/.local/bin/ansible-test + sanity + --color + --requirements + --docker + --python + "${{ matrix.python-version }}" + -vvv + working-directory: >- + /home/runner/.ansible/collections/ansible_collections/huawei/cloudengine + + unit-test-collection-via-vms: + name: Units in VM ${{ matrix.os.vm || 'ubuntu-latest' }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.os.vm || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + os: + - vm: ubuntu-latest + - vm: ubuntu-16.04 + - vm: macos-latest + python-version: + - 3.8 + - 3.7 + - 3.6 + - 3.5 + - 2.7 + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection unit tests + run: | + [[ ! -d 'tests/unit' ]] && echo This collection does not have unit tests. Skipping... || \ + ~/.local/bin/ansible-test units --color --coverage --requirements --venv --python "${{ matrix.python-version }}" -vvv + working-directory: >- + /${{ runner.os == 'Linux' && 'home' || 'Users' }}/runner/.ansible/collections/ansible_collections/huawei/cloudengine + + unit-test-collection-via-containers: + name: Units in container ${{ matrix.container-image }} + needs: + - build-collection-artifact + runs-on: ${{ matrix.runner-os }} + strategy: + fail-fast: false + matrix: + runner-os: + - ubuntu-latest + runner-python-version: + - 3.8 + ansible-version: + - git+https://github.com/ansible/ansible.git@devel + container-image: + - fedora31 + - ubuntu1804 + - centos8 + - opensuse15 + - fedora30 + - centos7 + - opensuse15py2 + - ubuntu1604 + - centos6 + steps: + - name: Set up Python ${{ matrix.runner-python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.runner-python-version }} + - name: Install Ansible ${{ matrix.ansible-version }} + run: >- + python -m + pip + install + --user + ${{ matrix.ansible-version }} + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: >- + collection + path: .cache/collection-tarballs + - name: Install the collection tarball + run: >- + ~/.local/bin/ansible-galaxy + collection + install + .cache/collection-tarballs/*.tar.gz + - name: Run collection unit tests + run: | + [[ ! -d 'tests/unit' ]] && echo This collection does not have unit tests. Skipping... || \ + ~/.local/bin/ansible-test units --color --coverage --requirements --docker "${{ matrix.container-image }}" -vvv + working-directory: >- + /home/runner/.ansible/collections/ansible_collections/huawei/cloudengine \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6fc14a --- /dev/null +++ b/.gitignore @@ -0,0 +1,387 @@ + +# Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv +# Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv + +### dotenv ### +.env + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +#!! ERROR: jupyternotebook is undefined. Use list command to see defined gitignore types !!# + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### pydev ### +.pydevproject + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### WebStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a016b2 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +[![GitHub Actions CI/CD build status — Collection test suite](https://github.com/ansible-collection-migration/huawei.cloudengine/workflows/Collection%20test%20suite/badge.svg?branch=master)](https://github.com/ansible-collection-migration/huawei.cloudengine/actions?query=workflow%3A%22Collection%20test%20suite%22) + +Ansible Collection: huawei.cloudengine +================================================= \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..4e51a9d --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,15 @@ +namespace: huawei +name: cloudengine +version: 1.0.0 +readme: README.md +authors: null +description: null +license: null +license_file: null +tags: null +dependencies: + ansible.netcommon: '>=1.0' +repository: git@github.com:ansible-collection-migration/huawei.cloudengine.git +documentation: https://github.com/ansible-collection-migration/huawei.cloudengine/tree/master/docs +homepage: https://github.com/ansible-collection-migration/huawei.cloudengine +issues: https://github.com/ansible-collection-migration/huawei.cloudengine/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc diff --git a/plugins/action/__init__.py b/plugins/action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/action/ce.py b/plugins/action/ce.py new file mode 100644 index 0000000..1b6ce6f --- /dev/null +++ b/plugins/action/ce.py @@ -0,0 +1,89 @@ +# +# Copyright: (c) 2016, 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 sys +import copy + +from ansible import constants as C +from ansible_collections.ansible.netcommon.plugins.action.network import ActionModule as ActionNetworkModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_provider_spec +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import load_provider +from ansible.utils.display import Display + +display = Display() + +CLI_SUPPORTED_MODULES = ['ce_rollback', 'ce_mlag_interface', 'ce_startup', 'ce_config', + 'ce_command', 'ce_facts', 'ce_evpn_global', 'ce_evpn_bgp_rr', + 'ce_mtu', 'ce_evpn_bgp', 'ce_snmp_location', 'ce_snmp_contact', + 'ce_snmp_traps', 'ce_netstream_global', 'ce_netstream_aging', + 'ce_netstream_export', 'ce_netstream_template', 'ce_ntp_auth', + 'ce_stp', 'ce_vxlan_global', 'ce_vxlan_arp', 'ce_vxlan_gateway', + 'ce_acl_interface'] + + +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 == 'ce_config' else False + socket_path = None + persistent_connection = self._play_context.connection.split('.')[-1] + + if self._play_context.connection == 'local': + provider = load_provider(ce_provider_spec, self._task.args) + transport = provider['transport'] or 'cli' + + display.vvvv('connection transport is %s' % transport, self._play_context.remote_addr) + + if transport == 'cli': + pc = copy.deepcopy(self._play_context) + pc.connection = 'network_cli' + pc.network_os = 'ce' + pc.remote_addr = provider['host'] or self._play_context.remote_addr + pc.port = int(provider['port'] or self._play_context.port or 22) + pc.remote_user = provider['username'] or self._play_context.connection_user + pc.password = provider['password'] or self._play_context.password + command_timeout = int(provider['timeout'] or C.PERSISTENT_COMMAND_TIMEOUT) + self._task.args['provider'] = provider.update( + host=pc.remote_addr, + port=pc.port, + username=pc.remote_user, + password=pc.password + ) + if module_name in ['ce_netconf'] or module_name not in CLI_SUPPORTED_MODULES: + pc.connection = 'netconf' + display.vvv('using connection plugin %s (was local)' % pc.connection, pc.remote_addr) + connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin, task_uuid=self._task._uuid) + connection.set_options(direct={'persistent_command_timeout': command_timeout}) + + socket_path = connection.run() + display.vvvv('socket_path: %s' % socket_path, pc.remote_addr) + if not socket_path: + return {'failed': True, + 'msg': 'unable to open shell. Please see: ' + + 'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'} + + task_vars['ansible_socket'] = socket_path + # make sure a transport value is set in args + self._task.args['transport'] = transport + self._task.args['provider'] = provider + elif persistent_connection in ('netconf', 'network_cli'): + provider = self._task.args.get('provider', {}) + if any(provider.values()): + display.warning('provider is unnecessary when using %s and will be ignored' % self._play_context.connection) + del self._task.args['provider'] + + if (persistent_connection == 'network_cli' and module_name not in CLI_SUPPORTED_MODULES) or \ + (persistent_connection == 'netconf' and module_name in CLI_SUPPORTED_MODULES): + return {'failed': True, 'msg': "Connection type '%s' is not valid for '%s' module." + % (self._play_context.connection, self._task.action)} + + result = super(ActionModule, self).run(task_vars=task_vars) + return result diff --git a/plugins/action/ce_template.py b/plugins/action/ce_template.py new file mode 100644 index 0000000..456e5a2 --- /dev/null +++ b/plugins/action/ce_template.py @@ -0,0 +1,104 @@ +# +# Copyright 2015 Peter Sprygada +# +# 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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import time +import glob +from ansible.module_utils.six.moves.urllib.parse import urlsplit + +from ansible.module_utils._text import to_text +from ansible_collections.huawei.cloudengine.plugins.action.ce import ActionModule as _ActionModule + + +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + + try: + self._handle_template() + except (ValueError, AttributeError) as exc: + return dict(failed=True, msg=exc.message) + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + if self._task.args.get('backup') and result.get('__backup__'): + # User requested backup and no error occurred in module. + # NOTE: If there is a parameter error, __backup__ key may not be in results. + self._write_backup(task_vars['inventory_hostname'], result['__backup__']) + + if '__backup__' in result: + del result['__backup__'] + + return result + + def _get_working_path(self): + cwd = self._loader.get_basedir() + if self._task._role is not None: + cwd = self._task._role._role_path + return cwd + + def _write_backup(self, host, contents): + backup_path = self._get_working_path() + '/backup' + if not os.path.exists(backup_path): + os.mkdir(backup_path) + for fn in glob.glob('%s/%s*' % (backup_path, host)): + os.remove(fn) + tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) + filename = '%s/%s_config.%s' % (backup_path, host, tstamp) + open(filename, 'w').write(contents) + + def _handle_template(self): + src = self._task.args.get('src') + if not src: + raise ValueError('missing required arguments: src') + + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit(src).scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, 'templates', src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + return + + try: + with open(source, 'r') as f: + template_data = to_text(f.read()) + except IOError: + return dict(failed=True, msg='unable to load src file') + + # Create a template search path in the following order: + # [working_path, self_role_path, dependent_role_paths, dirname(source)] + searchpath = [working_path] + if self._task._role is not None: + searchpath.append(self._task._role._role_path) + if hasattr(self._task, "_block:"): + dep_chain = self._task._block.get_dep_chain() + if dep_chain is not None: + for role in dep_chain: + searchpath.append(role._role_path) + searchpath.append(os.path.dirname(source)) + with self._templar.set_temporary_context(searchpath=searchpath): + self._task.args['src'] = self._templar.template(template_data) diff --git a/plugins/cliconf/__init__.py b/plugins/cliconf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/cliconf/ce.py b/plugins/cliconf/ce.py new file mode 100644 index 0000000..4af008c --- /dev/null +++ b/plugins/cliconf/ce.py @@ -0,0 +1,121 @@ +# +# (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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' +--- +cliconf: ce +short_description: Use ce cliconf to run command on HUAWEI CloudEngine platform +description: + - This ce plugin provides low level abstraction apis for + sending and receiving CLI commands from HUAWEI CloudEngine network devices. +''' + +import re +import json + +from itertools import chain + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_text +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list +from ansible.plugins.cliconf import CliconfBase, enable_mode + + +class Cliconf(CliconfBase): + + def get_device_info(self): + device_info = {} + + device_info['network_os'] = 'ce' + reply = self.get('display version') + data = to_text(reply, errors='surrogate_or_strict').strip() + + match = re.search(r'^Huawei.+\n.+\Version\s+(\S+)', data) + if match: + device_info['network_os_version'] = match.group(1).strip(',') + + match = re.search(r'^Huawei(.+)\n.+\(\S+\s+\S+\)', data, re.M) + if match: + device_info['network_os_model'] = match.group(1) + + match = re.search(r'HUAWEI\s+(\S+)\s+uptime', data, re.M) + if match: + device_info['network_os_hostname'] = match.group(1) + + return device_info + + @enable_mode + def get_config(self, source='running', format='text', flags=None): + if source not in ('running'): + return self.invalid_params("fetching configuration from %s is not supported" % source) + + if not flags: + flags = [] + + cmd = 'display current-configuration' + + return self.send_command(cmd) + + @enable_mode + def edit_config(self, command): + results = [] + for cmd in chain(['configure terminal'], to_list(command), ['end']): + if isinstance(cmd, dict): + command = cmd['command'] + prompt = cmd['prompt'] + answer = cmd['answer'] + newline = cmd.get('newline', True) + else: + command = cmd + prompt = None + answer = None + newline = True + + results.append(self.send_command(command, prompt, answer, False, newline)) + return results[1:-1] + + def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False): + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all) + + def get_capabilities(self): + result = super(Cliconf, self).get_capabilities() + 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: + out = self._connection.get_prompt() + + if out is None: + raise AnsibleConnectionFailure(message=u'cli prompt is not identified from the last received' + u' response window: %s' % self._connection._last_recv_window) + + prompt = to_text(out, errors='surrogate_then_replace').strip() + while prompt.endswith(']'): + self._connection.queue_message('vvvv', 'wrong context, sending return to device') + if prompt.startswith('[*'): + self._connection.exec_command('clear configuration candidate') + self._connection.exec_command('return') + out = self._connection.get_prompt() + prompt = to_text(out, errors='surrogate_then_replace').strip() diff --git a/plugins/doc_fragments/__init__.py b/plugins/doc_fragments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/doc_fragments/ce.py b/plugins/doc_fragments/ce.py new file mode 100644 index 0000000..0709ab2 --- /dev/null +++ b/plugins/doc_fragments/ce.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +options: + provider: + description: + - A dict object containing connection details. + suboptions: + host: + description: + - Specifies the DNS host name or address for connecting to the remote + device over the specified transport. The value of host is used as + the destination address for the transport. + type: str + required: true + port: + description: + - Specifies the port to use when building the connection to the remote + device. This value applies to either I(cli) or I(netconf). The port + value will default to the appropriate transport common port if + none is provided in the task. (cli=22, netconf=22). + type: int + default: 0 (use common port) + username: + description: + - Configures the username to use to authenticate the connection to + the remote device. This value is used to authenticate the CLI login. + If the value is not specified in the task, the value of environment + variable C(ANSIBLE_NET_USERNAME) will be used instead. + type: str + password: + description: + - Specifies the password to use to authenticate the connection to + the remote device. This is a common argument used for cli + transports. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead. + type: str + ssh_keyfile: + description: + - Specifies the SSH key to use to authenticate the connection to + the remote device. This argument is used for the I(cli) + transport. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_SSH_KEYFILE) will be used instead. + type: path + transport: + description: + - Configures the transport connection to use when connecting to the + remote device. The transport argument supports connectivity to the + device over cli (ssh). + type: str + required: true + choices: [ cli, netconf ] + default: cli +''' diff --git a/plugins/module_utils/__init__.py b/plugins/module_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/module_utils/network/cloudengine/__init__.py b/plugins/module_utils/network/cloudengine/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/module_utils/network/cloudengine/ce.py b/plugins/module_utils/network/cloudengine/ce.py new file mode 100644 index 0000000..b9fe91f --- /dev/null +++ b/plugins/module_utils/network/cloudengine/ce.py @@ -0,0 +1,421 @@ +# +# 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. +# + +import re +import socket +import sys +import traceback + +from ansible.module_utils.basic import env_fallback +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList +from ansible.module_utils.connection import exec_command, ConnectionError +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_native +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import NetconfConnection + + +try: + from ncclient.xml_ import to_xml, new_ele_ns + HAS_NCCLIENT = True +except ImportError: + HAS_NCCLIENT = False + + +try: + from lxml import etree +except ImportError: + from xml.etree import ElementTree as etree + +_DEVICE_CLI_CONNECTION = None +_DEVICE_NC_CONNECTION = None + +ce_provider_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + 'use_ssl': dict(type='bool'), + 'validate_certs': dict(type='bool'), + 'timeout': dict(type='int'), + 'transport': dict(default='cli', choices=['cli', 'netconf']), +} +ce_argument_spec = { + 'provider': dict(type='dict', options=ce_provider_spec), +} +ce_top_spec = { + 'host': dict(removed_in_version=2.9), + 'port': dict(removed_in_version=2.9, type='int'), + 'username': dict(removed_in_version=2.9), + 'password': dict(removed_in_version=2.9, no_log=True), + 'ssh_keyfile': dict(removed_in_version=2.9, type='path'), + 'use_ssl': dict(removed_in_version=2.9, type='bool'), + 'validate_certs': dict(removed_in_version=2.9, type='bool'), + 'timeout': dict(removed_in_version=2.9, type='int'), + 'transport': dict(removed_in_version=2.9, choices=['cli', 'netconf']), +} +ce_argument_spec.update(ce_top_spec) + + +def to_string(data): + return re.sub(r'|>)', r' 2 and err[0] in ["<", "["] and err[-1] in [">", "]"]: + continue + err.strip('.,\r\n\t ') + if err: + msg.append(err) + + if cmd: + msg.insert(0, "Command: %s" % cmd) + + return ", ".join(msg).capitalize() + "." + + +def to_command(module, commands): + default_output = 'text' + transform = ComplexList(dict( + command=dict(key=True), + output=dict(default=default_output), + prompt=dict(), + answer=dict() + ), module) + + commands = transform(to_list(commands)) + + return commands + + +def get_config(module, flags=None): + flags = [] 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) + + +def load_config(module, config): + """load_config""" + conn = get_connection(module) + return conn.load_config(config) + + +def ce_unknown_host_cb(host, fingerprint): + """ ce_unknown_host_cb """ + + return True + + +def get_nc_set_id(xml_str): + """get netconf set-id value""" + + result = re.findall(r'= 0 and index >= len(xml_list): + return None + if index < 0 and abs(index) > len(xml_list): + return None + + ele = xml_list[index] + if not ele.replace(" ", ""): + xml_list.pop(index) + ele = None + return ele + + +def merge_nc_xml(xml1, xml2): + """merge xml1 and xml2""" + + xml1_list = xml1.split("")[0].split("\n") + xml2_list = xml2.split("")[1].split("\n") + + while True: + xml1_ele1 = get_xml_line(xml1_list, -1) + xml1_ele2 = get_xml_line(xml1_list, -2) + xml2_ele1 = get_xml_line(xml2_list, 0) + xml2_ele2 = get_xml_line(xml2_list, 1) + if not xml1_ele1 or not xml1_ele2 or not xml2_ele1 or not xml2_ele2: + return xml1 + + if "xmlns" in xml2_ele1: + xml2_ele1 = xml2_ele1.lstrip().split(" ")[0] + ">" + if "xmlns" in xml2_ele2: + xml2_ele2 = xml2_ele2.lstrip().split(" ")[0] + ">" + if xml1_ele1.replace(" ", "").replace("/", "") == xml2_ele1.replace(" ", "").replace("/", ""): + if xml1_ele2.replace(" ", "").replace("/", "") == xml2_ele2.replace(" ", "").replace("/", ""): + xml1_list.pop() + xml2_list.pop(0) + else: + break + else: + break + + return "\n".join(xml1_list + xml2_list) + + +def get_nc_connection(module): + global _DEVICE_NC_CONNECTION + if not _DEVICE_NC_CONNECTION: + load_params(module) + conn = NetconfConnection(module._socket_path) + _DEVICE_NC_CONNECTION = conn + return _DEVICE_NC_CONNECTION + + +def set_nc_config(module, xml_str): + """ set_config """ + + conn = get_nc_connection(module) + try: + out = conn.edit_config(target='running', config=xml_str, default_operation='merge', + error_option='rollback-on-error') + finally: + # conn.unlock(target = 'candidate') + pass + return to_string(to_xml(out)) + + +def get_nc_next(module, xml_str): + """ get_nc_next for exchange capability """ + + conn = get_nc_connection(module) + result = None + if xml_str is not None: + response = conn.get(xml_str, if_rpc_reply=True) + result = response.find('./*') + set_id = response.get('set-id') + while True and set_id is not None: + try: + fetch_node = new_ele_ns('get-next', 'http://www.huawei.com/netconf/capability/base/1.0', {'set-id': set_id}) + next_xml = conn.dispatch_rpc(etree.tostring(fetch_node)) + if next_xml is not None: + result.extend(next_xml.find('./*')) + set_id = next_xml.get('set-id') + except ConnectionError: + break + if result is not None: + return etree.tostring(result) + return result + + +def get_nc_config(module, xml_str): + """ get_config """ + + conn = get_nc_connection(module) + if xml_str is not None: + response = conn.get(xml_str) + else: + return None + + return to_string(to_xml(response)) + + +def execute_nc_action(module, xml_str): + """ huawei execute-action """ + + conn = get_nc_connection(module) + response = conn.execute_action(xml_str) + return to_string(to_xml(response)) + + +def execute_nc_cli(module, xml_str): + """ huawei execute-cli """ + + if xml_str is not None: + try: + conn = get_nc_connection(module) + out = conn.execute_nc_cli(command=xml_str) + return to_string(to_xml(out)) + except Exception as exc: + raise Exception(exc) + + +def check_ip_addr(ipaddr): + """ check ip address, Supports IPv4 and IPv6 """ + + if not ipaddr or '\x00' in ipaddr: + return False + + try: + res = socket.getaddrinfo(ipaddr, 0, socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, socket.AI_NUMERICHOST) + return bool(res) + except socket.gaierror: + err = sys.exc_info()[1] + if err.args[0] == socket.EAI_NONAME: + return False + raise diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/modules/ce_aaa_server.py b/plugins/modules/ce_aaa_server.py new file mode 100644 index 0000000..fb4a125 --- /dev/null +++ b/plugins/modules/ce_aaa_server.py @@ -0,0 +1,2177 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: ce_aaa_server +short_description: Manages AAA server global configuration on HUAWEI CloudEngine switches. +description: + - Manages AAA server global configuration on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + type: str + choices: [ absent, present ] + default: present + authen_scheme_name: + description: + - Name of an authentication scheme. + The value is a string of 1 to 32 characters. + type: str + first_authen_mode: + description: + - Preferred authentication mode. + type: str + choices: ['invalid', 'local', 'hwtacacs', 'radius', 'none'] + default: local + author_scheme_name: + description: + - Name of an authorization scheme. + The value is a string of 1 to 32 characters. + type: str + first_author_mode: + description: + - Preferred authorization mode. + type: str + choices: ['invalid', 'local', 'hwtacacs', 'if-authenticated', 'none'] + default: local + acct_scheme_name: + description: + - Accounting scheme name. + The value is a string of 1 to 32 characters. + type: str + accounting_mode: + description: + - Accounting Mode. + type: str + choices: ['invalid', 'hwtacacs', 'radius', 'none'] + default: none + domain_name: + description: + - Name of a domain. + The value is a string of 1 to 64 characters. + type: str + radius_server_group: + description: + - RADIUS server group's name. + The value is a string of 1 to 32 case-insensitive characters. + type: str + hwtacas_template: + description: + - Name of a HWTACACS template. + The value is a string of 1 to 32 case-insensitive characters. + type: str + local_user_group: + description: + - Name of the user group where the user belongs. The user inherits all the rights of the user group. + The value is a string of 1 to 32 characters. + type: str +''' + +EXAMPLES = r''' + +- name: AAA server test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Radius authentication Server Basic settings" + ce_aaa_server: + state: present + authen_scheme_name: test1 + first_authen_mode: radius + radius_server_group: test2 + provider: "{{ cli }}" + + - name: "Undo radius authentication Server Basic settings" + ce_aaa_server: + state: absent + authen_scheme_name: test1 + first_authen_mode: radius + radius_server_group: test2 + provider: "{{ cli }}" + + - name: "Hwtacacs accounting Server Basic settings" + ce_aaa_server: + state: present + acct_scheme_name: test1 + accounting_mode: hwtacacs + hwtacas_template: test2 + provider: "{{ cli }}" + + - name: "Undo hwtacacs accounting Server Basic settings" + ce_aaa_server: + state: absent + acct_scheme_name: test1 + accounting_mode: hwtacacs + hwtacas_template: test2 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"accounting_mode": "hwtacacs", "acct_scheme_name": "test1", + "hwtacas_template": "test2", "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"accounting scheme": [["hwtacacs"], ["default"]], + "hwtacacs template": ["huawei"]} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"accounting scheme": [["hwtacacs", "test1"]], + "hwtacacs template": ["huawei", "test2"]} +updates: + description: command sent to the device + returned: always + type: list + sample: ["accounting-scheme test1", + "accounting-mode hwtacacs", + "hwtacacs server template test2", + "hwtacacs enable"] +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +SUCCESS = """success""" +FAILED = """failed""" + +INVALID_SCHEME_CHAR = [' ', '/', '\\', ':', '*', '?', '"', '|', '<', '>'] +INVALID_DOMAIN_CHAR = [' ', '*', '?', '"', '\''] +INVALID_GROUP_CHAR = ['/', '\\', ':', '*', '?', '"', '|', '<', '>'] + + +# get authentication scheme +CE_GET_AUTHENTICATION_SCHEME = """ + + + + + + + + + + + +""" + +# merge authentication scheme +CE_MERGE_AUTHENTICATION_SCHEME = """ + + + + + %s + %s + invalid + + + + +""" + +# create authentication scheme +CE_CREATE_AUTHENTICATION_SCHEME = """ + + + + + %s + %s + invalid + + + + +""" + +# delete authentication scheme +CE_DELETE_AUTHENTICATION_SCHEME = """ + + + + + %s + %s + invalid + + + + +""" + +# get authorization scheme +CE_GET_AUTHORIZATION_SCHEME = """ + + + + + + + + + + + +""" + +# merge authorization scheme +CE_MERGE_AUTHORIZATION_SCHEME = """ + + + + + %s + %s + invalid + + + + +""" + +# create authorization scheme +CE_CREATE_AUTHORIZATION_SCHEME = """ + + + + + %s + %s + invalid + + + + +""" + +# delete authorization scheme +CE_DELETE_AUTHORIZATION_SCHEME = """ + + + + + %s + %s + invalid + + + + +""" + +# get accounting scheme +CE_GET_ACCOUNTING_SCHEME = """ + + + + + + + + + + +""" + +# merge accounting scheme +CE_MERGE_ACCOUNTING_SCHEME = """ + + + + + %s + %s + + + + +""" + +# create accounting scheme +CE_CREATE_ACCOUNTING_SCHEME = """ + + + + + %s + %s + + + + +""" + +# delete accounting scheme +CE_DELETE_ACCOUNTING_SCHEME = """ + + + + + %s + %s + + + + +""" + +# get authentication domain +CE_GET_AUTHENTICATION_DOMAIN = """ + + + + + + + + + + +""" + +# merge authentication domain +CE_MERGE_AUTHENTICATION_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# create authentication domain +CE_CREATE_AUTHENTICATION_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# delete authentication domain +CE_DELETE_AUTHENTICATION_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# get authorization domain +CE_GET_AUTHORIZATION_DOMAIN = """ + + + + + + + + + + +""" + +# merge authorization domain +CE_MERGE_AUTHORIZATION_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# create authorization domain +CE_CREATE_AUTHORIZATION_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# delete authorization domain +CE_DELETE_AUTHORIZATION_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# get accounting domain +CE_GET_ACCOUNTING_DOMAIN = """ + + + + + + + + + + +""" + +# merge accounting domain +CE_MERGE_ACCOUNTING_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# create accounting domain +CE_CREATE_ACCOUNTING_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# delete accounting domain +CE_DELETE_ACCOUNTING_DOMAIN = """ + + + + + %s + %s + + + + +""" + +# get radius template +CE_GET_RADIUS_TEMPLATE = """ + + + + + + + + + + + +""" + +# merge radius template +CE_MERGE_RADIUS_TEMPLATE = """ + + + + + %s + 3 + 5 + + + + +""" + +# create radius template +CE_CREATE_RADIUS_TEMPLATE = """ + + + + + %s + 3 + 5 + + + + +""" + +# delete radius template +CE_DELETE_RADIUS_TEMPLATE = """ + + + + + %s + 3 + 5 + + + + +""" + +# get hwtacacs template +CE_GET_HWTACACS_TEMPLATE = """ + + + + + + + + + + + +""" + +# merge hwtacacs template +CE_MERGE_HWTACACS_TEMPLATE = """ + + + + + %s + true + 5 + + + + +""" + +# create hwtacacs template +CE_CREATE_HWTACACS_TEMPLATE = """ + + + + + %s + true + 5 + + + + +""" + +# delete hwtacacs template +CE_DELETE_HWTACACS_TEMPLATE = """ + + + + + %s + + + + +""" + +# get radius client +CE_GET_RADIUS_CLIENT = """ + + + + + + + + + +""" + +# merge radius client +CE_MERGE_RADIUS_CLIENT = """ + + + + %s + + + +""" + +# get hwtacacs global config +CE_GET_HWTACACS_GLOBAL_CFG = """ + + + + + + + + + +""" + +# merge hwtacacs global config +CE_MERGE_HWTACACS_GLOBAL_CFG = """ + + + + %s + + + +""" + +# get local user group +CE_GET_LOCAL_USER_GROUP = """ + + + + + + + + + +""" +# merge local user group +CE_MERGE_LOCAL_USER_GROUP = """ + + + + + %s + + + + +""" +# delete local user group +CE_DELETE_LOCAL_USER_GROUP = """ + + + + + %s + + + + +""" + + +class AaaServer(object): + """ Manages aaa configuration """ + + def netconf_get_config(self, **kwargs): + """ Get configure by netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ Set configure by netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + recv_xml = set_nc_config(module, conf_str) + + return recv_xml + + def get_authentication_scheme(self, **kwargs): + """ Get scheme of authentication """ + + module = kwargs["module"] + conf_str = CE_GET_AUTHENTICATION_SCHEME + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*\s*' + r'(.*).*\s*' + r'(.*).*\s*', xml_str) + + if re_find: + return re_find + else: + return result + + def get_authentication_domain(self, **kwargs): + """ Get domain of authentication """ + + module = kwargs["module"] + conf_str = CE_GET_AUTHENTICATION_DOMAIN + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*\s*' + r'(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_authentication_scheme(self, **kwargs): + """ Merge scheme of authentication """ + + authen_scheme_name = kwargs["authen_scheme_name"] + first_authen_mode = kwargs["first_authen_mode"] + module = kwargs["module"] + conf_str = CE_MERGE_AUTHENTICATION_SCHEME % ( + authen_scheme_name, first_authen_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge authentication scheme failed.') + + cmds = [] + cmd = "authentication-scheme %s" % authen_scheme_name + cmds.append(cmd) + cmd = "authentication-mode %s" % first_authen_mode + cmds.append(cmd) + + return cmds + + def merge_authentication_domain(self, **kwargs): + """ Merge domain of authentication """ + + domain_name = kwargs["domain_name"] + authen_scheme_name = kwargs["authen_scheme_name"] + module = kwargs["module"] + conf_str = CE_MERGE_AUTHENTICATION_DOMAIN % ( + domain_name, authen_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge authentication domain failed.') + + cmds = [] + cmd = "domain %s" % domain_name + cmds.append(cmd) + cmd = "authentication-scheme %s" % authen_scheme_name + cmds.append(cmd) + + return cmds + + def create_authentication_scheme(self, **kwargs): + """ Create scheme of authentication """ + + authen_scheme_name = kwargs["authen_scheme_name"] + first_authen_mode = kwargs["first_authen_mode"] + module = kwargs["module"] + conf_str = CE_CREATE_AUTHENTICATION_SCHEME % ( + authen_scheme_name, first_authen_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create authentication scheme failed.') + + cmds = [] + cmd = "authentication-scheme %s" % authen_scheme_name + cmds.append(cmd) + cmd = "authentication-mode %s" % first_authen_mode + cmds.append(cmd) + + return cmds + + def create_authentication_domain(self, **kwargs): + """ Create domain of authentication """ + + domain_name = kwargs["domain_name"] + authen_scheme_name = kwargs["authen_scheme_name"] + module = kwargs["module"] + conf_str = CE_CREATE_AUTHENTICATION_DOMAIN % ( + domain_name, authen_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create authentication domain failed.') + + cmds = [] + cmd = "domain %s" % domain_name + cmds.append(cmd) + cmd = "authentication-scheme %s" % authen_scheme_name + cmds.append(cmd) + + return cmds + + def delete_authentication_scheme(self, **kwargs): + """ Delete scheme of authentication """ + + authen_scheme_name = kwargs["authen_scheme_name"] + first_authen_mode = kwargs["first_authen_mode"] + module = kwargs["module"] + + if authen_scheme_name == "default": + return SUCCESS + + conf_str = CE_DELETE_AUTHENTICATION_SCHEME % ( + authen_scheme_name, first_authen_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete authentication scheme failed.') + + cmds = [] + cmd = "undo authentication-scheme %s" % authen_scheme_name + cmds.append(cmd) + cmd = "authentication-mode none" + cmds.append(cmd) + + return cmds + + def delete_authentication_domain(self, **kwargs): + """ Delete domain of authentication """ + + domain_name = kwargs["domain_name"] + authen_scheme_name = kwargs["authen_scheme_name"] + module = kwargs["module"] + + if domain_name == "default": + return SUCCESS + + conf_str = CE_DELETE_AUTHENTICATION_DOMAIN % ( + domain_name, authen_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete authentication domain failed.') + + cmds = [] + cmd = "undo authentication-scheme" + cmds.append(cmd) + cmd = "undo domain %s" % domain_name + cmds.append(cmd) + + return cmds + + def get_authorization_scheme(self, **kwargs): + """ Get scheme of authorization """ + + module = kwargs["module"] + conf_str = CE_GET_AUTHORIZATION_SCHEME + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*\s*' + r'(.*).*\s*' + r'(.*).*\s*', xml_str) + + if re_find: + return re_find + else: + return result + + def get_authorization_domain(self, **kwargs): + """ Get domain of authorization """ + + module = kwargs["module"] + conf_str = CE_GET_AUTHORIZATION_DOMAIN + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*\s*' + r'(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_authorization_scheme(self, **kwargs): + """ Merge scheme of authorization """ + + author_scheme_name = kwargs["author_scheme_name"] + first_author_mode = kwargs["first_author_mode"] + module = kwargs["module"] + conf_str = CE_MERGE_AUTHORIZATION_SCHEME % ( + author_scheme_name, first_author_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge authorization scheme failed.') + + cmds = [] + cmd = "authorization-scheme %s" % author_scheme_name + cmds.append(cmd) + cmd = "authorization-mode %s" % first_author_mode + cmds.append(cmd) + + return cmds + + def merge_authorization_domain(self, **kwargs): + """ Merge domain of authorization """ + + domain_name = kwargs["domain_name"] + author_scheme_name = kwargs["author_scheme_name"] + module = kwargs["module"] + conf_str = CE_MERGE_AUTHORIZATION_DOMAIN % ( + domain_name, author_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge authorization domain failed.') + + cmds = [] + cmd = "domain %s" % domain_name + cmds.append(cmd) + cmd = "authorization-scheme %s" % author_scheme_name + cmds.append(cmd) + + return cmds + + def create_authorization_scheme(self, **kwargs): + """ Create scheme of authorization """ + + author_scheme_name = kwargs["author_scheme_name"] + first_author_mode = kwargs["first_author_mode"] + module = kwargs["module"] + conf_str = CE_CREATE_AUTHORIZATION_SCHEME % ( + author_scheme_name, first_author_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create authorization scheme failed.') + + cmds = [] + cmd = "authorization-scheme %s" % author_scheme_name + cmds.append(cmd) + cmd = "authorization-mode %s" % first_author_mode + cmds.append(cmd) + + return cmds + + def create_authorization_domain(self, **kwargs): + """ Create domain of authorization """ + + domain_name = kwargs["domain_name"] + author_scheme_name = kwargs["author_scheme_name"] + module = kwargs["module"] + conf_str = CE_CREATE_AUTHORIZATION_DOMAIN % ( + domain_name, author_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create authorization domain failed.') + + cmds = [] + cmd = "domain %s" % domain_name + cmds.append(cmd) + cmd = "authorization-scheme %s" % author_scheme_name + cmds.append(cmd) + + return cmds + + def delete_authorization_scheme(self, **kwargs): + """ Delete scheme of authorization """ + + author_scheme_name = kwargs["author_scheme_name"] + first_author_mode = kwargs["first_author_mode"] + module = kwargs["module"] + + if author_scheme_name == "default": + return SUCCESS + + conf_str = CE_DELETE_AUTHORIZATION_SCHEME % ( + author_scheme_name, first_author_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete authorization scheme failed.') + + cmds = [] + cmd = "undo authorization-scheme %s" % author_scheme_name + cmds.append(cmd) + cmd = "authorization-mode none" + cmds.append(cmd) + + return cmds + + def delete_authorization_domain(self, **kwargs): + """ Delete domain of authorization """ + + domain_name = kwargs["domain_name"] + author_scheme_name = kwargs["author_scheme_name"] + module = kwargs["module"] + + if domain_name == "default": + return SUCCESS + + conf_str = CE_DELETE_AUTHORIZATION_DOMAIN % ( + domain_name, author_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete authorization domain failed.') + + cmds = [] + cmd = "undo authorization-scheme" + cmds.append(cmd) + cmd = "undo domain %s" % domain_name + cmds.append(cmd) + + return cmds + + def get_accounting_scheme(self, **kwargs): + """ Get scheme of accounting """ + + module = kwargs["module"] + conf_str = CE_GET_ACCOUNTING_SCHEME + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall(r'.*(.*)\s*(.*)', xml_str) + if re_find: + return re_find + else: + return result + + def get_accounting_domain(self, **kwargs): + """ Get domain of accounting """ + + module = kwargs["module"] + conf_str = CE_GET_ACCOUNTING_DOMAIN + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*\s*' + r'(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_accounting_scheme(self, **kwargs): + """ Merge scheme of accounting """ + + acct_scheme_name = kwargs["acct_scheme_name"] + accounting_mode = kwargs["accounting_mode"] + module = kwargs["module"] + conf_str = CE_MERGE_ACCOUNTING_SCHEME % ( + acct_scheme_name, accounting_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge accounting scheme failed.') + + cmds = [] + cmd = "accounting-scheme %s" % acct_scheme_name + cmds.append(cmd) + cmd = "accounting-mode %s" % accounting_mode + cmds.append(cmd) + + return cmds + + def merge_accounting_domain(self, **kwargs): + """ Merge domain of accounting """ + + domain_name = kwargs["domain_name"] + acct_scheme_name = kwargs["acct_scheme_name"] + module = kwargs["module"] + conf_str = CE_MERGE_ACCOUNTING_DOMAIN % (domain_name, acct_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge accounting domain failed.') + + cmds = [] + cmd = "domain %s" % domain_name + cmds.append(cmd) + cmd = "accounting-scheme %s" % acct_scheme_name + cmds.append(cmd) + + return cmds + + def create_accounting_scheme(self, **kwargs): + """ Create scheme of accounting """ + + acct_scheme_name = kwargs["acct_scheme_name"] + accounting_mode = kwargs["accounting_mode"] + module = kwargs["module"] + conf_str = CE_CREATE_ACCOUNTING_SCHEME % ( + acct_scheme_name, accounting_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create accounting scheme failed.') + + cmds = [] + cmd = "accounting-scheme %s" % acct_scheme_name + cmds.append(cmd) + cmd = "accounting-mode %s" % accounting_mode + cmds.append(cmd) + + return cmds + + def create_accounting_domain(self, **kwargs): + """ Create domain of accounting """ + + domain_name = kwargs["domain_name"] + acct_scheme_name = kwargs["acct_scheme_name"] + module = kwargs["module"] + conf_str = CE_CREATE_ACCOUNTING_DOMAIN % ( + domain_name, acct_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create accounting domain failed.') + + cmds = [] + cmd = "domain %s" % domain_name + cmds.append(cmd) + cmd = "accounting-scheme %s" % acct_scheme_name + cmds.append(cmd) + + return cmds + + def delete_accounting_scheme(self, **kwargs): + """ Delete scheme of accounting """ + + acct_scheme_name = kwargs["acct_scheme_name"] + accounting_mode = kwargs["accounting_mode"] + module = kwargs["module"] + + if acct_scheme_name == "default": + return SUCCESS + + conf_str = CE_DELETE_ACCOUNTING_SCHEME % ( + acct_scheme_name, accounting_mode) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete accounting scheme failed.') + + cmds = [] + cmd = "undo accounting-scheme %s" % acct_scheme_name + cmds.append(cmd) + cmd = "accounting-mode none" + cmds.append(cmd) + + return cmds + + def delete_accounting_domain(self, **kwargs): + """ Delete domain of accounting """ + + domain_name = kwargs["domain_name"] + acct_scheme_name = kwargs["acct_scheme_name"] + module = kwargs["module"] + + if domain_name == "default": + return SUCCESS + + conf_str = CE_DELETE_ACCOUNTING_DOMAIN % ( + domain_name, acct_scheme_name) + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete accounting domain failed.') + + cmds = [] + cmd = "undo domain %s" % domain_name + cmds.append(cmd) + cmd = "undo accounting-scheme" + cmds.append(cmd) + + return cmds + + def get_radius_template(self, **kwargs): + """ Get radius template """ + + module = kwargs["module"] + conf_str = CE_GET_RADIUS_TEMPLATE + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_radius_template(self, **kwargs): + """ Merge radius template """ + + radius_server_group = kwargs["radius_server_group"] + module = kwargs["module"] + conf_str = CE_MERGE_RADIUS_TEMPLATE % radius_server_group + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge radius template failed.') + + cmds = [] + cmd = "radius server group %s" % radius_server_group + cmds.append(cmd) + + return cmds + + def create_radius_template(self, **kwargs): + """ Create radius template """ + + radius_server_group = kwargs["radius_server_group"] + module = kwargs["module"] + conf_str = CE_CREATE_RADIUS_TEMPLATE % radius_server_group + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create radius template failed.') + + cmds = [] + cmd = "radius server group %s" % radius_server_group + cmds.append(cmd) + + return cmds + + def delete_radius_template(self, **kwargs): + """ Delete radius template """ + + radius_server_group = kwargs["radius_server_group"] + module = kwargs["module"] + conf_str = CE_DELETE_RADIUS_TEMPLATE % radius_server_group + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete radius template failed.') + + cmds = [] + cmd = "undo radius server group %s" % radius_server_group + cmds.append(cmd) + + return cmds + + def get_radius_client(self, **kwargs): + """ Get radius client """ + + module = kwargs["module"] + conf_str = CE_GET_RADIUS_CLIENT + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_radius_client(self, **kwargs): + """ Merge radius client """ + + enable = kwargs["isEnable"] + module = kwargs["module"] + conf_str = CE_MERGE_RADIUS_CLIENT % enable + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge radius client failed.') + + cmds = [] + if enable == "true": + cmd = "radius enable" + else: + cmd = "undo radius enable" + cmds.append(cmd) + + return cmds + + def get_hwtacacs_template(self, **kwargs): + """ Get hwtacacs template """ + + module = kwargs["module"] + conf_str = CE_GET_HWTACACS_TEMPLATE + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_hwtacacs_template(self, **kwargs): + """ Merge hwtacacs template """ + + hwtacas_template = kwargs["hwtacas_template"] + module = kwargs["module"] + conf_str = CE_MERGE_HWTACACS_TEMPLATE % hwtacas_template + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge hwtacacs template failed.') + + cmds = [] + cmd = "hwtacacs server template %s" % hwtacas_template + cmds.append(cmd) + + return cmds + + def create_hwtacacs_template(self, **kwargs): + """ Create hwtacacs template """ + + hwtacas_template = kwargs["hwtacas_template"] + module = kwargs["module"] + conf_str = CE_CREATE_HWTACACS_TEMPLATE % hwtacas_template + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Create hwtacacs template failed.') + + cmds = [] + cmd = "hwtacacs server template %s" % hwtacas_template + cmds.append(cmd) + + return cmds + + def delete_hwtacacs_template(self, **kwargs): + """ Delete hwtacacs template """ + + hwtacas_template = kwargs["hwtacas_template"] + module = kwargs["module"] + conf_str = CE_DELETE_HWTACACS_TEMPLATE % hwtacas_template + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete hwtacacs template failed.') + + cmds = [] + cmd = "undo hwtacacs server template %s" % hwtacas_template + cmds.append(cmd) + + return cmds + + def get_hwtacacs_global_cfg(self, **kwargs): + """ Get hwtacacs global configure """ + + module = kwargs["module"] + conf_str = CE_GET_HWTACACS_GLOBAL_CFG + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_hwtacacs_global_cfg(self, **kwargs): + """ Merge hwtacacs global configure """ + + enable = kwargs["isEnable"] + module = kwargs["module"] + conf_str = CE_MERGE_HWTACACS_GLOBAL_CFG % enable + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge hwtacacs global config failed.') + + cmds = [] + + if enable == "true": + cmd = "hwtacacs enable" + else: + cmd = "undo hwtacacs enable" + cmds.append(cmd) + + return cmds + + def get_local_user_group(self, **kwargs): + """ Get local user group """ + + module = kwargs["module"] + conf_str = CE_GET_LOCAL_USER_GROUP + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_local_user_group(self, **kwargs): + """ Merge local user group """ + + local_user_group = kwargs["local_user_group"] + module = kwargs["module"] + conf_str = CE_MERGE_LOCAL_USER_GROUP % local_user_group + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Merge local user group failed.') + + cmds = [] + cmd = "user-group %s" % local_user_group + cmds.append(cmd) + + return cmds + + def delete_local_user_group(self, **kwargs): + """ Delete local user group """ + + local_user_group = kwargs["local_user_group"] + module = kwargs["module"] + conf_str = CE_DELETE_LOCAL_USER_GROUP % local_user_group + + xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in xml: + module.fail_json(msg='Error: Delete local user group failed.') + + cmds = [] + cmd = "undo user-group %s" % local_user_group + cmds.append(cmd) + + return cmds + + +def check_name(**kwargs): + """ Check invalid name """ + + module = kwargs["module"] + name = kwargs["name"] + invalid_char = kwargs["invalid_char"] + + for item in invalid_char: + if item in name: + module.fail_json( + msg='Error: invalid char %s is in the name %s.' % (item, name)) + + +def check_module_argument(**kwargs): + """ Check module argument """ + + module = kwargs["module"] + + authen_scheme_name = module.params['authen_scheme_name'] + author_scheme_name = module.params['author_scheme_name'] + acct_scheme_name = module.params['acct_scheme_name'] + domain_name = module.params['domain_name'] + radius_server_group = module.params['radius_server_group'] + hwtacas_template = module.params['hwtacas_template'] + local_user_group = module.params['local_user_group'] + + if authen_scheme_name: + if len(authen_scheme_name) > 32: + module.fail_json( + msg='Error: authen_scheme_name %s ' + 'is large than 32.' % authen_scheme_name) + check_name(module=module, name=authen_scheme_name, + invalid_char=INVALID_SCHEME_CHAR) + + if author_scheme_name: + if len(author_scheme_name) > 32: + module.fail_json( + msg='Error: author_scheme_name %s ' + 'is large than 32.' % author_scheme_name) + check_name(module=module, name=author_scheme_name, + invalid_char=INVALID_SCHEME_CHAR) + + if acct_scheme_name: + if len(acct_scheme_name) > 32: + module.fail_json( + msg='Error: acct_scheme_name %s ' + 'is large than 32.' % acct_scheme_name) + check_name(module=module, name=acct_scheme_name, + invalid_char=INVALID_SCHEME_CHAR) + + if domain_name: + if len(domain_name) > 64: + module.fail_json( + msg='Error: domain_name %s ' + 'is large than 64.' % domain_name) + check_name(module=module, name=domain_name, + invalid_char=INVALID_DOMAIN_CHAR) + if domain_name == "-" or domain_name == "--": + module.fail_json(msg='domain_name %s ' + 'is invalid.' % domain_name) + + if radius_server_group and len(radius_server_group) > 32: + module.fail_json(msg='Error: radius_server_group %s ' + 'is large than 32.' % radius_server_group) + + if hwtacas_template and len(hwtacas_template) > 32: + module.fail_json( + msg='Error: hwtacas_template %s ' + 'is large than 32.' % hwtacas_template) + + if local_user_group: + if len(local_user_group) > 32: + module.fail_json( + msg='Error: local_user_group %s ' + 'is large than 32.' % local_user_group) + check_name(module=module, name=local_user_group, invalid_char=INVALID_GROUP_CHAR) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + authen_scheme_name=dict(type='str'), + first_authen_mode=dict(default='local', choices=['invalid', 'local', 'hwtacacs', 'radius', 'none']), + author_scheme_name=dict(type='str'), + first_author_mode=dict(default='local', choices=['invalid', 'local', 'hwtacacs', 'if-authenticated', 'none']), + acct_scheme_name=dict(type='str'), + accounting_mode=dict(default='none', choices=['invalid', 'hwtacacs', 'radius', 'none']), + domain_name=dict(type='str'), + radius_server_group=dict(type='str'), + hwtacas_template=dict(type='str'), + local_user_group=dict(type='str') + ) + + argument_spec.update(ce_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + check_module_argument(module=module) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + state = module.params['state'] + authen_scheme_name = module.params['authen_scheme_name'] + first_authen_mode = module.params['first_authen_mode'] + author_scheme_name = module.params['author_scheme_name'] + first_author_mode = module.params['first_author_mode'] + acct_scheme_name = module.params['acct_scheme_name'] + accounting_mode = module.params['accounting_mode'] + domain_name = module.params['domain_name'] + radius_server_group = module.params['radius_server_group'] + hwtacas_template = module.params['hwtacas_template'] + local_user_group = module.params['local_user_group'] + + ce_aaa_server = AaaServer() + + if not ce_aaa_server: + module.fail_json(msg='Error: init module failed.') + + # get proposed + proposed["state"] = state + if authen_scheme_name: + proposed["authen_scheme_name"] = authen_scheme_name + if first_authen_mode: + proposed["first_authen_mode"] = first_authen_mode + if author_scheme_name: + proposed["author_scheme_name"] = author_scheme_name + if first_author_mode: + proposed["first_author_mode"] = first_author_mode + if acct_scheme_name: + proposed["acct_scheme_name"] = acct_scheme_name + if accounting_mode: + proposed["accounting_mode"] = accounting_mode + if domain_name: + proposed["domain_name"] = domain_name + if radius_server_group: + proposed["radius_server_group"] = radius_server_group + if hwtacas_template: + proposed["hwtacas_template"] = hwtacas_template + if local_user_group: + proposed["local_user_group"] = local_user_group + + # authentication + if authen_scheme_name: + + scheme_exist = ce_aaa_server.get_authentication_scheme(module=module) + scheme_new = (authen_scheme_name.lower(), first_authen_mode.lower(), "invalid") + + existing["authentication scheme"] = scheme_exist + + if state == "present": + # present authentication scheme + if len(scheme_exist) == 0: + cmd = ce_aaa_server.create_authentication_scheme( + module=module, + authen_scheme_name=authen_scheme_name, + first_authen_mode=first_authen_mode) + + updates.append(cmd) + changed = True + + elif scheme_new not in scheme_exist: + cmd = ce_aaa_server.merge_authentication_scheme( + module=module, + authen_scheme_name=authen_scheme_name, + first_authen_mode=first_authen_mode) + updates.append(cmd) + changed = True + + # present authentication domain + if domain_name: + domain_exist = ce_aaa_server.get_authentication_domain( + module=module) + domain_new = (domain_name.lower(), authen_scheme_name.lower()) + + if len(domain_exist) == 0: + cmd = ce_aaa_server.create_authentication_domain( + module=module, + domain_name=domain_name, + authen_scheme_name=authen_scheme_name) + updates.append(cmd) + changed = True + + elif domain_new not in domain_exist: + cmd = ce_aaa_server.merge_authentication_domain( + module=module, + domain_name=domain_name, + authen_scheme_name=authen_scheme_name) + updates.append(cmd) + changed = True + + else: + # absent authentication scheme + if not domain_name: + if len(scheme_exist) == 0: + pass + elif scheme_new not in scheme_exist: + pass + else: + cmd = ce_aaa_server.delete_authentication_scheme( + module=module, + authen_scheme_name=authen_scheme_name, + first_authen_mode=first_authen_mode) + updates.append(cmd) + changed = True + + # absent authentication domain + else: + domain_exist = ce_aaa_server.get_authentication_domain( + module=module) + domain_new = (domain_name.lower(), authen_scheme_name.lower()) + + if len(domain_exist) == 0: + pass + elif domain_new not in domain_exist: + pass + else: + cmd = ce_aaa_server.delete_authentication_domain( + module=module, + domain_name=domain_name, + authen_scheme_name=authen_scheme_name) + updates.append(cmd) + changed = True + + scheme_end = ce_aaa_server.get_authentication_scheme(module=module) + end_state["authentication scheme"] = scheme_end + + # authorization + if author_scheme_name: + + scheme_exist = ce_aaa_server.get_authorization_scheme(module=module) + scheme_new = (author_scheme_name.lower(), first_author_mode.lower(), "invalid") + + existing["authorization scheme"] = scheme_exist + + if state == "present": + # present authorization scheme + if len(scheme_exist) == 0: + cmd = ce_aaa_server.create_authorization_scheme( + module=module, + author_scheme_name=author_scheme_name, + first_author_mode=first_author_mode) + updates.append(cmd) + changed = True + elif scheme_new not in scheme_exist: + cmd = ce_aaa_server.merge_authorization_scheme( + module=module, + author_scheme_name=author_scheme_name, + first_author_mode=first_author_mode) + updates.append(cmd) + changed = True + + # present authorization domain + if domain_name: + domain_exist = ce_aaa_server.get_authorization_domain( + module=module) + domain_new = (domain_name.lower(), author_scheme_name.lower()) + + if len(domain_exist) == 0: + cmd = ce_aaa_server.create_authorization_domain( + module=module, + domain_name=domain_name, + author_scheme_name=author_scheme_name) + updates.append(cmd) + changed = True + elif domain_new not in domain_exist: + cmd = ce_aaa_server.merge_authorization_domain( + module=module, + domain_name=domain_name, + author_scheme_name=author_scheme_name) + updates.append(cmd) + changed = True + + else: + # absent authorization scheme + if not domain_name: + if len(scheme_exist) == 0: + pass + elif scheme_new not in scheme_exist: + pass + else: + cmd = ce_aaa_server.delete_authorization_scheme( + module=module, + author_scheme_name=author_scheme_name, + first_author_mode=first_author_mode) + updates.append(cmd) + changed = True + + # absent authorization domain + else: + domain_exist = ce_aaa_server.get_authorization_domain( + module=module) + domain_new = (domain_name.lower(), author_scheme_name.lower()) + + if len(domain_exist) == 0: + pass + elif domain_new not in domain_exist: + pass + else: + cmd = ce_aaa_server.delete_authorization_domain( + module=module, + domain_name=domain_name, + author_scheme_name=author_scheme_name) + updates.append(cmd) + changed = True + + scheme_end = ce_aaa_server.get_authorization_scheme(module=module) + end_state["authorization scheme"] = scheme_end + + # accounting + if acct_scheme_name: + + scheme_exist = ce_aaa_server.get_accounting_scheme(module=module) + scheme_new = (acct_scheme_name.lower(), accounting_mode.lower()) + + existing["accounting scheme"] = scheme_exist + + if state == "present": + # present accounting scheme + if len(scheme_exist) == 0: + cmd = ce_aaa_server.create_accounting_scheme( + module=module, + acct_scheme_name=acct_scheme_name, + accounting_mode=accounting_mode) + updates.append(cmd) + changed = True + elif scheme_new not in scheme_exist: + cmd = ce_aaa_server.merge_accounting_scheme( + module=module, + acct_scheme_name=acct_scheme_name, + accounting_mode=accounting_mode) + updates.append(cmd) + changed = True + + # present accounting domain + if domain_name: + domain_exist = ce_aaa_server.get_accounting_domain( + module=module) + domain_new = (domain_name.lower(), acct_scheme_name.lower()) + + if len(domain_exist) == 0: + cmd = ce_aaa_server.create_accounting_domain( + module=module, + domain_name=domain_name, + acct_scheme_name=acct_scheme_name) + updates.append(cmd) + changed = True + elif domain_new not in domain_exist: + cmd = ce_aaa_server.merge_accounting_domain( + module=module, + domain_name=domain_name, + acct_scheme_name=acct_scheme_name) + updates.append(cmd) + changed = True + + else: + # absent accounting scheme + if not domain_name: + if len(scheme_exist) == 0: + pass + elif scheme_new not in scheme_exist: + pass + else: + cmd = ce_aaa_server.delete_accounting_scheme( + module=module, + acct_scheme_name=acct_scheme_name, + accounting_mode=accounting_mode) + updates.append(cmd) + changed = True + + # absent accounting domain + else: + domain_exist = ce_aaa_server.get_accounting_domain( + module=module) + domain_new = (domain_name.lower(), acct_scheme_name.lower()) + if len(domain_exist) == 0: + pass + elif domain_new not in domain_exist: + pass + else: + cmd = ce_aaa_server.delete_accounting_domain( + module=module, + domain_name=domain_name, + acct_scheme_name=acct_scheme_name) + updates.append(cmd) + changed = True + + scheme_end = ce_aaa_server.get_accounting_scheme(module=module) + end_state["accounting scheme"] = scheme_end + + # radius group name + if (authen_scheme_name and first_authen_mode.lower() == "radius") \ + or (acct_scheme_name and accounting_mode.lower() == "radius"): + + if not radius_server_group: + module.fail_json(msg='please input radius_server_group when use radius.') + + rds_template_exist = ce_aaa_server.get_radius_template(module=module) + rds_template_new = (radius_server_group) + + rds_enable_exist = ce_aaa_server.get_radius_client(module=module) + + existing["radius template"] = rds_template_exist + existing["radius enable"] = rds_enable_exist + + if state == "present": + # present radius group name + if len(rds_template_exist) == 0: + cmd = ce_aaa_server.create_radius_template( + module=module, radius_server_group=radius_server_group) + updates.append(cmd) + changed = True + elif rds_template_new not in rds_template_exist: + cmd = ce_aaa_server.merge_radius_template( + module=module, radius_server_group=radius_server_group) + updates.append(cmd) + changed = True + + rds_enable_new = ("true") + if rds_enable_new not in rds_enable_exist: + cmd = ce_aaa_server.merge_radius_client( + module=module, isEnable="true") + updates.append(cmd) + changed = True + + else: + # absent radius group name + if len(rds_template_exist) == 0: + pass + elif rds_template_new not in rds_template_exist: + pass + else: + cmd = ce_aaa_server.delete_radius_template( + module=module, radius_server_group=radius_server_group) + updates.append(cmd) + changed = True + + rds_enable_new = ("false") + if rds_enable_new not in rds_enable_exist: + cmd = ce_aaa_server.merge_radius_client( + module=module, isEnable="false") + updates.append(cmd) + changed = True + else: + pass + + rds_template_end = ce_aaa_server.get_radius_template(module=module) + end_state["radius template"] = rds_template_end + + rds_enable_end = ce_aaa_server.get_radius_client(module=module) + end_state["radius enable"] = rds_enable_end + + tmp_scheme = author_scheme_name + + # hwtacas template + if (authen_scheme_name and first_authen_mode.lower() == "hwtacacs") \ + or (tmp_scheme and first_author_mode.lower() == "hwtacacs") \ + or (acct_scheme_name and accounting_mode.lower() == "hwtacacs"): + + if not hwtacas_template: + module.fail_json( + msg='please input hwtacas_template when use hwtacas.') + + hwtacacs_exist = ce_aaa_server.get_hwtacacs_template(module=module) + hwtacacs_new = (hwtacas_template) + + hwtacacs_enbale_exist = ce_aaa_server.get_hwtacacs_global_cfg( + module=module) + + existing["hwtacacs template"] = hwtacacs_exist + existing["hwtacacs enable"] = hwtacacs_enbale_exist + + if state == "present": + # present hwtacas template + if len(hwtacacs_exist) == 0: + cmd = ce_aaa_server.create_hwtacacs_template( + module=module, hwtacas_template=hwtacas_template) + updates.append(cmd) + changed = True + elif hwtacacs_new not in hwtacacs_exist: + cmd = ce_aaa_server.merge_hwtacacs_template( + module=module, hwtacas_template=hwtacas_template) + updates.append(cmd) + changed = True + + hwtacacs_enbale_new = ("true") + if hwtacacs_enbale_new not in hwtacacs_enbale_exist: + cmd = ce_aaa_server.merge_hwtacacs_global_cfg( + module=module, isEnable="true") + updates.append(cmd) + changed = True + + else: + # absent hwtacas template + if len(hwtacacs_exist) == 0: + pass + elif hwtacacs_new not in hwtacacs_exist: + pass + else: + cmd = ce_aaa_server.delete_hwtacacs_template( + module=module, hwtacas_template=hwtacas_template) + updates.append(cmd) + changed = True + + hwtacacs_enbale_new = ("false") + if hwtacacs_enbale_new not in hwtacacs_enbale_exist: + cmd = ce_aaa_server.merge_hwtacacs_global_cfg( + module=module, isEnable="false") + updates.append(cmd) + changed = True + else: + pass + + hwtacacs_end = ce_aaa_server.get_hwtacacs_template(module=module) + end_state["hwtacacs template"] = hwtacacs_end + + hwtacacs_enable_end = ce_aaa_server.get_hwtacacs_global_cfg( + module=module) + end_state["hwtacacs enable"] = hwtacacs_enable_end + + # local user group + if local_user_group: + + user_group_exist = ce_aaa_server.get_local_user_group(module=module) + user_group_new = (local_user_group) + + existing["local user group"] = user_group_exist + + if state == "present": + # present local user group + if len(user_group_exist) == 0: + cmd = ce_aaa_server.merge_local_user_group( + module=module, local_user_group=local_user_group) + updates.append(cmd) + changed = True + elif user_group_new not in user_group_exist: + cmd = ce_aaa_server.merge_local_user_group( + module=module, local_user_group=local_user_group) + updates.append(cmd) + changed = True + + else: + # absent local user group + if len(user_group_exist) == 0: + pass + elif user_group_new not in user_group_exist: + pass + else: + cmd = ce_aaa_server.delete_local_user_group( + module=module, local_user_group=local_user_group) + updates.append(cmd) + changed = True + + user_group_end = ce_aaa_server.get_local_user_group(module=module) + end_state["local user group"] = user_group_end + + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_aaa_server_host.py b/plugins/modules/ce_aaa_server_host.py new file mode 100644 index 0000000..f337734 --- /dev/null +++ b/plugins/modules/ce_aaa_server_host.py @@ -0,0 +1,2637 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_aaa_server_host +short_description: Manages AAA server host configuration on HUAWEI CloudEngine switches. +description: + - Manages AAA server host configuration on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present', 'absent'] + local_user_name: + description: + - Name of a local user. + The value is a string of 1 to 253 characters. + local_password: + description: + - Login password of a user. The password can contain letters, numbers, and special characters. + The value is a string of 1 to 255 characters. + local_service_type: + description: + - The type of local user login through, such as ftp ssh snmp telnet. + local_ftp_dir: + description: + - FTP user directory. + The value is a string of 1 to 255 characters. + local_user_level: + description: + - Login level of a local user. + The value is an integer ranging from 0 to 15. + local_user_group: + description: + - Name of the user group where the user belongs. The user inherits all the rights of the user group. + The value is a string of 1 to 32 characters. + radius_group_name: + description: + - RADIUS server group's name. + The value is a string of 1 to 32 case-insensitive characters. + radius_server_type: + description: + - Type of Radius Server. + choices: ['Authentication', 'Accounting'] + radius_server_ip: + description: + - IPv4 address of configured server. + The value is a string of 0 to 255 characters, in dotted decimal notation. + radius_server_ipv6: + description: + - IPv6 address of configured server. + The total length is 128 bits. + radius_server_port: + description: + - Configured server port for a particular server. + The value is an integer ranging from 1 to 65535. + radius_server_mode: + description: + - Configured primary or secondary server for a particular server. + choices: ['Secondary-server', 'Primary-server'] + radius_vpn_name: + description: + - Set VPN instance. + The value is a string of 1 to 31 case-sensitive characters. + radius_server_name: + description: + - Hostname of configured server. + The value is a string of 0 to 255 case-sensitive characters. + hwtacacs_template: + description: + - Name of a HWTACACS template. + The value is a string of 1 to 32 case-insensitive characters. + hwtacacs_server_ip: + description: + - Server IPv4 address. Must be a valid unicast IP address. + The value is a string of 0 to 255 characters, in dotted decimal notation. + hwtacacs_server_ipv6: + description: + - Server IPv6 address. Must be a valid unicast IP address. + The total length is 128 bits. + hwtacacs_server_type: + description: + - Hwtacacs server type. + choices: ['Authentication', 'Authorization', 'Accounting', 'Common'] + hwtacacs_is_secondary_server: + description: + - Whether the server is secondary. + type: bool + default: 'no' + hwtacacs_vpn_name: + description: + - VPN instance name. + hwtacacs_is_public_net: + description: + - Set the public-net. + type: bool + default: 'no' + hwtacacs_server_host_name: + description: + - Hwtacacs server host name. +''' + +EXAMPLES = ''' + +- name: AAA server host test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config local user when use local scheme" + ce_aaa_server_host: + state: present + local_user_name: user1 + local_password: 123456 + provider: "{{ cli }}" + + - name: "Undo local user when use local scheme" + ce_aaa_server_host: + state: absent + local_user_name: user1 + local_password: 123456 + provider: "{{ cli }}" + + - name: "Config radius server ip" + ce_aaa_server_host: + state: present + radius_group_name: group1 + radius_server_type: Authentication + radius_server_ip: 10.1.10.1 + radius_server_port: 2000 + radius_server_mode: Primary-server + radius_vpn_name: _public_ + provider: "{{ cli }}" + + - name: "Undo radius server ip" + ce_aaa_server_host: + state: absent + radius_group_name: group1 + radius_server_type: Authentication + radius_server_ip: 10.1.10.1 + radius_server_port: 2000 + radius_server_mode: Primary-server + radius_vpn_name: _public_ + provider: "{{ cli }}" + + - name: "Config hwtacacs server ip" + ce_aaa_server_host: + state: present + hwtacacs_template: template + hwtacacs_server_ip: 10.10.10.10 + hwtacacs_server_type: Authorization + hwtacacs_vpn_name: _public_ + provider: "{{ cli }}" + + - name: "Undo hwtacacs server ip" + ce_aaa_server_host: + state: absent + hwtacacs_template: template + hwtacacs_server_ip: 10.10.10.10 + hwtacacs_server_type: Authorization + hwtacacs_vpn_name: _public_ + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"hwtacacs_is_public_net": "false", + "hwtacacs_is_secondary_server": "false", + "hwtacacs_server_ip": "10.135.182.157", + "hwtacacs_server_type": "Authorization", + "hwtacacs_template": "wdz", + "hwtacacs_vpn_name": "_public_", + "local_password": "******", + "state": "present"} +existing: + description: k/v pairs of existing aaa server host + returned: always + type: dict + sample: {"radius server ipv4": []} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"radius server ipv4": [ + [ + "10.1.10.1", + "Authentication", + "2000", + "Primary-server", + "_public_" + ] + ]} +updates: + description: command sent to the device + returned: always + type: list + sample: ["hwtacacs server template test", + "hwtacacs server authorization 10.135.182.157 vpn-instance test_vpn public-net"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + +SUCCESS = """success""" +FAILED = """failed""" + +INVALID_USER_NAME_CHAR = [' ', '/', '\\', + ':', '*', '?', '"', '\'', '<', '>', '%'] + +# get local user name +CE_GET_LOCAL_USER_INFO_HEADER = """ + + + + + + + +""" +CE_GET_LOCAL_USER_INFO_TAIL = """ + + + + + +""" + +# merge local user name +CE_MERGE_LOCAL_USER_INFO_HEADER = """ + + + + + + %s +""" +CE_MERGE_LOCAL_USER_INFO_TAIL = """ + + + + + +""" + +# delete local user name +CE_DELETE_LOCAL_USER_INFO_HEADER = """ + + + + + + %s +""" +CE_DELETE_LOCAL_USER_INFO_TAIL = """ + + + + + +""" + +# get radius server config ipv4 +CE_GET_RADIUS_SERVER_CFG_IPV4 = """ + + + + + %s + + + + + + + + + + + + + +""" + +# merge radius server config ipv4 +CE_MERGE_RADIUS_SERVER_CFG_IPV4 = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + +# delete radius server config ipv4 +CE_DELETE_RADIUS_SERVER_CFG_IPV4 = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + +# get radius server config ipv6 +CE_GET_RADIUS_SERVER_CFG_IPV6 = """ + + + + + %s + + + + + + + + + + + + +""" + +# merge radius server config ipv6 +CE_MERGE_RADIUS_SERVER_CFG_IPV6 = """ + + + + + %s + + + %s + %s + %s + %s + + + + + + +""" + +# delete radius server config ipv6 +CE_DELETE_RADIUS_SERVER_CFG_IPV6 = """ + + + + + %s + + + %s + %s + %s + %s + + + + + + +""" + +# get radius server name +CE_GET_RADIUS_SERVER_NAME = """ + + + + + %s + + + + + + + + + + + + + +""" + +# merge radius server name +CE_MERGE_RADIUS_SERVER_NAME = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + +# delete radius server name +CE_DELETE_RADIUS_SERVER_NAME = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + +# get hwtacacs server config ipv4 +CE_GET_HWTACACS_SERVER_CFG_IPV4 = """ + + + + + %s + + + + + + + + + + + + + +""" + +# merge hwtacacs server config ipv4 +CE_MERGE_HWTACACS_SERVER_CFG_IPV4 = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + +# delete hwtacacs server config ipv4 +CE_DELETE_HWTACACS_SERVER_CFG_IPV4 = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + +# get hwtacacs server config ipv6 +CE_GET_HWTACACS_SERVER_CFG_IPV6 = """ + + + + + %s + + + + + + + + + + + + +""" + +# merge hwtacacs server config ipv6 +CE_MERGE_HWTACACS_SERVER_CFG_IPV6 = """ + + + + + %s + + + %s + %s + %s + %s + + + + + + +""" + +# delete hwtacacs server config ipv6 +CE_DELETE_HWTACACS_SERVER_CFG_IPV6 = """ + + + + + %s + + + %s + %s + %s + %s + + + + + + +""" + +# get hwtacacs host server config +CE_GET_HWTACACS_HOST_SERVER_CFG = """ + + + + + %s + + + + + + + + + + + + + +""" + +# merge hwtacacs host server config +CE_MERGE_HWTACACS_HOST_SERVER_CFG = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + +# delete hwtacacs host server config +CE_DELETE_HWTACACS_HOST_SERVER_CFG = """ + + + + + %s + + + %s + %s + %s + %s + %s + + + + + + +""" + + +class AaaServerHost(object): + """ Manages aaa server host configuration """ + + def netconf_get_config(self, **kwargs): + """ Get configure by netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ Set configure by netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + recv_xml = set_nc_config(module, conf_str) + + return recv_xml + + def get_local_user_info(self, **kwargs): + """ Get local user information """ + + module = kwargs["module"] + local_user_name = module.params['local_user_name'] + local_service_type = module.params['local_service_type'] + local_ftp_dir = module.params['local_ftp_dir'] + local_user_level = module.params['local_user_level'] + local_user_group = module.params['local_user_group'] + state = module.params['state'] + + result = dict() + result["local_user_info"] = [] + need_cfg = False + + conf_str = CE_GET_LOCAL_USER_INFO_HEADER + + if local_service_type: + if local_service_type == "none": + conf_str += "" + conf_str += "" + conf_str += "" + conf_str += "" + conf_str += "" + conf_str += "" + elif local_service_type == "dot1x": + conf_str += "" + else: + option = local_service_type.split(" ") + for tmp in option: + if tmp == "dot1x": + module.fail_json( + msg='Error: Do not input dot1x with other service type.') + elif tmp == "none": + module.fail_json( + msg='Error: Do not input none with other service type.') + elif tmp == "ftp": + conf_str += "" + elif tmp == "snmp": + conf_str += "" + elif tmp == "ssh": + conf_str += "" + elif tmp == "telnet": + conf_str += "" + elif tmp == "terminal": + conf_str += "" + else: + module.fail_json( + msg='Error: Do not support the type [%s].' % tmp) + + if local_ftp_dir: + conf_str += "" + + if local_user_level: + conf_str += "" + + if local_user_group: + conf_str += "" + + conf_str += CE_GET_LOCAL_USER_INFO_TAIL + + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + local_user_info = root.findall("aaa/lam/users/user") + if local_user_info: + for tmp in local_user_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["userName", "password", "userLevel", "ftpDir", "userGroupName", + "serviceTerminal", "serviceTelnet", "serviceFtp", "serviceSsh", + "serviceSnmp", "serviceDot1x"]: + tmp_dict[site.tag] = site.text + + result["local_user_info"].append(tmp_dict) + + if state == "present": + need_cfg = True + else: + if result["local_user_info"]: + for tmp in result["local_user_info"]: + if "userName" in tmp.keys(): + if tmp["userName"] == local_user_name: + + if not local_service_type and not local_user_level \ + and not local_ftp_dir and not local_user_group: + + need_cfg = True + + if local_service_type: + if local_service_type == "none": + if tmp.get("serviceTerminal") == "true" or \ + tmp.get("serviceTelnet") == "true" or \ + tmp.get("serviceFtp") == "true" or \ + tmp.get("serviceSsh") == "true" or \ + tmp.get("serviceSnmp") == "true" or \ + tmp.get("serviceDot1x") == "true": + need_cfg = True + elif local_service_type == "dot1x": + if tmp.get("serviceDot1x") == "true": + need_cfg = True + elif tmp == "ftp": + if tmp.get("serviceFtp") == "true": + need_cfg = True + elif tmp == "snmp": + if tmp.get("serviceSnmp") == "true": + need_cfg = True + elif tmp == "ssh": + if tmp.get("serviceSsh") == "true": + need_cfg = True + elif tmp == "telnet": + if tmp.get("serviceTelnet") == "true": + need_cfg = True + elif tmp == "terminal": + if tmp.get("serviceTerminal") == "true": + need_cfg = True + + if local_user_level: + if tmp.get("userLevel") == local_user_level: + need_cfg = True + + if local_ftp_dir: + if tmp.get("ftpDir") == local_ftp_dir: + need_cfg = True + + if local_user_group: + if tmp.get("userGroupName") == local_user_group: + need_cfg = True + + break + + result["need_cfg"] = need_cfg + return result + + def merge_local_user_info(self, **kwargs): + """ Merge local user information by netconf """ + + module = kwargs["module"] + local_user_name = module.params['local_user_name'] + local_password = module.params['local_password'] + local_service_type = module.params['local_service_type'] + local_ftp_dir = module.params['local_ftp_dir'] + local_user_level = module.params['local_user_level'] + local_user_group = module.params['local_user_group'] + state = module.params['state'] + + cmds = [] + + conf_str = CE_MERGE_LOCAL_USER_INFO_HEADER % local_user_name + + if local_password: + conf_str += "%s" % local_password + + if state == "present": + cmd = "local-user %s password cipher %s" % ( + local_user_name, local_password) + cmds.append(cmd) + + if local_service_type: + if local_service_type == "none": + conf_str += "false" + conf_str += "false" + conf_str += "false" + conf_str += "false" + conf_str += "false" + conf_str += "false" + + cmd = "local-user %s service-type none" % local_user_name + cmds.append(cmd) + + elif local_service_type == "dot1x": + if state == "present": + conf_str += "true" + cmd = "local-user %s service-type dot1x" % local_user_name + else: + conf_str += "false" + cmd = "undo local-user %s service-type" % local_user_name + + cmds.append(cmd) + + else: + option = local_service_type.split(" ") + for tmp in option: + if tmp == "dot1x": + module.fail_json( + msg='Error: Do not input dot1x with other service type.') + if tmp == "none": + module.fail_json( + msg='Error: Do not input none with other service type.') + + if state == "present": + if tmp == "ftp": + conf_str += "true" + cmd = "local-user %s service-type ftp" % local_user_name + elif tmp == "snmp": + conf_str += "true" + cmd = "local-user %s service-type snmp" % local_user_name + elif tmp == "ssh": + conf_str += "true" + cmd = "local-user %s service-type ssh" % local_user_name + elif tmp == "telnet": + conf_str += "true" + cmd = "local-user %s service-type telnet" % local_user_name + elif tmp == "terminal": + conf_str += "true" + cmd = "local-user %s service-type terminal" % local_user_name + + cmds.append(cmd) + + else: + if tmp == "ftp": + conf_str += "false" + elif tmp == "snmp": + conf_str += "false" + elif tmp == "ssh": + conf_str += "false" + elif tmp == "telnet": + conf_str += "false" + elif tmp == "terminal": + conf_str += "false" + + if state == "absent": + cmd = "undo local-user %s service-type" % local_user_name + cmds.append(cmd) + + if local_ftp_dir: + if state == "present": + conf_str += "%s" % local_ftp_dir + cmd = "local-user %s ftp-directory %s" % ( + local_user_name, local_ftp_dir) + cmds.append(cmd) + else: + conf_str += "" + cmd = "undo local-user %s ftp-directory" % local_user_name + cmds.append(cmd) + + if local_user_level: + if state == "present": + conf_str += "%s" % local_user_level + cmd = "local-user %s level %s" % ( + local_user_name, local_user_level) + cmds.append(cmd) + else: + conf_str += "" + cmd = "undo local-user %s level" % local_user_name + cmds.append(cmd) + + if local_user_group: + if state == "present": + conf_str += "%s" % local_user_group + cmd = "local-user %s user-group %s" % ( + local_user_name, local_user_group) + cmds.append(cmd) + else: + conf_str += "" + cmd = "undo local-user %s user-group" % local_user_name + cmds.append(cmd) + + conf_str += CE_MERGE_LOCAL_USER_INFO_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge local user info failed.') + + return cmds + + def delete_local_user_info(self, **kwargs): + """ Delete local user information by netconf """ + + module = kwargs["module"] + local_user_name = module.params['local_user_name'] + conf_str = CE_DELETE_LOCAL_USER_INFO_HEADER % local_user_name + conf_str += CE_DELETE_LOCAL_USER_INFO_TAIL + + cmds = [] + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete local user info failed.') + + cmd = "undo local-user %s" % local_user_name + cmds.append(cmd) + + return cmds + + def get_radius_server_cfg_ipv4(self, **kwargs): + """ Get radius server configure ipv4 """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_ip = module.params['radius_server_ip'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + radius_vpn_name = module.params['radius_vpn_name'] + state = module.params['state'] + + result = dict() + result["radius_server_ip_v4"] = [] + need_cfg = False + + conf_str = CE_GET_RADIUS_SERVER_CFG_IPV4 % radius_group_name + + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + radius_server_ip_v4 = root.findall( + "radius/rdsTemplates/rdsTemplate/rdsServerIPV4s/rdsServerIPV4") + if radius_server_ip_v4: + for tmp in radius_server_ip_v4: + tmp_dict = dict() + for site in tmp: + if site.tag in ["serverType", "serverIPAddress", "serverPort", "serverMode", "vpnName"]: + tmp_dict[site.tag] = site.text + + result["radius_server_ip_v4"].append(tmp_dict) + + if result["radius_server_ip_v4"]: + cfg = dict() + config_list = list() + if radius_server_type: + cfg["serverType"] = radius_server_type.lower() + if radius_server_ip: + cfg["serverIPAddress"] = radius_server_ip.lower() + if radius_server_port: + cfg["serverPort"] = radius_server_port.lower() + if radius_server_mode: + cfg["serverMode"] = radius_server_mode.lower() + if radius_vpn_name: + cfg["vpnName"] = radius_vpn_name.lower() + + for tmp in result["radius_server_ip_v4"]: + exist_cfg = dict() + if radius_server_type: + exist_cfg["serverType"] = tmp.get("serverType").lower() + if radius_server_ip: + exist_cfg["serverIPAddress"] = tmp.get("serverIPAddress").lower() + if radius_server_port: + exist_cfg["serverPort"] = tmp.get("serverPort").lower() + if radius_server_mode: + exist_cfg["serverMode"] = tmp.get("serverMode").lower() + if radius_vpn_name: + exist_cfg["vpnName"] = tmp.get("vpnName").lower() + config_list.append(exist_cfg) + if cfg in config_list: + if state == "present": + need_cfg = False + else: + need_cfg = True + else: + if state == "present": + need_cfg = True + else: + need_cfg = False + result["need_cfg"] = need_cfg + return result + + def merge_radius_server_cfg_ipv4(self, **kwargs): + """ Merge radius server configure ipv4 """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_ip = module.params['radius_server_ip'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + radius_vpn_name = module.params['radius_vpn_name'] + + conf_str = CE_MERGE_RADIUS_SERVER_CFG_IPV4 % ( + radius_group_name, radius_server_type, + radius_server_ip, radius_server_port, + radius_server_mode, radius_vpn_name) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Merge radius server config ipv4 failed.') + + cmds = [] + + cmd = "radius server group %s" % radius_group_name + cmds.append(cmd) + + if radius_server_type == "Authentication": + cmd = "radius server authentication %s %s" % ( + radius_server_ip, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + else: + cmd = "radius server accounting %s %s" % ( + radius_server_ip, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def delete_radius_server_cfg_ipv4(self, **kwargs): + """ Delete radius server configure ipv4 """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_ip = module.params['radius_server_ip'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + radius_vpn_name = module.params['radius_vpn_name'] + + conf_str = CE_DELETE_RADIUS_SERVER_CFG_IPV4 % ( + radius_group_name, radius_server_type, + radius_server_ip, radius_server_port, + radius_server_mode, radius_vpn_name) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Create radius server config ipv4 failed.') + + cmds = [] + + cmd = "radius server group %s" % radius_group_name + cmds.append(cmd) + + if radius_server_type == "Authentication": + cmd = "undo radius server authentication %s %s" % ( + radius_server_ip, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + else: + cmd = "undo radius server accounting %s %s" % ( + radius_server_ip, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def get_radius_server_cfg_ipv6(self, **kwargs): + """ Get radius server configure ipv6 """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_ipv6 = module.params['radius_server_ipv6'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + state = module.params['state'] + + result = dict() + result["radius_server_ip_v6"] = [] + need_cfg = False + + conf_str = CE_GET_RADIUS_SERVER_CFG_IPV6 % radius_group_name + + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + radius_server_ip_v6 = root.findall( + "radius/rdsTemplates/rdsTemplate/rdsServerIPV6s/rdsServerIPV6") + if radius_server_ip_v6: + for tmp in radius_server_ip_v6: + tmp_dict = dict() + for site in tmp: + if site.tag in ["serverType", "serverIPAddress", "serverPort", "serverMode"]: + tmp_dict[site.tag] = site.text + + result["radius_server_ip_v6"].append(tmp_dict) + + if result["radius_server_ip_v6"]: + cfg = dict() + config_list = list() + if radius_server_type: + cfg["serverType"] = radius_server_type.lower() + if radius_server_ipv6: + cfg["serverIPAddress"] = radius_server_ipv6.lower() + if radius_server_port: + cfg["serverPort"] = radius_server_port.lower() + if radius_server_mode: + cfg["serverMode"] = radius_server_mode.lower() + + for tmp in result["radius_server_ip_v6"]: + exist_cfg = dict() + if radius_server_type: + exist_cfg["serverType"] = tmp.get("serverType").lower() + if radius_server_ipv6: + exist_cfg["serverIPAddress"] = tmp.get("serverIPAddress").lower() + if radius_server_port: + exist_cfg["serverPort"] = tmp.get("serverPort").lower() + if radius_server_mode: + exist_cfg["serverMode"] = tmp.get("serverMode").lower() + config_list.append(exist_cfg) + if cfg in config_list: + if state == "present": + need_cfg = False + else: + need_cfg = True + else: + if state == "present": + need_cfg = True + else: + need_cfg = False + + result["need_cfg"] = need_cfg + return result + + def merge_radius_server_cfg_ipv6(self, **kwargs): + """ Merge radius server configure ipv6 """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_ipv6 = module.params['radius_server_ipv6'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + + conf_str = CE_MERGE_RADIUS_SERVER_CFG_IPV6 % ( + radius_group_name, radius_server_type, + radius_server_ipv6, radius_server_port, + radius_server_mode) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Merge radius server config ipv6 failed.') + + cmds = [] + + cmd = "radius server group %s" % radius_group_name + cmds.append(cmd) + + if radius_server_type == "Authentication": + cmd = "radius server authentication %s %s" % ( + radius_server_ipv6, radius_server_port) + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + else: + cmd = "radius server accounting %s %s" % ( + radius_server_ipv6, radius_server_port) + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def delete_radius_server_cfg_ipv6(self, **kwargs): + """ Delete radius server configure ipv6 """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_ipv6 = module.params['radius_server_ipv6'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + + conf_str = CE_DELETE_RADIUS_SERVER_CFG_IPV6 % ( + radius_group_name, radius_server_type, + radius_server_ipv6, radius_server_port, + radius_server_mode) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Create radius server config ipv6 failed.') + + cmds = [] + + cmd = "radius server group %s" % radius_group_name + cmds.append(cmd) + + if radius_server_type == "Authentication": + cmd = "undo radius server authentication %s %s" % ( + radius_server_ipv6, radius_server_port) + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + else: + cmd = "undo radius server accounting %s %s" % ( + radius_server_ipv6, radius_server_port) + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def get_radius_server_name(self, **kwargs): + """ Get radius server name """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_name = module.params['radius_server_name'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + radius_vpn_name = module.params['radius_vpn_name'] + state = module.params['state'] + + result = dict() + result["radius_server_name_cfg"] = [] + need_cfg = False + + conf_str = CE_GET_RADIUS_SERVER_NAME % radius_group_name + + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + radius_server_name_cfg = root.findall( + "radius/rdsTemplates/rdsTemplate/rdsServerNames/rdsServerName") + if radius_server_name_cfg: + for tmp in radius_server_name_cfg: + tmp_dict = dict() + for site in tmp: + if site.tag in ["serverType", "serverName", "serverPort", "serverMode", "vpnName"]: + tmp_dict[site.tag] = site.text + + result["radius_server_name_cfg"].append(tmp_dict) + + if result["radius_server_name_cfg"]: + cfg = dict() + config_list = list() + if radius_server_type: + cfg["serverType"] = radius_server_type.lower() + if radius_server_name: + cfg["serverName"] = radius_server_name.lower() + if radius_server_port: + cfg["serverPort"] = radius_server_port.lower() + if radius_server_mode: + cfg["serverMode"] = radius_server_mode.lower() + if radius_vpn_name: + cfg["vpnName"] = radius_vpn_name.lower() + + for tmp in result["radius_server_name_cfg"]: + exist_cfg = dict() + if radius_server_type: + exist_cfg["serverType"] = tmp.get("serverType").lower() + if radius_server_name: + exist_cfg["serverName"] = tmp.get("serverName").lower() + if radius_server_port: + exist_cfg["serverPort"] = tmp.get("serverPort").lower() + if radius_server_mode: + exist_cfg["serverMode"] = tmp.get("serverMode").lower() + if radius_vpn_name: + exist_cfg["vpnName"] = tmp.get("vpnName").lower() + config_list.append(exist_cfg) + if cfg in config_list: + if state == "present": + need_cfg = False + else: + need_cfg = True + else: + if state == "present": + need_cfg = True + else: + need_cfg = False + result["need_cfg"] = need_cfg + return result + + def merge_radius_server_name(self, **kwargs): + """ Merge radius server name """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_name = module.params['radius_server_name'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + radius_vpn_name = module.params['radius_vpn_name'] + + conf_str = CE_MERGE_RADIUS_SERVER_NAME % ( + radius_group_name, radius_server_type, + radius_server_name, radius_server_port, + radius_server_mode, radius_vpn_name) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge radius server name failed.') + + cmds = [] + + cmd = "radius server group %s" % radius_group_name + cmds.append(cmd) + + if radius_server_type == "Authentication": + cmd = "radius server authentication hostname %s %s" % ( + radius_server_name, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + else: + cmd = "radius server accounting hostname %s %s" % ( + radius_server_name, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def delete_radius_server_name(self, **kwargs): + """ Delete radius server name """ + + module = kwargs["module"] + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_name = module.params['radius_server_name'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + radius_vpn_name = module.params['radius_vpn_name'] + + conf_str = CE_DELETE_RADIUS_SERVER_NAME % ( + radius_group_name, radius_server_type, + radius_server_name, radius_server_port, + radius_server_mode, radius_vpn_name) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: delete radius server name failed.') + + cmds = [] + + cmd = "radius server group %s" % radius_group_name + cmds.append(cmd) + + if radius_server_type == "Authentication": + cmd = "undo radius server authentication hostname %s %s" % ( + radius_server_name, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + else: + cmd = "undo radius server accounting hostname %s %s" % ( + radius_server_name, radius_server_port) + + if radius_vpn_name and radius_vpn_name != "_public_": + cmd += " vpn-instance %s" % radius_vpn_name + + if radius_server_mode == "Secondary-server": + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def get_hwtacacs_server_cfg_ipv4(self, **kwargs): + """ Get hwtacacs server configure ipv4 """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_ip = module.params["hwtacacs_server_ip"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + hwtacacs_is_public_net = module.params["hwtacacs_is_public_net"] + state = module.params["state"] + + result = dict() + result["hwtacacs_server_cfg_ipv4"] = [] + need_cfg = False + + conf_str = CE_GET_HWTACACS_SERVER_CFG_IPV4 % hwtacacs_template + + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + hwtacacs_server_cfg_ipv4 = root.findall( + "hwtacacs/hwTacTempCfgs/hwTacTempCfg/hwTacSrvCfgs/hwTacSrvCfg") + if hwtacacs_server_cfg_ipv4: + for tmp in hwtacacs_server_cfg_ipv4: + tmp_dict = dict() + for site in tmp: + if site.tag in ["serverIpAddress", "serverType", "isSecondaryServer", "isPublicNet", "vpnName"]: + tmp_dict[site.tag] = site.text + + result["hwtacacs_server_cfg_ipv4"].append(tmp_dict) + + if result["hwtacacs_server_cfg_ipv4"]: + cfg = dict() + config_list = list() + + if hwtacacs_server_ip: + cfg["serverIpAddress"] = hwtacacs_server_ip.lower() + if hwtacacs_server_type: + cfg["serverType"] = hwtacacs_server_type.lower() + if hwtacacs_is_secondary_server: + cfg["isSecondaryServer"] = str(hwtacacs_is_secondary_server).lower() + if hwtacacs_is_public_net: + cfg["isPublicNet"] = str(hwtacacs_is_public_net).lower() + if hwtacacs_vpn_name: + cfg["vpnName"] = hwtacacs_vpn_name.lower() + + for tmp in result["hwtacacs_server_cfg_ipv4"]: + exist_cfg = dict() + if hwtacacs_server_ip: + exist_cfg["serverIpAddress"] = tmp.get("serverIpAddress").lower() + if hwtacacs_server_type: + exist_cfg["serverType"] = tmp.get("serverType").lower() + if hwtacacs_is_secondary_server: + exist_cfg["isSecondaryServer"] = tmp.get("isSecondaryServer").lower() + if hwtacacs_is_public_net: + exist_cfg["isPublicNet"] = tmp.get("isPublicNet").lower() + if hwtacacs_vpn_name: + exist_cfg["vpnName"] = tmp.get("vpnName").lower() + config_list.append(exist_cfg) + if cfg in config_list: + if state == "present": + need_cfg = False + else: + need_cfg = True + else: + if state == "present": + need_cfg = True + else: + need_cfg = False + result["need_cfg"] = need_cfg + return result + + def merge_hwtacacs_server_cfg_ipv4(self, **kwargs): + """ Merge hwtacacs server configure ipv4 """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_ip = module.params["hwtacacs_server_ip"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + hwtacacs_is_public_net = module.params["hwtacacs_is_public_net"] + + conf_str = CE_MERGE_HWTACACS_SERVER_CFG_IPV4 % ( + hwtacacs_template, hwtacacs_server_ip, + hwtacacs_server_type, str(hwtacacs_is_secondary_server).lower(), + hwtacacs_vpn_name, str(hwtacacs_is_public_net).lower()) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Merge hwtacacs server config ipv4 failed.') + + cmds = [] + + cmd = "hwtacacs server template %s" % hwtacacs_template + cmds.append(cmd) + + if hwtacacs_server_type == "Authentication": + cmd = "hwtacacs server authentication %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Authorization": + cmd = "hwtacacs server authorization %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Accounting": + cmd = "hwtacacs server accounting %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Common": + cmd = "hwtacacs server %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def delete_hwtacacs_server_cfg_ipv4(self, **kwargs): + """ Delete hwtacacs server configure ipv4 """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_ip = module.params["hwtacacs_server_ip"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + hwtacacs_is_public_net = module.params["hwtacacs_is_public_net"] + + conf_str = CE_DELETE_HWTACACS_SERVER_CFG_IPV4 % ( + hwtacacs_template, hwtacacs_server_ip, + hwtacacs_server_type, str(hwtacacs_is_secondary_server).lower(), + hwtacacs_vpn_name, str(hwtacacs_is_public_net).lower()) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Delete hwtacacs server config ipv4 failed.') + + cmds = [] + + cmd = "hwtacacs server template %s" % hwtacacs_template + cmds.append(cmd) + + if hwtacacs_server_type == "Authentication": + cmd = "undo hwtacacs server authentication %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Authorization": + cmd = "undo hwtacacs server authorization %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Accounting": + cmd = "undo hwtacacs server accounting %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Common": + cmd = "undo hwtacacs server %s" % hwtacacs_server_ip + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def get_hwtacacs_server_cfg_ipv6(self, **kwargs): + """ Get hwtacacs server configure ipv6 """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_ipv6 = module.params["hwtacacs_server_ipv6"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + state = module.params["state"] + + result = dict() + result["hwtacacs_server_cfg_ipv6"] = [] + need_cfg = False + + conf_str = CE_GET_HWTACACS_SERVER_CFG_IPV6 % hwtacacs_template + + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + hwtacacs_server_cfg_ipv6 = root.findall( + "hwtacacs/hwTacTempCfgs/hwTacTempCfg/hwTacIpv6SrvCfgs/hwTacIpv6SrvCfg") + if hwtacacs_server_cfg_ipv6: + for tmp in hwtacacs_server_cfg_ipv6: + tmp_dict = dict() + for site in tmp: + if site.tag in ["serverIpAddress", "serverType", "isSecondaryServer", "vpnName"]: + tmp_dict[site.tag] = site.text + + result["hwtacacs_server_cfg_ipv6"].append(tmp_dict) + + if result["hwtacacs_server_cfg_ipv6"]: + cfg = dict() + config_list = list() + + if hwtacacs_server_ipv6: + cfg["serverIpAddress"] = hwtacacs_server_ipv6.lower() + if hwtacacs_server_type: + cfg["serverType"] = hwtacacs_server_type.lower() + if hwtacacs_is_secondary_server: + cfg["isSecondaryServer"] = str(hwtacacs_is_secondary_server).lower() + if hwtacacs_vpn_name: + cfg["vpnName"] = hwtacacs_vpn_name.lower() + + for tmp in result["hwtacacs_server_cfg_ipv6"]: + exist_cfg = dict() + if hwtacacs_server_ipv6: + exist_cfg["serverIpAddress"] = tmp.get("serverIpAddress").lower() + if hwtacacs_server_type: + exist_cfg["serverType"] = tmp.get("serverType").lower() + if hwtacacs_is_secondary_server: + exist_cfg["isSecondaryServer"] = tmp.get("isSecondaryServer").lower() + if hwtacacs_vpn_name: + exist_cfg["vpnName"] = tmp.get("vpnName").lower() + config_list.append(exist_cfg) + if cfg in config_list: + if state == "present": + need_cfg = False + else: + need_cfg = True + else: + if state == "present": + need_cfg = True + else: + need_cfg = False + result["need_cfg"] = need_cfg + return result + + def merge_hwtacacs_server_cfg_ipv6(self, **kwargs): + """ Merge hwtacacs server configure ipv6 """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_ipv6 = module.params["hwtacacs_server_ipv6"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + + conf_str = CE_MERGE_HWTACACS_SERVER_CFG_IPV6 % ( + hwtacacs_template, hwtacacs_server_ipv6, + hwtacacs_server_type, str(hwtacacs_is_secondary_server).lower(), + hwtacacs_vpn_name) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Merge hwtacacs server config ipv6 failed.') + + cmds = [] + + cmd = "hwtacacs server template %s" % hwtacacs_template + cmds.append(cmd) + + if hwtacacs_server_type == "Authentication": + cmd = "hwtacacs server authentication %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Authorization": + cmd = "hwtacacs server authorization %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Accounting": + cmd = "hwtacacs server accounting %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Common": + cmd = "hwtacacs server %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def delete_hwtacacs_server_cfg_ipv6(self, **kwargs): + """ Delete hwtacacs server configure ipv6 """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_ipv6 = module.params["hwtacacs_server_ipv6"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + + conf_str = CE_DELETE_HWTACACS_SERVER_CFG_IPV6 % ( + hwtacacs_template, hwtacacs_server_ipv6, + hwtacacs_server_type, str(hwtacacs_is_secondary_server).lower(), + hwtacacs_vpn_name) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Delete hwtacacs server config ipv6 failed.') + + cmds = [] + + cmd = "hwtacacs server template %s" % hwtacacs_template + cmds.append(cmd) + + if hwtacacs_server_type == "Authentication": + cmd = "undo hwtacacs server authentication %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Authorization": + cmd = "undo hwtacacs server authorization %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Accounting": + cmd = "undo hwtacacs server accounting %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Common": + cmd = "undo hwtacacs server %s" % hwtacacs_server_ipv6 + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_secondary_server: + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def get_hwtacacs_host_server_cfg(self, **kwargs): + """ Get hwtacacs host server configure """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_host_name = module.params["hwtacacs_server_host_name"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = "true" if module.params[ + "hwtacacs_is_secondary_server"] is True else "false" + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + hwtacacs_is_public_net = "true" if module.params[ + "hwtacacs_is_public_net"] is True else "false" + state = module.params["state"] + + result = dict() + result["hwtacacs_server_name_cfg"] = [] + need_cfg = False + + conf_str = CE_GET_HWTACACS_HOST_SERVER_CFG % hwtacacs_template + + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + hwtacacs_server_name_cfg = root.findall( + "hwtacacs/hwTacTempCfgs/hwTacTempCfg/hwTacHostSrvCfgs/hwTacHostSrvCfg") + if hwtacacs_server_name_cfg: + for tmp in hwtacacs_server_name_cfg: + tmp_dict = dict() + for site in tmp: + if site.tag in ["serverHostName", "serverType", "isSecondaryServer", "isPublicNet", "vpnName"]: + tmp_dict[site.tag] = site.text + + result["hwtacacs_server_name_cfg"].append(tmp_dict) + + if result["hwtacacs_server_name_cfg"]: + cfg = dict() + config_list = list() + + if hwtacacs_server_host_name: + cfg["serverHostName"] = hwtacacs_server_host_name.lower() + if hwtacacs_server_type: + cfg["serverType"] = hwtacacs_server_type.lower() + if hwtacacs_is_secondary_server: + cfg["isSecondaryServer"] = str(hwtacacs_is_secondary_server).lower() + if hwtacacs_is_public_net: + cfg["isPublicNet"] = str(hwtacacs_is_public_net).lower() + if hwtacacs_vpn_name: + cfg["vpnName"] = hwtacacs_vpn_name.lower() + + for tmp in result["hwtacacs_server_name_cfg"]: + exist_cfg = dict() + if hwtacacs_server_host_name: + exist_cfg["serverHostName"] = tmp.get("serverHostName").lower() + if hwtacacs_server_type: + exist_cfg["serverType"] = tmp.get("serverType").lower() + if hwtacacs_is_secondary_server: + exist_cfg["isSecondaryServer"] = tmp.get("isSecondaryServer").lower() + if hwtacacs_is_public_net: + exist_cfg["isPublicNet"] = tmp.get("isPublicNet").lower() + if hwtacacs_vpn_name: + exist_cfg["vpnName"] = tmp.get("vpnName").lower() + config_list.append(exist_cfg) + if cfg in config_list: + if state == "present": + need_cfg = False + else: + need_cfg = True + else: + if state == "present": + need_cfg = True + else: + need_cfg = False + result["need_cfg"] = need_cfg + return result + + def merge_hwtacacs_host_server_cfg(self, **kwargs): + """ Merge hwtacacs host server configure """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_host_name = module.params["hwtacacs_server_host_name"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + hwtacacs_is_public_net = module.params["hwtacacs_is_public_net"] + + conf_str = CE_MERGE_HWTACACS_HOST_SERVER_CFG % ( + hwtacacs_template, hwtacacs_server_host_name, + hwtacacs_server_type, str(hwtacacs_is_secondary_server).lower(), + hwtacacs_vpn_name, str(hwtacacs_is_public_net).lower()) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Merge hwtacacs host server config failed.') + + cmds = [] + + if hwtacacs_server_type == "Authentication": + cmd = "hwtacacs server authentication host %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Authorization": + cmd = "hwtacacs server authorization host %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Accounting": + cmd = "hwtacacs server accounting host %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Common": + cmd = "hwtacacs server host host-name %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + cmds.append(cmd) + return cmds + + def delete_hwtacacs_host_server_cfg(self, **kwargs): + """ Delete hwtacacs host server configure """ + + module = kwargs["module"] + hwtacacs_template = module.params["hwtacacs_template"] + hwtacacs_server_host_name = module.params["hwtacacs_server_host_name"] + hwtacacs_server_type = module.params["hwtacacs_server_type"] + hwtacacs_is_secondary_server = module.params[ + "hwtacacs_is_secondary_server"] + hwtacacs_vpn_name = module.params["hwtacacs_vpn_name"] + hwtacacs_is_public_net = module.params["hwtacacs_is_public_net"] + + conf_str = CE_DELETE_HWTACACS_HOST_SERVER_CFG % ( + hwtacacs_template, hwtacacs_server_host_name, + hwtacacs_server_type, str(hwtacacs_is_secondary_server).lower(), + hwtacacs_vpn_name, str(hwtacacs_is_public_net).lower()) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Delete hwtacacs host server config failed.') + + cmds = [] + + if hwtacacs_server_type == "Authentication": + cmd = "undo hwtacacs server authentication host %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Authorization": + cmd = "undo hwtacacs server authorization host %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Accounting": + cmd = "undo hwtacacs server accounting host %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + elif hwtacacs_server_type == "Common": + cmd = "undo hwtacacs server host %s" % hwtacacs_server_host_name + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + cmd += " vpn-instance %s" % hwtacacs_vpn_name + if hwtacacs_is_public_net: + cmd += " public-net" + if hwtacacs_is_secondary_server: + cmd += " secondary" + + cmds.append(cmd) + return cmds + + +def check_name(**kwargs): + """ Check invalid name """ + + module = kwargs["module"] + name = kwargs["name"] + invalid_char = kwargs["invalid_char"] + + for item in invalid_char: + if item in name: + module.fail_json( + msg='Error: Invalid char %s is in the name %s ' % (item, name)) + + +def check_module_argument(**kwargs): + """ Check module argument """ + + module = kwargs["module"] + + # local para + local_user_name = module.params['local_user_name'] + local_password = module.params['local_password'] + local_ftp_dir = module.params['local_ftp_dir'] + local_user_level = module.params['local_user_level'] + local_user_group = module.params['local_user_group'] + + # radius para + radius_group_name = module.params['radius_group_name'] + radius_server_ip = module.params['radius_server_ip'] + radius_server_port = module.params['radius_server_port'] + radius_vpn_name = module.params['radius_vpn_name'] + radius_server_name = module.params['radius_server_name'] + + # hwtacacs para + hwtacacs_template = module.params['hwtacacs_template'] + hwtacacs_server_ip = module.params['hwtacacs_server_ip'] + hwtacacs_vpn_name = module.params['hwtacacs_vpn_name'] + hwtacacs_server_host_name = module.params['hwtacacs_server_host_name'] + + if local_user_name: + if len(local_user_name) > 253: + module.fail_json( + msg='Error: The local_user_name %s is large than 253.' % local_user_name) + check_name(module=module, name=local_user_name, + invalid_char=INVALID_USER_NAME_CHAR) + + if local_password and len(local_password) > 255: + module.fail_json( + msg='Error: The local_password %s is large than 255.' % local_password) + + if local_user_level: + if int(local_user_level) > 15 or int(local_user_level) < 0: + module.fail_json( + msg='Error: The local_user_level %s is out of [0 - 15].' % local_user_level) + + if local_ftp_dir: + if len(local_ftp_dir) > 255: + module.fail_json( + msg='Error: The local_ftp_dir %s is large than 255.' % local_ftp_dir) + + if local_user_group: + if len(local_user_group) > 32 or len(local_user_group) < 1: + module.fail_json( + msg='Error: The local_user_group %s is out of [1 - 32].' % local_user_group) + + if radius_group_name and len(radius_group_name) > 32: + module.fail_json( + msg='Error: The radius_group_name %s is large than 32.' % radius_group_name) + + if radius_server_ip and not check_ip_addr(radius_server_ip): + module.fail_json( + msg='Error: The radius_server_ip %s is invalid.' % radius_server_ip) + + if radius_server_port and not radius_server_port.isdigit(): + module.fail_json( + msg='Error: The radius_server_port %s is invalid.' % radius_server_port) + + if radius_vpn_name: + if len(radius_vpn_name) > 31: + module.fail_json( + msg='Error: The radius_vpn_name %s is large than 31.' % radius_vpn_name) + if ' ' in radius_vpn_name: + module.fail_json( + msg='Error: The radius_vpn_name %s include space.' % radius_vpn_name) + + if radius_server_name: + if len(radius_server_name) > 255: + module.fail_json( + msg='Error: The radius_server_name %s is large than 255.' % radius_server_name) + if ' ' in radius_server_name: + module.fail_json( + msg='Error: The radius_server_name %s include space.' % radius_server_name) + + if hwtacacs_template and len(hwtacacs_template) > 32: + module.fail_json( + msg='Error: The hwtacacs_template %s is large than 32.' % hwtacacs_template) + + if hwtacacs_server_ip and not check_ip_addr(hwtacacs_server_ip): + module.fail_json( + msg='Error: The hwtacacs_server_ip %s is invalid.' % hwtacacs_server_ip) + + if hwtacacs_vpn_name: + if len(hwtacacs_vpn_name) > 31: + module.fail_json( + msg='Error: The hwtacacs_vpn_name %s is large than 31.' % hwtacacs_vpn_name) + if ' ' in hwtacacs_vpn_name: + module.fail_json( + msg='Error: The hwtacacs_vpn_name %s include space.' % hwtacacs_vpn_name) + + if hwtacacs_server_host_name: + if len(hwtacacs_server_host_name) > 255: + module.fail_json( + msg='Error: The hwtacacs_server_host_name %s is large than 255.' % hwtacacs_server_host_name) + if ' ' in hwtacacs_server_host_name: + module.fail_json( + msg='Error: The hwtacacs_server_host_name %s include space.' % hwtacacs_server_host_name) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + local_user_name=dict(type='str'), + local_password=dict(type='str', no_log=True), + local_service_type=dict(type='str'), + local_ftp_dir=dict(type='str'), + local_user_level=dict(type='str'), + local_user_group=dict(type='str'), + radius_group_name=dict(type='str'), + radius_server_type=dict(choices=['Authentication', 'Accounting']), + radius_server_ip=dict(type='str'), + radius_server_ipv6=dict(type='str'), + radius_server_port=dict(type='str'), + radius_server_mode=dict( + choices=['Secondary-server', 'Primary-server']), + radius_vpn_name=dict(type='str'), + radius_server_name=dict(type='str'), + hwtacacs_template=dict(type='str'), + hwtacacs_server_ip=dict(type='str'), + hwtacacs_server_ipv6=dict(type='str'), + hwtacacs_server_type=dict( + choices=['Authentication', 'Authorization', 'Accounting', 'Common']), + hwtacacs_is_secondary_server=dict( + required=False, default=False, type='bool'), + hwtacacs_vpn_name=dict(type='str'), + hwtacacs_is_public_net=dict( + required=False, default=False, type='bool'), + hwtacacs_server_host_name=dict(type='str') + ) + + argument_spec.update(ce_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + check_module_argument(module=module) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + # common para + state = module.params['state'] + + # local para + local_user_name = module.params['local_user_name'] + local_password = module.params['local_password'] + local_service_type = module.params['local_service_type'] + local_ftp_dir = module.params['local_ftp_dir'] + local_user_level = module.params['local_user_level'] + local_user_group = module.params['local_user_group'] + + # radius para + radius_group_name = module.params['radius_group_name'] + radius_server_type = module.params['radius_server_type'] + radius_server_ip = module.params['radius_server_ip'] + radius_server_ipv6 = module.params['radius_server_ipv6'] + radius_server_port = module.params['radius_server_port'] + radius_server_mode = module.params['radius_server_mode'] + radius_vpn_name = module.params['radius_vpn_name'] + radius_server_name = module.params['radius_server_name'] + + # hwtacacs para + hwtacacs_template = module.params['hwtacacs_template'] + hwtacacs_server_ip = module.params['hwtacacs_server_ip'] + hwtacacs_server_ipv6 = module.params['hwtacacs_server_ipv6'] + hwtacacs_server_type = module.params['hwtacacs_server_type'] + hwtacacs_is_secondary_server = module.params[ + 'hwtacacs_is_secondary_server'] + hwtacacs_vpn_name = module.params['hwtacacs_vpn_name'] + hwtacacs_is_public_net = module.params['hwtacacs_is_public_net'] + hwtacacs_server_host_name = module.params['hwtacacs_server_host_name'] + + ce_aaa_server_host = AaaServerHost() + + if not ce_aaa_server_host: + module.fail_json(msg='Error: Construct ce_aaa_server failed.') + + # get proposed + proposed["state"] = state + if local_user_name: + proposed["local_user_name"] = local_user_name + if local_password: + proposed["local_password"] = "******" + if local_service_type: + proposed["local_service_type"] = local_service_type + if local_ftp_dir: + proposed["local_ftp_dir"] = local_ftp_dir + if local_user_level: + proposed["local_user_level"] = local_user_level + if local_user_group: + proposed["local_user_group"] = local_user_group + if radius_group_name: + proposed["radius_group_name"] = radius_group_name + if radius_server_type: + proposed["radius_server_type"] = radius_server_type + if radius_server_ip: + proposed["radius_server_ip"] = radius_server_ip + if radius_server_ipv6: + proposed["radius_server_ipv6"] = radius_server_ipv6 + if radius_server_port: + proposed["radius_server_port"] = radius_server_port + if radius_server_mode: + proposed["radius_server_mode"] = radius_server_mode + if radius_vpn_name: + proposed["radius_vpn_name"] = radius_vpn_name + if radius_server_name: + proposed["radius_server_name"] = radius_server_name + if hwtacacs_template: + proposed["hwtacacs_template"] = hwtacacs_template + if hwtacacs_server_ip: + proposed["hwtacacs_server_ip"] = hwtacacs_server_ip + if hwtacacs_server_ipv6: + proposed["hwtacacs_server_ipv6"] = hwtacacs_server_ipv6 + if hwtacacs_server_type: + proposed["hwtacacs_server_type"] = hwtacacs_server_type + proposed["hwtacacs_is_secondary_server"] = hwtacacs_is_secondary_server + if hwtacacs_vpn_name: + proposed["hwtacacs_vpn_name"] = hwtacacs_vpn_name + proposed["hwtacacs_is_public_net"] = hwtacacs_is_public_net + if hwtacacs_server_host_name: + proposed["hwtacacs_server_host_name"] = hwtacacs_server_host_name + + if local_user_name: + + if state == "present" and not local_password: + module.fail_json( + msg='Error: Please input local_password when config local user.') + + local_user_result = ce_aaa_server_host.get_local_user_info( + module=module) + existing["local user name"] = local_user_result["local_user_info"] + + if state == "present": + # present local user + if local_user_result["need_cfg"]: + cmd = ce_aaa_server_host.merge_local_user_info(module=module) + + changed = True + updates.append(cmd) + + else: + # absent local user + if local_user_result["need_cfg"]: + if not local_service_type and not local_ftp_dir and not local_user_level and not local_user_group: + cmd = ce_aaa_server_host.delete_local_user_info( + module=module) + else: + cmd = ce_aaa_server_host.merge_local_user_info( + module=module) + + changed = True + updates.append(cmd) + + local_user_result = ce_aaa_server_host.get_local_user_info( + module=module) + end_state["local user name"] = local_user_result["local_user_info"] + + if radius_group_name: + + if not radius_server_ip and not radius_server_ipv6 and not radius_server_name: + module.fail_json( + msg='Error: Please input radius_server_ip or radius_server_ipv6 or radius_server_name.') + + if radius_server_ip and radius_server_ipv6: + module.fail_json( + msg='Error: Please do not input radius_server_ip and radius_server_ipv6 at the same time.') + + if not radius_server_type or not radius_server_port or not radius_server_mode or not radius_vpn_name: + module.fail_json( + msg='Error: Please input radius_server_type radius_server_port radius_server_mode radius_vpn_name.') + + if radius_server_ip: + rds_server_ipv4_result = ce_aaa_server_host.get_radius_server_cfg_ipv4( + module=module) + if radius_server_ipv6: + rds_server_ipv6_result = ce_aaa_server_host.get_radius_server_cfg_ipv6( + module=module) + if radius_server_name: + rds_server_name_result = ce_aaa_server_host.get_radius_server_name( + module=module) + + if radius_server_ip and rds_server_ipv4_result["radius_server_ip_v4"]: + existing["radius server ipv4"] = rds_server_ipv4_result[ + "radius_server_ip_v4"] + if radius_server_ipv6 and rds_server_ipv6_result["radius_server_ip_v6"]: + existing["radius server ipv6"] = rds_server_ipv6_result[ + "radius_server_ip_v6"] + if radius_server_name and rds_server_name_result["radius_server_name_cfg"]: + existing["radius server name cfg"] = rds_server_name_result[ + "radius_server_name_cfg"] + + if state == "present": + if radius_server_ip and rds_server_ipv4_result["need_cfg"]: + cmd = ce_aaa_server_host.merge_radius_server_cfg_ipv4( + module=module) + changed = True + updates.append(cmd) + + if radius_server_ipv6 and rds_server_ipv6_result["need_cfg"]: + cmd = ce_aaa_server_host.merge_radius_server_cfg_ipv6( + module=module) + changed = True + updates.append(cmd) + + if radius_server_name and rds_server_name_result["need_cfg"]: + cmd = ce_aaa_server_host.merge_radius_server_name( + module=module) + changed = True + updates.append(cmd) + else: + if radius_server_ip and rds_server_ipv4_result["need_cfg"]: + cmd = ce_aaa_server_host.delete_radius_server_cfg_ipv4( + module=module) + changed = True + updates.append(cmd) + + if radius_server_ipv6 and rds_server_ipv6_result["need_cfg"]: + cmd = ce_aaa_server_host.delete_radius_server_cfg_ipv6( + module=module) + changed = True + updates.append(cmd) + + if radius_server_name and rds_server_name_result["need_cfg"]: + cmd = ce_aaa_server_host.delete_radius_server_name( + module=module) + changed = True + updates.append(cmd) + + if radius_server_ip: + rds_server_ipv4_result = ce_aaa_server_host.get_radius_server_cfg_ipv4( + module=module) + if radius_server_ipv6: + rds_server_ipv6_result = ce_aaa_server_host.get_radius_server_cfg_ipv6( + module=module) + if radius_server_name: + rds_server_name_result = ce_aaa_server_host.get_radius_server_name( + module=module) + + if radius_server_ip and rds_server_ipv4_result["radius_server_ip_v4"]: + end_state["radius server ipv4"] = rds_server_ipv4_result[ + "radius_server_ip_v4"] + if radius_server_ipv6 and rds_server_ipv6_result["radius_server_ip_v6"]: + end_state["radius server ipv6"] = rds_server_ipv6_result[ + "radius_server_ip_v6"] + if radius_server_name and rds_server_name_result["radius_server_name_cfg"]: + end_state["radius server name cfg"] = rds_server_name_result[ + "radius_server_name_cfg"] + + if hwtacacs_template: + + if not hwtacacs_server_ip and not hwtacacs_server_ipv6 and not hwtacacs_server_host_name: + module.fail_json( + msg='Error: Please input hwtacacs_server_ip or hwtacacs_server_ipv6 or hwtacacs_server_host_name.') + + if not hwtacacs_server_type or not hwtacacs_vpn_name: + module.fail_json( + msg='Error: Please input hwtacacs_server_type hwtacacs_vpn_name.') + + if hwtacacs_server_ip and hwtacacs_server_ipv6: + module.fail_json( + msg='Error: Please do not set hwtacacs_server_ip and hwtacacs_server_ipv6 at the same time.') + + if hwtacacs_vpn_name and hwtacacs_vpn_name != "_public_": + if hwtacacs_is_public_net: + module.fail_json( + msg='Error: Please do not set vpn and public net at the same time.') + + if hwtacacs_server_ip: + hwtacacs_server_ipv4_result = ce_aaa_server_host.get_hwtacacs_server_cfg_ipv4( + module=module) + if hwtacacs_server_ipv6: + hwtacacs_server_ipv6_result = ce_aaa_server_host.get_hwtacacs_server_cfg_ipv6( + module=module) + if hwtacacs_server_host_name: + hwtacacs_host_name_result = ce_aaa_server_host.get_hwtacacs_host_server_cfg( + module=module) + + if hwtacacs_server_ip and hwtacacs_server_ipv4_result["hwtacacs_server_cfg_ipv4"]: + existing["hwtacacs server cfg ipv4"] = hwtacacs_server_ipv4_result[ + "hwtacacs_server_cfg_ipv4"] + if hwtacacs_server_ipv6 and hwtacacs_server_ipv6_result["hwtacacs_server_cfg_ipv6"]: + existing["hwtacacs server cfg ipv6"] = hwtacacs_server_ipv6_result[ + "hwtacacs_server_cfg_ipv6"] + if hwtacacs_server_host_name and hwtacacs_host_name_result["hwtacacs_server_name_cfg"]: + existing["hwtacacs server name cfg"] = hwtacacs_host_name_result[ + "hwtacacs_server_name_cfg"] + + if state == "present": + if hwtacacs_server_ip and hwtacacs_server_ipv4_result["need_cfg"]: + cmd = ce_aaa_server_host.merge_hwtacacs_server_cfg_ipv4( + module=module) + changed = True + updates.append(cmd) + + if hwtacacs_server_ipv6 and hwtacacs_server_ipv6_result["need_cfg"]: + cmd = ce_aaa_server_host.merge_hwtacacs_server_cfg_ipv6( + module=module) + changed = True + updates.append(cmd) + + if hwtacacs_server_host_name and hwtacacs_host_name_result["need_cfg"]: + cmd = ce_aaa_server_host.merge_hwtacacs_host_server_cfg( + module=module) + changed = True + updates.append(cmd) + + else: + if hwtacacs_server_ip and hwtacacs_server_ipv4_result["need_cfg"]: + cmd = ce_aaa_server_host.delete_hwtacacs_server_cfg_ipv4( + module=module) + changed = True + updates.append(cmd) + + if hwtacacs_server_ipv6 and hwtacacs_server_ipv6_result["need_cfg"]: + cmd = ce_aaa_server_host.delete_hwtacacs_server_cfg_ipv6( + module=module) + changed = True + updates.append(cmd) + + if hwtacacs_server_host_name and hwtacacs_host_name_result["need_cfg"]: + cmd = ce_aaa_server_host.delete_hwtacacs_host_server_cfg( + module=module) + changed = True + updates.append(cmd) + + if hwtacacs_server_ip: + hwtacacs_server_ipv4_result = ce_aaa_server_host.get_hwtacacs_server_cfg_ipv4( + module=module) + if hwtacacs_server_ipv6: + hwtacacs_server_ipv6_result = ce_aaa_server_host.get_hwtacacs_server_cfg_ipv6( + module=module) + if hwtacacs_server_host_name: + hwtacacs_host_name_result = ce_aaa_server_host.get_hwtacacs_host_server_cfg( + module=module) + + if hwtacacs_server_ip and hwtacacs_server_ipv4_result["hwtacacs_server_cfg_ipv4"]: + end_state["hwtacacs server cfg ipv4"] = hwtacacs_server_ipv4_result[ + "hwtacacs_server_cfg_ipv4"] + if hwtacacs_server_ipv6 and hwtacacs_server_ipv6_result["hwtacacs_server_cfg_ipv6"]: + end_state["hwtacacs server cfg ipv6"] = hwtacacs_server_ipv6_result[ + "hwtacacs_server_cfg_ipv6"] + if hwtacacs_server_host_name and hwtacacs_host_name_result["hwtacacs_server_name_cfg"]: + end_state["hwtacacs server name cfg"] = hwtacacs_host_name_result[ + "hwtacacs_server_name_cfg"] + + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_acl.py b/plugins/modules/ce_acl.py new file mode 100644 index 0000000..c3e374d --- /dev/null +++ b/plugins/modules/ce_acl.py @@ -0,0 +1,1001 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_acl +short_description: Manages base ACL configuration on HUAWEI CloudEngine switches. +description: + - Manages base ACL configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent','delete_acl'] + acl_name: + description: + - ACL number or name. + For a numbered rule group, the value ranging from 2000 to 2999 indicates a basic ACL. + For a named rule group, the value is a string of 1 to 32 case-sensitive characters starting + with a letter, spaces not supported. + required: true + acl_num: + description: + - ACL number. + The value is an integer ranging from 2000 to 2999. + acl_step: + description: + - ACL step. + The value is an integer ranging from 1 to 20. The default value is 5. + acl_description: + description: + - ACL description. + The value is a string of 1 to 127 characters. + rule_name: + description: + - Name of a basic ACL rule. + The value is a string of 1 to 32 characters. + The value is case-insensitive, and cannot contain spaces or begin with an underscore (_). + rule_id: + description: + - ID of a basic ACL rule in configuration mode. + The value is an integer ranging from 0 to 4294967294. + rule_action: + description: + - Matching mode of basic ACL rules. + choices: ['permit','deny'] + source_ip: + description: + - Source IP address. + The value is a string of 0 to 255 characters.The default value is 0.0.0.0. + The value is in dotted decimal notation. + src_mask: + description: + - Mask of a source IP address. + The value is an integer ranging from 1 to 32. + frag_type: + description: + - Type of packet fragmentation. + choices: ['fragment', 'clear_fragment'] + vrf_name: + description: + - VPN instance name. + The value is a string of 1 to 31 characters.The default value is _public_. + time_range: + description: + - Name of a time range in which an ACL rule takes effect. + The value is a string of 1 to 32 characters. + The value is case-insensitive, and cannot contain spaces. The name must start with an uppercase + or lowercase letter. In addition, the word "all" cannot be specified as a time range name. + rule_description: + description: + - Description about an ACL rule. + The value is a string of 1 to 127 characters. + log_flag: + description: + - Flag of logging matched data packets. + type: bool + default: 'no' +''' + +EXAMPLES = ''' + +- name: CloudEngine acl test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config ACL" + ce_acl: + state: present + acl_name: 2200 + provider: "{{ cli }}" + + - name: "Undo ACL" + ce_acl: + state: delete_acl + acl_name: 2200 + provider: "{{ cli }}" + + - name: "Config ACL base rule" + ce_acl: + state: present + acl_name: 2200 + rule_name: test_rule + rule_id: 111 + rule_action: permit + source_ip: 10.10.10.10 + src_mask: 24 + frag_type: fragment + time_range: wdz_acl_time + provider: "{{ cli }}" + + - name: "undo ACL base rule" + ce_acl: + state: absent + acl_name: 2200 + rule_name: test_rule + rule_id: 111 + rule_action: permit + source_ip: 10.10.10.10 + src_mask: 24 + frag_type: fragment + time_range: wdz_acl_time + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"acl_name": "test", "state": "delete_acl"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"aclNumOrName": "test", "aclType": "Basic"} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {} +updates: + description: command sent to the device + returned: always + type: list + sample: ["undo acl name test"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + +# get acl +CE_GET_ACL_HEADER = """ + + + + + +""" +CE_GET_ACL_TAIL = """ + + + + +""" +# merge acl +CE_MERGE_ACL_HEADER = """ + + + + + %s +""" +CE_MERGE_ACL_TAIL = """ + + + + +""" +# delete acl +CE_DELETE_ACL_HEADER = """ + + + + + %s +""" +CE_DELETE_ACL_TAIL = """ + + + + +""" + +# get acl base rule +CE_GET_ACL_BASE_RULE_HEADER = """ + + + + + %s + + + +""" +CE_GET_ACL_BASE_RULE_TAIL = """ + + + + + + +""" +# merge acl base rule +CE_MERGE_ACL_BASE_RULE_HEADER = """ + + + + + %s + + + %s +""" +CE_MERGE_ACL_BASE_RULE_TAIL = """ + + + + + + +""" +# delete acl base rule +CE_DELETE_ACL_BASE_RULE_HEADER = """ + + + + + %s + + + %s +""" +CE_DELETE_ACL_BASE_RULE_TAIL = """ + + + + + + +""" + + +class BaseAcl(object): + """ Manages base acl configuration """ + + def __init__(self, **kwargs): + """ Class init """ + + # argument spec + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # module args + self.state = self.module.params['state'] + self.acl_name = self.module.params['acl_name'] or None + self.acl_num = self.module.params['acl_num'] or None + self.acl_type = None + self.acl_step = self.module.params['acl_step'] or None + self.acl_description = self.module.params['acl_description'] or None + self.rule_name = self.module.params['rule_name'] or None + self.rule_id = self.module.params['rule_id'] or None + self.rule_action = self.module.params['rule_action'] or None + self.source_ip = self.module.params['source_ip'] or None + self.src_mask = self.module.params['src_mask'] or None + self.src_wild = None + self.frag_type = self.module.params['frag_type'] or None + self.vrf_name = self.module.params['vrf_name'] or None + self.time_range = self.module.params['time_range'] or None + self.rule_description = self.module.params['rule_description'] or None + self.log_flag = self.module.params['log_flag'] + + # cur config + self.cur_acl_cfg = dict() + self.cur_base_rule_cfg = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def netconf_get_config(self, conf_str): + """ Get configure by netconf """ + + xml_str = get_nc_config(self.module, conf_str) + + return xml_str + + def netconf_set_config(self, conf_str): + """ Set configure by netconf """ + + xml_str = set_nc_config(self.module, conf_str) + + return xml_str + + def get_wildcard_mask(self): + """ convert mask length to ip address wildcard mask, i.e. 24 to 0.0.0.255 """ + + mask_int = ["255"] * 4 + value = int(self.src_mask) + + if value > 32: + self.module.fail_json(msg='Error: IPv4 ipaddress mask length is invalid.') + if value < 8: + mask_int[0] = str(int(~(0xFF << (8 - value % 8)) & 0xFF)) + if value >= 8: + mask_int[0] = '0' + mask_int[1] = str(int(~(0xFF << (16 - (value % 16))) & 0xFF)) + if value >= 16: + mask_int[1] = '0' + mask_int[2] = str(int(~(0xFF << (24 - (value % 24))) & 0xFF)) + if value >= 24: + mask_int[2] = '0' + mask_int[3] = str(int(~(0xFF << (32 - (value % 32))) & 0xFF)) + if value == 32: + mask_int[3] = '0' + + return '.'.join(mask_int) + + def check_acl_args(self): + """ Check acl invalid args """ + + need_cfg = False + find_flag = False + self.cur_acl_cfg["acl_info"] = [] + + if self.acl_name: + + if self.acl_name.isdigit(): + if int(self.acl_name) < 2000 or int(self.acl_name) > 2999: + self.module.fail_json( + msg='Error: The value of acl_name is out of [2000-2999] for base ACL.') + + if self.acl_num: + self.module.fail_json( + msg='Error: The acl_name is digit, so should not input acl_num at the same time.') + else: + + self.acl_type = "Basic" + + if len(self.acl_name) < 1 or len(self.acl_name) > 32: + self.module.fail_json( + msg='Error: The len of acl_name is out of [1 - 32].') + + if self.state == "present": + if not self.acl_num and not self.acl_type and not self.rule_name: + self.module.fail_json( + msg='Error: Please input acl_num or acl_type when config ACL.') + + if self.acl_num: + if self.acl_num.isdigit(): + if int(self.acl_num) < 2000 or int(self.acl_num) > 2999: + self.module.fail_json( + msg='Error: The value of acl_name is out of [2000-2999] for base ACL.') + else: + self.module.fail_json( + msg='Error: The acl_num is not digit.') + + if self.acl_step: + if self.acl_step.isdigit(): + if int(self.acl_step) < 1 or int(self.acl_step) > 20: + self.module.fail_json( + msg='Error: The value of acl_step is out of [1 - 20].') + else: + self.module.fail_json( + msg='Error: The acl_step is not digit.') + + if self.acl_description: + if len(self.acl_description) < 1 or len(self.acl_description) > 127: + self.module.fail_json( + msg='Error: The len of acl_description is out of [1 - 127].') + + conf_str = CE_GET_ACL_HEADER + + if self.acl_type: + conf_str += "" + if self.acl_num or self.acl_name.isdigit(): + conf_str += "" + if self.acl_step: + conf_str += "" + if self.acl_description: + conf_str += "" + + conf_str += CE_GET_ACL_TAIL + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + find_flag = False + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # parse acl + acl_info = root.findall( + "acl/aclGroups/aclGroup") + if acl_info: + for tmp in acl_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["aclNumOrName", "aclType", "aclNumber", "aclStep", "aclDescription"]: + tmp_dict[site.tag] = site.text + + self.cur_acl_cfg["acl_info"].append(tmp_dict) + + if self.cur_acl_cfg["acl_info"]: + find_list = list() + for tmp in self.cur_acl_cfg["acl_info"]: + cur_cfg_dict = dict() + exist_cfg_dict = dict() + if self.acl_name: + if self.acl_name.isdigit() and tmp.get("aclNumber"): + cur_cfg_dict["aclNumber"] = self.acl_name + exist_cfg_dict["aclNumber"] = tmp.get("aclNumber") + else: + cur_cfg_dict["aclNumOrName"] = self.acl_name + exist_cfg_dict["aclNumOrName"] = tmp.get("aclNumOrName") + if self.acl_type: + cur_cfg_dict["aclType"] = self.acl_type + exist_cfg_dict["aclType"] = tmp.get("aclType") + if self.acl_num: + cur_cfg_dict["aclNumber"] = self.acl_num + exist_cfg_dict["aclNumber"] = tmp.get("aclNumber") + if self.acl_step: + cur_cfg_dict["aclStep"] = self.acl_step + exist_cfg_dict["aclStep"] = tmp.get("aclStep") + if self.acl_description: + cur_cfg_dict["aclDescription"] = self.acl_description + exist_cfg_dict["aclDescription"] = tmp.get("aclDescription") + + if cur_cfg_dict == exist_cfg_dict: + find_bool = True + else: + find_bool = False + find_list.append(find_bool) + + for mem in find_list: + if mem: + find_flag = True + break + else: + find_flag = False + + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + elif self.state == "delete_acl": + need_cfg = bool(find_flag) + else: + need_cfg = False + + self.cur_acl_cfg["need_cfg"] = need_cfg + + def check_base_rule_args(self): + """ Check base rule invalid args """ + + need_cfg = False + find_flag = False + self.cur_base_rule_cfg["base_rule_info"] = [] + + if self.acl_name: + + if self.state == "absent": + if not self.rule_name: + self.module.fail_json( + msg='Error: Please input rule_name when state is absent.') + + # config rule + if self.rule_name: + if len(self.rule_name) < 1 or len(self.rule_name) > 32: + self.module.fail_json( + msg='Error: The len of rule_name is out of [1 - 32].') + + if self.state != "delete_acl" and not self.rule_id: + self.module.fail_json( + msg='Error: Please input rule_id.') + + if self.rule_id: + if self.rule_id.isdigit(): + if int(self.rule_id) < 0 or int(self.rule_id) > 4294967294: + self.module.fail_json( + msg='Error: The value of rule_id is out of [0 - 4294967294].') + else: + self.module.fail_json( + msg='Error: The rule_id is not digit.') + + if self.source_ip: + if not check_ip_addr(self.source_ip): + self.module.fail_json( + msg='Error: The source_ip %s is invalid.' % self.source_ip) + if not self.src_mask: + self.module.fail_json( + msg='Error: Please input src_mask.') + + if self.src_mask: + if self.src_mask.isdigit(): + if int(self.src_mask) < 1 or int(self.src_mask) > 32: + self.module.fail_json( + msg='Error: The src_mask is out of [1 - 32].') + self.src_wild = self.get_wildcard_mask() + else: + self.module.fail_json( + msg='Error: The src_mask is not digit.') + + if self.vrf_name: + if len(self.vrf_name) < 1 or len(self.vrf_name) > 31: + self.module.fail_json( + msg='Error: The len of vrf_name is out of [1 - 31].') + + if self.time_range: + if len(self.time_range) < 1 or len(self.time_range) > 32: + self.module.fail_json( + msg='Error: The len of time_range is out of [1 - 32].') + + if self.rule_description: + if len(self.rule_description) < 1 or len(self.rule_description) > 127: + self.module.fail_json( + msg='Error: The len of rule_description is out of [1 - 127].') + + if self.state != "delete_acl" and not self.rule_id: + self.module.fail_json( + msg='Error: Please input rule_id.') + + conf_str = CE_GET_ACL_BASE_RULE_HEADER % self.acl_name + + if self.rule_id: + conf_str += "" + if self.rule_action: + conf_str += "" + if self.source_ip: + conf_str += "" + if self.src_wild: + conf_str += "" + if self.frag_type: + conf_str += "" + if self.vrf_name: + conf_str += "" + if self.time_range: + conf_str += "" + if self.rule_description: + conf_str += "" + conf_str += "" + + conf_str += CE_GET_ACL_BASE_RULE_TAIL + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + find_flag = False + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + + # parse base rule + base_rule_info = root.findall( + "acl/aclGroups/aclGroup/aclRuleBas4s/aclRuleBas4") + if base_rule_info: + for tmp in base_rule_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["aclRuleName", "aclRuleID", "aclAction", "aclSourceIp", "aclSrcWild", + "aclFragType", "vrfName", "aclTimeName", "aclRuleDescription", + "aclLogFlag"]: + tmp_dict[site.tag] = site.text + + self.cur_base_rule_cfg[ + "base_rule_info"].append(tmp_dict) + + if self.cur_base_rule_cfg["base_rule_info"]: + for tmp in self.cur_base_rule_cfg["base_rule_info"]: + find_flag = True + + if self.rule_name and tmp.get("aclRuleName") != self.rule_name: + find_flag = False + if self.rule_id and tmp.get("aclRuleID") != self.rule_id: + find_flag = False + if self.rule_action and tmp.get("aclAction") != self.rule_action: + find_flag = False + if self.source_ip: + tmp_src_ip = self.source_ip.split(".") + tmp_src_wild = self.src_wild.split(".") + tmp_addr_item = [] + for idx in range(4): + item1 = 255 - int(tmp_src_wild[idx]) + item2 = item1 & int(tmp_src_ip[idx]) + tmp_addr_item.append(item2) + tmp_addr = "%s.%s.%s.%s" % (tmp_addr_item[0], tmp_addr_item[1], + tmp_addr_item[2], tmp_addr_item[3]) + if tmp_addr != tmp.get("aclSourceIp"): + find_flag = False + if self.src_wild and tmp.get("aclSrcWild") != self.src_wild: + find_flag = False + frag_type = "clear_fragment" if tmp.get("aclFragType") is None else tmp.get("aclFragType") + if self.frag_type and frag_type != self.frag_type: + find_flag = False + if self.vrf_name and tmp.get("vrfName") != self.vrf_name: + find_flag = False + if self.time_range and tmp.get("aclTimeName") != self.time_range: + find_flag = False + if self.rule_description and tmp.get("aclRuleDescription") != self.rule_description: + find_flag = False + if tmp.get("aclLogFlag") != str(self.log_flag).lower(): + find_flag = False + + if find_flag: + break + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + elif self.state == "absent": + need_cfg = bool(find_flag) + else: + need_cfg = False + + self.cur_base_rule_cfg["need_cfg"] = need_cfg + + def get_proposed(self): + """ Get proposed state """ + + self.proposed["state"] = self.state + + if self.acl_name: + self.proposed["acl_name"] = self.acl_name + if self.acl_num: + self.proposed["acl_num"] = self.acl_num + if self.acl_step: + self.proposed["acl_step"] = self.acl_step + if self.acl_description: + self.proposed["acl_description"] = self.acl_description + if self.rule_name: + self.proposed["rule_name"] = self.rule_name + if self.rule_id: + self.proposed["rule_id"] = self.rule_id + if self.rule_action: + self.proposed["rule_action"] = self.rule_action + if self.source_ip: + self.proposed["source_ip"] = self.source_ip + if self.src_mask: + self.proposed["src_mask"] = self.src_mask + if self.frag_type: + self.proposed["frag_type"] = self.frag_type + if self.vrf_name: + self.proposed["vrf_name"] = self.vrf_name + if self.time_range: + self.proposed["time_range"] = self.time_range + if self.rule_description: + self.proposed["rule_description"] = self.rule_description + if self.log_flag: + self.proposed["log_flag"] = self.log_flag + + def get_existing(self): + """ Get existing state """ + + self.existing["acl_info"] = self.cur_acl_cfg["acl_info"] + self.existing["base_rule_info"] = self.cur_base_rule_cfg[ + "base_rule_info"] + + def get_end_state(self): + """ Get end state """ + + self.check_acl_args() + self.end_state["acl_info"] = self.cur_acl_cfg["acl_info"] + + self.check_base_rule_args() + self.end_state["base_rule_info"] = self.cur_base_rule_cfg[ + "base_rule_info"] + + def merge_acl(self): + """ Merge acl operation """ + + conf_str = CE_MERGE_ACL_HEADER % self.acl_name + + if self.acl_type: + conf_str += "%s" % self.acl_type + if self.acl_num: + conf_str += "%s" % self.acl_num + if self.acl_step: + conf_str += "%s" % self.acl_step + if self.acl_description: + conf_str += "%s" % self.acl_description + + conf_str += CE_MERGE_ACL_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge acl failed.') + + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + if self.acl_type and not self.acl_num: + cmd = "acl name %s %s" % (self.acl_name, self.acl_type.lower()) + elif self.acl_type and self.acl_num: + cmd = "acl name %s number %s" % (self.acl_name, self.acl_num) + elif not self.acl_type and self.acl_num: + cmd = "acl name %s number %s" % (self.acl_name, self.acl_num) + self.updates_cmd.append(cmd) + + if self.acl_description: + cmd = "description %s" % self.acl_description + self.updates_cmd.append(cmd) + + if self.acl_step: + cmd = "step %s" % self.acl_step + self.updates_cmd.append(cmd) + + self.changed = True + + def delete_acl(self): + """ Delete acl operation """ + + conf_str = CE_DELETE_ACL_HEADER % self.acl_name + + if self.acl_type: + conf_str += "%s" % self.acl_type + if self.acl_num: + conf_str += "%s" % self.acl_num + if self.acl_step: + conf_str += "%s" % self.acl_step + if self.acl_description: + conf_str += "%s" % self.acl_description + + conf_str += CE_DELETE_ACL_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Delete acl failed.') + + if self.acl_description: + cmd = "undo description" + self.updates_cmd.append(cmd) + + if self.acl_step: + cmd = "undo step" + self.updates_cmd.append(cmd) + + if self.acl_name.isdigit(): + cmd = "undo acl number %s" % self.acl_name + else: + cmd = "undo acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + self.changed = True + + def merge_base_rule(self): + """ Merge base rule operation """ + + conf_str = CE_MERGE_ACL_BASE_RULE_HEADER % ( + self.acl_name, self.rule_name) + + if self.rule_id: + conf_str += "%s" % self.rule_id + if self.rule_action: + conf_str += "%s" % self.rule_action + if self.source_ip: + conf_str += "%s" % self.source_ip + if self.src_wild: + conf_str += "%s" % self.src_wild + if self.frag_type: + conf_str += "%s" % self.frag_type + if self.vrf_name: + conf_str += "%s" % self.vrf_name + if self.time_range: + conf_str += "%s" % self.time_range + if self.rule_description: + conf_str += "%s" % self.rule_description + conf_str += "%s" % str(self.log_flag).lower() + + conf_str += CE_MERGE_ACL_BASE_RULE_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge acl base rule failed.') + + if self.rule_action: + cmd = "rule" + if self.rule_id: + cmd += " %s" % self.rule_id + cmd += " %s" % self.rule_action + if self.frag_type == "fragment": + cmd += " fragment-type fragment" + if self.source_ip and self.src_wild: + cmd += " source %s %s" % (self.source_ip, self.src_wild) + if self.time_range: + cmd += " time-range %s" % self.time_range + if self.vrf_name: + cmd += " vpn-instance %s" % self.vrf_name + if self.log_flag: + cmd += " logging" + self.updates_cmd.append(cmd) + + if self.rule_description: + cmd = "rule %s description %s" % ( + self.rule_id, self.rule_description) + self.updates_cmd.append(cmd) + + self.changed = True + + def delete_base_rule(self): + """ Delete base rule operation """ + + conf_str = CE_DELETE_ACL_BASE_RULE_HEADER % ( + self.acl_name, self.rule_name) + + if self.rule_id: + conf_str += "%s" % self.rule_id + if self.rule_action: + conf_str += "%s" % self.rule_action + if self.source_ip: + conf_str += "%s" % self.source_ip + if self.src_wild: + conf_str += "%s" % self.src_wild + if self.frag_type: + conf_str += "%s" % self.frag_type + if self.vrf_name: + conf_str += "%s" % self.vrf_name + if self.time_range: + conf_str += "%s" % self.time_range + if self.rule_description: + conf_str += "%s" % self.rule_description + conf_str += "%s" % str(self.log_flag).lower() + + conf_str += CE_DELETE_ACL_BASE_RULE_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Delete acl base rule failed.') + + if self.rule_description: + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + cmd = "acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + cmd = "undo rule %s description" % self.rule_id + self.updates_cmd.append(cmd) + + if self.rule_id: + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + cmd = "acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + cmd = "undo rule %s" % self.rule_id + self.updates_cmd.append(cmd) + elif self.rule_action: + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + cmd = "acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + cmd = "undo rule" + cmd += " %s" % self.rule_action + if self.frag_type == "fragment": + cmd += " fragment-type fragment" + if self.source_ip and self.src_wild: + cmd += " source %s %s" % (self.source_ip, self.src_wild) + if self.time_range: + cmd += " time-range %s" % self.time_range + if self.vrf_name: + cmd += " vpn-instance %s" % self.vrf_name + if self.log_flag: + cmd += " logging" + self.updates_cmd.append(cmd) + + self.changed = True + + def work(self): + """ Main work function """ + + self.check_acl_args() + self.check_base_rule_args() + self.get_proposed() + self.get_existing() + + if self.state == "present": + if self.cur_acl_cfg["need_cfg"]: + self.merge_acl() + if self.cur_base_rule_cfg["need_cfg"]: + self.merge_base_rule() + + elif self.state == "absent": + if self.cur_base_rule_cfg["need_cfg"]: + self.delete_base_rule() + + elif self.state == "delete_acl": + if self.cur_acl_cfg["need_cfg"]: + self.delete_acl() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent', + 'delete_acl'], default='present'), + acl_name=dict(type='str', required=True), + acl_num=dict(type='str'), + acl_step=dict(type='str'), + acl_description=dict(type='str'), + rule_name=dict(type='str'), + rule_id=dict(type='str'), + rule_action=dict(choices=['permit', 'deny']), + source_ip=dict(type='str'), + src_mask=dict(type='str'), + frag_type=dict(choices=['fragment', 'clear_fragment']), + vrf_name=dict(type='str'), + time_range=dict(type='str'), + rule_description=dict(type='str'), + log_flag=dict(required=False, default=False, type='bool') + ) + + argument_spec.update(ce_argument_spec) + module = BaseAcl(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_acl_advance.py b/plugins/modules/ce_acl_advance.py new file mode 100644 index 0000000..6dc2ad4 --- /dev/null +++ b/plugins/modules/ce_acl_advance.py @@ -0,0 +1,1747 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_acl_advance +short_description: Manages advanced ACL configuration on HUAWEI CloudEngine switches. +description: + - Manages advanced ACL configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + required: false + default: present + choices: ['present','absent','delete_acl'] + acl_name: + description: + - ACL number or name. + For a numbered rule group, the value ranging from 3000 to 3999 indicates a advance ACL. + For a named rule group, the value is a string of 1 to 32 case-sensitive characters starting + with a letter, spaces not supported. + required: true + acl_num: + description: + - ACL number. + The value is an integer ranging from 3000 to 3999. + acl_step: + description: + - ACL step. + The value is an integer ranging from 1 to 20. The default value is 5. + acl_description: + description: + - ACL description. + The value is a string of 1 to 127 characters. + rule_name: + description: + - Name of a basic ACL rule. + The value is a string of 1 to 32 characters. + rule_id: + description: + - ID of a basic ACL rule in configuration mode. + The value is an integer ranging from 0 to 4294967294. + rule_action: + description: + - Matching mode of basic ACL rules. + choices: ['permit','deny'] + protocol: + description: + - Protocol type. + choices: ['ip', 'icmp', 'igmp', 'ipinip', 'tcp', 'udp', 'gre', 'ospf'] + source_ip: + description: + - Source IP address. + The value is a string of 0 to 255 characters.The default value is 0.0.0.0. + The value is in dotted decimal notation. + src_mask: + description: + - Source IP address mask. + The value is an integer ranging from 1 to 32. + src_pool_name: + description: + - Name of a source pool. + The value is a string of 1 to 32 characters. + dest_ip: + description: + - Destination IP address. + The value is a string of 0 to 255 characters.The default value is 0.0.0.0. + The value is in dotted decimal notation. + dest_mask: + description: + - Destination IP address mask. + The value is an integer ranging from 1 to 32. + dest_pool_name: + description: + - Name of a destination pool. + The value is a string of 1 to 32 characters. + src_port_op: + description: + - Range type of the source port. + choices: ['lt','eq', 'gt', 'range'] + src_port_begin: + description: + - Start port number of the source port. + The value is an integer ranging from 0 to 65535. + src_port_end: + description: + - End port number of the source port. + The value is an integer ranging from 0 to 65535. + src_port_pool_name: + description: + - Name of a source port pool. + The value is a string of 1 to 32 characters. + dest_port_op: + description: + - Range type of the destination port. + choices: ['lt','eq', 'gt', 'range'] + dest_port_begin: + description: + - Start port number of the destination port. + The value is an integer ranging from 0 to 65535. + dest_port_end: + description: + - End port number of the destination port. + The value is an integer ranging from 0 to 65535. + dest_port_pool_name: + description: + - Name of a destination port pool. + The value is a string of 1 to 32 characters. + frag_type: + description: + - Type of packet fragmentation. + choices: ['fragment', 'clear_fragment'] + precedence: + description: + - Data packets can be filtered based on the priority field. + The value is an integer ranging from 0 to 7. + tos: + description: + - ToS value on which data packet filtering is based. + The value is an integer ranging from 0 to 15. + dscp: + description: + - Differentiated Services Code Point. + The value is an integer ranging from 0 to 63. + icmp_name: + description: + - ICMP name. + choices: ['unconfiged', 'echo', 'echo-reply', 'fragmentneed-DFset', 'host-redirect', + 'host-tos-redirect', 'host-unreachable', 'information-reply', 'information-request', + 'net-redirect', 'net-tos-redirect', 'net-unreachable', 'parameter-problem', + 'port-unreachable', 'protocol-unreachable', 'reassembly-timeout', 'source-quench', + 'source-route-failed', 'timestamp-reply', 'timestamp-request', 'ttl-exceeded', + 'address-mask-reply', 'address-mask-request', 'custom'] + icmp_type: + description: + - ICMP type. This parameter is available only when the packet protocol is ICMP. + The value is an integer ranging from 0 to 255. + icmp_code: + description: + - ICMP message code. Data packets can be filtered based on the ICMP message code. + The value is an integer ranging from 0 to 255. + ttl_expired: + description: + - Whether TTL Expired is matched, with the TTL value of 1. + type: bool + default: 'no' + vrf_name: + description: + - VPN instance name. + The value is a string of 1 to 31 characters.The default value is _public_. + syn_flag: + description: + - TCP flag value. + The value is an integer ranging from 0 to 63. + tcp_flag_mask: + description: + - TCP flag mask value. + The value is an integer ranging from 0 to 63. + established: + description: + - Match established connections. + type: bool + default: 'no' + time_range: + description: + - Name of a time range in which an ACL rule takes effect. + rule_description: + description: + - Description about an ACL rule. + igmp_type: + description: + - Internet Group Management Protocol. + choices: ['host-query', 'mrouter-adver', 'mrouter-solic', 'mrouter-termi', 'mtrace-resp', 'mtrace-route', + 'v1host-report', 'v2host-report', 'v2leave-group', 'v3host-report'] + log_flag: + description: + - Flag of logging matched data packets. + type: bool + default: 'no' +''' + +EXAMPLES = ''' + +- name: CloudEngine advance acl test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config ACL" + ce_acl_advance: + state: present + acl_name: 3200 + provider: "{{ cli }}" + + - name: "Undo ACL" + ce_acl_advance: + state: delete_acl + acl_name: 3200 + provider: "{{ cli }}" + + - name: "Config ACL advance rule" + ce_acl_advance: + state: present + acl_name: test + rule_name: test_rule + rule_id: 111 + rule_action: permit + protocol: tcp + source_ip: 10.10.10.10 + src_mask: 24 + frag_type: fragment + provider: "{{ cli }}" + + - name: "Undo ACL advance rule" + ce_acl_advance: + state: absent + acl_name: test + rule_name: test_rule + rule_id: 111 + rule_action: permit + protocol: tcp + source_ip: 10.10.10.10 + src_mask: 24 + frag_type: fragment + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"acl_name": "test", "state": "delete_acl"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"aclNumOrName": "test", "aclType": "Advance"} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {} +updates: + description: command sent to the device + returned: always + type: list + sample: ["undo acl name test"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + + +# get acl +CE_GET_ACL_HEADER = """ + + + + + +""" +CE_GET_ACL_TAIL = """ + + + + +""" +# merge acl +CE_MERGE_ACL_HEADER = """ + + + + + %s +""" +CE_MERGE_ACL_TAIL = """ + + + + +""" +# delete acl +CE_DELETE_ACL_HEADER = """ + + + + + %s +""" +CE_DELETE_ACL_TAIL = """ + + + + +""" + +# get acl advance rule +CE_GET_ACL_ADVANCE_RULE_HEADER = """ + + + + + %s + + + +""" +CE_GET_ACL_ADVANCE_RULE_TAIL = """ + + + + + + +""" +# merge acl advance rule +CE_MERGE_ACL_ADVANCE_RULE_HEADER = """ + + + + + %s + + + %s +""" +CE_MERGE_ACL_ADVANCE_RULE_TAIL = """ + + + + + + +""" +# delete acl advance rule +CE_DELETE_ACL_ADVANCE_RULE_HEADER = """ + + + + + %s + + + %s +""" +CE_DELETE_ACL_ADVANCE_RULE_TAIL = """ + + + + + + +""" + + +PROTOCOL_NUM = {"ip": "0", + "icmp": "1", + "igmp": "2", + "ipinip": "4", + "tcp": "6", + "udp": "17", + "gre": "47", + "ospf": "89"} + +IGMP_TYPE_NUM = {"host-query": "17", + "mrouter-adver": "48", + "mrouter-solic": "49", + "mrouter-termi": "50", + "mtrace-resp": "30", + "mtrace-route": "31", + "v1host-report": "18", + "v2host-report": "22", + "v2leave-group": "23", + "v3host-report": "34"} + + +def get_wildcard_mask(mask): + """ convert mask length to ip address wildcard mask, i.e. 24 to 0.0.0.255 """ + + mask_int = ["255"] * 4 + value = int(mask) + + if value > 32: + return None + if value < 8: + mask_int[0] = str(int(~(0xFF << (8 - value % 8)) & 0xFF)) + if value >= 8: + mask_int[0] = '0' + mask_int[1] = str(int(~(0xFF << (16 - (value % 16))) & 0xFF)) + if value >= 16: + mask_int[1] = '0' + mask_int[2] = str(int(~(0xFF << (24 - (value % 24))) & 0xFF)) + if value >= 24: + mask_int[2] = '0' + mask_int[3] = str(int(~(0xFF << (32 - (value % 32))) & 0xFF)) + if value == 32: + mask_int[3] = '0' + + return '.'.join(mask_int) + + +class AdvanceAcl(object): + """ Manages advance acl configuration """ + + def __init__(self, **kwargs): + """ Class init """ + + # argument spec + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # module args + self.state = self.module.params['state'] + self.acl_name = self.module.params['acl_name'] or None + self.acl_num = self.module.params['acl_num'] or None + self.acl_type = None + self.acl_step = self.module.params['acl_step'] or None + self.acl_description = self.module.params['acl_description'] or None + self.rule_name = self.module.params['rule_name'] or None + self.rule_id = self.module.params['rule_id'] or None + self.rule_action = self.module.params['rule_action'] or None + self.protocol = self.module.params['protocol'] or None + self.protocol_num = None + self.source_ip = self.module.params['source_ip'] or None + self.src_mask = self.module.params['src_mask'] or None + self.src_wild = None + self.src_pool_name = self.module.params['src_pool_name'] or None + self.dest_ip = self.module.params['dest_ip'] or None + self.dest_mask = self.module.params['dest_mask'] or None + self.dest_wild = None + self.dest_pool_name = self.module.params['dest_pool_name'] or None + self.src_port_op = self.module.params['src_port_op'] or None + self.src_port_begin = self.module.params['src_port_begin'] or None + self.src_port_end = self.module.params['src_port_end'] or None + self.src_port_pool_name = self.module.params[ + 'src_port_pool_name'] or None + self.dest_port_op = self.module.params['dest_port_op'] or None + self.dest_port_begin = self.module.params['dest_port_begin'] or None + self.dest_port_end = self.module.params['dest_port_end'] or None + self.dest_port_pool_name = self.module.params[ + 'dest_port_pool_name'] or None + self.frag_type = self.module.params['frag_type'] or None + self.precedence = self.module.params['precedence'] or None + self.tos = self.module.params['tos'] or None + self.dscp = self.module.params['dscp'] or None + self.icmp_name = self.module.params['icmp_name'] or None + self.icmp_type = self.module.params['icmp_type'] or None + self.icmp_code = self.module.params['icmp_code'] or None + self.ttl_expired = self.module.params['ttl_expired'] + self.vrf_name = self.module.params['vrf_name'] or None + self.syn_flag = self.module.params['syn_flag'] or None + self.tcp_flag_mask = self.module.params['tcp_flag_mask'] or None + self.established = self.module.params['established'] + self.time_range = self.module.params['time_range'] or None + self.rule_description = self.module.params['rule_description'] or None + self.igmp_type = self.module.params['igmp_type'] or None + self.igmp_type_num = None + self.log_flag = self.module.params['log_flag'] + + self.precedence_name = dict() + self.precedence_name["0"] = "routine" + self.precedence_name["1"] = "priority" + self.precedence_name["2"] = "immediate" + self.precedence_name["3"] = "flash" + self.precedence_name["4"] = "flash-override" + self.precedence_name["5"] = "critical" + self.precedence_name["6"] = "internet" + self.precedence_name["7"] = "network" + + # cur config + self.cur_acl_cfg = dict() + self.cur_advance_rule_cfg = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def netconf_get_config(self, conf_str): + """ Get configure by netconf """ + + xml_str = get_nc_config(self.module, conf_str) + + return xml_str + + def netconf_set_config(self, conf_str): + """ Set configure by netconf """ + + xml_str = set_nc_config(self.module, conf_str) + + return xml_str + + def get_protocol_num(self): + """ Get protocol num by name """ + + if self.protocol: + self.protocol_num = PROTOCOL_NUM.get(self.protocol) + + def get_igmp_type_num(self): + """ Get igmp type num by type """ + + if self.igmp_type: + self.igmp_type_num = IGMP_TYPE_NUM.get(self.igmp_type) + + def check_acl_args(self): + """ Check acl invalid args """ + + need_cfg = False + find_flag = False + self.cur_acl_cfg["acl_info"] = [] + + if self.acl_name: + + if self.acl_name.isdigit(): + if int(self.acl_name) < 3000 or int(self.acl_name) > 3999: + self.module.fail_json( + msg='Error: The value of acl_name is out of [3000-3999] for advance ACL.') + + if self.acl_num: + self.module.fail_json( + msg='Error: The acl_name is digit, so should not input acl_num at the same time.') + else: + + self.acl_type = "Advance" + + if len(self.acl_name) < 1 or len(self.acl_name) > 32: + self.module.fail_json( + msg='Error: The len of acl_name is out of [1 - 32].') + + if self.state == "present": + if not self.acl_num and not self.acl_type and not self.rule_name: + self.module.fail_json( + msg='Error: Please input acl_num or acl_type when config ACL.') + + if self.acl_num: + if self.acl_num.isdigit(): + if int(self.acl_num) < 3000 or int(self.acl_num) > 3999: + self.module.fail_json( + msg='Error: The value of acl_name is out of [3000-3999] for advance ACL.') + else: + self.module.fail_json( + msg='Error: The acl_num is not digit.') + + if self.acl_step: + if self.acl_step.isdigit(): + if int(self.acl_step) < 1 or int(self.acl_step) > 20: + self.module.fail_json( + msg='Error: The value of acl_step is out of [1 - 20].') + else: + self.module.fail_json( + msg='Error: The acl_step is not digit.') + + if self.acl_description: + if len(self.acl_description) < 1 or len(self.acl_description) > 127: + self.module.fail_json( + msg='Error: The len of acl_description is out of [1 - 127].') + + conf_str = CE_GET_ACL_HEADER + + if self.acl_type: + conf_str += "" + if self.acl_num or self.acl_name.isdigit(): + conf_str += "" + if self.acl_step: + conf_str += "" + if self.acl_description: + conf_str += "" + + conf_str += CE_GET_ACL_TAIL + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + find_flag = False + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + + # parse acl + acl_info = root.findall( + "acl/aclGroups/aclGroup") + if acl_info: + for tmp in acl_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["aclNumOrName", "aclType", "aclNumber", "aclStep", "aclDescription"]: + tmp_dict[site.tag] = site.text + + self.cur_acl_cfg["acl_info"].append(tmp_dict) + + if self.cur_acl_cfg["acl_info"]: + find_list = list() + for tmp in self.cur_acl_cfg["acl_info"]: + cur_cfg_dict = dict() + exist_cfg_dict = dict() + + if self.acl_name: + if self.acl_name.isdigit() and tmp.get("aclNumber"): + cur_cfg_dict["aclNumber"] = self.acl_name + exist_cfg_dict["aclNumber"] = tmp.get("aclNumber") + else: + cur_cfg_dict["aclNumOrName"] = self.acl_name + exist_cfg_dict["aclNumOrName"] = tmp.get("aclNumOrName") + if self.acl_type: + cur_cfg_dict["aclType"] = self.acl_type + exist_cfg_dict["aclType"] = tmp.get("aclType") + if self.acl_num: + cur_cfg_dict["aclNumber"] = self.acl_num + exist_cfg_dict["aclNumber"] = tmp.get("aclNumber") + if self.acl_step: + cur_cfg_dict["aclStep"] = self.acl_step + exist_cfg_dict["aclStep"] = tmp.get("aclStep") + if self.acl_description: + cur_cfg_dict["aclDescription"] = self.acl_description + exist_cfg_dict["aclDescription"] = tmp.get("aclDescription") + + if cur_cfg_dict == exist_cfg_dict: + find_bool = True + else: + find_bool = False + find_list.append(find_bool) + for mem in find_list: + if mem: + find_flag = True + break + else: + find_flag = False + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + elif self.state == "delete_acl": + need_cfg = bool(find_flag) + else: + need_cfg = False + + self.cur_acl_cfg["need_cfg"] = need_cfg + + def check_advance_rule_args(self): + """ Check advance rule invalid args """ + + need_cfg = False + find_flag = False + self.cur_advance_rule_cfg["adv_rule_info"] = [] + + if self.acl_name: + + if self.state == "absent": + if not self.rule_name: + self.module.fail_json( + msg='Error: Please input rule_name when state is absent.') + + # config rule + if self.rule_name: + if len(self.rule_name) < 1 or len(self.rule_name) > 32: + self.module.fail_json( + msg='Error: The len of rule_name is out of [1 - 32].') + + if self.state != "delete_acl" and not self.rule_id: + self.module.fail_json( + msg='Error: Please input rule_id.') + + if self.rule_id: + if self.rule_id.isdigit(): + if int(self.rule_id) < 0 or int(self.rule_id) > 4294967294: + self.module.fail_json( + msg='Error: The value of rule_id is out of [0 - 4294967294].') + else: + self.module.fail_json( + msg='Error: The rule_id is not digit.') + + if self.rule_action and not self.protocol: + self.module.fail_json( + msg='Error: The rule_action and the protocol must input at the same time.') + + if not self.rule_action and self.protocol: + self.module.fail_json( + msg='Error: The rule_action and the protocol must input at the same time.') + + if self.protocol: + self.get_protocol_num() + + if self.source_ip: + if not check_ip_addr(self.source_ip): + self.module.fail_json( + msg='Error: The source_ip %s is invalid.' % self.source_ip) + if not self.src_mask: + self.module.fail_json( + msg='Error: Please input src_mask.') + + if self.src_mask: + if self.src_mask.isdigit(): + if int(self.src_mask) < 1 or int(self.src_mask) > 32: + self.module.fail_json( + msg='Error: The value of src_mask is out of [1 - 32].') + self.src_wild = get_wildcard_mask(self.src_mask) + else: + self.module.fail_json( + msg='Error: The src_mask is not digit.') + + if self.src_pool_name: + if len(self.src_pool_name) < 1 or len(self.src_pool_name) > 32: + self.module.fail_json( + msg='Error: The len of src_pool_name is out of [1 - 32].') + + if self.dest_ip: + if not check_ip_addr(self.dest_ip): + self.module.fail_json( + msg='Error: The dest_ip %s is invalid.' % self.dest_ip) + if not self.dest_mask: + self.module.fail_json( + msg='Error: Please input dest_mask.') + + if self.dest_mask: + if self.dest_mask.isdigit(): + if int(self.dest_mask) < 1 or int(self.dest_mask) > 32: + self.module.fail_json( + msg='Error: The value of dest_mask is out of [1 - 32].') + self.dest_wild = get_wildcard_mask(self.dest_mask) + else: + self.module.fail_json( + msg='Error: The dest_mask is not digit.') + + if self.dest_pool_name: + if len(self.dest_pool_name) < 1 or len(self.dest_pool_name) > 32: + self.module.fail_json( + msg='Error: The len of dest_pool_name is out of [1 - 32].') + + if self.src_port_op: + if self.src_port_op == "lt": + if not self.src_port_end: + self.module.fail_json( + msg='Error: The src_port_end must input.') + if self.src_port_begin: + self.module.fail_json( + msg='Error: The src_port_begin should not input.') + if self.src_port_op == "eq" or self.src_port_op == "gt": + if not self.src_port_begin: + self.module.fail_json( + msg='Error: The src_port_begin must input.') + if self.src_port_end: + self.module.fail_json( + msg='Error: The src_port_end should not input.') + if self.src_port_op == "range": + if not self.src_port_begin or not self.src_port_end: + self.module.fail_json( + msg='Error: The src_port_begin and src_port_end must input.') + + if self.src_port_begin: + if self.src_port_begin.isdigit(): + if int(self.src_port_begin) < 0 or int(self.src_port_begin) > 65535: + self.module.fail_json( + msg='Error: The value of src_port_begin is out of [0 - 65535].') + else: + self.module.fail_json( + msg='Error: The src_port_begin is not digit.') + + if self.src_port_end: + if self.src_port_end.isdigit(): + if int(self.src_port_end) < 0 or int(self.src_port_end) > 65535: + self.module.fail_json( + msg='Error: The value of src_port_end is out of [0 - 65535].') + else: + self.module.fail_json( + msg='Error: The src_port_end is not digit.') + + if self.src_port_pool_name: + if len(self.src_port_pool_name) < 1 or len(self.src_port_pool_name) > 32: + self.module.fail_json( + msg='Error: The len of src_port_pool_name is out of [1 - 32].') + + if self.dest_port_op: + if self.dest_port_op == "lt": + if not self.dest_port_end: + self.module.fail_json( + msg='Error: The dest_port_end must input.') + if self.dest_port_begin: + self.module.fail_json( + msg='Error: The dest_port_begin should not input.') + if self.dest_port_op == "eq" or self.dest_port_op == "gt": + if not self.dest_port_begin: + self.module.fail_json( + msg='Error: The dest_port_begin must input.') + if self.dest_port_end: + self.module.fail_json( + msg='Error: The dest_port_end should not input.') + if self.dest_port_op == "range": + if not self.dest_port_begin or not self.dest_port_end: + self.module.fail_json( + msg='Error: The dest_port_begin and dest_port_end must input.') + + if self.dest_port_begin: + if self.dest_port_begin.isdigit(): + if int(self.dest_port_begin) < 0 or int(self.dest_port_begin) > 65535: + self.module.fail_json( + msg='Error: The value of dest_port_begin is out of [0 - 65535].') + else: + self.module.fail_json( + msg='Error: The dest_port_begin is not digit.') + + if self.dest_port_end: + if self.dest_port_end.isdigit(): + if int(self.dest_port_end) < 0 or int(self.dest_port_end) > 65535: + self.module.fail_json( + msg='Error: The value of dest_port_end is out of [0 - 65535].') + else: + self.module.fail_json( + msg='Error: The dest_port_end is not digit.') + + if self.dest_port_pool_name: + if len(self.dest_port_pool_name) < 1 or len(self.dest_port_pool_name) > 32: + self.module.fail_json( + msg='Error: The len of dest_port_pool_name is out of [1 - 32].') + + if self.precedence: + if self.precedence.isdigit(): + if int(self.precedence) < 0 or int(self.precedence) > 7: + self.module.fail_json( + msg='Error: The value of precedence is out of [0 - 7].') + else: + self.module.fail_json( + msg='Error: The precedence is not digit.') + + if self.tos: + if self.tos.isdigit(): + if int(self.tos) < 0 or int(self.tos) > 15: + self.module.fail_json( + msg='Error: The value of tos is out of [0 - 15].') + else: + self.module.fail_json( + msg='Error: The tos is not digit.') + + if self.dscp: + if self.dscp.isdigit(): + if int(self.dscp) < 0 or int(self.dscp) > 63: + self.module.fail_json( + msg='Error: The value of dscp is out of [0 - 63].') + else: + self.module.fail_json( + msg='Error: The dscp is not digit.') + + if self.icmp_type: + if self.icmp_type.isdigit(): + if int(self.icmp_type) < 0 or int(self.icmp_type) > 255: + self.module.fail_json( + msg='Error: The value of icmp_type is out of [0 - 255].') + else: + self.module.fail_json( + msg='Error: The icmp_type is not digit.') + + if self.icmp_code: + if self.icmp_code.isdigit(): + if int(self.icmp_code) < 0 or int(self.icmp_code) > 255: + self.module.fail_json( + msg='Error: The value of icmp_code is out of [0 - 255].') + else: + self.module.fail_json( + msg='Error: The icmp_code is not digit.') + + if self.vrf_name: + if len(self.vrf_name) < 1 or len(self.vrf_name) > 31: + self.module.fail_json( + msg='Error: The len of vrf_name is out of [1 - 31].') + + if self.syn_flag: + if self.syn_flag.isdigit(): + if int(self.syn_flag) < 0 or int(self.syn_flag) > 63: + self.module.fail_json( + msg='Error: The value of syn_flag is out of [0 - 63].') + else: + self.module.fail_json( + msg='Error: The syn_flag is not digit.') + + if self.tcp_flag_mask: + if self.tcp_flag_mask.isdigit(): + if int(self.tcp_flag_mask) < 0 or int(self.tcp_flag_mask) > 63: + self.module.fail_json( + msg='Error: The value of tcp_flag_mask is out of [0 - 63].') + else: + self.module.fail_json( + msg='Error: The tcp_flag_mask is not digit.') + + if self.time_range: + if len(self.time_range) < 1 or len(self.time_range) > 32: + self.module.fail_json( + msg='Error: The len of time_range is out of [1 - 32].') + + if self.rule_description: + if len(self.rule_description) < 1 or len(self.rule_description) > 127: + self.module.fail_json( + msg='Error: The len of rule_description is out of [1 - 127].') + + if self.igmp_type: + self.get_igmp_type_num() + + conf_str = CE_GET_ACL_ADVANCE_RULE_HEADER % self.acl_name + + if self.rule_id: + conf_str += "" + if self.rule_action: + conf_str += "" + if self.protocol: + conf_str += "" + if self.source_ip: + conf_str += "" + if self.src_wild: + conf_str += "" + if self.src_pool_name: + conf_str += "" + if self.dest_ip: + conf_str += "" + if self.dest_wild: + conf_str += "" + if self.dest_pool_name: + conf_str += "" + if self.src_port_op: + conf_str += "" + if self.src_port_begin: + conf_str += "" + if self.src_port_end: + conf_str += "" + if self.src_port_pool_name: + conf_str += "" + if self.dest_port_op: + conf_str += "" + if self.dest_port_begin: + conf_str += "" + if self.dest_port_end: + conf_str += "" + if self.dest_port_pool_name: + conf_str += "" + if self.frag_type: + conf_str += "" + if self.precedence: + conf_str += "" + if self.tos: + conf_str += "" + if self.dscp: + conf_str += "" + if self.icmp_name: + conf_str += "" + if self.icmp_type: + conf_str += "" + if self.icmp_code: + conf_str += "" + conf_str += "" + if self.vrf_name: + conf_str += "" + if self.syn_flag: + conf_str += "" + if self.tcp_flag_mask: + conf_str += "" + conf_str += "" + if self.time_range: + conf_str += "" + if self.rule_description: + conf_str += "" + if self.igmp_type: + conf_str += "" + conf_str += "" + + conf_str += CE_GET_ACL_ADVANCE_RULE_TAIL + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + find_flag = False + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + + # parse advance rule + adv_rule_info = root.findall( + "acl/aclGroups/aclGroup/aclRuleAdv4s/aclRuleAdv4") + if adv_rule_info: + for tmp in adv_rule_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["aclRuleName", "aclRuleID", "aclAction", "aclProtocol", "aclSourceIp", + "aclSrcWild", "aclSPoolName", "aclDestIp", "aclDestWild", + "aclDPoolName", "aclSrcPortOp", "aclSrcPortBegin", "aclSrcPortEnd", + "aclSPortPoolName", "aclDestPortOp", "aclDestPortB", "aclDestPortE", + "aclDPortPoolName", "aclFragType", "aclPrecedence", "aclTos", + "aclDscp", "aclIcmpName", "aclIcmpType", "aclIcmpCode", "aclTtlExpired", + "vrfName", "aclSynFlag", "aclTcpFlagMask", "aclEstablished", + "aclTimeName", "aclRuleDescription", "aclIgmpType", "aclLogFlag"]: + tmp_dict[site.tag] = site.text + + self.cur_advance_rule_cfg[ + "adv_rule_info"].append(tmp_dict) + + if self.cur_advance_rule_cfg["adv_rule_info"]: + for tmp in self.cur_advance_rule_cfg["adv_rule_info"]: + find_flag = True + + if self.rule_name and tmp.get("aclRuleName") != self.rule_name: + find_flag = False + if self.rule_id and tmp.get("aclRuleID") != self.rule_id: + find_flag = False + if self.rule_action and tmp.get("aclAction") != self.rule_action: + find_flag = False + if self.protocol and tmp.get("aclProtocol") != self.protocol_num: + find_flag = False + if self.source_ip: + tmp_src_ip = self.source_ip.split(".") + tmp_src_wild = self.src_wild.split(".") + tmp_addr_item = [] + for idx in range(4): + item1 = 255 - int(tmp_src_wild[idx]) + item2 = item1 & int(tmp_src_ip[idx]) + tmp_addr_item.append(item2) + tmp_addr = "%s.%s.%s.%s" % (tmp_addr_item[0], tmp_addr_item[1], + tmp_addr_item[2], tmp_addr_item[3]) + if tmp_addr != tmp.get("aclSourceIp"): + find_flag = False + if self.src_wild and tmp.get("aclSrcWild") != self.src_wild: + find_flag = False + if self.src_pool_name and tmp.get("aclSPoolName") != self.src_pool_name: + find_flag = False + if self.dest_ip: + tmp_src_ip = self.dest_ip.split(".") + tmp_src_wild = self.dest_wild.split(".") + tmp_addr_item = [] + for idx in range(4): + item1 = 255 - int(tmp_src_wild[idx]) + item2 = item1 & int(tmp_src_ip[idx]) + tmp_addr_item.append(item2) + tmp_addr = "%s.%s.%s.%s" % (tmp_addr_item[0], tmp_addr_item[1], + tmp_addr_item[2], tmp_addr_item[3]) + if tmp_addr != tmp.get("aclDestIp"): + find_flag = False + if self.dest_wild and tmp.get("aclDestWild") != self.dest_wild: + find_flag = False + if self.dest_pool_name and tmp.get("aclDPoolName") != self.dest_pool_name: + find_flag = False + if self.src_port_op and tmp.get("aclSrcPortOp") != self.src_port_op: + find_flag = False + if self.src_port_begin and tmp.get("aclSrcPortBegin") != self.src_port_begin: + find_flag = False + if self.src_port_end and tmp.get("aclSrcPortEnd") != self.src_port_end: + find_flag = False + if self.src_port_pool_name and tmp.get("aclSPortPoolName") != self.src_port_pool_name: + find_flag = False + if self.dest_port_op and tmp.get("aclDestPortOp") != self.dest_port_op: + find_flag = False + if self.dest_port_begin and tmp.get("aclDestPortB") != self.dest_port_begin: + find_flag = False + if self.dest_port_end and tmp.get("aclDestPortE") != self.dest_port_end: + find_flag = False + if self.dest_port_pool_name and tmp.get("aclDPortPoolName") != self.dest_port_pool_name: + find_flag = False + frag_type = "clear_fragment" if tmp.get("aclFragType") is None else tmp.get("aclFragType") + if self.frag_type and frag_type != self.frag_type: + find_flag = False + if self.precedence and tmp.get("aclPrecedence") != self.precedence: + find_flag = False + if self.tos and tmp.get("aclTos") != self.tos: + find_flag = False + if self.dscp and tmp.get("aclDscp") != self.dscp: + find_flag = False + if self.icmp_name and tmp.get("aclIcmpName") != self.icmp_name: + find_flag = False + if self.icmp_type and tmp.get("aclIcmpType") != self.icmp_type: + find_flag = False + if self.icmp_code and tmp.get("aclIcmpCode") != self.icmp_code: + find_flag = False + if tmp.get("aclTtlExpired").lower() != str(self.ttl_expired).lower(): + find_flag = False + if self.vrf_name and tmp.get("vrfName") != self.vrf_name: + find_flag = False + if self.syn_flag and tmp.get("aclSynFlag") != self.syn_flag: + find_flag = False + if self.tcp_flag_mask and tmp.get("aclTcpFlagMask") != self.tcp_flag_mask: + find_flag = False + if self.protocol == "tcp" and \ + tmp.get("aclEstablished").lower() != str(self.established).lower(): + find_flag = False + if self.time_range and tmp.get("aclTimeName") != self.time_range: + find_flag = False + if self.rule_description and tmp.get("aclRuleDescription") != self.rule_description: + find_flag = False + if self.igmp_type and tmp.get("aclIgmpType") != self.igmp_type_num: + find_flag = False + if tmp.get("aclLogFlag").lower() != str(self.log_flag).lower(): + find_flag = False + + if find_flag: + break + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + elif self.state == "absent": + need_cfg = bool(find_flag) + else: + need_cfg = False + + self.cur_advance_rule_cfg["need_cfg"] = need_cfg + + def get_proposed(self): + """ Get proposed state """ + + self.proposed["state"] = self.state + + if self.acl_name: + self.proposed["acl_name"] = self.acl_name + if self.acl_num: + self.proposed["acl_num"] = self.acl_num + if self.acl_step: + self.proposed["acl_step"] = self.acl_step + if self.acl_description: + self.proposed["acl_description"] = self.acl_description + if self.rule_name: + self.proposed["rule_name"] = self.rule_name + if self.rule_id: + self.proposed["rule_id"] = self.rule_id + if self.rule_action: + self.proposed["rule_action"] = self.rule_action + if self.protocol: + self.proposed["protocol"] = self.protocol + if self.source_ip: + self.proposed["source_ip"] = self.source_ip + if self.src_mask: + self.proposed["src_mask"] = self.src_mask + if self.src_pool_name: + self.proposed["src_pool_name"] = self.src_pool_name + if self.dest_ip: + self.proposed["dest_ip"] = self.dest_ip + if self.dest_mask: + self.proposed["dest_mask"] = self.dest_mask + if self.dest_pool_name: + self.proposed["dest_pool_name"] = self.dest_pool_name + if self.src_port_op: + self.proposed["src_port_op"] = self.src_port_op + if self.src_port_begin: + self.proposed["src_port_begin"] = self.src_port_begin + if self.src_port_end: + self.proposed["src_port_end"] = self.src_port_end + if self.src_port_pool_name: + self.proposed["src_port_pool_name"] = self.src_port_pool_name + if self.dest_port_op: + self.proposed["dest_port_op"] = self.dest_port_op + if self.dest_port_begin: + self.proposed["dest_port_begin"] = self.dest_port_begin + if self.dest_port_end: + self.proposed["dest_port_end"] = self.dest_port_end + if self.dest_port_pool_name: + self.proposed["dest_port_pool_name"] = self.dest_port_pool_name + if self.frag_type: + self.proposed["frag_type"] = self.frag_type + if self.precedence: + self.proposed["precedence"] = self.precedence + if self.tos: + self.proposed["tos"] = self.tos + if self.dscp: + self.proposed["dscp"] = self.dscp + if self.icmp_name: + self.proposed["icmp_name"] = self.icmp_name + if self.icmp_type: + self.proposed["icmp_type"] = self.icmp_type + if self.icmp_code: + self.proposed["icmp_code"] = self.icmp_code + if self.ttl_expired: + self.proposed["ttl_expired"] = self.ttl_expired + if self.vrf_name: + self.proposed["vrf_name"] = self.vrf_name + if self.syn_flag: + self.proposed["syn_flag"] = self.syn_flag + if self.tcp_flag_mask: + self.proposed["tcp_flag_mask"] = self.tcp_flag_mask + self.proposed["established"] = self.established + if self.time_range: + self.proposed["time_range"] = self.time_range + if self.rule_description: + self.proposed["rule_description"] = self.rule_description + if self.igmp_type: + self.proposed["igmp_type"] = self.igmp_type + self.proposed["log_flag"] = self.log_flag + + def get_existing(self): + """ Get existing state """ + + self.existing["acl_info"] = self.cur_acl_cfg["acl_info"] + self.existing["adv_rule_info"] = self.cur_advance_rule_cfg[ + "adv_rule_info"] + + def get_end_state(self): + """ Get end state """ + + self.check_acl_args() + self.end_state["acl_info"] = self.cur_acl_cfg["acl_info"] + + self.check_advance_rule_args() + self.end_state["adv_rule_info"] = self.cur_advance_rule_cfg[ + "adv_rule_info"] + if self.end_state == self.existing: + self.changed = False + self.updates_cmd = list() + + def merge_acl(self): + """ Merge acl operation """ + + conf_str = CE_MERGE_ACL_HEADER % self.acl_name + + if self.acl_type: + conf_str += "%s" % self.acl_type + if self.acl_num: + conf_str += "%s" % self.acl_num + if self.acl_step: + conf_str += "%s" % self.acl_step + if self.acl_description: + conf_str += "%s" % self.acl_description + + conf_str += CE_MERGE_ACL_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge acl failed.') + + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + if self.acl_type and not self.acl_num: + cmd = "acl name %s %s" % (self.acl_name, self.acl_type.lower()) + elif self.acl_type and self.acl_num: + cmd = "acl name %s number %s" % (self.acl_name, self.acl_num) + elif not self.acl_type and self.acl_num: + cmd = "acl name %s number %s" % (self.acl_name, self.acl_num) + self.updates_cmd.append(cmd) + + if self.acl_description: + cmd = "description %s" % self.acl_description + self.updates_cmd.append(cmd) + + if self.acl_step: + cmd = "step %s" % self.acl_step + self.updates_cmd.append(cmd) + + self.changed = True + + def delete_acl(self): + """ Delete acl operation """ + + conf_str = CE_DELETE_ACL_HEADER % self.acl_name + + if self.acl_type: + conf_str += "%s" % self.acl_type + if self.acl_num: + conf_str += "%s" % self.acl_num + if self.acl_step: + conf_str += "%s" % self.acl_step + if self.acl_description: + conf_str += "%s" % self.acl_description + + conf_str += CE_DELETE_ACL_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Delete acl failed.') + + if self.acl_description: + cmd = "undo description" + self.updates_cmd.append(cmd) + + if self.acl_step: + cmd = "undo step" + self.updates_cmd.append(cmd) + + if self.acl_name.isdigit(): + cmd = "undo acl number %s" % self.acl_name + else: + cmd = "undo acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + self.changed = True + + def merge_adv_rule(self): + """ Merge advance rule operation """ + + conf_str = CE_MERGE_ACL_ADVANCE_RULE_HEADER % ( + self.acl_name, self.rule_name) + + if self.rule_id: + conf_str += "%s" % self.rule_id + if self.rule_action: + conf_str += "%s" % self.rule_action + if self.protocol: + conf_str += "%s" % self.protocol_num + if self.source_ip: + conf_str += "%s" % self.source_ip + if self.src_wild: + conf_str += "%s" % self.src_wild + if self.src_pool_name: + conf_str += "%s" % self.src_pool_name + if self.dest_ip: + conf_str += "%s" % self.dest_ip + if self.dest_wild: + conf_str += "%s" % self.dest_wild + if self.dest_pool_name: + conf_str += "%s" % self.dest_pool_name + if self.src_port_op: + conf_str += "%s" % self.src_port_op + if self.src_port_begin: + conf_str += "%s" % self.src_port_begin + if self.src_port_end: + conf_str += "%s" % self.src_port_end + if self.src_port_pool_name: + conf_str += "%s" % self.src_port_pool_name + if self.dest_port_op: + conf_str += "%s" % self.dest_port_op + if self.dest_port_begin: + conf_str += "%s" % self.dest_port_begin + if self.dest_port_end: + conf_str += "%s" % self.dest_port_end + if self.dest_port_pool_name: + conf_str += "%s" % self.dest_port_pool_name + if self.frag_type: + conf_str += "%s" % self.frag_type + if self.precedence: + conf_str += "%s" % self.precedence + if self.tos: + conf_str += "%s" % self.tos + if self.dscp: + conf_str += "%s" % self.dscp + if self.icmp_name: + conf_str += "%s" % self.icmp_name + if self.icmp_type: + conf_str += "%s" % self.icmp_type + if self.icmp_code: + conf_str += "%s" % self.icmp_code + conf_str += "%s" % str(self.ttl_expired).lower() + if self.vrf_name: + conf_str += "%s" % self.vrf_name + if self.syn_flag: + conf_str += "%s" % self.syn_flag + if self.tcp_flag_mask: + conf_str += "%s" % self.tcp_flag_mask + if self.protocol == "tcp": + conf_str += "%s" % str(self.established).lower() + if self.time_range: + conf_str += "%s" % self.time_range + if self.rule_description: + conf_str += "%s" % self.rule_description + if self.igmp_type: + conf_str += "%s" % self.igmp_type_num + conf_str += "%s" % str(self.log_flag).lower() + + conf_str += CE_MERGE_ACL_ADVANCE_RULE_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge acl base rule failed.') + + if self.rule_action and self.protocol: + cmd = "rule" + if self.rule_id: + cmd += " %s" % self.rule_id + cmd += " %s" % self.rule_action + cmd += " %s" % self.protocol + if self.dscp: + cmd += " dscp %s" % self.dscp + if self.tos: + cmd += " tos %s" % self.tos + if self.source_ip and self.src_wild: + cmd += " source %s %s" % (self.source_ip, self.src_wild) + if self.src_pool_name: + cmd += " source-pool %s" % self.src_pool_name + if self.src_port_op: + cmd += " source-port" + if self.src_port_op == "lt": + cmd += " lt %s" % self.src_port_end + elif self.src_port_op == "eq": + cmd += " eq %s" % self.src_port_begin + elif self.src_port_op == "gt": + cmd += " gt %s" % self.src_port_begin + elif self.src_port_op == "range": + cmd += " range %s %s" % (self.src_port_begin, + self.src_port_end) + if self.src_port_pool_name: + cmd += " source-port-pool %s" % self.src_port_pool_name + if self.dest_ip and self.dest_wild: + cmd += " destination %s %s" % (self.dest_ip, self.dest_wild) + if self.dest_pool_name: + cmd += " destination-pool %s" % self.dest_pool_name + if self.dest_port_op: + cmd += " destination-port" + if self.dest_port_op == "lt": + cmd += " lt %s" % self.dest_port_end + elif self.dest_port_op == "eq": + cmd += " eq %s" % self.dest_port_begin + elif self.dest_port_op == "gt": + cmd += " gt %s" % self.dest_port_begin + elif self.dest_port_op == "range": + cmd += " range %s %s" % (self.dest_port_begin, + self.dest_port_end) + if self.dest_port_pool_name: + cmd += " destination-port-pool %s" % self.dest_port_pool_name + if self.frag_type == "fragment": + cmd += " fragment-type fragment" + if self.precedence: + cmd += " precedence %s" % self.precedence_name[self.precedence] + + if self.protocol == "icmp": + if self.icmp_name: + cmd += " icmp-type %s" % self.icmp_name + elif self.icmp_type and self.icmp_code: + cmd += " icmp-type %s %s" % (self.icmp_type, self.icmp_code) + elif self.icmp_type: + cmd += " icmp-type %s" % self.icmp_type + if self.protocol == "tcp": + if self.syn_flag: + cmd += " tcp-flag %s" % self.syn_flag + if self.tcp_flag_mask: + cmd += " mask %s" % self.tcp_flag_mask + if self.established: + cmd += " established" + if self.protocol == "igmp": + if self.igmp_type: + cmd += " igmp-type %s" % self.igmp_type + if self.time_range: + cmd += " time-range %s" % self.time_range + if self.vrf_name: + cmd += " vpn-instance %s" % self.vrf_name + if self.ttl_expired: + cmd += " ttl-expired" + if self.log_flag: + cmd += " logging" + self.updates_cmd.append(cmd) + + if self.rule_description: + cmd = "rule %s description %s" % ( + self.rule_id, self.rule_description) + self.updates_cmd.append(cmd) + + self.changed = True + + def delete_adv_rule(self): + """ Delete advance rule operation """ + + conf_str = CE_DELETE_ACL_ADVANCE_RULE_HEADER % ( + self.acl_name, self.rule_name) + + if self.rule_id: + conf_str += "%s" % self.rule_id + if self.rule_action: + conf_str += "%s" % self.rule_action + if self.protocol: + conf_str += "%s" % self.protocol_num + if self.source_ip: + conf_str += "%s" % self.source_ip + if self.src_wild: + conf_str += "%s" % self.src_wild + if self.src_pool_name: + conf_str += "%s" % self.src_pool_name + if self.dest_ip: + conf_str += "%s" % self.dest_ip + if self.dest_wild: + conf_str += "%s" % self.dest_wild + if self.dest_pool_name: + conf_str += "%s" % self.dest_pool_name + if self.src_port_op: + conf_str += "%s" % self.src_port_op + if self.src_port_begin: + conf_str += "%s" % self.src_port_begin + if self.src_port_end: + conf_str += "%s" % self.src_port_end + if self.src_port_pool_name: + conf_str += "%s" % self.src_port_pool_name + if self.dest_port_op: + conf_str += "%s" % self.dest_port_op + if self.dest_port_begin: + conf_str += "%s" % self.dest_port_begin + if self.dest_port_end: + conf_str += "%s" % self.dest_port_end + if self.dest_port_pool_name: + conf_str += "%s" % self.dest_port_pool_name + if self.frag_type: + conf_str += "%s" % self.frag_type + if self.precedence: + conf_str += "%s" % self.precedence + if self.tos: + conf_str += "%s" % self.tos + if self.dscp: + conf_str += "%s" % self.dscp + if self.icmp_name: + conf_str += "%s" % self.icmp_name + if self.icmp_type: + conf_str += "%s" % self.icmp_type + if self.icmp_code: + conf_str += "%s" % self.icmp_code + conf_str += "%s" % str(self.ttl_expired).lower() + if self.vrf_name: + conf_str += "%s" % self.vrf_name + if self.syn_flag: + conf_str += "%s" % self.syn_flag + if self.tcp_flag_mask: + conf_str += "%s" % self.tcp_flag_mask + if self.protocol == "tcp": + conf_str += "%s" % str(self.established).lower() + if self.time_range: + conf_str += "%s" % self.time_range + if self.rule_description: + conf_str += "%s" % self.rule_description + if self.igmp_type: + conf_str += "%s" % self.igmp_type + conf_str += "%s" % str(self.log_flag).lower() + + conf_str += CE_DELETE_ACL_ADVANCE_RULE_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Delete acl base rule failed.') + + if self.rule_description: + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + cmd = "acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + cmd = "undo rule %s description" % self.rule_id + self.updates_cmd.append(cmd) + + if self.rule_id: + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + cmd = "acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + cmd = "undo rule %s" % self.rule_id + self.updates_cmd.append(cmd) + elif self.rule_action and self.protocol: + if self.acl_name.isdigit(): + cmd = "acl number %s" % self.acl_name + else: + cmd = "acl name %s" % self.acl_name + self.updates_cmd.append(cmd) + + cmd = "undo rule" + cmd += " %s" % self.rule_action + cmd += " %s" % self.protocol + if self.dscp: + cmd += " dscp %s" % self.dscp + if self.tos: + cmd += " tos %s" % self.tos + if self.source_ip and self.src_mask: + cmd += " source %s %s" % (self.source_ip, self.src_mask) + if self.src_pool_name: + cmd += " source-pool %s" % self.src_pool_name + if self.src_port_op: + cmd += " source-port" + if self.src_port_op == "lt": + cmd += " lt %s" % self.src_port_end + elif self.src_port_op == "eq": + cmd += " eq %s" % self.src_port_begin + elif self.src_port_op == "gt": + cmd += " gt %s" % self.src_port_begin + elif self.src_port_op == "range": + cmd += " range %s %s" % (self.src_port_begin, + self.src_port_end) + if self.src_port_pool_name: + cmd += " source-port-pool %s" % self.src_port_pool_name + if self.dest_ip and self.dest_mask: + cmd += " destination %s %s" % (self.dest_ip, self.dest_mask) + if self.dest_pool_name: + cmd += " destination-pool %s" % self.dest_pool_name + if self.dest_port_op: + cmd += " destination-port" + if self.dest_port_op == "lt": + cmd += " lt %s" % self.dest_port_end + elif self.dest_port_op == "eq": + cmd += " eq %s" % self.dest_port_begin + elif self.dest_port_op == "gt": + cmd += " gt %s" % self.dest_port_begin + elif self.dest_port_op == "range": + cmd += " range %s %s" % (self.dest_port_begin, + self.dest_port_end) + if self.dest_port_pool_name: + cmd += " destination-port-pool %s" % self.dest_port_pool_name + if self.frag_type == "fragment": + cmd += " fragment-type fragment" + if self.precedence: + cmd += " precedence %s" % self.precedence_name[self.precedence] + if self.time_range: + cmd += " time-range %s" % self.time_range + if self.vrf_name: + cmd += " vpn-instance %s" % self.vrf_name + if self.ttl_expired: + cmd += " ttl-expired" + if self.log_flag: + cmd += " logging" + self.updates_cmd.append(cmd) + + self.changed = True + + def work(self): + """ Main work function """ + + self.check_acl_args() + self.check_advance_rule_args() + self.get_proposed() + self.get_existing() + + if self.state == "present": + if self.cur_acl_cfg["need_cfg"]: + self.merge_acl() + if self.cur_advance_rule_cfg["need_cfg"]: + self.merge_adv_rule() + + elif self.state == "absent": + if self.cur_advance_rule_cfg["need_cfg"]: + self.delete_adv_rule() + + elif self.state == "delete_acl": + if self.cur_acl_cfg["need_cfg"]: + self.delete_acl() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent', + 'delete_acl'], default='present'), + acl_name=dict(type='str', required=True), + acl_num=dict(type='str'), + acl_step=dict(type='str'), + acl_description=dict(type='str'), + rule_name=dict(type='str'), + rule_id=dict(type='str'), + rule_action=dict(choices=['permit', 'deny']), + protocol=dict(choices=['ip', 'icmp', 'igmp', + 'ipinip', 'tcp', 'udp', 'gre', 'ospf']), + source_ip=dict(type='str'), + src_mask=dict(type='str'), + src_pool_name=dict(type='str'), + dest_ip=dict(type='str'), + dest_mask=dict(type='str'), + dest_pool_name=dict(type='str'), + src_port_op=dict(choices=['lt', 'eq', 'gt', 'range']), + src_port_begin=dict(type='str'), + src_port_end=dict(type='str'), + src_port_pool_name=dict(type='str'), + dest_port_op=dict(choices=['lt', 'eq', 'gt', 'range']), + dest_port_begin=dict(type='str'), + dest_port_end=dict(type='str'), + dest_port_pool_name=dict(type='str'), + frag_type=dict(choices=['fragment', 'clear_fragment']), + precedence=dict(type='str'), + tos=dict(type='str'), + dscp=dict(type='str'), + icmp_name=dict(choices=['unconfiged', 'echo', 'echo-reply', 'fragmentneed-DFset', 'host-redirect', + 'host-tos-redirect', 'host-unreachable', 'information-reply', 'information-request', + 'net-redirect', 'net-tos-redirect', 'net-unreachable', 'parameter-problem', + 'port-unreachable', 'protocol-unreachable', 'reassembly-timeout', 'source-quench', + 'source-route-failed', 'timestamp-reply', 'timestamp-request', 'ttl-exceeded', + 'address-mask-reply', 'address-mask-request', 'custom']), + icmp_type=dict(type='str'), + icmp_code=dict(type='str'), + ttl_expired=dict(required=False, default=False, type='bool'), + vrf_name=dict(type='str'), + syn_flag=dict(type='str'), + tcp_flag_mask=dict(type='str'), + established=dict(required=False, default=False, type='bool'), + time_range=dict(type='str'), + rule_description=dict(type='str'), + igmp_type=dict(choices=['host-query', 'mrouter-adver', 'mrouter-solic', 'mrouter-termi', 'mtrace-resp', + 'mtrace-route', 'v1host-report', 'v2host-report', 'v2leave-group', 'v3host-report']), + log_flag=dict(required=False, default=False, type='bool') + ) + + argument_spec.update(ce_argument_spec) + module = AdvanceAcl(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_acl_interface.py b/plugins/modules/ce_acl_interface.py new file mode 100644 index 0000000..0760c70 --- /dev/null +++ b/plugins/modules/ce_acl_interface.py @@ -0,0 +1,324 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_acl_interface +short_description: Manages applying ACLs to interfaces on HUAWEI CloudEngine switches. +description: + - Manages applying ACLs to interfaces on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + acl_name: + description: + - ACL number or name. + For a numbered rule group, the value ranging from 2000 to 4999. + For a named rule group, the value is a string of 1 to 32 case-sensitive characters starting + with a letter, spaces not supported. + required: true + interface: + description: + - Interface name. + Only support interface full name, such as "40GE2/0/1". + required: true + direction: + description: + - Direction ACL to be applied in on the interface. + required: true + choices: ['inbound', 'outbound'] + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' + +- name: CloudEngine acl interface test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Apply acl to interface" + ce_acl_interface: + state: present + acl_name: 2000 + interface: 40GE1/0/1 + direction: outbound + provider: "{{ cli }}" + + - name: "Undo acl from interface" + ce_acl_interface: + state: absent + acl_name: 2000 + interface: 40GE1/0/1 + direction: outbound + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"acl_name": "2000", + "direction": "outbound", + "interface": "40GE2/0/1", + "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"acl interface": "traffic-filter acl lb inbound"} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"acl interface": ["traffic-filter acl lb inbound", "traffic-filter acl 2000 outbound"]} +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface 40ge2/0/1", + "traffic-filter acl 2000 outbound"] +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_config, exec_command, cli_err_msg +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + + +class AclInterface(object): + """ Manages acl interface configuration """ + + def __init__(self, **kwargs): + """ Class init """ + + # argument spec + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # config + self.cur_cfg = dict() + self.cur_cfg["acl interface"] = [] + + # module args + self.state = self.module.params['state'] + self.acl_name = self.module.params['acl_name'] + self.interface = self.module.params['interface'] + self.direction = self.module.params['direction'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def check_args(self): + """ Check args """ + + if self.acl_name: + if self.acl_name.isdigit(): + if int(self.acl_name) < 2000 or int(self.acl_name) > 4999: + self.module.fail_json( + msg='Error: The value of acl_name is out of [2000 - 4999].') + else: + if len(self.acl_name) < 1 or len(self.acl_name) > 32: + self.module.fail_json( + msg='Error: The len of acl_name is out of [1 - 32].') + + if self.interface: + cmd = "display current-configuration | ignore-case section include interface %s" % self.interface + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + result = str(out).strip() + if result: + tmp = result.split('\n') + if "display" in tmp[0]: + tmp.pop(0) + if not tmp: + self.module.fail_json( + msg='Error: The interface %s is not in the device.' % self.interface) + + def get_proposed(self): + """ Get proposed config """ + + self.proposed["state"] = self.state + + if self.acl_name: + self.proposed["acl_name"] = self.acl_name + + if self.interface: + self.proposed["interface"] = self.interface + + if self.direction: + self.proposed["direction"] = self.direction + + def get_existing(self): + """ Get existing config """ + + cmd = "display current-configuration | ignore-case section include interface %s | include traffic-filter" % self.interface + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + result = str(out).strip() + end = [] + if result: + tmp = result.split('\n') + if "display" in tmp[0]: + tmp.pop(0) + for item in tmp: + end.append(item.strip()) + self.cur_cfg["acl interface"] = end + self.existing["acl interface"] = end + + def get_end_state(self): + """ Get config end state """ + + cmd = "display current-configuration | ignore-case section include interface %s | include traffic-filter" % self.interface + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + result = str(out).strip() + end = [] + if result: + tmp = result.split('\n') + if "display" in tmp[0]: + tmp.pop(0) + for item in tmp: + end.append(item.strip()) + self.end_state["acl interface"] = end + + def load_config(self, config): + """Sends configuration commands to the remote device""" + + rc, out, err = exec_command(self.module, 'mmi-mode enable') + if rc != 0: + self.module.fail_json(msg='unable to set mmi-mode enable', output=err) + rc, out, err = exec_command(self.module, 'system-view immediately') + if rc != 0: + self.module.fail_json(msg='unable to enter system-view', output=err) + + for cmd in config: + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + if "unrecognized command found" in err.lower(): + self.module.fail_json(msg="Error:The parameter is incorrect or the interface does not support this parameter.") + else: + self.module.fail_json(msg=cli_err_msg(cmd.strip(), err)) + + exec_command(self.module, 'return') + + def cli_load_config(self, commands): + """ Cli method to load config """ + + if not self.module.check_mode: + self.load_config(commands) + + def work(self): + """ Work function """ + + self.check_args() + self.get_proposed() + self.get_existing() + + cmds = list() + tmp_cmd = "traffic-filter acl %s %s" % (self.acl_name, self.direction) + undo_tmp_cmd = "undo traffic-filter acl %s %s" % ( + self.acl_name, self.direction) + + if self.state == "present": + if tmp_cmd not in self.cur_cfg["acl interface"]: + interface_cmd = "interface %s" % self.interface.lower() + cmds.append(interface_cmd) + cmds.append(tmp_cmd) + + self.cli_load_config(cmds) + + self.changed = True + self.updates_cmd.append(interface_cmd) + self.updates_cmd.append(tmp_cmd) + + else: + if tmp_cmd in self.cur_cfg["acl interface"]: + interface_cmd = "interface %s" % self.interface + cmds.append(interface_cmd) + cmds.append(undo_tmp_cmd) + self.cli_load_config(cmds) + + self.changed = True + self.updates_cmd.append(interface_cmd) + self.updates_cmd.append(undo_tmp_cmd) + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + acl_name=dict(type='str', required=True), + interface=dict(type='str', required=True), + direction=dict(choices=['inbound', 'outbound'], required=True) + ) + + argument_spec.update(ce_argument_spec) + module = AclInterface(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_bfd_global.py b/plugins/modules/ce_bfd_global.py new file mode 100644 index 0000000..5585a1c --- /dev/null +++ b/plugins/modules/ce_bfd_global.py @@ -0,0 +1,555 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_bfd_global +short_description: Manages BFD global configuration on HUAWEI CloudEngine devices. +description: + - Manages BFD global configuration on HUAWEI CloudEngine devices. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + bfd_enable: + description: + - Enables the global Bidirectional Forwarding Detection (BFD) function. + choices: ['enable', 'disable'] + default_ip: + description: + - Specifies the default multicast IP address. + The value ranges from 224.0.0.107 to 224.0.0.250. + tos_exp_dynamic: + description: + - Indicates the priority of BFD control packets for dynamic BFD sessions. + The value is an integer ranging from 0 to 7. + The default priority is 7, which is the highest priority of BFD control packets. + tos_exp_static: + description: + - Indicates the priority of BFD control packets for static BFD sessions. + The value is an integer ranging from 0 to 7. + The default priority is 7, which is the highest priority of BFD control packets. + damp_init_wait_time: + description: + - Specifies an initial flapping suppression time for a BFD session. + The value is an integer ranging from 1 to 3600000, in milliseconds. + The default value is 2000. + damp_max_wait_time: + description: + - Specifies a maximum flapping suppression time for a BFD session. + The value is an integer ranging from 1 to 3600000, in milliseconds. + The default value is 15000. + damp_second_wait_time: + description: + - Specifies a secondary flapping suppression time for a BFD session. + The value is an integer ranging from 1 to 3600000, in milliseconds. + The default value is 5000. + delay_up_time: + description: + - Specifies the delay before a BFD session becomes Up. + The value is an integer ranging from 1 to 600, in seconds. + The default value is 0, indicating that a BFD session immediately becomes Up. + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +- name: bfd global module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Enable the global BFD function + ce_bfd_global: + bfd_enable: enable + provider: '{{ cli }}' + + - name: Set the default multicast IP address to 224.0.0.150 + ce_bfd_global: + bfd_enable: enable + default_ip: 224.0.0.150 + state: present + provider: '{{ cli }}' + + - name: Set the priority of BFD control packets for dynamic and static BFD sessions + ce_bfd_global: + bfd_enable: enable + tos_exp_dynamic: 5 + tos_exp_static: 6 + state: present + provider: '{{ cli }}' + + - name: Disable the global BFD function + ce_bfd_global: + bfd_enable: disable + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: { + "bfd_enalbe": "enable", + "damp_init_wait_time": null, + "damp_max_wait_time": null, + "damp_second_wait_time": null, + "default_ip": null, + "delayUpTimer": null, + "state": "present", + "tos_exp_dynamic": null, + "tos_exp_static": null + } +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: { + "global": { + "bfdEnable": "false", + "dampInitWaitTime": "2000", + "dampMaxWaitTime": "12000", + "dampSecondWaitTime": "5000", + "defaultIp": "224.0.0.184", + "delayUpTimer": null, + "tosExp": "7", + "tosExpStatic": "7" + } + } +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: { + "global": { + "bfdEnable": "true", + "dampInitWaitTime": "2000", + "dampMaxWaitTime": "12000", + "dampSecondWaitTime": "5000", + "defaultIp": "224.0.0.184", + "delayUpTimer": null, + "tosExp": "7", + "tosExpStatic": "7" + } + } +updates: + description: commands sent to the device + returned: always + type: list + sample: [ "bfd" ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import sys +import socket +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + +CE_NC_GET_BFD = """ + + + %s + + +""" + +CE_NC_GET_BFD_GLB = """ + + + + + + + + + + +""" + + +def check_default_ip(ipaddr): + """check the default multicast IP address""" + + # The value ranges from 224.0.0.107 to 224.0.0.250 + if not check_ip_addr(ipaddr): + return False + + if ipaddr.count(".") != 3: + return False + + ips = ipaddr.split(".") + if ips[0] != "224" or ips[1] != "0" or ips[2] != "0": + return False + + if not ips[3].isdigit() or int(ips[3]) < 107 or int(ips[3]) > 250: + return False + + return True + + +class BfdGlobal(object): + """Manages BFD Global""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.bfd_enable = self.module.params['bfd_enable'] + self.default_ip = self.module.params['default_ip'] + self.tos_exp_dynamic = self.module.params['tos_exp_dynamic'] + self.tos_exp_static = self.module.params['tos_exp_static'] + self.damp_init_wait_time = self.module.params['damp_init_wait_time'] + self.damp_max_wait_time = self.module.params['damp_max_wait_time'] + self.damp_second_wait_time = self.module.params['damp_second_wait_time'] + self.delay_up_time = self.module.params['delay_up_time'] + self.state = self.module.params['state'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.changed = False + self.bfd_dict = dict() + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + required_together = [('damp_init_wait_time', 'damp_max_wait_time', 'damp_second_wait_time')] + self.module = AnsibleModule(argument_spec=self.spec, + required_together=required_together, + supports_check_mode=True) + + def get_bfd_dict(self): + """bfd config dict""" + + bfd_dict = dict() + bfd_dict["global"] = dict() + conf_str = CE_NC_GET_BFD % CE_NC_GET_BFD_GLB + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return bfd_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get bfd global info + glb = root.find("bfd/bfdSchGlobal") + if glb: + for attr in glb: + if attr.text is not None: + bfd_dict["global"][attr.tag] = attr.text + + return bfd_dict + + def config_global(self): + """configures bfd global params""" + + xml_str = "" + damp_chg = False + + # bfd_enable + if self.bfd_enable: + if bool(self.bfd_dict["global"].get("bfdEnable", "false") == "true") != bool(self.bfd_enable == "enable"): + if self.bfd_enable == "enable": + xml_str = "true" + self.updates_cmd.append("bfd") + else: + xml_str = "false" + self.updates_cmd.append("undo bfd") + + # get bfd end state + bfd_state = "disable" + if self.bfd_enable: + bfd_state = self.bfd_enable + elif self.bfd_dict["global"].get("bfdEnable", "false") == "true": + bfd_state = "enable" + + # default_ip + if self.default_ip: + if bfd_state == "enable": + if self.state == "present" and self.default_ip != self.bfd_dict["global"].get("defaultIp"): + xml_str += "%s" % self.default_ip + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("default-ip-address %s" % self.default_ip) + elif self.state == "absent" and self.default_ip == self.bfd_dict["global"].get("defaultIp"): + xml_str += "" + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("undo default-ip-address") + + # tos_exp_dynamic + if self.tos_exp_dynamic is not None: + if bfd_state == "enable": + if self.state == "present" and self.tos_exp_dynamic != int(self.bfd_dict["global"].get("tosExp", "7")): + xml_str += "%s" % self.tos_exp_dynamic + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("tos-exp %s dynamic" % self.tos_exp_dynamic) + elif self.state == "absent" and self.tos_exp_dynamic == int(self.bfd_dict["global"].get("tosExp", "7")): + xml_str += "" + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("undo tos-exp dynamic") + + # tos_exp_static + if self.tos_exp_static is not None: + if bfd_state == "enable": + if self.state == "present" \ + and self.tos_exp_static != int(self.bfd_dict["global"].get("tosExpStatic", "7")): + xml_str += "%s" % self.tos_exp_static + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("tos-exp %s static" % self.tos_exp_static) + elif self.state == "absent" \ + and self.tos_exp_static == int(self.bfd_dict["global"].get("tosExpStatic", "7")): + xml_str += "" + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("undo tos-exp static") + + # delay_up_time + if self.delay_up_time is not None: + if bfd_state == "enable": + delay_time = self.bfd_dict["global"].get("delayUpTimer", "0") + if not delay_time or not delay_time.isdigit(): + delay_time = "0" + if self.state == "present" \ + and self.delay_up_time != int(delay_time): + xml_str += "%s" % self.delay_up_time + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("delay-up %s" % self.delay_up_time) + elif self.state == "absent" \ + and self.delay_up_time == int(delay_time): + xml_str += "" + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("undo delay-up") + + # damp_init_wait_time damp_max_wait_time damp_second_wait_time + if self.damp_init_wait_time is not None and self.damp_second_wait_time is not None \ + and self.damp_second_wait_time is not None: + if bfd_state == "enable": + if self.state == "present": + if self.damp_max_wait_time != int(self.bfd_dict["global"].get("dampMaxWaitTime", "2000")): + xml_str += "%s" % self.damp_max_wait_time + damp_chg = True + if self.damp_init_wait_time != int(self.bfd_dict["global"].get("dampInitWaitTime", "12000")): + xml_str += "%s" % self.damp_init_wait_time + damp_chg = True + if self.damp_second_wait_time != int(self.bfd_dict["global"].get("dampSecondWaitTime", "5000")): + xml_str += "%s" % self.damp_second_wait_time + damp_chg = True + if damp_chg: + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("dampening timer-interval maximum %s initial %s secondary %s" % ( + self.damp_max_wait_time, self.damp_init_wait_time, self.damp_second_wait_time)) + else: + damp_chg = True + if self.damp_max_wait_time != int(self.bfd_dict["global"].get("dampMaxWaitTime", "2000")): + damp_chg = False + if self.damp_init_wait_time != int(self.bfd_dict["global"].get("dampInitWaitTime", "12000")): + damp_chg = False + if self.damp_second_wait_time != int(self.bfd_dict["global"].get("dampSecondWaitTime", "5000")): + damp_chg = False + + if damp_chg: + xml_str += "" + if "bfd" not in self.updates_cmd: + self.updates_cmd.append("bfd") + self.updates_cmd.append("undo dampening timer-interval maximum %s initial %s secondary %s" % ( + self.damp_max_wait_time, self.damp_init_wait_time, self.damp_second_wait_time)) + if xml_str: + return '' + xml_str + '' + else: + return "" + + def netconf_load_config(self, xml_str): + """load bfd config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + set_nc_config(self.module, xml_cfg) + self.changed = True + + def check_params(self): + """Check all input params""" + + # check default_ip + if self.default_ip: + if not check_default_ip(self.default_ip): + self.module.fail_json(msg="Error: Default ip is invalid.") + + # check tos_exp_dynamic + if self.tos_exp_dynamic is not None: + if self.tos_exp_dynamic < 0 or self.tos_exp_dynamic > 7: + self.module.fail_json(msg="Error: Session tos_exp_dynamic is not ranges from 0 to 7.") + + # check tos_exp_static + if self.tos_exp_static is not None: + if self.tos_exp_static < 0 or self.tos_exp_static > 7: + self.module.fail_json(msg="Error: Session tos_exp_static is not ranges from 0 to 7.") + + # check damp_init_wait_time + if self.damp_init_wait_time is not None: + if self.damp_init_wait_time < 1 or self.damp_init_wait_time > 3600000: + self.module.fail_json(msg="Error: Session damp_init_wait_time is not ranges from 1 to 3600000.") + + # check damp_max_wait_time + if self.damp_max_wait_time is not None: + if self.damp_max_wait_time < 1 or self.damp_max_wait_time > 3600000: + self.module.fail_json(msg="Error: Session damp_max_wait_time is not ranges from 1 to 3600000.") + + # check damp_second_wait_time + if self.damp_second_wait_time is not None: + if self.damp_second_wait_time < 1 or self.damp_second_wait_time > 3600000: + self.module.fail_json(msg="Error: Session damp_second_wait_time is not ranges from 1 to 3600000.") + + # check delay_up_time + if self.delay_up_time is not None: + if self.delay_up_time < 1 or self.delay_up_time > 600: + self.module.fail_json(msg="Error: Session delay_up_time is not ranges from 1 to 600.") + + def get_proposed(self): + """get proposed info""" + + self.proposed["bfd_enalbe"] = self.bfd_enable + self.proposed["default_ip"] = self.default_ip + self.proposed["tos_exp_dynamic"] = self.tos_exp_dynamic + self.proposed["tos_exp_static"] = self.tos_exp_static + self.proposed["damp_init_wait_time"] = self.damp_init_wait_time + self.proposed["damp_max_wait_time"] = self.damp_max_wait_time + self.proposed["damp_second_wait_time"] = self.damp_second_wait_time + self.proposed["delay_up_time"] = self.delay_up_time + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.bfd_dict: + return + + self.existing["global"] = self.bfd_dict.get("global") + + def get_end_state(self): + """get end state info""" + + bfd_dict = self.get_bfd_dict() + if not bfd_dict: + return + + self.end_state["global"] = bfd_dict.get("global") + if self.existing == self.end_state: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.bfd_dict = self.get_bfd_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = self.config_global() + + # update to device + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + bfd_enable=dict(required=False, type='str', choices=['enable', 'disable']), + default_ip=dict(required=False, type='str'), + tos_exp_dynamic=dict(required=False, type='int'), + tos_exp_static=dict(required=False, type='int'), + damp_init_wait_time=dict(required=False, type='int'), + damp_max_wait_time=dict(required=False, type='int'), + damp_second_wait_time=dict(required=False, type='int'), + delay_up_time=dict(required=False, type='int'), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = BfdGlobal(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_bfd_session.py b/plugins/modules/ce_bfd_session.py new file mode 100644 index 0000000..bd6a582 --- /dev/null +++ b/plugins/modules/ce_bfd_session.py @@ -0,0 +1,655 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_bfd_session +short_description: Manages BFD session configuration on HUAWEI CloudEngine devices. +description: + - Manages BFD session configuration, creates a BFD session or deletes a specified BFD session + on HUAWEI CloudEngine devices. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + session_name: + description: + - Specifies the name of a BFD session. + The value is a string of 1 to 15 case-sensitive characters without spaces. + required: true + create_type: + description: + - BFD session creation mode, the currently created BFD session + only supports static or static auto-negotiation mode. + choices: ['static', 'auto'] + default: static + addr_type: + description: + - Specifies the peer IP address type. + choices: ['ipv4'] + out_if_name: + description: + - Specifies the type and number of the interface bound to the BFD session. + dest_addr: + description: + - Specifies the peer IP address bound to the BFD session. + src_addr: + description: + - Indicates the source IP address carried in BFD packets. + local_discr: + description: + - The BFD session local identifier does not need to be configured when the mode is auto. + remote_discr: + description: + - The BFD session remote identifier does not need to be configured when the mode is auto. + vrf_name: + description: + - Specifies the name of a Virtual Private Network (VPN) instance that is bound to a BFD session. + The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + When double quotation marks are used around the string, spaces are allowed in the string. + The value _public_ is reserved and cannot be used as the VPN instance name. + use_default_ip: + description: + - Indicates the default multicast IP address that is bound to a BFD session. + By default, BFD uses the multicast IP address 224.0.0.184. + You can set the multicast IP address by running the default-ip-address command. + The value is a bool type. + type: bool + default: 'no' + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +- name: bfd session module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Configuring Single-hop BFD for Detecting Faults on a Layer 2 Link + ce_bfd_session: + session_name: bfd_l2link + use_default_ip: true + out_if_name: 10GE1/0/1 + local_discr: 163 + remote_discr: 163 + provider: '{{ cli }}' + + - name: Configuring Single-Hop BFD on a VLANIF Interface + ce_bfd_session: + session_name: bfd_vlanif + dest_addr: 10.1.1.6 + out_if_name: Vlanif100 + local_discr: 163 + remote_discr: 163 + provider: '{{ cli }}' + + - name: Configuring Multi-Hop BFD + ce_bfd_session: + session_name: bfd_multi_hop + dest_addr: 10.1.1.1 + local_discr: 163 + remote_discr: 163 + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "addr_type": null, + "create_type": null, + "dest_addr": null, + "out_if_name": "10GE1/0/1", + "session_name": "bfd_l2link", + "src_addr": null, + "state": "present", + "use_default_ip": true, + "vrf_name": null + } +existing: + description: k/v pairs of existing configuration + returned: always + type: dict + sample: { + "session": {} + } +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: { + "session": { + "addrType": "IPV4", + "createType": "SESS_STATIC", + "destAddr": null, + "outIfName": "10GE1/0/1", + "sessName": "bfd_l2link", + "srcAddr": null, + "useDefaultIp": "true", + "vrfName": null + } + } +updates: + description: commands sent to the device + returned: always + type: list + sample: [ + "bfd bfd_l2link bind peer-ip default-ip interface 10ge1/0/1" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import sys +import socket +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + + +CE_NC_GET_BFD = """ + + + + + + + + + %s + + + + + + + + + + + + + +""" + + +def is_valid_ip_vpn(vpname): + """check ip vpn""" + + if not vpname: + return False + + if vpname == "_public_": + return False + + if len(vpname) < 1 or len(vpname) > 31: + return False + + return True + + +def check_default_ip(ipaddr): + """check the default multicast IP address""" + + # The value ranges from 224.0.0.107 to 224.0.0.250 + if not check_ip_addr(ipaddr): + return False + + if ipaddr.count(".") != 3: + return False + + ips = ipaddr.split(".") + if ips[0] != "224" or ips[1] != "0" or ips[2] != "0": + return False + + if not ips[3].isdigit() or int(ips[3]) < 107 or int(ips[3]) > 250: + return False + + return True + + +def get_interface_type(interface): + """get the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class BfdSession(object): + """Manages BFD Session""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.session_name = self.module.params['session_name'] + self.create_type = self.module.params['create_type'] + self.addr_type = self.module.params['addr_type'] + self.out_if_name = self.module.params['out_if_name'] + self.dest_addr = self.module.params['dest_addr'] + self.src_addr = self.module.params['src_addr'] + self.vrf_name = self.module.params['vrf_name'] + self.use_default_ip = self.module.params['use_default_ip'] + self.state = self.module.params['state'] + self.local_discr = self.module.params['local_discr'] + self.remote_discr = self.module.params['remote_discr'] + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.changed = False + self.bfd_dict = dict() + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + mutually_exclusive = [('use_default_ip', 'dest_addr')] + self.module = AnsibleModule(argument_spec=self.spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + def get_bfd_dict(self): + """bfd config dict""" + + bfd_dict = dict() + bfd_dict["global"] = dict() + bfd_dict["session"] = dict() + conf_str = CE_NC_GET_BFD % self.session_name + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return bfd_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get bfd global info + glb = root.find("bfd/bfdSchGlobal") + if glb: + for attr in glb: + bfd_dict["global"][attr.tag] = attr.text + + # get bfd session info + sess = root.find("bfd/bfdCfgSessions/bfdCfgSession") + if sess: + for attr in sess: + bfd_dict["session"][attr.tag] = attr.text + + return bfd_dict + + def is_session_match(self): + """is bfd session match""" + + if not self.bfd_dict["session"] or not self.session_name: + return False + + session = self.bfd_dict["session"] + if self.session_name != session.get("sessName", ""): + return False + + if self.create_type and self.create_type.upper() not in session.get("createType", "").upper(): + return False + + if self.addr_type and self.addr_type != session.get("addrType").lower(): + return False + + if self.dest_addr and self.dest_addr != session.get("destAddr"): + return False + + if self.src_addr and self.src_addr != session.get("srcAddr"): + return False + + if self.out_if_name: + if not session.get("outIfName"): + return False + if self.out_if_name.replace(" ", "").lower() != session.get("outIfName").replace(" ", "").lower(): + return False + + if self.vrf_name and self.vrf_name != session.get("vrfName"): + return False + + if str(self.use_default_ip).lower() != session.get("useDefaultIp"): + return False + + if self.create_type == "static" and self.state == "present": + if str(self.local_discr).lower() != session.get("localDiscr", ""): + return False + if str(self.remote_discr).lower() != session.get("remoteDiscr", ""): + return False + + return True + + def config_session(self): + """configures bfd session""" + + xml_str = "" + cmd_list = list() + discr = list() + + if not self.session_name: + return xml_str + + if self.bfd_dict["global"].get("bfdEnable", "false") != "true": + self.module.fail_json(msg="Error: Please enable BFD globally first.") + + xml_str = "%s" % self.session_name + cmd_session = "bfd %s" % self.session_name + + if self.state == "present": + if not self.bfd_dict["session"]: + # Parameter check + if not self.dest_addr and not self.use_default_ip: + self.module.fail_json( + msg="Error: dest_addr or use_default_ip must be set when bfd session is creating.") + + # Creates a BFD session + if self.create_type == "auto": + xml_str += "SESS_%s" % self.create_type.upper() + else: + xml_str += "SESS_STATIC" + xml_str += "IP" + cmd_session += " bind" + if self.addr_type: + xml_str += "%s" % self.addr_type.upper() + else: + xml_str += "IPV4" + if self.dest_addr: + xml_str += "%s" % self.dest_addr + cmd_session += " peer-%s %s" % ("ipv6" if self.addr_type == "ipv6" else "ip", self.dest_addr) + if self.use_default_ip: + xml_str += "%s" % str(self.use_default_ip).lower() + cmd_session += " peer-ip default-ip" + if self.vrf_name: + xml_str += "%s" % self.vrf_name + cmd_session += " vpn-instance %s" % self.vrf_name + if self.out_if_name: + xml_str += "%s" % self.out_if_name + cmd_session += " interface %s" % self.out_if_name.lower() + if self.src_addr: + xml_str += "%s" % self.src_addr + cmd_session += " source-%s %s" % ("ipv6" if self.addr_type == "ipv6" else "ip", self.src_addr) + + if self.create_type == "auto": + cmd_session += " auto" + else: + xml_str += "%s" % self.local_discr + discr.append("discriminator local %s" % self.local_discr) + xml_str += "%s" % self.remote_discr + discr.append("discriminator remote %s" % self.remote_discr) + + elif not self.is_session_match(): + # Bfd session is not match + self.module.fail_json(msg="Error: The specified BFD configuration view has been created.") + else: + pass + else: # absent + if not self.bfd_dict["session"]: + self.module.fail_json(msg="Error: BFD session is not exist.") + if not self.is_session_match(): + self.module.fail_json(msg="Error: BFD session parameter is invalid.") + + if self.state == "present": + if xml_str.endswith(""): + # no config update + return "" + else: + cmd_list.insert(0, cmd_session) + cmd_list.extend(discr) + self.updates_cmd.extend(cmd_list) + return '' + xml_str\ + + '' + else: # absent + cmd_list.append("undo " + cmd_session) + self.updates_cmd.extend(cmd_list) + return '' + xml_str\ + + '' + + def netconf_load_config(self, xml_str): + """load bfd config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + set_nc_config(self.module, xml_cfg) + self.changed = True + + def check_params(self): + """Check all input params""" + + # check session_name + if not self.session_name: + self.module.fail_json(msg="Error: Missing required arguments: session_name.") + + if self.session_name: + if len(self.session_name) < 1 or len(self.session_name) > 15: + self.module.fail_json(msg="Error: Session name is invalid.") + + # check local_discr + # check remote_discr + + if self.local_discr: + if self.local_discr < 1 or self.local_discr > 16384: + self.module.fail_json(msg="Error: Session local_discr is not ranges from 1 to 16384.") + if self.remote_discr: + if self.remote_discr < 1 or self.remote_discr > 4294967295: + self.module.fail_json(msg="Error: Session remote_discr is not ranges from 1 to 4294967295.") + + if self.state == "present" and self.create_type == "static": + if not self.local_discr: + self.module.fail_json(msg="Error: Missing required arguments: local_discr.") + if not self.remote_discr: + self.module.fail_json(msg="Error: Missing required arguments: remote_discr.") + + # check out_if_name + if self.out_if_name: + if not get_interface_type(self.out_if_name): + self.module.fail_json(msg="Error: Session out_if_name is invalid.") + + # check dest_addr + if self.dest_addr: + if not check_ip_addr(self.dest_addr): + self.module.fail_json(msg="Error: Session dest_addr is invalid.") + + # check src_addr + if self.src_addr: + if not check_ip_addr(self.src_addr): + self.module.fail_json(msg="Error: Session src_addr is invalid.") + + # check vrf_name + if self.vrf_name: + if not is_valid_ip_vpn(self.vrf_name): + self.module.fail_json(msg="Error: Session vrf_name is invalid.") + if not self.dest_addr: + self.module.fail_json(msg="Error: vrf_name and dest_addr must set at the same time.") + + # check use_default_ip + if self.use_default_ip and not self.out_if_name: + self.module.fail_json(msg="Error: use_default_ip and out_if_name must set at the same time.") + + def get_proposed(self): + """get proposed info""" + + # base config + self.proposed["session_name"] = self.session_name + self.proposed["create_type"] = self.create_type + self.proposed["addr_type"] = self.addr_type + self.proposed["out_if_name"] = self.out_if_name + self.proposed["dest_addr"] = self.dest_addr + self.proposed["src_addr"] = self.src_addr + self.proposed["vrf_name"] = self.vrf_name + self.proposed["use_default_ip"] = self.use_default_ip + self.proposed["state"] = self.state + self.proposed["local_discr"] = self.local_discr + self.proposed["remote_discr"] = self.remote_discr + + def get_existing(self): + """get existing info""" + + if not self.bfd_dict: + return + + self.existing["session"] = self.bfd_dict.get("session") + + def get_end_state(self): + """get end state info""" + + bfd_dict = self.get_bfd_dict() + if not bfd_dict: + return + + self.end_state["session"] = bfd_dict.get("session") + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.bfd_dict = self.get_bfd_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.session_name: + xml_str += self.config_session() + + # update to device + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + session_name=dict(required=True, type='str'), + create_type=dict(required=False, default='static', type='str', choices=['static', 'auto']), + addr_type=dict(required=False, type='str', choices=['ipv4']), + out_if_name=dict(required=False, type='str'), + dest_addr=dict(required=False, type='str'), + src_addr=dict(required=False, type='str'), + vrf_name=dict(required=False, type='str'), + use_default_ip=dict(required=False, type='bool', default=False), + state=dict(required=False, default='present', choices=['present', 'absent']), + local_discr=dict(required=False, type='int'), + remote_discr=dict(required=False, type='int') + ) + + argument_spec.update(ce_argument_spec) + module = BfdSession(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_bfd_view.py b/plugins/modules/ce_bfd_view.py new file mode 100644 index 0000000..8c45f8b --- /dev/null +++ b/plugins/modules/ce_bfd_view.py @@ -0,0 +1,562 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_bfd_view +short_description: Manages BFD session view configuration on HUAWEI CloudEngine devices. +description: + - Manages BFD session view configuration on HUAWEI CloudEngine devices. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + session_name: + description: + - Specifies the name of a BFD session. + The value is a string of 1 to 15 case-sensitive characters without spaces. + required: true + local_discr: + description: + - Specifies the local discriminator of a BFD session. + The value is an integer that ranges from 1 to 16384. + remote_discr: + description: + - Specifies the remote discriminator of a BFD session. + The value is an integer that ranges from 1 to 4294967295. + min_tx_interval: + description: + - Specifies the minimum interval for receiving BFD packets. + The value is an integer that ranges from 50 to 1000, in milliseconds. + min_rx_interval: + description: + - Specifies the minimum interval for sending BFD packets. + The value is an integer that ranges from 50 to 1000, in milliseconds. + detect_multi: + description: + - Specifies the local detection multiplier of a BFD session. + The value is an integer that ranges from 3 to 50. + wtr_interval: + description: + - Specifies the WTR time of a BFD session. + The value is an integer that ranges from 1 to 60, in minutes. + The default value is 0. + tos_exp: + description: + - Specifies a priority for BFD control packets. + The value is an integer ranging from 0 to 7. + The default value is 7, which is the highest priority. + admin_down: + description: + - Enables the BFD session to enter the AdminDown state. + By default, a BFD session is enabled. + The default value is bool type. + type: bool + default: 'no' + description: + description: + - Specifies the description of a BFD session. + The value is a string of 1 to 51 case-sensitive characters with spaces. + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: ['present', 'absent'] + +extends_documentation_fragment: +- huawei.cloudengine.ce +''' + +EXAMPLES = ''' +- name: bfd view module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Set the local discriminator of a BFD session to 80 and the remote discriminator to 800 + ce_bfd_view: + session_name: atob + local_discr: 80 + remote_discr: 800 + state: present + provider: '{{ cli }}' + + - name: Set the minimum interval for receiving BFD packets to 500 ms + ce_bfd_view: + session_name: atob + min_rx_interval: 500 + state: present + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "admin_down": false, + "description": null, + "detect_multi": null, + "local_discr": 80, + "min_rx_interval": null, + "min_tx_interval": null, + "remote_discr": 800, + "session_name": "atob", + "state": "present", + "tos_exp": null, + "wtr_interval": null + } +existing: + description: k/v pairs of existing configuration + returned: always + type: dict + sample: { + "session": { + "adminDown": "false", + "createType": "SESS_STATIC", + "description": null, + "detectMulti": "3", + "localDiscr": null, + "minRxInt": null, + "minTxInt": null, + "remoteDiscr": null, + "sessName": "atob", + "tosExp": null, + "wtrTimerInt": null + } + } +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: { + "session": { + "adminDown": "false", + "createType": "SESS_STATIC", + "description": null, + "detectMulti": "3", + "localDiscr": "80", + "minRxInt": null, + "minTxInt": null, + "remoteDiscr": "800", + "sessName": "atob", + "tosExp": null, + "wtrTimerInt": null + } + } +updates: + description: commands sent to the device + returned: always + type: list + sample: [ + "bfd atob", + "discriminator local 80", + "discriminator remote 800" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import sys +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_BFD = """ + + + %s + + +""" + +CE_NC_GET_BFD_GLB = """ + + + +""" + +CE_NC_GET_BFD_SESSION = """ + + + %s + + + + + + + + + + + + +""" + + +class BfdView(object): + """Manages BFD View""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.session_name = self.module.params['session_name'] + self.local_discr = self.module.params['local_discr'] + self.remote_discr = self.module.params['remote_discr'] + self.min_tx_interval = self.module.params['min_tx_interval'] + self.min_rx_interval = self.module.params['min_rx_interval'] + self.detect_multi = self.module.params['detect_multi'] + self.wtr_interval = self.module.params['wtr_interval'] + self.tos_exp = self.module.params['tos_exp'] + self.admin_down = self.module.params['admin_down'] + self.description = self.module.params['description'] + self.state = self.module.params['state'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.changed = False + self.bfd_dict = dict() + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + self.module = AnsibleModule(argument_spec=self.spec, + supports_check_mode=True) + + def get_bfd_dict(self): + """bfd config dict""" + + bfd_dict = dict() + bfd_dict["global"] = dict() + bfd_dict["session"] = dict() + conf_str = CE_NC_GET_BFD % (CE_NC_GET_BFD_GLB + (CE_NC_GET_BFD_SESSION % self.session_name)) + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return bfd_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get bfd global info + glb = root.find("bfd/bfdSchGlobal") + if glb: + for attr in glb: + bfd_dict["global"][attr.tag] = attr.text + + # get bfd session info + sess = root.find("bfd/bfdCfgSessions/bfdCfgSession") + if sess: + for attr in sess: + bfd_dict["session"][attr.tag] = attr.text + + return bfd_dict + + def config_session(self): + """configures bfd session""" + + xml_str = "" + cmd_list = list() + cmd_session = "" + + if not self.session_name: + return xml_str + + if self.bfd_dict["global"].get("bfdEnable", "false") != "true": + self.module.fail_json(msg="Error: Please enable BFD globally first.") + + if not self.bfd_dict["session"]: + self.module.fail_json(msg="Error: BFD session is not exist.") + + session = self.bfd_dict["session"] + xml_str = "%s" % self.session_name + cmd_session = "bfd %s" % self.session_name + + # BFD session view + if self.local_discr is not None: + if self.state == "present" and str(self.local_discr) != session.get("localDiscr"): + xml_str += "%s" % self.local_discr + cmd_list.append("discriminator local %s" % self.local_discr) + elif self.state == "absent" and str(self.local_discr) == session.get("localDiscr"): + xml_str += "" + cmd_list.append("undo discriminator local") + + if self.remote_discr is not None: + if self.state == "present" and str(self.remote_discr) != session.get("remoteDiscr"): + xml_str += "%s" % self.remote_discr + cmd_list.append("discriminator remote %s" % self.remote_discr) + elif self.state == "absent" and str(self.remote_discr) == session.get("remoteDiscr"): + xml_str += "" + cmd_list.append("undo discriminator remote") + + if self.min_tx_interval is not None: + if self.state == "present" and str(self.min_tx_interval) != session.get("minTxInt"): + xml_str += "%s" % self.min_tx_interval + cmd_list.append("min-tx-interval %s" % self.min_tx_interval) + elif self.state == "absent" and str(self.min_tx_interval) == session.get("minTxInt"): + xml_str += "" + cmd_list.append("undo min-tx-interval") + + if self.min_rx_interval is not None: + if self.state == "present" and str(self.min_rx_interval) != session.get("minRxInt"): + xml_str += "%s" % self.min_rx_interval + cmd_list.append("min-rx-interval %s" % self.min_rx_interval) + elif self.state == "absent" and str(self.min_rx_interval) == session.get("minRxInt"): + xml_str += "" + cmd_list.append("undo min-rx-interval") + + if self.detect_multi is not None: + if self.state == "present" and str(self.detect_multi) != session.get("detectMulti"): + xml_str += " %s" % self.detect_multi + cmd_list.append("detect-multiplier %s" % self.detect_multi) + elif self.state == "absent" and str(self.detect_multi) == session.get("detectMulti"): + xml_str += " " + cmd_list.append("undo detect-multiplier") + + if self.wtr_interval is not None: + if self.state == "present" and str(self.wtr_interval) != session.get("wtrTimerInt"): + xml_str += " %s" % self.wtr_interval + cmd_list.append("wtr %s" % self.wtr_interval) + elif self.state == "absent" and str(self.wtr_interval) == session.get("wtrTimerInt"): + xml_str += " " + cmd_list.append("undo wtr") + + if self.tos_exp is not None: + if self.state == "present" and str(self.tos_exp) != session.get("tosExp"): + xml_str += " %s" % self.tos_exp + cmd_list.append("tos-exp %s" % self.tos_exp) + elif self.state == "absent" and str(self.tos_exp) == session.get("tosExp"): + xml_str += " " + cmd_list.append("undo tos-exp") + + if self.admin_down and session.get("adminDown", "false") == "false": + xml_str += " true" + cmd_list.append("shutdown") + elif not self.admin_down and session.get("adminDown", "false") == "true": + xml_str += " false" + cmd_list.append("undo shutdown") + + if self.description: + if self.state == "present" and self.description != session.get("description"): + xml_str += "%s" % self.description + cmd_list.append("description %s" % self.description) + elif self.state == "absent" and self.description == session.get("description"): + xml_str += "" + cmd_list.append("undo description") + + if xml_str.endswith(""): + # no config update + return "" + else: + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + return '' + xml_str\ + + '' + + def netconf_load_config(self, xml_str): + """load bfd config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + + set_nc_config(self.min_rx_interval, xml_cfg) + self.changed = True + + def check_params(self): + """Check all input params""" + + # check session_name + if not self.session_name: + self.module.fail_json(msg="Error: Missing required arguments: session_name.") + + if self.session_name: + if len(self.session_name) < 1 or len(self.session_name) > 15: + self.module.fail_json(msg="Error: Session name is invalid.") + + # check local_discr + if self.local_discr is not None: + if self.local_discr < 1 or self.local_discr > 16384: + self.module.fail_json(msg="Error: Session local_discr is not ranges from 1 to 16384.") + + # check remote_discr + if self.remote_discr is not None: + if self.remote_discr < 1 or self.remote_discr > 4294967295: + self.module.fail_json(msg="Error: Session remote_discr is not ranges from 1 to 4294967295.") + + # check min_tx_interval + if self.min_tx_interval is not None: + if self.min_tx_interval < 50 or self.min_tx_interval > 1000: + self.module.fail_json(msg="Error: Session min_tx_interval is not ranges from 50 to 1000.") + + # check min_rx_interval + if self.min_rx_interval is not None: + if self.min_rx_interval < 50 or self.min_rx_interval > 1000: + self.module.fail_json(msg="Error: Session min_rx_interval is not ranges from 50 to 1000.") + + # check detect_multi + if self.detect_multi is not None: + if self.detect_multi < 3 or self.detect_multi > 50: + self.module.fail_json(msg="Error: Session detect_multi is not ranges from 3 to 50.") + + # check wtr_interval + if self.wtr_interval is not None: + if self.wtr_interval < 1 or self.wtr_interval > 60: + self.module.fail_json(msg="Error: Session wtr_interval is not ranges from 1 to 60.") + + # check tos_exp + if self.tos_exp is not None: + if self.tos_exp < 0 or self.tos_exp > 7: + self.module.fail_json(msg="Error: Session tos_exp is not ranges from 0 to 7.") + + # check description + if self.description: + if len(self.description) < 1 or len(self.description) > 51: + self.module.fail_json(msg="Error: Session description is invalid.") + + def get_proposed(self): + """get proposed info""" + + # base config + self.proposed["session_name"] = self.session_name + self.proposed["local_discr"] = self.local_discr + self.proposed["remote_discr"] = self.remote_discr + self.proposed["min_tx_interval"] = self.min_tx_interval + self.proposed["min_rx_interval"] = self.min_rx_interval + self.proposed["detect_multi"] = self.detect_multi + self.proposed["wtr_interval"] = self.wtr_interval + self.proposed["tos_exp"] = self.tos_exp + self.proposed["admin_down"] = self.admin_down + self.proposed["description"] = self.description + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.bfd_dict: + return + + self.existing["session"] = self.bfd_dict.get("session") + + def get_end_state(self): + """get end state info""" + + bfd_dict = self.get_bfd_dict() + if not bfd_dict: + return + + self.end_state["session"] = bfd_dict.get("session") + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.bfd_dict = self.get_bfd_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.session_name: + xml_str += self.config_session() + + # update to device + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + session_name=dict(required=True, type='str'), + local_discr=dict(required=False, type='int'), + remote_discr=dict(required=False, type='int'), + min_tx_interval=dict(required=False, type='int'), + min_rx_interval=dict(required=False, type='int'), + detect_multi=dict(required=False, type='int'), + wtr_interval=dict(required=False, type='int'), + tos_exp=dict(required=False, type='int'), + admin_down=dict(required=False, type='bool', default=False), + description=dict(required=False, type='str'), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = BfdView(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_bgp.py b/plugins/modules/ce_bgp.py new file mode 100644 index 0000000..b745090 --- /dev/null +++ b/plugins/modules/ce_bgp.py @@ -0,0 +1,2328 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_bgp +short_description: Manages BGP configuration on HUAWEI CloudEngine switches. +description: + - Manages BGP configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] + as_number: + description: + - Local AS number. + The value is a string of 1 to 11 characters. + graceful_restart: + description: + - Enable GR of the BGP speaker in the specified address family, peer address, or peer group. + default: no_use + choices: ['no_use','true','false'] + time_wait_for_rib: + description: + - Period of waiting for the End-Of-RIB flag. + The value is an integer ranging from 3 to 3000. The default value is 600. + as_path_limit: + description: + - Maximum number of AS numbers in the AS_Path attribute. The default value is 255. + check_first_as: + description: + - Check the first AS in the AS_Path of the update messages from EBGP peers. + default: no_use + choices: ['no_use','true','false'] + confed_id_number: + description: + - Confederation ID. + The value is a string of 1 to 11 characters. + confed_nonstanded: + description: + - Configure the device to be compatible with devices in a nonstandard confederation. + default: no_use + choices: ['no_use','true','false'] + bgp_rid_auto_sel: + description: + - The function to automatically select router IDs for all VPN BGP instances is enabled. + default: no_use + choices: ['no_use','true','false'] + keep_all_routes: + description: + - If the value is true, the system stores all route update messages received from all peers (groups) after + BGP connection setup. + If the value is false, the system stores only BGP update messages that are received from peers and pass + the configured import policy. + default: no_use + choices: ['no_use','true','false'] + memory_limit: + description: + - Support BGP RIB memory protection. + default: no_use + choices: ['no_use','true','false'] + gr_peer_reset: + description: + - Peer disconnection through GR. + default: no_use + choices: ['no_use','true','false'] + is_shutdown: + description: + - Interrupt BGP all neighbor. + default: no_use + choices: ['no_use','true','false'] + suppress_interval: + description: + - Suppress interval. + hold_interval: + description: + - Hold interval. + clear_interval: + description: + - Clear interval. + confed_peer_as_num: + description: + - Confederation AS number, in two-byte or four-byte format. + The value is a string of 1 to 11 characters. + vrf_name: + description: + - Name of a BGP instance. The name is a case-sensitive string of characters. + vrf_rid_auto_sel: + description: + - If the value is true, VPN BGP instances are enabled to automatically select router IDs. + If the value is false, VPN BGP instances are disabled from automatically selecting router IDs. + default: no_use + choices: ['no_use','true','false'] + router_id: + description: + - ID of a router that is in IPv4 address format. + keepalive_time: + description: + - If the value of a timer changes, the BGP peer relationship between the routers is disconnected. + The value is an integer ranging from 0 to 21845. The default value is 60. + hold_time: + description: + - Hold time, in seconds. The value of the hold time can be 0 or range from 3 to 65535. + min_hold_time: + description: + - Min hold time, in seconds. The value of the hold time can be 0 or range from 20 to 65535. + conn_retry_time: + description: + - ConnectRetry interval. The value is an integer, in seconds. The default value is 32s. + ebgp_if_sensitive: + description: + - If the value is true, After the fast EBGP interface awareness function is enabled, EBGP sessions on + an interface are deleted immediately when the interface goes Down. + If the value is false, After the fast EBGP interface awareness function is enabled, EBGP sessions + on an interface are not deleted immediately when the interface goes Down. + default: no_use + choices: ['no_use','true','false'] + default_af_type: + description: + - Type of a created address family, which can be IPv4 unicast or IPv6 unicast. + The default type is IPv4 unicast. + choices: ['ipv4uni','ipv6uni'] +''' + +EXAMPLES = ''' + +- name: CloudEngine BGP test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Enable BGP" + ce_bgp: + state: present + as_number: 100 + confed_id_number: 250 + provider: "{{ cli }}" + + - name: "Disable BGP" + ce_bgp: + state: absent + as_number: 100 + confed_id_number: 250 + provider: "{{ cli }}" + + - name: "Create confederation peer AS num" + ce_bgp: + state: present + confed_peer_as_num: 260 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"as_number": "100", state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"bgp_enable": [["100"], ["true"]]} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"bgp_enable": [["100"], ["true"]]} +updates: + description: command sent to the device + returned: always + type: list + sample: ["bgp 100"] +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +SUCCESS = """success""" +FAILED = """failed""" + + +# get bgp enable +CE_GET_BGP_ENABLE = """ + + + + + + + + + + +""" + +CE_GET_BGP_ENABLE_HEADER = """ + + + + +""" + +CE_GET_BGP_ENABLE_TAIL = """ + + + + +""" + +# merge bgp enable +CE_MERGE_BGP_ENABLE_HEADER = """ + + + + +""" +CE_MERGE_BGP_ENABLE_TAIL = """ + + + + +""" + +# get bgp confederation peer as +CE_GET_BGP_CONFED_PEER_AS = """ + + + + + + + + + + + +""" + +# merge bgp confederation peer as +CE_MERGE_BGP_CONFED_PEER_AS = """ + + + + + + %s + + + + + +""" + +# create bgp confederation peer as +CE_CREATE_BGP_CONFED_PEER_AS = """ + + + + + + %s + + + + + +""" + +# delete bgp confederation peer as +CE_DELETE_BGP_CONFED_PEER_AS = """ + + + + + + %s + + + + + +""" + +# get bgp instance +CE_GET_BGP_INSTANCE = """ + + + + + + + + + + + +""" + +# get bgp instance +CE_GET_BGP_INSTANCE_HEADER = """ + + + + + +""" +CE_GET_BGP_INSTANCE_TAIL = """ + + + + + +""" + +# merge bgp instance +CE_MERGE_BGP_INSTANCE_HEADER = """ + + + + + +""" +CE_MERGE_BGP_INSTANCE_TAIL = """ + + + + + +""" + +# create bgp instance +CE_CREATE_BGP_INSTANCE_HEADER = """ + + + + + +""" +CE_CREATE_BGP_INSTANCE_TAIL = """ + + + + + +""" + +# delete bgp instance +CE_DELETE_BGP_INSTANCE_HEADER = """ + + + + + +""" +CE_DELETE_BGP_INSTANCE_TAIL = """ + + + + + +""" + + +def check_ip_addr(**kwargs): + """ check_ip_addr """ + + ipaddr = kwargs["ipaddr"] + + addr = ipaddr.strip().split('.') + + if len(addr) != 4: + return FAILED + + for i in range(4): + addr[i] = int(addr[i]) + + if addr[i] <= 255 and addr[i] >= 0: + pass + else: + return FAILED + return SUCCESS + + +def check_bgp_enable_args(**kwargs): + """ check_bgp_enable_args """ + + module = kwargs["module"] + + need_cfg = False + + as_number = module.params['as_number'] + if as_number: + if len(as_number) > 11 or len(as_number) == 0: + module.fail_json( + msg='Error: The len of as_number %s is out of [1 - 11].' % as_number) + else: + need_cfg = True + + return need_cfg + + +def check_bgp_confed_args(**kwargs): + """ check_bgp_confed_args """ + + module = kwargs["module"] + + need_cfg = False + + confed_peer_as_num = module.params['confed_peer_as_num'] + if confed_peer_as_num: + if len(confed_peer_as_num) > 11 or len(confed_peer_as_num) == 0: + module.fail_json( + msg='Error: The len of confed_peer_as_num %s is out of [1 - 11].' % confed_peer_as_num) + else: + need_cfg = True + + return need_cfg + + +class Bgp(object): + """ Manages BGP configuration """ + + def netconf_get_config(self, **kwargs): + """ netconf_get_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ netconf_set_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = set_nc_config(module, conf_str) + + return xml_str + + def check_bgp_enable_other_args(self, **kwargs): + """ check_bgp_enable_other_args """ + + module = kwargs["module"] + state = module.params['state'] + result = dict() + need_cfg = False + + graceful_restart = module.params['graceful_restart'] + if graceful_restart != 'no_use': + + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["graceful_restart"] = re_find + if re_find[0] != graceful_restart: + need_cfg = True + else: + need_cfg = True + + time_wait_for_rib = module.params['time_wait_for_rib'] + if time_wait_for_rib: + if int(time_wait_for_rib) > 3000 or int(time_wait_for_rib) < 3: + module.fail_json( + msg='Error: The time_wait_for_rib %s is out of [3 - 3000].' % time_wait_for_rib) + else: + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["time_wait_for_rib"] = re_find + if re_find[0] != time_wait_for_rib: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["time_wait_for_rib"] = re_find + if re_find[0] == time_wait_for_rib: + need_cfg = True + + as_path_limit = module.params['as_path_limit'] + if as_path_limit: + if int(as_path_limit) > 2000 or int(as_path_limit) < 1: + module.fail_json( + msg='Error: The as_path_limit %s is out of [1 - 2000].' % as_path_limit) + else: + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["as_path_limit"] = re_find + if re_find[0] != as_path_limit: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["as_path_limit"] = re_find + if re_find[0] == as_path_limit: + need_cfg = True + + check_first_as = module.params['check_first_as'] + if check_first_as != 'no_use': + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["check_first_as"] = re_find + if re_find[0] != check_first_as: + need_cfg = True + else: + need_cfg = True + + confed_id_number = module.params['confed_id_number'] + if confed_id_number: + if len(confed_id_number) > 11 or len(confed_id_number) == 0: + module.fail_json( + msg='Error: The len of confed_id_number %s is out of [1 - 11].' % confed_id_number) + else: + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["confed_id_number"] = re_find + if re_find[0] != confed_id_number: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["confed_id_number"] = re_find + if re_find[0] == confed_id_number: + need_cfg = True + + confed_nonstanded = module.params['confed_nonstanded'] + if confed_nonstanded != 'no_use': + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["confed_nonstanded"] = re_find + if re_find[0] != confed_nonstanded: + need_cfg = True + else: + need_cfg = True + + bgp_rid_auto_sel = module.params['bgp_rid_auto_sel'] + if bgp_rid_auto_sel != 'no_use': + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["bgp_rid_auto_sel"] = re_find + if re_find[0] != bgp_rid_auto_sel: + need_cfg = True + else: + need_cfg = True + + keep_all_routes = module.params['keep_all_routes'] + if keep_all_routes != 'no_use': + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["keep_all_routes"] = re_find + if re_find[0] != keep_all_routes: + need_cfg = True + else: + need_cfg = True + + memory_limit = module.params['memory_limit'] + if memory_limit != 'no_use': + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["memory_limit"] = re_find + if re_find[0] != memory_limit: + need_cfg = True + else: + need_cfg = True + + gr_peer_reset = module.params['gr_peer_reset'] + if gr_peer_reset != 'no_use': + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["gr_peer_reset"] = re_find + if re_find[0] != gr_peer_reset: + need_cfg = True + else: + need_cfg = True + + is_shutdown = module.params['is_shutdown'] + if is_shutdown != 'no_use': + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_shutdown"] = re_find + if re_find[0] != is_shutdown: + need_cfg = True + else: + need_cfg = True + + suppress_interval = module.params['suppress_interval'] + hold_interval = module.params['hold_interval'] + clear_interval = module.params['clear_interval'] + if suppress_interval: + + if not hold_interval or not clear_interval: + module.fail_json( + msg='Error: Please input suppress_interval hold_interval clear_interval at the same time.') + + if int(suppress_interval) > 65535 or int(suppress_interval) < 1: + module.fail_json( + msg='Error: The suppress_interval %s is out of [1 - 65535].' % suppress_interval) + else: + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["suppress_interval"] = re_find + if re_find[0] != suppress_interval: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["suppress_interval"] = re_find + if re_find[0] == suppress_interval: + need_cfg = True + + if hold_interval: + + if not suppress_interval or not clear_interval: + module.fail_json( + msg='Error: Please input suppress_interval hold_interval clear_interval at the same time.') + + if int(hold_interval) > 65535 or int(hold_interval) < 1: + module.fail_json( + msg='Error: The hold_interval %s is out of [1 - 65535].' % hold_interval) + else: + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["hold_interval"] = re_find + if re_find[0] != hold_interval: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["hold_interval"] = re_find + if re_find[0] == hold_interval: + need_cfg = True + + if clear_interval: + + if not suppress_interval or not hold_interval: + module.fail_json( + msg='Error: Please input suppress_interval hold_interval clear_interval at the same time.') + + if int(clear_interval) > 65535 or int(clear_interval) < 1: + module.fail_json( + msg='Error: The clear_interval %s is out of [1 - 65535].' % clear_interval) + else: + conf_str = CE_GET_BGP_ENABLE_HEADER + \ + "" + CE_GET_BGP_ENABLE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["clear_interval"] = re_find + if re_find[0] != clear_interval: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["clear_interval"] = re_find + if re_find[0] == clear_interval: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def check_bgp_instance_args(self, **kwargs): + """ check_bgp_instance_args """ + + module = kwargs["module"] + state = module.params['state'] + need_cfg = False + + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='the len of vrf_name %s is out of [1 - 31].' % vrf_name) + conf_str = CE_GET_BGP_INSTANCE_HEADER + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + check_vrf_name = vrf_name + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if check_vrf_name not in re_find: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if check_vrf_name in re_find: + need_cfg = True + + return need_cfg + + def check_bgp_instance_other_args(self, **kwargs): + """ check_bgp_instance_other_args """ + + module = kwargs["module"] + state = module.params['state'] + result = dict() + need_cfg = False + + vrf_name = module.params['vrf_name'] + + router_id = module.params['router_id'] + if router_id: + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + if check_ip_addr(ipaddr=router_id) == FAILED: + module.fail_json( + msg='Error: The router_id %s is invalid.' % router_id) + + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["router_id"] = re_find + if re_find[0] != router_id: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["router_id"] = re_find + if re_find[0] == router_id: + need_cfg = True + + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + if vrf_rid_auto_sel != 'no_use': + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["vrf_rid_auto_sel"] = re_find + + if re_find[0] != vrf_rid_auto_sel: + need_cfg = True + else: + need_cfg = True + + keepalive_time = module.params['keepalive_time'] + if keepalive_time: + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + if int(keepalive_time) > 21845 or int(keepalive_time) < 0: + module.fail_json( + msg='keepalive_time %s is out of [0 - 21845].' % keepalive_time) + else: + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["keepalive_time"] = re_find + if re_find[0] != keepalive_time: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["keepalive_time"] = re_find + if re_find[0] == keepalive_time: + need_cfg = True + + hold_time = module.params['hold_time'] + if hold_time: + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + if int(hold_time) > 65535 or int(hold_time) < 3: + module.fail_json( + msg='hold_time %s is out of [3 - 65535].' % hold_time) + else: + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["hold_time"] = re_find + if re_find[0] != hold_time: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["hold_time"] = re_find + if re_find[0] == hold_time: + need_cfg = True + + min_hold_time = module.params['min_hold_time'] + if min_hold_time: + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + if int(min_hold_time) != 0 and (int(min_hold_time) > 65535 or int(min_hold_time) < 20): + module.fail_json( + msg='min_hold_time %s is out of [0, or 20 - 65535].' % min_hold_time) + else: + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["min_hold_time"] = re_find + if re_find[0] != min_hold_time: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["min_hold_time"] = re_find + if re_find[0] == min_hold_time: + need_cfg = True + + conn_retry_time = module.params['conn_retry_time'] + if conn_retry_time: + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + if int(conn_retry_time) > 65535 or int(conn_retry_time) < 1: + module.fail_json( + msg='conn_retry_time %s is out of [1 - 65535].' % conn_retry_time) + else: + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config( + module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["conn_retry_time"] = re_find + if re_find[0] != conn_retry_time: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["conn_retry_time"] = re_find + if re_find[0] == conn_retry_time: + need_cfg = True + else: + pass + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ebgp_if_sensitive"] = re_find + if re_find[0] != ebgp_if_sensitive: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ebgp_if_sensitive"] = re_find + if re_find[0] == ebgp_if_sensitive: + need_cfg = True + else: + pass + + default_af_type = module.params['default_af_type'] + if default_af_type: + + if not vrf_name: + module.fail_json( + msg='Error: Please input vrf_name.') + + conf_str = CE_GET_BGP_INSTANCE_HEADER + "%s" % vrf_name + \ + "" + CE_GET_BGP_INSTANCE_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_af_type"] = re_find + if re_find[0] != default_af_type: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_af_type"] = re_find + if re_find[0] == default_af_type: + need_cfg = True + else: + pass + + result["need_cfg"] = need_cfg + return result + + def get_bgp_enable(self, **kwargs): + """ get_bgp_enable """ + + module = kwargs["module"] + + conf_str = CE_GET_BGP_ENABLE + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*\s*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_bgp_enable(self, **kwargs): + """ merge_bgp_enable """ + + module = kwargs["module"] + conf_str = CE_MERGE_BGP_ENABLE_HEADER + + state = module.params['state'] + + if state == "present": + conf_str += "true" + else: + conf_str += "false" + + as_number = module.params['as_number'] + if as_number: + conf_str += "%s" % as_number + + conf_str += CE_MERGE_BGP_ENABLE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp enable failed.') + + cmds = [] + if state == "present": + cmd = "bgp %s" % as_number + else: + cmd = "undo bgp %s" % as_number + cmds.append(cmd) + + return cmds + + def merge_bgp_enable_other(self, **kwargs): + """ merge_bgp_enable_other """ + + module = kwargs["module"] + conf_str = CE_MERGE_BGP_ENABLE_HEADER + + cmds = [] + + graceful_restart = module.params['graceful_restart'] + if graceful_restart != 'no_use': + conf_str += "%s" % graceful_restart + + if graceful_restart == "true": + cmd = "graceful-restart" + else: + cmd = "undo graceful-restart" + cmds.append(cmd) + + time_wait_for_rib = module.params['time_wait_for_rib'] + if time_wait_for_rib: + conf_str += "%s" % time_wait_for_rib + + cmd = "graceful-restart timer wait-for-rib %s" % time_wait_for_rib + cmds.append(cmd) + + as_path_limit = module.params['as_path_limit'] + if as_path_limit: + conf_str += "%s" % as_path_limit + + cmd = "as-path-limit %s" % as_path_limit + cmds.append(cmd) + + check_first_as = module.params['check_first_as'] + if check_first_as != 'no_use': + conf_str += "%s" % check_first_as + + if check_first_as == "true": + cmd = "check-first-as" + else: + cmd = "undo check-first-as" + cmds.append(cmd) + + confed_id_number = module.params['confed_id_number'] + if confed_id_number: + conf_str += "%s" % confed_id_number + + cmd = "confederation id %s" % confed_id_number + cmds.append(cmd) + + confed_nonstanded = module.params['confed_nonstanded'] + if confed_nonstanded != 'no_use': + conf_str += "%s" % confed_nonstanded + + if confed_nonstanded == "true": + cmd = "confederation nonstandard" + else: + cmd = "undo confederation nonstandard" + cmds.append(cmd) + + bgp_rid_auto_sel = module.params['bgp_rid_auto_sel'] + if bgp_rid_auto_sel != 'no_use': + conf_str += "%s" % bgp_rid_auto_sel + + if bgp_rid_auto_sel == "true": + cmd = "router-id vpn-instance auto-select" + else: + cmd = "undo router-id" + cmds.append(cmd) + + keep_all_routes = module.params['keep_all_routes'] + if keep_all_routes != 'no_use': + conf_str += "%s" % keep_all_routes + + if keep_all_routes == "true": + cmd = "keep-all-routes" + else: + cmd = "undo keep-all-routes" + cmds.append(cmd) + + memory_limit = module.params['memory_limit'] + if memory_limit != 'no_use': + conf_str += "%s" % memory_limit + + if memory_limit == "true": + cmd = "prefix memory-limit" + else: + cmd = "undo prefix memory-limit" + cmds.append(cmd) + + gr_peer_reset = module.params['gr_peer_reset'] + if gr_peer_reset != 'no_use': + conf_str += "%s" % gr_peer_reset + + if gr_peer_reset == "true": + cmd = "graceful-restart peer-reset" + else: + cmd = "undo graceful-restart peer-reset" + cmds.append(cmd) + + is_shutdown = module.params['is_shutdown'] + if is_shutdown != 'no_use': + conf_str += "%s" % is_shutdown + + if is_shutdown == "true": + cmd = "shutdown" + else: + cmd = "undo shutdown" + cmds.append(cmd) + + suppress_interval = module.params['suppress_interval'] + hold_interval = module.params['hold_interval'] + clear_interval = module.params['clear_interval'] + if suppress_interval: + conf_str += "%s" % suppress_interval + + cmd = "nexthop recursive-lookup restrain suppress-interval %s hold-interval %s " \ + "clear-interval %s" % (suppress_interval, hold_interval, clear_interval) + cmds.append(cmd) + + if hold_interval: + conf_str += "%s" % hold_interval + + if clear_interval: + conf_str += "%s" % clear_interval + + conf_str += CE_MERGE_BGP_ENABLE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp enable failed.') + + return cmds + + def delete_bgp_enable_other(self, **kwargs): + """ delete bgp enable other args """ + + module = kwargs["module"] + conf_str = CE_MERGE_BGP_ENABLE_HEADER + + cmds = [] + + graceful_restart = module.params['graceful_restart'] + if graceful_restart != 'no_use': + conf_str += "%s" % graceful_restart + + if graceful_restart == "true": + cmd = "graceful-restart" + else: + cmd = "undo graceful-restart" + cmds.append(cmd) + + time_wait_for_rib = module.params['time_wait_for_rib'] + if time_wait_for_rib: + conf_str += "600" + + cmd = "undo graceful-restart timer wait-for-rib" + cmds.append(cmd) + + as_path_limit = module.params['as_path_limit'] + if as_path_limit: + conf_str += "255" + + cmd = "undo as-path-limit" + cmds.append(cmd) + + check_first_as = module.params['check_first_as'] + if check_first_as != 'no_use': + conf_str += "%s" % check_first_as + + if check_first_as == "true": + cmd = "check-first-as" + else: + cmd = "undo check-first-as" + cmds.append(cmd) + + confed_id_number = module.params['confed_id_number'] + confed_peer_as_num = module.params['confed_peer_as_num'] + if confed_id_number and not confed_peer_as_num: + conf_str += "" + + cmd = "undo confederation id" + cmds.append(cmd) + + confed_nonstanded = module.params['confed_nonstanded'] + if confed_nonstanded != 'no_use': + conf_str += "%s" % confed_nonstanded + + if confed_nonstanded == "true": + cmd = "confederation nonstandard" + else: + cmd = "undo confederation nonstandard" + cmds.append(cmd) + + bgp_rid_auto_sel = module.params['bgp_rid_auto_sel'] + if bgp_rid_auto_sel != 'no_use': + conf_str += "%s" % bgp_rid_auto_sel + + if bgp_rid_auto_sel == "true": + cmd = "router-id vpn-instance auto-select" + else: + cmd = "undo router-id" + cmds.append(cmd) + + keep_all_routes = module.params['keep_all_routes'] + if keep_all_routes != 'no_use': + conf_str += "%s" % keep_all_routes + + if keep_all_routes == "true": + cmd = "keep-all-routes" + else: + cmd = "undo keep-all-routes" + cmds.append(cmd) + + memory_limit = module.params['memory_limit'] + if memory_limit != 'no_use': + conf_str += "%s" % memory_limit + + if memory_limit == "true": + cmd = "prefix memory-limit" + else: + cmd = "undo prefix memory-limit" + cmds.append(cmd) + + gr_peer_reset = module.params['gr_peer_reset'] + if gr_peer_reset != 'no_use': + conf_str += "%s" % gr_peer_reset + + if gr_peer_reset == "true": + cmd = "graceful-restart peer-reset" + else: + cmd = "undo graceful-restart peer-reset" + cmds.append(cmd) + + is_shutdown = module.params['is_shutdown'] + if is_shutdown != 'no_use': + conf_str += "%s" % is_shutdown + + if is_shutdown == "true": + cmd = "shutdown" + else: + cmd = "undo shutdown" + cmds.append(cmd) + + suppress_interval = module.params['suppress_interval'] + hold_interval = module.params['hold_interval'] + clear_interval = module.params['clear_interval'] + if suppress_interval: + conf_str += "60" + + cmd = "undo nexthop recursive-lookup restrain suppress-interval hold-interval clear-interval" + cmds.append(cmd) + + if hold_interval: + conf_str += "120" + + if clear_interval: + conf_str += "600" + + conf_str += CE_MERGE_BGP_ENABLE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp enable failed.') + + return cmds + + def get_bgp_confed_peer_as(self, **kwargs): + """ get_bgp_confed_peer_as """ + + module = kwargs["module"] + + conf_str = CE_GET_BGP_CONFED_PEER_AS + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_bgp_confed_peer_as(self, **kwargs): + """ merge_bgp_confed_peer_as """ + + module = kwargs["module"] + confed_peer_as_num = module.params['confed_peer_as_num'] + + conf_str = CE_MERGE_BGP_CONFED_PEER_AS % confed_peer_as_num + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp confed peer as failed.') + + cmds = [] + cmd = "confederation peer-as %s" % confed_peer_as_num + cmds.append(cmd) + + return cmds + + def create_bgp_confed_peer_as(self, **kwargs): + """ create_bgp_confed_peer_as """ + + module = kwargs["module"] + confed_peer_as_num = module.params['confed_peer_as_num'] + + conf_str = CE_CREATE_BGP_CONFED_PEER_AS % confed_peer_as_num + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create bgp confed peer as failed.') + + cmds = [] + cmd = "confederation peer-as %s" % confed_peer_as_num + cmds.append(cmd) + + return cmds + + def delete_bgp_confed_peer_as(self, **kwargs): + """ delete_bgp_confed_peer_as """ + + module = kwargs["module"] + confed_peer_as_num = module.params['confed_peer_as_num'] + + conf_str = CE_DELETE_BGP_CONFED_PEER_AS % confed_peer_as_num + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp confed peer as failed.') + + cmds = [] + cmd = "undo confederation peer-as %s" % confed_peer_as_num + cmds.append(cmd) + + return cmds + + def get_bgp_instance(self, **kwargs): + """ get_bgp_instance """ + + module = kwargs["module"] + conf_str = CE_GET_BGP_INSTANCE + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_bgp_instance(self, **kwargs): + """ merge_bgp_instance """ + + module = kwargs["module"] + conf_str = CE_MERGE_BGP_INSTANCE_HEADER + + vrf_name = module.params['vrf_name'] + conf_str += "%s" % vrf_name + + conf_str += CE_MERGE_BGP_INSTANCE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp instance failed.') + + cmds = [] + + if vrf_name != "_public_": + cmd = "ipv4-family vpn-instance %s" % vrf_name + cmds.append(cmd) + + return cmds + + def create_bgp_instance(self, **kwargs): + """ create_bgp_instance """ + + module = kwargs["module"] + conf_str = CE_CREATE_BGP_INSTANCE_HEADER + + cmds = [] + + vrf_name = module.params['vrf_name'] + if vrf_name: + if vrf_name == "_public_": + return cmds + conf_str += "%s" % vrf_name + + conf_str += CE_CREATE_BGP_INSTANCE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create bgp instance failed.') + + if vrf_name != "_public_": + cmd = "ipv4-family vpn-instance %s" % vrf_name + cmds.append(cmd) + + return cmds + + def delete_bgp_instance(self, **kwargs): + """ delete_bgp_instance """ + + module = kwargs["module"] + conf_str = CE_DELETE_BGP_INSTANCE_HEADER + + vrf_name = module.params['vrf_name'] + if vrf_name: + conf_str += "%s" % vrf_name + + conf_str += CE_DELETE_BGP_INSTANCE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp instance failed.') + + cmds = [] + if vrf_name != "_public_": + cmd = "undo ipv4-family vpn-instance %s" % vrf_name + cmds.append(cmd) + + return cmds + + def merge_bgp_instance_other(self, **kwargs): + """ merge_bgp_instance_other """ + + module = kwargs["module"] + conf_str = CE_MERGE_BGP_INSTANCE_HEADER + + vrf_name = module.params['vrf_name'] + conf_str += "%s" % vrf_name + + cmds = [] + + default_af_type = module.params['default_af_type'] + if default_af_type: + conf_str += "%s" % default_af_type + + if vrf_name != "_public_": + if default_af_type == "ipv6uni": + cmd = "ipv6-family vpn-instance %s" % vrf_name + cmds.append(cmd) + + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + if vrf_rid_auto_sel != 'no_use': + conf_str += "%s" % vrf_rid_auto_sel + + if vrf_rid_auto_sel == "true": + cmd = "router-id auto-select" + else: + cmd = "undo router-id auto-select" + cmds.append(cmd) + + router_id = module.params['router_id'] + if router_id: + conf_str += "%s" % router_id + + cmd = "router-id %s" % router_id + cmds.append(cmd) + + keepalive_time = module.params['keepalive_time'] + if keepalive_time: + conf_str += "%s" % keepalive_time + + cmd = "timer keepalive %s" % keepalive_time + cmds.append(cmd) + + hold_time = module.params['hold_time'] + if hold_time: + conf_str += "%s" % hold_time + + cmd = "timer hold %s" % hold_time + cmds.append(cmd) + + min_hold_time = module.params['min_hold_time'] + if min_hold_time: + conf_str += "%s" % min_hold_time + + cmd = "timer min-holdtime %s" % min_hold_time + cmds.append(cmd) + + conn_retry_time = module.params['conn_retry_time'] + if conn_retry_time: + conf_str += "%s" % conn_retry_time + + cmd = "timer connect-retry %s" % conn_retry_time + cmds.append(cmd) + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + conf_str += "%s" % ebgp_if_sensitive + + if ebgp_if_sensitive == "true": + cmd = "ebgp-interface-sensitive" + else: + cmd = "undo ebgp-interface-sensitive" + cmds.append(cmd) + + conf_str += CE_MERGE_BGP_INSTANCE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp instance other failed.') + + return cmds + + def delete_bgp_instance_other_comm(self, **kwargs): + """ delete_bgp_instance_other_comm """ + + module = kwargs["module"] + conf_str = CE_DELETE_BGP_INSTANCE_HEADER + + vrf_name = module.params['vrf_name'] + conf_str += "%s" % vrf_name + + cmds = [] + + router_id = module.params['router_id'] + if router_id: + conf_str += "%s" % router_id + + cmd = "undo router-id" + cmds.append(cmd) + + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + if vrf_rid_auto_sel != 'no_use': + conf_str += "%s" % vrf_rid_auto_sel + + cmd = "undo router-id vpn-instance auto-select" + cmds.append(cmd) + + keepalive_time = module.params['keepalive_time'] + if keepalive_time: + conf_str += "%s" % keepalive_time + + cmd = "undo timer keepalive" + cmds.append(cmd) + + hold_time = module.params['hold_time'] + if hold_time: + conf_str += "%s" % hold_time + + cmd = "undo timer hold" + cmds.append(cmd) + + min_hold_time = module.params['min_hold_time'] + if min_hold_time: + conf_str += "%s" % min_hold_time + + cmd = "undo timer min-holdtime" + cmds.append(cmd) + + conn_retry_time = module.params['conn_retry_time'] + if conn_retry_time: + conf_str += "%s" % conn_retry_time + + cmd = "undo timer connect-retry" + cmds.append(cmd) + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + conf_str += "%s" % ebgp_if_sensitive + + cmd = "undo ebgp-interface-sensitive" + cmds.append(cmd) + + default_af_type = module.params['default_af_type'] + if default_af_type: + conf_str += "%s" % default_af_type + + if vrf_name != "_public_": + if default_af_type == "ipv6uni": + cmd = "undo ipv6-family vpn-instance %s" % vrf_name + cmds.append(cmd) + else: + cmd = "undo ipv4-family vpn-instance %s" % vrf_name + cmds.append(cmd) + else: + if vrf_name != "_public_": + cmd = "undo ipv4-family vpn-instance %s" % vrf_name + cmds.append(cmd) + + conf_str += CE_DELETE_BGP_INSTANCE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Delete common vpn bgp instance other args failed.') + + return cmds + + def delete_instance_other_public(self, **kwargs): + """ delete_instance_other_public """ + + module = kwargs["module"] + conf_str = CE_MERGE_BGP_INSTANCE_HEADER + + vrf_name = module.params['vrf_name'] + conf_str += "%s" % vrf_name + + cmds = [] + + router_id = module.params['router_id'] + if router_id: + conf_str += "" + + cmd = "undo router-id" + cmds.append(cmd) + + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + if vrf_rid_auto_sel != 'no_use': + conf_str += "%s" % "false" + + cmd = "undo router-id vpn-instance auto-select" + cmds.append(cmd) + + keepalive_time = module.params['keepalive_time'] + if keepalive_time: + conf_str += "%s" % "60" + + cmd = "undo timer keepalive" + cmds.append(cmd) + + hold_time = module.params['hold_time'] + if hold_time: + conf_str += "%s" % "180" + + cmd = "undo timer hold" + cmds.append(cmd) + + min_hold_time = module.params['min_hold_time'] + if min_hold_time: + conf_str += "%s" % "0" + + cmd = "undo timer min-holdtime" + cmds.append(cmd) + + conn_retry_time = module.params['conn_retry_time'] + if conn_retry_time: + conf_str += "%s" % "32" + + cmd = "undo timer connect-retry" + cmds.append(cmd) + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + conf_str += "%s" % "true" + + cmd = "ebgp-interface-sensitive" + cmds.append(cmd) + + default_af_type = module.params['default_af_type'] + if default_af_type: + conf_str += "%s" % "ipv4uni" + + if vrf_name != "_public_": + if default_af_type == "ipv6uni": + cmd = "undo ipv6-family vpn-instance %s" % vrf_name + cmds.append(cmd) + else: + cmd = "undo ipv4-family vpn-instance %s" % vrf_name + cmds.append(cmd) + else: + if vrf_name != "_public_": + cmd = "undo ipv4-family vpn-instance %s" % vrf_name + cmds.append(cmd) + + conf_str += CE_MERGE_BGP_INSTANCE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Delete default vpn bgp instance other args failed.') + + return cmds + + +def main(): + """ main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + as_number=dict(type='str'), + graceful_restart=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + time_wait_for_rib=dict(type='str'), + as_path_limit=dict(type='str'), + check_first_as=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + confed_id_number=dict(type='str'), + confed_nonstanded=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + bgp_rid_auto_sel=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + keep_all_routes=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + memory_limit=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + gr_peer_reset=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + is_shutdown=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + suppress_interval=dict(type='str'), + hold_interval=dict(type='str'), + clear_interval=dict(type='str'), + confed_peer_as_num=dict(type='str'), + vrf_name=dict(type='str'), + vrf_rid_auto_sel=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + router_id=dict(type='str'), + keepalive_time=dict(type='str'), + hold_time=dict(type='str'), + min_hold_time=dict(type='str'), + conn_retry_time=dict(type='str'), + ebgp_if_sensitive=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + default_af_type=dict(type='str', choices=['ipv4uni', 'ipv6uni']) + ) + + argument_spec.update(ce_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + state = module.params['state'] + as_number = module.params['as_number'] + graceful_restart = module.params['graceful_restart'] + time_wait_for_rib = module.params['time_wait_for_rib'] + as_path_limit = module.params['as_path_limit'] + check_first_as = module.params['check_first_as'] + confed_id_number = module.params['confed_id_number'] + confed_nonstanded = module.params['confed_nonstanded'] + bgp_rid_auto_sel = module.params['bgp_rid_auto_sel'] + keep_all_routes = module.params['keep_all_routes'] + memory_limit = module.params['memory_limit'] + gr_peer_reset = module.params['gr_peer_reset'] + is_shutdown = module.params['is_shutdown'] + suppress_interval = module.params['suppress_interval'] + hold_interval = module.params['hold_interval'] + clear_interval = module.params['clear_interval'] + confed_peer_as_num = module.params['confed_peer_as_num'] + router_id = module.params['router_id'] + vrf_name = module.params['vrf_name'] + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + keepalive_time = module.params['keepalive_time'] + hold_time = module.params['hold_time'] + min_hold_time = module.params['min_hold_time'] + conn_retry_time = module.params['conn_retry_time'] + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + default_af_type = module.params['default_af_type'] + + ce_bgp_obj = Bgp() + + if not ce_bgp_obj: + module.fail_json(msg='Error: Init module failed.') + + # get proposed + proposed["state"] = state + if as_number: + proposed["as_number"] = as_number + if graceful_restart != 'no_use': + proposed["graceful_restart"] = graceful_restart + if time_wait_for_rib: + proposed["time_wait_for_rib"] = time_wait_for_rib + if as_path_limit: + proposed["as_path_limit"] = as_path_limit + if check_first_as != 'no_use': + proposed["check_first_as"] = check_first_as + if confed_id_number: + proposed["confed_id_number"] = confed_id_number + if confed_nonstanded != 'no_use': + proposed["confed_nonstanded"] = confed_nonstanded + if bgp_rid_auto_sel != 'no_use': + proposed["bgp_rid_auto_sel"] = bgp_rid_auto_sel + if keep_all_routes != 'no_use': + proposed["keep_all_routes"] = keep_all_routes + if memory_limit != 'no_use': + proposed["memory_limit"] = memory_limit + if gr_peer_reset != 'no_use': + proposed["gr_peer_reset"] = gr_peer_reset + if is_shutdown != 'no_use': + proposed["is_shutdown"] = is_shutdown + if suppress_interval: + proposed["suppress_interval"] = suppress_interval + if hold_interval: + proposed["hold_interval"] = hold_interval + if clear_interval: + proposed["clear_interval"] = clear_interval + if confed_peer_as_num: + proposed["confed_peer_as_num"] = confed_peer_as_num + if router_id: + proposed["router_id"] = router_id + if vrf_name: + proposed["vrf_name"] = vrf_name + if vrf_rid_auto_sel != 'no_use': + proposed["vrf_rid_auto_sel"] = vrf_rid_auto_sel + if keepalive_time: + proposed["keepalive_time"] = keepalive_time + if hold_time: + proposed["hold_time"] = hold_time + if min_hold_time: + proposed["min_hold_time"] = min_hold_time + if conn_retry_time: + proposed["conn_retry_time"] = conn_retry_time + if ebgp_if_sensitive != 'no_use': + proposed["ebgp_if_sensitive"] = ebgp_if_sensitive + if default_af_type: + proposed["default_af_type"] = default_af_type + + need_bgp_enable = check_bgp_enable_args(module=module) + need_bgp_enable_other_rst = ce_bgp_obj.check_bgp_enable_other_args( + module=module) + need_bgp_confed = check_bgp_confed_args(module=module) + need_bgp_instance = ce_bgp_obj.check_bgp_instance_args(module=module) + need_bgp_instance_other_rst = ce_bgp_obj.check_bgp_instance_other_args( + module=module) + + router_id_exist = ce_bgp_obj.get_bgp_instance(module=module) + existing["bgp instance"] = router_id_exist + + # bgp enable/disable + if need_bgp_enable: + + bgp_enable_exist = ce_bgp_obj.get_bgp_enable(module=module) + existing["bgp enable"] = bgp_enable_exist + if bgp_enable_exist: + asnumber_exist = bgp_enable_exist[0][1] + bgpenable_exist = bgp_enable_exist[0][0] + else: + asnumber_exist = None + bgpenable_exist = None + + if state == "present": + bgp_enable_new = ("true", as_number) + + if bgp_enable_new in bgp_enable_exist: + pass + elif bgpenable_exist == "true" and asnumber_exist != as_number: + module.fail_json( + msg='Error: BGP is already running. The AS is %s.' % asnumber_exist) + else: + cmd = ce_bgp_obj.merge_bgp_enable(module=module) + changed = True + for item in cmd: + updates.append(item) + + else: + if need_bgp_enable_other_rst["need_cfg"] or need_bgp_confed or \ + need_bgp_instance_other_rst["need_cfg"] or need_bgp_instance: + pass + elif bgpenable_exist == "false": + pass + elif bgpenable_exist == "true" and asnumber_exist == as_number: + cmd = ce_bgp_obj.merge_bgp_enable(module=module) + changed = True + for item in cmd: + updates.append(item) + + else: + module.fail_json( + msg='Error: BGP is already running. The AS is %s.' % asnumber_exist) + + bgp_enable_end = ce_bgp_obj.get_bgp_enable(module=module) + end_state["bgp enable"] = bgp_enable_end + + # bgp enable/disable other args + exist_tmp = dict() + for item in need_bgp_enable_other_rst: + if item != "need_cfg": + exist_tmp[item] = need_bgp_enable_other_rst[item] + + if exist_tmp: + existing["bgp enable other"] = exist_tmp + + if need_bgp_enable_other_rst["need_cfg"]: + if state == "present": + cmd = ce_bgp_obj.merge_bgp_enable_other(module=module) + changed = True + for item in cmd: + updates.append(item) + else: + cmd = ce_bgp_obj.delete_bgp_enable_other(module=module) + changed = True + for item in cmd: + updates.append(item) + + need_bgp_enable_other_rst = ce_bgp_obj.check_bgp_enable_other_args( + module=module) + + end_tmp = dict() + for item in need_bgp_enable_other_rst: + if item != "need_cfg": + end_tmp[item] = need_bgp_enable_other_rst[item] + + if end_tmp: + end_state["bgp enable other"] = end_tmp + + # bgp confederation peer as + if need_bgp_confed: + confed_exist = ce_bgp_obj.get_bgp_confed_peer_as(module=module) + existing["confederation peer as"] = confed_exist + confed_new = (confed_peer_as_num) + + if state == "present": + if len(confed_exist) == 0: + cmd = ce_bgp_obj.create_bgp_confed_peer_as(module=module) + changed = True + for item in cmd: + updates.append(item) + + elif confed_new not in confed_exist: + cmd = ce_bgp_obj.merge_bgp_confed_peer_as(module=module) + changed = True + for item in cmd: + updates.append(item) + + else: + if len(confed_exist) == 0: + pass + + elif confed_new not in confed_exist: + pass + + else: + cmd = ce_bgp_obj.delete_bgp_confed_peer_as(module=module) + changed = True + for item in cmd: + updates.append(item) + + confed_end = ce_bgp_obj.get_bgp_confed_peer_as(module=module) + end_state["confederation peer as"] = confed_end + + # bgp instance + if need_bgp_instance and default_af_type != "ipv6uni": + router_id_new = vrf_name + + if state == "present": + if len(router_id_exist) == 0: + cmd = ce_bgp_obj.create_bgp_instance(module=module) + changed = True + updates.extend(cmd) + elif router_id_new not in router_id_exist: + cmd = ce_bgp_obj.merge_bgp_instance(module=module) + changed = True + updates.extend(cmd) + else: + if not need_bgp_instance_other_rst["need_cfg"]: + if vrf_name != "_public_": + if len(router_id_exist) == 0: + pass + elif router_id_new not in router_id_exist: + pass + else: + cmd = ce_bgp_obj.delete_bgp_instance(module=module) + changed = True + for item in cmd: + updates.append(item) + + # bgp instance other + exist_tmp = dict() + for item in need_bgp_instance_other_rst: + if item != "need_cfg": + exist_tmp[item] = need_bgp_instance_other_rst[item] + + if exist_tmp: + existing["bgp instance other"] = exist_tmp + + if need_bgp_instance_other_rst["need_cfg"]: + if state == "present": + cmd = ce_bgp_obj.merge_bgp_instance_other(module=module) + changed = True + for item in cmd: + updates.append(item) + + else: + if vrf_name == "_public_": + cmd = ce_bgp_obj.delete_instance_other_public( + module=module) + changed = True + for item in cmd: + updates.append(item) + else: + cmd = ce_bgp_obj.delete_bgp_instance_other_comm(module=module) + changed = True + for item in cmd: + updates.append(item) + + need_bgp_instance_other_rst = ce_bgp_obj.check_bgp_instance_other_args( + module=module) + + router_id_end = ce_bgp_obj.get_bgp_instance(module=module) + end_state["bgp instance"] = router_id_end + + end_tmp = dict() + for item in need_bgp_instance_other_rst: + if item != "need_cfg": + end_tmp[item] = need_bgp_instance_other_rst[item] + + if end_tmp: + end_state["bgp instance other"] = end_tmp + if end_state == existing: + changed = False + updates = list() + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_bgp_af.py b/plugins/modules/ce_bgp_af.py new file mode 100644 index 0000000..be78874 --- /dev/null +++ b/plugins/modules/ce_bgp_af.py @@ -0,0 +1,3431 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_bgp_af +short_description: Manages BGP Address-family configuration on HUAWEI CloudEngine switches. +description: + - Manages BGP Address-family configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] + vrf_name: + description: + - Name of a BGP instance. The name is a case-sensitive string of characters. + The BGP instance can be used only after the corresponding VPN instance is created. + The value is a string of 1 to 31 case-sensitive characters. + required: true + af_type: + description: + - Address family type of a BGP instance. + required: true + choices: ['ipv4uni','ipv4multi', 'ipv4vpn', 'ipv6uni', 'ipv6vpn', 'evpn'] + max_load_ibgp_num: + description: + - Specify the maximum number of equal-cost IBGP routes. + The value is an integer ranging from 1 to 65535. + ibgp_ecmp_nexthop_changed: + description: + - If the value is true, the next hop of an advertised route is changed to the advertiser itself in IBGP + load-balancing scenarios. + If the value is false, the next hop of an advertised route is not changed to the advertiser itself in + IBGP load-balancing scenarios. + choices: ['no_use','true','false'] + default: no_use + max_load_ebgp_num: + description: + - Specify the maximum number of equal-cost EBGP routes. + The value is an integer ranging from 1 to 65535. + ebgp_ecmp_nexthop_changed: + description: + - If the value is true, the next hop of an advertised route is changed to the advertiser itself in EBGP + load-balancing scenarios. + If the value is false, the next hop of an advertised route is not changed to the advertiser itself in + EBGP load-balancing scenarios. + choices: ['no_use','true','false'] + default: no_use + maximum_load_balance: + description: + - Specify the maximum number of equal-cost routes in the BGP routing table. + The value is an integer ranging from 1 to 65535. + ecmp_nexthop_changed: + description: + - If the value is true, the next hop of an advertised route is changed to the advertiser itself in BGP + load-balancing scenarios. + If the value is false, the next hop of an advertised route is not changed to the advertiser itself + in BGP load-balancing scenarios. + choices: ['no_use','true','false'] + default: no_use + default_local_pref: + description: + - Set the Local-Preference attribute. The value is an integer. + The value is an integer ranging from 0 to 4294967295. + default_med: + description: + - Specify the Multi-Exit-Discriminator (MED) of BGP routes. + The value is an integer ranging from 0 to 4294967295. + default_rt_import_enable: + description: + - If the value is true, importing default routes to the BGP routing table is allowed. + If the value is false, importing default routes to the BGP routing table is not allowed. + choices: ['no_use','true','false'] + default: no_use + router_id: + description: + - ID of a router that is in IPv4 address format. + The value is a string of 0 to 255 characters. + The value is in dotted decimal notation. + vrf_rid_auto_sel: + description: + - If the value is true, VPN BGP instances are enabled to automatically select router IDs. + If the value is false, VPN BGP instances are disabled from automatically selecting router IDs. + choices: ['no_use','true','false'] + default: no_use + nexthop_third_party: + description: + - If the value is true, the third-party next hop function is enabled. + If the value is false, the third-party next hop function is disabled. + choices: ['no_use','true','false'] + default: no_use + summary_automatic: + description: + - If the value is true, automatic aggregation is enabled for locally imported routes. + If the value is false, automatic aggregation is disabled for locally imported routes. + choices: ['no_use','true','false'] + default: no_use + auto_frr_enable: + description: + - If the value is true, BGP auto FRR is enabled. + If the value is false, BGP auto FRR is disabled. + choices: ['no_use','true','false'] + default: no_use + load_balancing_as_path_ignore: + description: + - Load balancing as path ignore. + choices: ['no_use','true','false'] + default: no_use + rib_only_enable: + description: + - If the value is true, BGP routes cannot be advertised to the IP routing table. + If the value is false, Routes preferred by BGP are advertised to the IP routing table. + choices: ['no_use','true','false'] + default: no_use + rib_only_policy_name: + description: + - Specify the name of a routing policy. + The value is a string of 1 to 40 characters. + active_route_advertise: + description: + - If the value is true, BGP is enabled to advertise only optimal routes in the RM to peers. + If the value is false, BGP is not enabled to advertise only optimal routes in the RM to peers. + choices: ['no_use','true','false'] + default: no_use + as_path_neglect: + description: + - If the value is true, the AS path attribute is ignored when BGP selects an optimal route. + If the value is false, the AS path attribute is not ignored when BGP selects an optimal route. + An AS path with a smaller length has a higher priority. + choices: ['no_use','true','false'] + default: no_use + med_none_as_maximum: + description: + - If the value is true, when BGP selects an optimal route, the system uses 4294967295 as the + MED value of a route if the route's attribute does not carry a MED value. + If the value is false, the system uses 0 as the MED value of a route if the route's attribute + does not carry a MED value. + choices: ['no_use','true','false'] + default: no_use + router_id_neglect: + description: + - If the value is true, the router ID attribute is ignored when BGP selects the optimal route. + If the value is false, the router ID attribute is not ignored when BGP selects the optimal route. + choices: ['no_use','true','false'] + default: no_use + igp_metric_ignore: + description: + - If the value is true, the metrics of next-hop IGP routes are not compared when BGP selects + an optimal route. + If the value is false, the metrics of next-hop IGP routes are not compared when BGP selects + an optimal route. + A route with a smaller metric has a higher priority. + choices: ['no_use','true','false'] + default: no_use + always_compare_med: + description: + - If the value is true, the MEDs of routes learned from peers in different autonomous systems + are compared when BGP selects an optimal route. + If the value is false, the MEDs of routes learned from peers in different autonomous systems + are not compared when BGP selects an optimal route. + choices: ['no_use','true','false'] + default: no_use + determin_med: + description: + - If the value is true, BGP deterministic-MED is enabled. + If the value is false, BGP deterministic-MED is disabled. + choices: ['no_use','true','false'] + default: no_use + preference_external: + description: + - Set the protocol priority of EBGP routes. + The value is an integer ranging from 1 to 255. + preference_internal: + description: + - Set the protocol priority of IBGP routes. + The value is an integer ranging from 1 to 255. + preference_local: + description: + - Set the protocol priority of a local BGP route. + The value is an integer ranging from 1 to 255. + prefrence_policy_name: + description: + - Set a routing policy to filter routes so that a configured priority is applied to + the routes that match the specified policy. + The value is a string of 1 to 40 characters. + reflect_between_client: + description: + - If the value is true, route reflection is enabled between clients. + If the value is false, route reflection is disabled between clients. + choices: ['no_use','true','false'] + default: no_use + reflector_cluster_id: + description: + - Set a cluster ID. Configuring multiple RRs in a cluster can enhance the stability of the network. + The value is an integer ranging from 1 to 4294967295. + reflector_cluster_ipv4: + description: + - Set a cluster ipv4 address. The value is expressed in the format of an IPv4 address. + rr_filter_number: + description: + - Set the number of the extended community filter supported by an RR group. + The value is a string of 1 to 51 characters. + policy_vpn_target: + description: + - If the value is true, VPN-Target filtering function is performed for received VPN routes. + If the value is false, VPN-Target filtering function is not performed for received VPN routes. + choices: ['no_use','true','false'] + default: no_use + next_hop_sel_depend_type: + description: + - Next hop select depend type. + choices: ['default','dependTunnel', 'dependIp'] + default: default + nhp_relay_route_policy_name: + description: + - Specify the name of a route-policy for route iteration. + The value is a string of 1 to 40 characters. + ebgp_if_sensitive: + description: + - If the value is true, after the fast EBGP interface awareness function is enabled, + EBGP sessions on an interface are deleted immediately when the interface goes Down. + If the value is false, after the fast EBGP interface awareness function is enabled, + EBGP sessions on an interface are not deleted immediately when the interface goes Down. + choices: ['no_use','true','false'] + default: no_use + reflect_chg_path: + description: + - If the value is true, the route reflector is enabled to modify route path attributes + based on an export policy. + If the value is false, the route reflector is disabled from modifying route path attributes + based on an export policy. + choices: ['no_use','true','false'] + default: no_use + add_path_sel_num: + description: + - Number of Add-Path routes. + The value is an integer ranging from 2 to 64. + route_sel_delay: + description: + - Route selection delay. + The value is an integer ranging from 0 to 3600. + allow_invalid_as: + description: + - Allow routes with BGP origin AS validation result Invalid to be selected. + If the value is true, invalid routes can participate in route selection. + If the value is false, invalid routes cannot participate in route selection. + choices: ['no_use','true','false'] + default: no_use + policy_ext_comm_enable: + description: + - If the value is true, modifying extended community attributes is allowed. + If the value is false, modifying extended community attributes is not allowed. + choices: ['no_use','true','false'] + default: no_use + supernet_uni_adv: + description: + - If the value is true, the function to advertise supernetwork unicast routes is enabled. + If the value is false, the function to advertise supernetwork unicast routes is disabled. + choices: ['no_use','true','false'] + default: no_use + supernet_label_adv: + description: + - If the value is true, the function to advertise supernetwork label is enabled. + If the value is false, the function to advertise supernetwork label is disabled. + choices: ['no_use','true','false'] + default: no_use + ingress_lsp_policy_name: + description: + - Ingress lsp policy name. + originator_prior: + description: + - Originator prior. + choices: ['no_use','true','false'] + default: no_use + lowest_priority: + description: + - If the value is true, enable reduce priority to advertise route. + If the value is false, disable reduce priority to advertise route. + choices: ['no_use','true','false'] + default: no_use + relay_delay_enable: + description: + - If the value is true, relay delay enable. + If the value is false, relay delay disable. + choices: ['no_use','true','false'] + default: no_use + import_protocol: + description: + - Routing protocol from which routes can be imported. + choices: ['direct', 'ospf', 'isis', 'static', 'rip', 'ospfv3', 'ripng'] + import_process_id: + description: + - Process ID of an imported routing protocol. + The value is an integer ranging from 0 to 4294967295. + network_address: + description: + - Specify the IP address advertised by BGP. + The value is a string of 0 to 255 characters. + mask_len: + description: + - Specify the mask length of an IP address. + The value is an integer ranging from 0 to 128. +''' + +EXAMPLES = ''' +- name: CloudEngine BGP address family test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + tasks: + - name: "Config BGP Address_Family" + ce_bgp_af: + state: present + vrf_name: js + af_type: ipv4uni + provider: "{{ cli }}" + - name: "Undo BGP Address_Family" + ce_bgp_af: + state: absent + vrf_name: js + af_type: ipv4uni + provider: "{{ cli }}" + - name: "Config import route" + ce_bgp_af: + state: present + vrf_name: js + af_type: ipv4uni + import_protocol: ospf + import_process_id: 123 + provider: "{{ cli }}" + - name: "Undo import route" + ce_bgp_af: + state: absent + vrf_name: js + af_type: ipv4uni + import_protocol: ospf + import_process_id: 123 + provider: "{{ cli }}" + - name: "Config network route" + ce_bgp_af: + state: present + vrf_name: js + af_type: ipv4uni + network_address: 1.1.1.1 + mask_len: 24 + provider: "{{ cli }}" + - name: "Undo network route" + ce_bgp_af: + state: absent + vrf_name: js + af_type: ipv4uni + network_address: 1.1.1.1 + mask_len: 24 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"af_type": "ipv4uni", + "state": "present", "vrf_name": "js"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"af_type": "ipv4uni", "vrf_name": "js"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["ipv4-family vpn-instance js"] +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + +# get bgp address family +CE_GET_BGP_ADDRESS_FAMILY_HEADER = """ + + + + + + %s + + + %s +""" +CE_GET_BGP_ADDRESS_FAMILY_TAIL = """ + + + + + + + +""" + +# merge bgp address family +CE_MERGE_BGP_ADDRESS_FAMILY_HEADER = """ + + + + + + %s + + + %s +""" +CE_MERGE_BGP_ADDRESS_FAMILY_TAIL = """ + + + + + + + +""" + +# create bgp address family +CE_CREATE_BGP_ADDRESS_FAMILY_HEADER = """ + + + + + + %s + + + %s +""" +CE_CREATE_BGP_ADDRESS_FAMILY_TAIL = """ + + + + + + + +""" + +# delete bgp address family +CE_DELETE_BGP_ADDRESS_FAMILY_HEADER = """ + + + + + + %s + + + %s +""" +CE_DELETE_BGP_ADDRESS_FAMILY_TAIL = """ + + + + + + + +""" + +# get bgp import route +CE_GET_BGP_IMPORT_AND_NETWORK_ROUTE = """ + + + + + + %s + + + %s + + + + + + + + + + + + + + + + + + + +""" + +# merge bgp import route +CE_MERGE_BGP_IMPORT_ROUTE_HEADER = """ + + + + + + %s + + + %s + + + %s + %s +""" +CE_MERGE_BGP_IMPORT_ROUTE_TAIL = """ + + + + + + + + + +""" + +# create bgp import route +CE_CREATE_BGP_IMPORT_ROUTE = """ + + + + + + %s + + + %s + + + %s + %s + + + + + + + + + +""" + +# delete bgp import route +CE_DELETE_BGP_IMPORT_ROUTE = """ + + + + + + %s + + + %s + + + %s + %s + + + + + + + + + +""" + +# get bgp network route +CE_GET_BGP_NETWORK_ROUTE_HEADER = """ + + + + + + %s + + + %s + + + + +""" +CE_GET_BGP_NETWORK_ROUTE_TAIL = """ + + + + + + + + + +""" + +# merge bgp network route +CE_MERGE_BGP_NETWORK_ROUTE_HEADER = """ + + + + + + %s + + + %s + + + %s + %s +""" +CE_MERGE_BGP_NETWORK_ROUTE_TAIL = """ + + + + + + + + + +""" + +# create bgp network route +CE_CREATE_BGP_NETWORK_ROUTE = """ + + + + + + %s + + + %s + + + %s + %s + + + + + + + + + +""" + +# delete bgp network route +CE_DELETE_BGP_NETWORK_ROUTE = """ + + + + + + %s + + + %s + + + %s + %s + + + + + + + + + +""" + +# bgp import and network route header +CE_BGP_IMPORT_NETWORK_ROUTE_HEADER = """ + + + + + + %s + + + %s +""" +CE_BGP_IMPORT_NETWORK_ROUTE_TAIL = """ + + + + + + + +""" +CE_BGP_MERGE_IMPORT_UNIT = """ + + + %s + %s + + +""" +CE_BGP_CREATE_IMPORT_UNIT = """ + + + %s + %s + + +""" +CE_BGP_DELETE_IMPORT_UNIT = """ + + + %s + %s + + +""" +CE_BGP_MERGE_NETWORK_UNIT = """ + + + %s + %s + + +""" +CE_BGP_CREATE_NETWORK_UNIT = """ + + + %s + %s + + +""" +CE_BGP_DELETE_NETWORK_UNIT = """ + + + %s + %s + + +""" + + +class BgpAf(object): + """ Manages BGP Address-family configuration """ + + def netconf_get_config(self, **kwargs): + """ netconf_get_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ netconf_set_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = set_nc_config(module, conf_str) + + return xml_str + + def check_bgp_af_args(self, **kwargs): + """ check_bgp_af_args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + else: + module.fail_json(msg='Error: Please input vrf_name.') + + state = module.params['state'] + af_type = module.params['af_type'] + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["af_type"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != af_type: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["af_type"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] == af_type: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def check_bgp_af_other_can_del(self, **kwargs): + """ check_bgp_af_other_can_del """ + module = kwargs["module"] + result = dict() + need_cfg = False + + state = module.params['state'] + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + + router_id = module.params['router_id'] + if router_id: + if len(router_id) > 255: + module.fail_json( + msg='Error: The len of router_id %s is out of [0 - 255].' % router_id) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] != router_id: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] == router_id: + need_cfg = True + else: + pass + + determin_med = module.params['determin_med'] + if determin_med != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] != determin_med: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] == determin_med: + need_cfg = True + else: + pass + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] != ebgp_if_sensitive: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] == ebgp_if_sensitive: + need_cfg = True + else: + pass + + relay_delay_enable = module.params['relay_delay_enable'] + if relay_delay_enable != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] != relay_delay_enable: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + if re_find[0] == relay_delay_enable: + need_cfg = True + else: + pass + + result["need_cfg"] = need_cfg + return result + + def check_bgp_af_other_args(self, **kwargs): + """ check_bgp_af_other_args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + + max_load_ibgp_num = module.params['max_load_ibgp_num'] + if max_load_ibgp_num: + if int(max_load_ibgp_num) > 65535 or int(max_load_ibgp_num) < 1: + module.fail_json( + msg='Error: The value of max_load_ibgp_num %s is out of [1 - 65535].' % max_load_ibgp_num) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["max_load_ibgp_num"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != max_load_ibgp_num: + need_cfg = True + else: + need_cfg = True + + ibgp_ecmp_nexthop_changed = module.params['ibgp_ecmp_nexthop_changed'] + if ibgp_ecmp_nexthop_changed != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ibgp_ecmp_nexthop_changed"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != ibgp_ecmp_nexthop_changed: + need_cfg = True + else: + need_cfg = True + + max_load_ebgp_num = module.params['max_load_ebgp_num'] + if max_load_ebgp_num: + if int(max_load_ebgp_num) > 65535 or int(max_load_ebgp_num) < 1: + module.fail_json( + msg='Error: The value of max_load_ebgp_num %s is out of [1 - 65535].' % max_load_ebgp_num) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["max_load_ebgp_num"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != max_load_ebgp_num: + need_cfg = True + else: + need_cfg = True + + ebgp_ecmp_nexthop_changed = module.params['ebgp_ecmp_nexthop_changed'] + if ebgp_ecmp_nexthop_changed != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ebgp_ecmp_nexthop_changed"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != ebgp_ecmp_nexthop_changed: + need_cfg = True + else: + need_cfg = True + + maximum_load_balance = module.params['maximum_load_balance'] + if maximum_load_balance: + if int(maximum_load_balance) > 65535 or int(maximum_load_balance) < 1: + module.fail_json( + msg='Error: The value of maximum_load_balance %s is out of [1 - 65535].' % maximum_load_balance) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["maximum_load_balance"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != maximum_load_balance: + need_cfg = True + else: + need_cfg = True + + ecmp_nexthop_changed = module.params['ecmp_nexthop_changed'] + if ecmp_nexthop_changed != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ecmp_nexthop_changed"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != ecmp_nexthop_changed: + need_cfg = True + else: + need_cfg = True + + default_local_pref = module.params['default_local_pref'] + if default_local_pref: + if int(default_local_pref) < 0: + module.fail_json( + msg='Error: The value of default_local_pref %s is out of [0 - 4294967295].' % default_local_pref) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_local_pref"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != default_local_pref: + need_cfg = True + else: + need_cfg = True + + default_med = module.params['default_med'] + if default_med: + if int(default_med) < 0: + module.fail_json( + msg='Error: The value of default_med %s is out of [0 - 4294967295].' % default_med) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_med"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != default_med: + need_cfg = True + else: + need_cfg = True + + default_rt_import_enable = module.params['default_rt_import_enable'] + if default_rt_import_enable != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_rt_import_enable"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != default_rt_import_enable: + need_cfg = True + else: + need_cfg = True + + router_id = module.params['router_id'] + if router_id: + if len(router_id) > 255: + module.fail_json( + msg='Error: The len of router_id %s is out of [0 - 255].' % router_id) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["router_id"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != router_id: + need_cfg = True + else: + need_cfg = True + + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + if vrf_rid_auto_sel != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["vrf_rid_auto_sel"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != vrf_rid_auto_sel: + need_cfg = True + else: + need_cfg = True + + nexthop_third_party = module.params['nexthop_third_party'] + if nexthop_third_party != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["nexthop_third_party"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != nexthop_third_party: + need_cfg = True + else: + need_cfg = True + + summary_automatic = module.params['summary_automatic'] + if summary_automatic != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["summary_automatic"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != summary_automatic: + need_cfg = True + else: + need_cfg = True + + auto_frr_enable = module.params['auto_frr_enable'] + if auto_frr_enable != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["auto_frr_enable"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != auto_frr_enable: + need_cfg = True + else: + need_cfg = True + + load_balancing_as_path_ignore = module.params['load_balancing_as_path_ignore'] + if load_balancing_as_path_ignore != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + \ + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["load_balancing_as_path_ignore"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != load_balancing_as_path_ignore: + need_cfg = True + else: + need_cfg = True + + rib_only_enable = module.params['rib_only_enable'] + if rib_only_enable != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["rib_only_enable"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != rib_only_enable: + need_cfg = True + else: + need_cfg = True + + rib_only_policy_name = module.params['rib_only_policy_name'] + if rib_only_policy_name: + if len(rib_only_policy_name) > 40 or len(rib_only_policy_name) < 1: + module.fail_json( + msg='Error: The len of rib_only_policy_name %s is out of [1 - 40].' % rib_only_policy_name) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["rib_only_policy_name"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != rib_only_policy_name: + need_cfg = True + else: + need_cfg = True + + active_route_advertise = module.params['active_route_advertise'] + if active_route_advertise != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["active_route_advertise"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != active_route_advertise: + need_cfg = True + else: + need_cfg = True + + as_path_neglect = module.params['as_path_neglect'] + if as_path_neglect != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["as_path_neglect"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != as_path_neglect: + need_cfg = True + else: + need_cfg = True + + med_none_as_maximum = module.params['med_none_as_maximum'] + if med_none_as_maximum != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["med_none_as_maximum"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != med_none_as_maximum: + need_cfg = True + else: + need_cfg = True + + router_id_neglect = module.params['router_id_neglect'] + if router_id_neglect != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["router_id_neglect"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != router_id_neglect: + need_cfg = True + else: + need_cfg = True + + igp_metric_ignore = module.params['igp_metric_ignore'] + if igp_metric_ignore != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["igp_metric_ignore"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != igp_metric_ignore: + need_cfg = True + else: + need_cfg = True + + always_compare_med = module.params['always_compare_med'] + if always_compare_med != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["always_compare_med"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != always_compare_med: + need_cfg = True + else: + need_cfg = True + + determin_med = module.params['determin_med'] + if determin_med != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["determin_med"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != determin_med: + need_cfg = True + else: + need_cfg = True + + preference_external = module.params['preference_external'] + if preference_external: + if int(preference_external) > 255 or int(preference_external) < 1: + module.fail_json( + msg='Error: The value of preference_external %s is out of [1 - 255].' % preference_external) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["preference_external"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != preference_external: + need_cfg = True + else: + need_cfg = True + + preference_internal = module.params['preference_internal'] + if preference_internal: + if int(preference_internal) > 255 or int(preference_internal) < 1: + module.fail_json( + msg='Error: The value of preference_internal %s is out of [1 - 255].' % preference_internal) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["preference_internal"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != preference_internal: + need_cfg = True + else: + need_cfg = True + + preference_local = module.params['preference_local'] + if preference_local: + if int(preference_local) > 255 or int(preference_local) < 1: + module.fail_json( + msg='Error: The value of preference_local %s is out of [1 - 255].' % preference_local) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["preference_local"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != preference_local: + need_cfg = True + else: + need_cfg = True + + prefrence_policy_name = module.params['prefrence_policy_name'] + if prefrence_policy_name: + if len(prefrence_policy_name) > 40 or len(prefrence_policy_name) < 1: + module.fail_json( + msg='Error: The len of prefrence_policy_name %s is out of [1 - 40].' % prefrence_policy_name) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["prefrence_policy_name"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != prefrence_policy_name: + need_cfg = True + else: + need_cfg = True + + reflect_between_client = module.params['reflect_between_client'] + if reflect_between_client != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["reflect_between_client"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != reflect_between_client: + need_cfg = True + else: + need_cfg = True + + reflector_cluster_id = module.params['reflector_cluster_id'] + if reflector_cluster_id: + if int(reflector_cluster_id) < 0: + module.fail_json( + msg='Error: The value of reflector_cluster_id %s is out of ' + '[1 - 4294967295].' % reflector_cluster_id) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["reflector_cluster_id"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != reflector_cluster_id: + need_cfg = True + else: + need_cfg = True + + reflector_cluster_ipv4 = module.params['reflector_cluster_ipv4'] + if reflector_cluster_ipv4: + if len(reflector_cluster_ipv4) > 255: + module.fail_json( + msg='Error: The len of reflector_cluster_ipv4 %s is out of [0 - 255].' % reflector_cluster_ipv4) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["reflector_cluster_ipv4"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != reflector_cluster_ipv4: + need_cfg = True + else: + need_cfg = True + + rr_filter_number = module.params['rr_filter_number'] + if rr_filter_number: + if len(rr_filter_number) > 51 or len(rr_filter_number) < 1: + module.fail_json( + msg='Error: The len of rr_filter_number %s is out of [1 - 51].' % rr_filter_number) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["rr_filter_number"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != rr_filter_number: + need_cfg = True + else: + need_cfg = True + + policy_vpn_target = module.params['policy_vpn_target'] + if policy_vpn_target != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["policy_vpn_target"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != policy_vpn_target: + need_cfg = True + else: + need_cfg = True + + next_hop_sel_depend_type = module.params['next_hop_sel_depend_type'] + if next_hop_sel_depend_type: + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["next_hop_sel_depend_type"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != next_hop_sel_depend_type: + need_cfg = True + else: + need_cfg = True + + nhp_relay_route_policy_name = module.params[ + 'nhp_relay_route_policy_name'] + if nhp_relay_route_policy_name: + if len(nhp_relay_route_policy_name) > 40 or len(nhp_relay_route_policy_name) < 1: + module.fail_json( + msg='Error: The len of nhp_relay_route_policy_name %s is ' + 'out of [1 - 40].' % nhp_relay_route_policy_name) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + \ + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["nhp_relay_route_policy_name"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != nhp_relay_route_policy_name: + need_cfg = True + else: + need_cfg = True + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ebgp_if_sensitive"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != ebgp_if_sensitive: + need_cfg = True + else: + need_cfg = True + + reflect_chg_path = module.params['reflect_chg_path'] + if reflect_chg_path != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["reflect_chg_path"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != reflect_chg_path: + need_cfg = True + else: + need_cfg = True + + add_path_sel_num = module.params['add_path_sel_num'] + if add_path_sel_num: + if int(add_path_sel_num) > 64 or int(add_path_sel_num) < 2: + module.fail_json( + msg='Error: The value of add_path_sel_num %s is out of [2 - 64].' % add_path_sel_num) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["add_path_sel_num"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != add_path_sel_num: + need_cfg = True + else: + need_cfg = True + + route_sel_delay = module.params['route_sel_delay'] + if route_sel_delay: + if int(route_sel_delay) > 3600 or int(route_sel_delay) < 0: + module.fail_json( + msg='Error: The value of route_sel_delay %s is out of [0 - 3600].' % route_sel_delay) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["route_sel_delay"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != route_sel_delay: + need_cfg = True + else: + need_cfg = True + + allow_invalid_as = module.params['allow_invalid_as'] + if allow_invalid_as != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["allow_invalid_as"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != allow_invalid_as: + need_cfg = True + else: + need_cfg = True + + policy_ext_comm_enable = module.params['policy_ext_comm_enable'] + if policy_ext_comm_enable != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["policy_ext_comm_enable"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != policy_ext_comm_enable: + need_cfg = True + else: + need_cfg = True + + supernet_uni_adv = module.params['supernet_uni_adv'] + if supernet_uni_adv != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["supernet_uni_adv"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != supernet_uni_adv: + need_cfg = True + else: + need_cfg = True + + supernet_label_adv = module.params['supernet_label_adv'] + if supernet_label_adv != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["supernet_label_adv"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != supernet_label_adv: + need_cfg = True + else: + need_cfg = True + + ingress_lsp_policy_name = module.params['ingress_lsp_policy_name'] + if ingress_lsp_policy_name: + if len(ingress_lsp_policy_name) > 40 or len(ingress_lsp_policy_name) < 1: + module.fail_json( + msg='Error: The len of ingress_lsp_policy_name %s is out of [1 - 40].' % ingress_lsp_policy_name) + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ingress_lsp_policy_name"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != ingress_lsp_policy_name: + need_cfg = True + else: + need_cfg = True + + originator_prior = module.params['originator_prior'] + if originator_prior != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["originator_prior"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != originator_prior: + need_cfg = True + else: + need_cfg = True + + lowest_priority = module.params['lowest_priority'] + if lowest_priority != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["lowest_priority"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != lowest_priority: + need_cfg = True + else: + need_cfg = True + + relay_delay_enable = module.params['relay_delay_enable'] + if relay_delay_enable != 'no_use': + + conf_str = CE_GET_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + \ + "" + CE_GET_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["relay_delay_enable"] = re_find + result["vrf_name"] = vrf_name + if re_find[0] != relay_delay_enable: + need_cfg = True + else: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def check_bgp_import_network_route(self, **kwargs): + """ check_bgp_import_network_route """ + + module = kwargs["module"] + result = dict() + import_need_cfg = False + network_need_cfg = False + + vrf_name = module.params['vrf_name'] + + state = module.params['state'] + af_type = module.params['af_type'] + import_protocol = module.params['import_protocol'] + import_process_id = module.params['import_process_id'] + + if import_protocol and (import_protocol != "direct" and import_protocol != "static"): + if not import_process_id: + module.fail_json( + msg='Error: Please input import_protocol and import_process_id value at the same time.') + else: + if int(import_process_id) < 0: + module.fail_json( + msg='Error: The value of import_process_id %s is out of [0 - 4294967295].' % import_process_id) + + if import_process_id: + if not import_protocol: + module.fail_json( + msg='Error: Please input import_protocol and import_process_id value at the same time.') + + network_address = module.params['network_address'] + mask_len = module.params['mask_len'] + + if network_address: + if not mask_len: + module.fail_json( + msg='Error: Please input network_address and mask_len value at the same time.') + if mask_len: + if not network_address: + module.fail_json( + msg='Error: Please input network_address and mask_len value at the same time.') + + conf_str = CE_GET_BGP_IMPORT_AND_NETWORK_ROUTE % (vrf_name, af_type) + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if import_protocol: + + if import_protocol == "direct" or import_protocol == "static": + import_process_id = "0" + else: + if not import_process_id or import_process_id == "0": + module.fail_json( + msg='Error: Please input import_process_id not 0 when import_protocol is ' + '[ospf, isis, rip, ospfv3, ripng].') + + bgp_import_route_new = (import_protocol, import_process_id) + + if state == "present": + if "" in recv_xml: + import_need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*\s.*(.*).*', + recv_xml) + + if re_find: + result["bgp_import_route"] = re_find + result["vrf_name"] = vrf_name + if bgp_import_route_new not in re_find: + import_need_cfg = True + else: + import_need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*\s.*(.*).*', + recv_xml) + + if re_find: + result["bgp_import_route"] = re_find + result["vrf_name"] = vrf_name + if bgp_import_route_new in re_find: + import_need_cfg = True + + if network_address and mask_len: + + bgp_network_route_new = (network_address, mask_len) + + if not check_ip_addr(ipaddr=network_address): + module.fail_json( + msg='Error: The network_address %s is invalid.' % network_address) + + if len(mask_len) > 128: + module.fail_json( + msg='Error: The len of mask_len %s is out of [0 - 128].' % mask_len) + + if state == "present": + if "" in recv_xml: + network_need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*\s.*(.*).*', recv_xml) + + if re_find: + result["bgp_network_route"] = re_find + result["vrf_name"] = vrf_name + if bgp_network_route_new not in re_find: + network_need_cfg = True + else: + network_need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*\s.*(.*).*', recv_xml) + + if re_find: + result["bgp_network_route"] = re_find + result["vrf_name"] = vrf_name + if bgp_network_route_new in re_find: + network_need_cfg = True + + result["import_need_cfg"] = import_need_cfg + result["network_need_cfg"] = network_need_cfg + return result + + def merge_bgp_af(self, **kwargs): + """ merge_bgp_af """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + + conf_str = CE_MERGE_BGP_ADDRESS_FAMILY_HEADER % ( + vrf_name, af_type) + CE_MERGE_BGP_ADDRESS_FAMILY_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp address family failed.') + + cmds = [] + + cmd = "ipv4-family vpn-instance %s" % vrf_name + + if af_type == "ipv4multi": + cmd = "ipv4-family multicast" + elif af_type == "ipv4vpn": + cmd = "ipv4-family vpnv4" + elif af_type == "ipv6uni": + cmd = "ipv6-family vpn-instance %s" % vrf_name + if vrf_name == "_public_": + cmd = "ipv6-family unicast" + elif af_type == "ipv6vpn": + cmd = "ipv6-family vpnv6" + elif af_type == "evpn": + cmd = "l2vpn-family evpn" + cmds.append(cmd) + + return cmds + + def create_bgp_af(self, **kwargs): + """ create_bgp_af """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + + conf_str = CE_CREATE_BGP_ADDRESS_FAMILY_HEADER % ( + vrf_name, af_type) + CE_CREATE_BGP_ADDRESS_FAMILY_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create bgp address family failed.') + + cmds = [] + + cmd = "ipv4-family vpn-instance %s" % vrf_name + + if af_type == "ipv4multi": + cmd = "ipv4-family multicast" + elif af_type == "ipv4vpn": + cmd = "ipv4-family vpnv4" + elif af_type == "ipv6uni": + cmd = "ipv6-family vpn-instance %s" % vrf_name + if vrf_name == "_public_": + cmd = "ipv6-family unicast" + elif af_type == "ipv6vpn": + cmd = "ipv6-family vpnv6" + elif af_type == "evpn": + cmd = "l2vpn-family evpn" + cmds.append(cmd) + + return cmds + + def delete_bgp_af(self, **kwargs): + """ delete_bgp_af """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + + conf_str = CE_DELETE_BGP_ADDRESS_FAMILY_HEADER % ( + vrf_name, af_type) + CE_DELETE_BGP_ADDRESS_FAMILY_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp address family failed.') + + cmds = [] + + cmd = "undo ipv4-family vpn-instance %s" % vrf_name + + if af_type == "ipv4multi": + cmd = "undo ipv4-family multicast" + elif af_type == "ipv4vpn": + cmd = "undo ipv4-family vpnv4" + elif af_type == "ipv6uni": + cmd = "undo ipv6-family vpn-instance %s" % vrf_name + if vrf_name == "_public_": + cmd = "undo ipv6-family unicast" + elif af_type == "ipv6vpn": + cmd = "undo ipv6-family vpnv6" + elif af_type == "evpn": + cmd = "l2vpn-family evpn" + cmds.append(cmd) + + return cmds + + def merge_bgp_af_other(self, **kwargs): + """ merge_bgp_af_other """ + + module = kwargs["module"] + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + + conf_str = CE_MERGE_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + + cmds = [] + + max_load_ibgp_num = module.params['max_load_ibgp_num'] + if max_load_ibgp_num: + conf_str += "%s" % max_load_ibgp_num + + cmd = "maximum load-balancing ibgp %s" % max_load_ibgp_num + cmds.append(cmd) + + ibgp_ecmp_nexthop_changed = module.params['ibgp_ecmp_nexthop_changed'] + if ibgp_ecmp_nexthop_changed != 'no_use': + conf_str += "%s" % ibgp_ecmp_nexthop_changed + + if ibgp_ecmp_nexthop_changed == "true": + cmd = "maximum load-balancing ibgp %s ecmp-nexthop-changed" % max_load_ibgp_num + cmds.append(cmd) + else: + cmd = "undo maximum load-balancing ibgp %s ecmp-nexthop-changed" % max_load_ibgp_num + cmds.append(cmd) + max_load_ebgp_num = module.params['max_load_ebgp_num'] + if max_load_ebgp_num: + conf_str += "%s" % max_load_ebgp_num + + cmd = "maximum load-balancing ebgp %s" % max_load_ebgp_num + cmds.append(cmd) + + ebgp_ecmp_nexthop_changed = module.params['ebgp_ecmp_nexthop_changed'] + if ebgp_ecmp_nexthop_changed != 'no_use': + conf_str += "%s" % ebgp_ecmp_nexthop_changed + + if ebgp_ecmp_nexthop_changed == "true": + cmd = "maximum load-balancing ebgp %s ecmp-nexthop-changed" % max_load_ebgp_num + else: + cmd = "undo maximum load-balancing ebgp %s ecmp-nexthop-changed" % max_load_ebgp_num + cmds.append(cmd) + + maximum_load_balance = module.params['maximum_load_balance'] + if maximum_load_balance: + conf_str += "%s" % maximum_load_balance + + cmd = "maximum load-balancing %s" % maximum_load_balance + cmds.append(cmd) + + ecmp_nexthop_changed = module.params['ecmp_nexthop_changed'] + if ecmp_nexthop_changed != 'no_use': + conf_str += "%s" % ecmp_nexthop_changed + + if ecmp_nexthop_changed == "true": + cmd = "maximum load-balancing %s ecmp-nexthop-changed" % maximum_load_balance + else: + cmd = "undo maximum load-balancing %s ecmp-nexthop-changed" % maximum_load_balance + cmds.append(cmd) + + default_local_pref = module.params['default_local_pref'] + if default_local_pref: + conf_str += "%s" % default_local_pref + + cmd = "default local-preference %s" % default_local_pref + cmds.append(cmd) + + default_med = module.params['default_med'] + if default_med: + conf_str += "%s" % default_med + + cmd = "default med %s" % default_med + cmds.append(cmd) + + default_rt_import_enable = module.params['default_rt_import_enable'] + if default_rt_import_enable != 'no_use': + conf_str += "%s" % default_rt_import_enable + + if default_rt_import_enable == "true": + cmd = "default-route imported" + else: + cmd = "undo default-route imported" + cmds.append(cmd) + + router_id = module.params['router_id'] + if router_id: + conf_str += "%s" % router_id + + cmd = "router-id %s" % router_id + cmds.append(cmd) + + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + if vrf_rid_auto_sel != 'no_use': + conf_str += "%s" % vrf_rid_auto_sel + family = "ipv4-family" + if af_type == "ipv6uni": + family = "ipv6-family" + if vrf_rid_auto_sel == "true": + cmd = "%s vpn-instance %s" % (family, vrf_name) + cmds.append(cmd) + cmd = "router-id auto-select" + cmds.append(cmd) + else: + cmd = "%s vpn-instance %s" % (family, vrf_name) + cmds.append(cmd) + cmd = "undo router-id auto-select" + cmds.append(cmd) + + nexthop_third_party = module.params['nexthop_third_party'] + if nexthop_third_party != 'no_use': + conf_str += "%s" % nexthop_third_party + + if nexthop_third_party == "true": + cmd = "nexthop third-party" + else: + cmd = "undo nexthop third-party" + cmds.append(cmd) + + summary_automatic = module.params['summary_automatic'] + if summary_automatic != 'no_use': + conf_str += "%s" % summary_automatic + + if summary_automatic == "true": + cmd = "summary automatic" + else: + cmd = "undo summary automatic" + cmds.append(cmd) + + auto_frr_enable = module.params['auto_frr_enable'] + if auto_frr_enable != 'no_use': + conf_str += "%s" % auto_frr_enable + + if auto_frr_enable == "true": + cmd = "auto-frr" + else: + cmd = "undo auto-frr" + cmds.append(cmd) + + load_balancing_as_path_ignore = module.params[ + 'load_balancing_as_path_ignore'] + if load_balancing_as_path_ignore != 'no_use': + conf_str += "%s" % load_balancing_as_path_ignore + + if load_balancing_as_path_ignore == "true": + cmd = "load-balancing as-path-ignore" + else: + cmd = "undo load-balancing as-path-ignore" + cmds.append(cmd) + + rib_only_enable = module.params['rib_only_enable'] + if rib_only_enable != 'no_use': + conf_str += "%s" % rib_only_enable + + if rib_only_enable == "true": + cmd = "routing-table rib-only" + else: + cmd = "undo routing-table rib-only" + cmds.append(cmd) + + rib_only_policy_name = module.params['rib_only_policy_name'] + if rib_only_policy_name and rib_only_enable == "true": + conf_str += "%s" % rib_only_policy_name + + cmd = "routing-table rib-only route-policy %s" % rib_only_policy_name + cmds.append(cmd) + + active_route_advertise = module.params['active_route_advertise'] + if active_route_advertise != 'no_use': + conf_str += "%s" % active_route_advertise + + if active_route_advertise == "true": + cmd = "active-route-advertise" + else: + cmd = "undo active-route-advertise" + cmds.append(cmd) + + as_path_neglect = module.params['as_path_neglect'] + if as_path_neglect != 'no_use': + conf_str += "%s" % as_path_neglect + + if as_path_neglect == "true": + cmd = "bestroute as-path-ignore" + else: + cmd = "undo bestroute as-path-ignore" + cmds.append(cmd) + + med_none_as_maximum = module.params['med_none_as_maximum'] + if med_none_as_maximum != 'no_use': + conf_str += "%s" % med_none_as_maximum + + if med_none_as_maximum == "true": + cmd = "bestroute med-none-as-maximum" + else: + cmd = "undo bestroute med-none-as-maximum" + cmds.append(cmd) + + router_id_neglect = module.params['router_id_neglect'] + if router_id_neglect != 'no_use': + conf_str += "%s" % router_id_neglect + + if router_id_neglect == "true": + cmd = "bestroute router-id-ignore" + else: + cmd = "undo bestroute router-id-ignore" + cmds.append(cmd) + + igp_metric_ignore = module.params['igp_metric_ignore'] + if igp_metric_ignore != 'no_use': + conf_str += "%s" % igp_metric_ignore + + if igp_metric_ignore == "true": + cmd = "bestroute igp-metric-ignore" + cmds.append(cmd) + else: + cmd = "undo bestroute igp-metric-ignore" + cmds.append(cmd) + always_compare_med = module.params['always_compare_med'] + if always_compare_med != 'no_use': + conf_str += "%s" % always_compare_med + + if always_compare_med == "true": + cmd = "compare-different-as-med" + cmds.append(cmd) + else: + cmd = "undo compare-different-as-med" + cmds.append(cmd) + determin_med = module.params['determin_med'] + if determin_med != 'no_use': + conf_str += "%s" % determin_med + + if determin_med == "true": + cmd = "deterministic-med" + cmds.append(cmd) + else: + cmd = "undo deterministic-med" + cmds.append(cmd) + + preference_external = module.params['preference_external'] + preference_internal = module.params['preference_internal'] + preference_local = module.params['preference_local'] + if any([preference_external, preference_internal, preference_local]): + preference_external = preference_external or "255" + preference_internal = preference_internal or "255" + preference_local = preference_local or "255" + + conf_str += "%s" % preference_external + conf_str += "%s" % preference_internal + conf_str += "%s" % preference_local + + cmd = "preference %s %s %s" % ( + preference_external, preference_internal, preference_local) + cmds.append(cmd) + + prefrence_policy_name = module.params['prefrence_policy_name'] + if prefrence_policy_name: + conf_str += "%s" % prefrence_policy_name + + cmd = "preference route-policy %s" % prefrence_policy_name + cmds.append(cmd) + + reflect_between_client = module.params['reflect_between_client'] + if reflect_between_client != 'no_use': + conf_str += "%s" % reflect_between_client + + if reflect_between_client == "true": + cmd = "reflect between-clients" + else: + cmd = "undo reflect between-clients" + cmds.append(cmd) + + reflector_cluster_id = module.params['reflector_cluster_id'] + if reflector_cluster_id: + conf_str += "%s" % reflector_cluster_id + + cmd = "reflector cluster-id %s" % reflector_cluster_id + cmds.append(cmd) + + reflector_cluster_ipv4 = module.params['reflector_cluster_ipv4'] + if reflector_cluster_ipv4: + conf_str += "%s" % reflector_cluster_ipv4 + + cmd = "reflector cluster-id %s" % reflector_cluster_ipv4 + cmds.append(cmd) + + rr_filter_number = module.params['rr_filter_number'] + if rr_filter_number: + conf_str += "%s" % rr_filter_number + cmd = 'rr-filter %s' % rr_filter_number + cmds.append(cmd) + + policy_vpn_target = module.params['policy_vpn_target'] + if policy_vpn_target != 'no_use': + conf_str += "%s" % policy_vpn_target + if policy_vpn_target == 'true': + cmd = 'policy vpn-target' + else: + cmd = 'undo policy vpn-target' + cmds.append(cmd) + + next_hop_sel_depend_type = module.params['next_hop_sel_depend_type'] + if next_hop_sel_depend_type: + conf_str += "%s" % next_hop_sel_depend_type + + nhp_relay_route_policy_name = module.params[ + 'nhp_relay_route_policy_name'] + if nhp_relay_route_policy_name: + conf_str += "%s" % nhp_relay_route_policy_name + + cmd = "nexthop recursive-lookup route-policy %s" % nhp_relay_route_policy_name + cmds.append(cmd) + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + conf_str += "%s" % ebgp_if_sensitive + + if ebgp_if_sensitive == "true": + cmd = "ebgp-interface-sensitive" + else: + cmd = "undo ebgp-interface-sensitive" + cmds.append(cmd) + + reflect_chg_path = module.params['reflect_chg_path'] + if reflect_chg_path != 'no_use': + conf_str += "%s" % reflect_chg_path + + if reflect_chg_path == "true": + cmd = "reflect change-path-attribute" + else: + cmd = "undo reflect change-path-attribute" + cmds.append(cmd) + + add_path_sel_num = module.params['add_path_sel_num'] + if add_path_sel_num: + conf_str += "%s" % add_path_sel_num + + cmd = "bestroute add-path path-number %s" % add_path_sel_num + cmds.append(cmd) + + route_sel_delay = module.params['route_sel_delay'] + if route_sel_delay: + conf_str += "%s" % route_sel_delay + + cmd = "route-select delay %s" % route_sel_delay + cmds.append(cmd) + + allow_invalid_as = module.params['allow_invalid_as'] + if allow_invalid_as != 'no_use': + conf_str += "%s" % allow_invalid_as + + policy_ext_comm_enable = module.params['policy_ext_comm_enable'] + if policy_ext_comm_enable != 'no_use': + conf_str += "%s" % policy_ext_comm_enable + + if policy_ext_comm_enable == "true": + cmd = "ext-community-change enable" + else: + cmd = "undo ext-community-change enable" + cmds.append(cmd) + + supernet_uni_adv = module.params['supernet_uni_adv'] + if supernet_uni_adv != 'no_use': + conf_str += "%s" % supernet_uni_adv + + if supernet_uni_adv == "true": + cmd = "supernet unicast advertise enable" + else: + cmd = "undo supernet unicast advertise enable" + cmds.append(cmd) + + supernet_label_adv = module.params['supernet_label_adv'] + if supernet_label_adv != 'no_use': + conf_str += "%s" % supernet_label_adv + + if supernet_label_adv == "true": + cmd = "supernet label-route advertise enable" + else: + cmd = "undo supernet label-route advertise enable" + cmds.append(cmd) + + ingress_lsp_policy_name = module.params['ingress_lsp_policy_name'] + if ingress_lsp_policy_name: + conf_str += "%s" % ingress_lsp_policy_name + cmd = "ingress-lsp trigger route-policy %s" % ingress_lsp_policy_name + cmds.append(cmd) + + originator_prior = module.params['originator_prior'] + if originator_prior != 'no_use': + conf_str += "%s" % originator_prior + if originator_prior == "true": + cmd = "bestroute routerid-prior-clusterlist" + else: + cmd = "undo bestroute routerid-prior-clusterlist" + cmds.append(cmd) + + lowest_priority = module.params['lowest_priority'] + if lowest_priority != 'no_use': + conf_str += "%s" % lowest_priority + + if lowest_priority == "true": + cmd = "advertise lowest-priority on-startup" + else: + cmd = "undo advertise lowest-priority on-startup" + cmds.append(cmd) + + relay_delay_enable = module.params['relay_delay_enable'] + if relay_delay_enable != 'no_use': + conf_str += "%s" % relay_delay_enable + + if relay_delay_enable == "true": + cmd = "nexthop recursive-lookup restrain enable" + else: + cmd = "nexthop recursive-lookup restrain disable" + cmds.append(cmd) + conf_str += CE_MERGE_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Merge bgp address family other agrus failed.') + + return cmds + + def delete_bgp_af_other(self, **kwargs): + """ delete_bgp_af_other """ + + module = kwargs["module"] + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + + conf_str = CE_MERGE_BGP_ADDRESS_FAMILY_HEADER % (vrf_name, af_type) + + cmds = [] + + router_id = module.params['router_id'] + if router_id: + conf_str += "" + + cmd = "undo router-id %s" % router_id + cmds.append(cmd) + + determin_med = module.params['determin_med'] + if determin_med != 'no_use': + conf_str += "" + + cmd = "undo deterministic-med" + cmds.append(cmd) + + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + if ebgp_if_sensitive != 'no_use': + conf_str += "" + + cmd = "undo ebgp-interface-sensitive" + cmds.append(cmd) + + relay_delay_enable = module.params['relay_delay_enable'] + if relay_delay_enable != 'no_use': + conf_str += "" + + conf_str += CE_MERGE_BGP_ADDRESS_FAMILY_TAIL + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Merge bgp address family other agrus failed.') + + return cmds + + def merge_bgp_import_route(self, **kwargs): + """ merge_bgp_import_route """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + import_protocol = module.params['import_protocol'] + import_process_id = module.params['import_process_id'] + + if import_protocol == "direct" or import_protocol == "static": + import_process_id = "0" + + conf_str = CE_MERGE_BGP_IMPORT_ROUTE_HEADER % ( + vrf_name, af_type, import_protocol, import_process_id) + CE_MERGE_BGP_IMPORT_ROUTE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp import route failed.') + + cmds = [] + cmd = "import-route %s %s" % (import_protocol, import_process_id) + if import_protocol == "direct" or import_protocol == "static": + cmd = "import-route %s" % import_protocol + cmds.append(cmd) + + return cmds + + def create_bgp_import_route(self, **kwargs): + """ create_bgp_import_route """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + import_protocol = module.params['import_protocol'] + import_process_id = module.params['import_process_id'] + + if import_protocol == "direct" or import_protocol == "static": + import_process_id = "0" + + conf_str = CE_CREATE_BGP_IMPORT_ROUTE % ( + vrf_name, af_type, import_protocol, import_process_id) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create bgp import route failed.') + + cmds = [] + cmd = "import-route %s %s" % (import_protocol, import_process_id) + if import_protocol == "direct" or import_protocol == "static": + cmd = "import-route %s" % import_protocol + cmds.append(cmd) + + return cmds + + def delete_bgp_import_route(self, **kwargs): + """ delete_bgp_import_route """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + import_protocol = module.params['import_protocol'] + import_process_id = module.params['import_process_id'] + + if import_protocol == "direct" or import_protocol == "static": + import_process_id = "0" + + conf_str = CE_DELETE_BGP_IMPORT_ROUTE % ( + vrf_name, af_type, import_protocol, import_process_id) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp import route failed.') + + cmds = [] + cmd = "undo import-route %s %s" % (import_protocol, import_process_id) + if import_protocol == "direct" or import_protocol == "static": + cmd = "undo import-route %s" % import_protocol + cmds.append(cmd) + + return cmds + + def merge_bgp_network_route(self, **kwargs): + """ merge_bgp_network_route """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + network_address = module.params['network_address'] + mask_len = module.params['mask_len'] + + conf_str = CE_MERGE_BGP_NETWORK_ROUTE_HEADER % ( + vrf_name, af_type, network_address, mask_len) + CE_MERGE_BGP_NETWORK_ROUTE_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp network route failed.') + + cmds = [] + cmd = "network %s %s" % (network_address, mask_len) + cmds.append(cmd) + + return cmds + + def create_bgp_network_route(self, **kwargs): + """ create_bgp_network_route """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + network_address = module.params['network_address'] + mask_len = module.params['mask_len'] + + conf_str = CE_CREATE_BGP_NETWORK_ROUTE % ( + vrf_name, af_type, network_address, mask_len) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create bgp network route failed.') + + cmds = [] + cmd = "network %s %s" % (network_address, mask_len) + cmds.append(cmd) + + return cmds + + def delete_bgp_network_route(self, **kwargs): + """ delete_bgp_network_route """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + af_type = module.params['af_type'] + network_address = module.params['network_address'] + mask_len = module.params['mask_len'] + + conf_str = CE_DELETE_BGP_NETWORK_ROUTE % ( + vrf_name, af_type, network_address, mask_len) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp network route failed.') + + cmds = [] + cmd = "undo network %s %s" % (network_address, mask_len) + cmds.append(cmd) + + return cmds + + +def main(): + """ main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + vrf_name=dict(type='str', required=True), + af_type=dict(choices=['ipv4uni', 'ipv4multi', 'ipv4vpn', + 'ipv6uni', 'ipv6vpn', 'evpn'], required=True), + max_load_ibgp_num=dict(type='str'), + ibgp_ecmp_nexthop_changed=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + max_load_ebgp_num=dict(type='str'), + ebgp_ecmp_nexthop_changed=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + maximum_load_balance=dict(type='str'), + ecmp_nexthop_changed=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + default_local_pref=dict(type='str'), + default_med=dict(type='str'), + default_rt_import_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + router_id=dict(type='str'), + vrf_rid_auto_sel=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + nexthop_third_party=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + summary_automatic=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + auto_frr_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + load_balancing_as_path_ignore=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + rib_only_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + rib_only_policy_name=dict(type='str'), + active_route_advertise=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + as_path_neglect=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + med_none_as_maximum=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + router_id_neglect=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + igp_metric_ignore=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + always_compare_med=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + determin_med=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + preference_external=dict(type='str'), + preference_internal=dict(type='str'), + preference_local=dict(type='str'), + prefrence_policy_name=dict(type='str'), + reflect_between_client=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + reflector_cluster_id=dict(type='str'), + reflector_cluster_ipv4=dict(type='str'), + rr_filter_number=dict(type='str'), + policy_vpn_target=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + next_hop_sel_depend_type=dict( + choices=['default', 'dependTunnel', 'dependIp']), + nhp_relay_route_policy_name=dict(type='str'), + ebgp_if_sensitive=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + reflect_chg_path=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + add_path_sel_num=dict(type='str'), + route_sel_delay=dict(type='str'), + allow_invalid_as=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + policy_ext_comm_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + supernet_uni_adv=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + supernet_label_adv=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + ingress_lsp_policy_name=dict(type='str'), + originator_prior=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + lowest_priority=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + relay_delay_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + import_protocol=dict( + choices=['direct', 'ospf', 'isis', 'static', 'rip', 'ospfv3', 'ripng']), + import_process_id=dict(type='str'), + network_address=dict(type='str'), + mask_len=dict(type='str')) + + argument_spec.update(ce_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + state = module.params['state'] + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + max_load_ibgp_num = module.params['max_load_ibgp_num'] + ibgp_ecmp_nexthop_changed = module.params['ibgp_ecmp_nexthop_changed'] + max_load_ebgp_num = module.params['max_load_ebgp_num'] + ebgp_ecmp_nexthop_changed = module.params['ebgp_ecmp_nexthop_changed'] + maximum_load_balance = module.params['maximum_load_balance'] + ecmp_nexthop_changed = module.params['ecmp_nexthop_changed'] + default_local_pref = module.params['default_local_pref'] + default_med = module.params['default_med'] + default_rt_import_enable = module.params['default_rt_import_enable'] + router_id = module.params['router_id'] + vrf_rid_auto_sel = module.params['vrf_rid_auto_sel'] + nexthop_third_party = module.params['nexthop_third_party'] + summary_automatic = module.params['summary_automatic'] + auto_frr_enable = module.params['auto_frr_enable'] + load_balancing_as_path_ignore = module.params[ + 'load_balancing_as_path_ignore'] + rib_only_enable = module.params['rib_only_enable'] + rib_only_policy_name = module.params['rib_only_policy_name'] + active_route_advertise = module.params['active_route_advertise'] + as_path_neglect = module.params['as_path_neglect'] + med_none_as_maximum = module.params['med_none_as_maximum'] + router_id_neglect = module.params['router_id_neglect'] + igp_metric_ignore = module.params['igp_metric_ignore'] + always_compare_med = module.params['always_compare_med'] + determin_med = module.params['determin_med'] + preference_external = module.params['preference_external'] + preference_internal = module.params['preference_internal'] + preference_local = module.params['preference_local'] + prefrence_policy_name = module.params['prefrence_policy_name'] + reflect_between_client = module.params['reflect_between_client'] + reflector_cluster_id = module.params['reflector_cluster_id'] + reflector_cluster_ipv4 = module.params['reflector_cluster_ipv4'] + rr_filter_number = module.params['rr_filter_number'] + policy_vpn_target = module.params['policy_vpn_target'] + next_hop_sel_depend_type = module.params['next_hop_sel_depend_type'] + nhp_relay_route_policy_name = module.params['nhp_relay_route_policy_name'] + ebgp_if_sensitive = module.params['ebgp_if_sensitive'] + reflect_chg_path = module.params['reflect_chg_path'] + add_path_sel_num = module.params['add_path_sel_num'] + route_sel_delay = module.params['route_sel_delay'] + allow_invalid_as = module.params['allow_invalid_as'] + policy_ext_comm_enable = module.params['policy_ext_comm_enable'] + supernet_uni_adv = module.params['supernet_uni_adv'] + supernet_label_adv = module.params['supernet_label_adv'] + ingress_lsp_policy_name = module.params['ingress_lsp_policy_name'] + originator_prior = module.params['originator_prior'] + lowest_priority = module.params['lowest_priority'] + relay_delay_enable = module.params['relay_delay_enable'] + import_protocol = module.params['import_protocol'] + import_process_id = module.params['import_process_id'] + network_address = module.params['network_address'] + mask_len = module.params['mask_len'] + + ce_bgp_af_obj = BgpAf() + + if not ce_bgp_af_obj: + module.fail_json(msg='Error: Init module failed.') + + # get proposed + proposed["state"] = state + if vrf_name: + proposed["vrf_name"] = vrf_name + if af_type: + proposed["af_type"] = af_type + if max_load_ibgp_num: + proposed["max_load_ibgp_num"] = max_load_ibgp_num + if ibgp_ecmp_nexthop_changed != 'no_use': + proposed["ibgp_ecmp_nexthop_changed"] = ibgp_ecmp_nexthop_changed + if max_load_ebgp_num: + proposed["max_load_ebgp_num"] = max_load_ebgp_num + if ebgp_ecmp_nexthop_changed != 'no_use': + proposed["ebgp_ecmp_nexthop_changed"] = ebgp_ecmp_nexthop_changed + if maximum_load_balance: + proposed["maximum_load_balance"] = maximum_load_balance + if ecmp_nexthop_changed != 'no_use': + proposed["ecmp_nexthop_changed"] = ecmp_nexthop_changed + if default_local_pref: + proposed["default_local_pref"] = default_local_pref + if default_med: + proposed["default_med"] = default_med + if default_rt_import_enable != 'no_use': + proposed["default_rt_import_enable"] = default_rt_import_enable + if router_id: + proposed["router_id"] = router_id + if vrf_rid_auto_sel != 'no_use': + proposed["vrf_rid_auto_sel"] = vrf_rid_auto_sel + if nexthop_third_party != 'no_use': + proposed["nexthop_third_party"] = nexthop_third_party + if summary_automatic != 'no_use': + proposed["summary_automatic"] = summary_automatic + if auto_frr_enable != 'no_use': + proposed["auto_frr_enable"] = auto_frr_enable + if load_balancing_as_path_ignore != 'no_use': + proposed["load_balancing_as_path_ignore"] = load_balancing_as_path_ignore + if rib_only_enable != 'no_use': + proposed["rib_only_enable"] = rib_only_enable + if rib_only_policy_name: + proposed["rib_only_policy_name"] = rib_only_policy_name + if active_route_advertise != 'no_use': + proposed["active_route_advertise"] = active_route_advertise + if as_path_neglect != 'no_use': + proposed["as_path_neglect"] = as_path_neglect + if med_none_as_maximum != 'no_use': + proposed["med_none_as_maximum"] = med_none_as_maximum + if router_id_neglect != 'no_use': + proposed["router_id_neglect"] = router_id_neglect + if igp_metric_ignore != 'no_use': + proposed["igp_metric_ignore"] = igp_metric_ignore + if always_compare_med != 'no_use': + proposed["always_compare_med"] = always_compare_med + if determin_med != 'no_use': + proposed["determin_med"] = determin_med + if preference_external: + proposed["preference_external"] = preference_external + if preference_internal: + proposed["preference_internal"] = preference_internal + if preference_local: + proposed["preference_local"] = preference_local + if prefrence_policy_name: + proposed["prefrence_policy_name"] = prefrence_policy_name + if reflect_between_client != 'no_use': + proposed["reflect_between_client"] = reflect_between_client + if reflector_cluster_id: + proposed["reflector_cluster_id"] = reflector_cluster_id + if reflector_cluster_ipv4: + proposed["reflector_cluster_ipv4"] = reflector_cluster_ipv4 + if rr_filter_number: + proposed["rr_filter_number"] = rr_filter_number + if policy_vpn_target != 'no_use': + proposed["policy_vpn_target"] = policy_vpn_target + if next_hop_sel_depend_type: + proposed["next_hop_sel_depend_type"] = next_hop_sel_depend_type + if nhp_relay_route_policy_name: + proposed["nhp_relay_route_policy_name"] = nhp_relay_route_policy_name + if ebgp_if_sensitive != 'no_use': + proposed["ebgp_if_sensitive"] = ebgp_if_sensitive + if reflect_chg_path != 'no_use': + proposed["reflect_chg_path"] = reflect_chg_path + if add_path_sel_num: + proposed["add_path_sel_num"] = add_path_sel_num + if route_sel_delay: + proposed["route_sel_delay"] = route_sel_delay + if allow_invalid_as != 'no_use': + proposed["allow_invalid_as"] = allow_invalid_as + if policy_ext_comm_enable != 'no_use': + proposed["policy_ext_comm_enable"] = policy_ext_comm_enable + if supernet_uni_adv != 'no_use': + proposed["supernet_uni_adv"] = supernet_uni_adv + if supernet_label_adv != 'no_use': + proposed["supernet_label_adv"] = supernet_label_adv + if ingress_lsp_policy_name: + proposed["ingress_lsp_policy_name"] = ingress_lsp_policy_name + if originator_prior != 'no_use': + proposed["originator_prior"] = originator_prior + if lowest_priority != 'no_use': + proposed["lowest_priority"] = lowest_priority + if relay_delay_enable != 'no_use': + proposed["relay_delay_enable"] = relay_delay_enable + if import_protocol: + proposed["import_protocol"] = import_protocol + if import_process_id: + proposed["import_process_id"] = import_process_id + if network_address: + proposed["network_address"] = network_address + if mask_len: + proposed["mask_len"] = mask_len + + bgp_af_rst = ce_bgp_af_obj.check_bgp_af_args(module=module) + bgp_af_other_rst = ce_bgp_af_obj.check_bgp_af_other_args(module=module) + bgp_af_other_can_del_rst = ce_bgp_af_obj.check_bgp_af_other_can_del( + module=module) + bgp_import_network_route_rst = ce_bgp_af_obj.check_bgp_import_network_route( + module=module) + + # state exist bgp address family config + exist_tmp = dict() + for item in bgp_af_rst: + if item != "need_cfg": + exist_tmp[item] = bgp_af_rst[item] + + if exist_tmp: + existing["bgp af"] = exist_tmp + # state exist bgp address family other config + exist_tmp = dict() + for item in bgp_af_other_rst: + if item != "need_cfg": + exist_tmp[item] = bgp_af_other_rst[item] + if exist_tmp: + existing["bgp af other"] = exist_tmp + # state exist bgp import route config + exist_tmp = dict() + for item in bgp_import_network_route_rst: + if item != "need_cfg": + exist_tmp[item] = bgp_import_network_route_rst[item] + + if exist_tmp: + existing["bgp import & network route"] = exist_tmp + + if state == "present": + if bgp_af_rst["need_cfg"] and bgp_import_network_route_rst["import_need_cfg"] and \ + bgp_import_network_route_rst["network_need_cfg"]: + changed = True + if "af_type" in bgp_af_rst.keys(): + conf_str = CE_MERGE_BGP_ADDRESS_FAMILY_HEADER % ( + vrf_name, af_type) + else: + conf_str = CE_CREATE_BGP_ADDRESS_FAMILY_HEADER % ( + vrf_name, af_type) + + if "bgp_import_route" in bgp_import_network_route_rst.keys(): + conf_str += CE_BGP_MERGE_IMPORT_UNIT % ( + import_protocol, import_process_id) + else: + conf_str += CE_BGP_CREATE_IMPORT_UNIT % ( + import_protocol, import_process_id) + + if "bgp_network_route" in bgp_import_network_route_rst.keys(): + conf_str += CE_BGP_MERGE_NETWORK_UNIT % ( + network_address, mask_len) + else: + conf_str += CE_BGP_CREATE_NETWORK_UNIT % ( + network_address, mask_len) + + conf_str += CE_MERGE_BGP_ADDRESS_FAMILY_TAIL + recv_xml = ce_bgp_af_obj.netconf_set_config( + module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Present bgp af_type import and network route failed.') + + cmd = "import-route %s %s" % (import_protocol, import_process_id) + updates.append(cmd) + cmd = "network %s %s" % (network_address, mask_len) + updates.append(cmd) + + elif bgp_import_network_route_rst["import_need_cfg"] and bgp_import_network_route_rst["network_need_cfg"]: + changed = True + conf_str = CE_BGP_IMPORT_NETWORK_ROUTE_HEADER % (vrf_name, af_type) + + if "bgp_import_route" in bgp_import_network_route_rst.keys(): + conf_str += CE_BGP_MERGE_IMPORT_UNIT % ( + import_protocol, import_process_id) + else: + conf_str += CE_BGP_CREATE_IMPORT_UNIT % ( + import_protocol, import_process_id) + + if "bgp_network_route" in bgp_import_network_route_rst.keys(): + conf_str += CE_BGP_MERGE_NETWORK_UNIT % ( + network_address, mask_len) + else: + conf_str += CE_BGP_CREATE_NETWORK_UNIT % ( + network_address, mask_len) + + conf_str += CE_BGP_IMPORT_NETWORK_ROUTE_TAIL + recv_xml = ce_bgp_af_obj.netconf_set_config( + module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Present bgp import and network route failed.') + + cmd = "import-route %s %s" % (import_protocol, import_process_id) + updates.append(cmd) + cmd = "network %s %s" % (network_address, mask_len) + updates.append(cmd) + + else: + if bgp_af_rst["need_cfg"]: + if "af_type" in bgp_af_rst.keys(): + cmd = ce_bgp_af_obj.merge_bgp_af(module=module) + changed = True + for item in cmd: + updates.append(item) + else: + cmd = ce_bgp_af_obj.create_bgp_af(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_af_other_rst["need_cfg"]: + cmd = ce_bgp_af_obj.merge_bgp_af_other(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_import_network_route_rst["import_need_cfg"]: + if "bgp_import_route" in bgp_import_network_route_rst.keys(): + cmd = ce_bgp_af_obj.merge_bgp_import_route(module=module) + changed = True + for item in cmd: + updates.append(item) + else: + cmd = ce_bgp_af_obj.create_bgp_import_route(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_import_network_route_rst["network_need_cfg"]: + if "bgp_network_route" in bgp_import_network_route_rst.keys(): + cmd = ce_bgp_af_obj.merge_bgp_network_route(module=module) + changed = True + for item in cmd: + updates.append(item) + else: + cmd = ce_bgp_af_obj.create_bgp_network_route(module=module) + changed = True + for item in cmd: + updates.append(item) + + else: + if bgp_import_network_route_rst["import_need_cfg"] and bgp_import_network_route_rst["network_need_cfg"]: + changed = True + conf_str = CE_BGP_IMPORT_NETWORK_ROUTE_HEADER % (vrf_name, af_type) + conf_str += CE_BGP_DELETE_IMPORT_UNIT % ( + import_protocol, import_process_id) + conf_str += CE_BGP_DELETE_NETWORK_UNIT % ( + network_address, mask_len) + + conf_str += CE_BGP_IMPORT_NETWORK_ROUTE_TAIL + recv_xml = ce_bgp_af_obj.netconf_set_config( + module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json( + msg='Error: Absent bgp import and network route failed.') + + cmd = "undo import-route %s %s" % (import_protocol, + import_process_id) + updates.append(cmd) + cmd = "undo network %s %s" % (network_address, mask_len) + updates.append(cmd) + + else: + if bgp_import_network_route_rst["import_need_cfg"]: + cmd = ce_bgp_af_obj.delete_bgp_import_route(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_import_network_route_rst["network_need_cfg"]: + cmd = ce_bgp_af_obj.delete_bgp_network_route(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_af_other_can_del_rst["need_cfg"]: + cmd = ce_bgp_af_obj.delete_bgp_af_other(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_af_rst["need_cfg"] and not bgp_af_other_can_del_rst["need_cfg"]: + cmd = ce_bgp_af_obj.delete_bgp_af(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_af_other_rst["need_cfg"]: + pass + + # state end bgp address family config + bgp_af_rst = ce_bgp_af_obj.check_bgp_af_args(module=module) + end_tmp = dict() + for item in bgp_af_rst: + if item != "need_cfg": + end_tmp[item] = bgp_af_rst[item] + if end_tmp: + end_state["bgp af"] = end_tmp + # state end bgp address family other config + bgp_af_other_rst = ce_bgp_af_obj.check_bgp_af_other_args(module=module) + end_tmp = dict() + for item in bgp_af_other_rst: + if item != "need_cfg": + end_tmp[item] = bgp_af_other_rst[item] + if end_tmp: + end_state["bgp af other"] = end_tmp + # state end bgp import route config + bgp_import_network_route_rst = ce_bgp_af_obj.check_bgp_import_network_route( + module=module) + end_tmp = dict() + for item in bgp_import_network_route_rst: + if item != "need_cfg": + end_tmp[item] = bgp_import_network_route_rst[item] + if end_tmp: + end_state["bgp import & network route"] = end_tmp + if end_state == existing: + changed = False + updates = list() + + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_bgp_neighbor.py b/plugins/modules/ce_bgp_neighbor.py new file mode 100644 index 0000000..afe96d2 --- /dev/null +++ b/plugins/modules/ce_bgp_neighbor.py @@ -0,0 +1,2048 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_bgp_neighbor +short_description: Manages BGP peer configuration on HUAWEI CloudEngine switches. +description: + - Manages BGP peer configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] + vrf_name: + description: + - Name of a BGP instance. The name is a case-sensitive string of characters. + The BGP instance can be used only after the corresponding VPN instance is created. + required: true + peer_addr: + description: + - Connection address of a peer, which can be an IPv4 or IPv6 address. + required: true + remote_as: + description: + - AS number of a peer. + The value is a string of 1 to 11 characters. + required: true + description: + description: + - Description of a peer, which can be letters or digits. + The value is a string of 1 to 80 characters. + fake_as: + description: + - Fake AS number that is specified for a local peer. + The value is a string of 1 to 11 characters. + dual_as: + description: + - If the value is true, the EBGP peer can use either a fake AS number or the actual AS number. + If the value is false, the EBGP peer can only use a fake AS number. + choices: ['no_use','true','false'] + default: no_use + conventional: + description: + - If the value is true, the router has all extended capabilities. + If the value is false, the router does not have all extended capabilities. + choices: ['no_use','true','false'] + default: no_use + route_refresh: + description: + - If the value is true, BGP is enabled to advertise REFRESH packets. + If the value is false, the route refresh function is enabled. + choices: ['no_use','true','false'] + default: no_use + is_ignore: + description: + - If the value is true, the session with a specified peer is torn down and all related + routing entries are cleared. + If the value is false, the session with a specified peer is retained. + choices: ['no_use','true','false'] + default: no_use + local_if_name: + description: + - Name of a source interface that sends BGP packets. + The value is a string of 1 to 63 characters. + ebgp_max_hop: + description: + - Maximum number of hops in an indirect EBGP connection. + The value is an ranging from 1 to 255. + valid_ttl_hops: + description: + - Enable GTSM on a peer or peer group. + The valid-TTL-Value parameter is used to specify the number of TTL hops to be detected. + The value is an integer ranging from 1 to 255. + connect_mode: + description: + - The value can be Connect-only, Listen-only, or Both. + is_log_change: + description: + - If the value is true, BGP is enabled to record peer session status and event information. + If the value is false, BGP is disabled from recording peer session status and event information. + choices: ['no_use','true','false'] + default: no_use + pswd_type: + description: + - Enable BGP peers to establish a TCP connection and perform the Message Digest 5 (MD5) + authentication for BGP messages. + choices: ['null','cipher','simple'] + pswd_cipher_text: + description: + - The character string in a password identifies the contents of the password, spaces not supported. + The value is a string of 1 to 255 characters. + keep_alive_time: + description: + - Specify the Keepalive time of a peer or peer group. + The value is an integer ranging from 0 to 21845. The default value is 60. + hold_time: + description: + - Specify the Hold time of a peer or peer group. + The value is 0 or an integer ranging from 3 to 65535. + min_hold_time: + description: + - Specify the Min hold time of a peer or peer group. + key_chain_name: + description: + - Specify the Keychain authentication name used when BGP peers establish a TCP connection. + The value is a string of 1 to 47 case-insensitive characters. + conn_retry_time: + description: + - ConnectRetry interval. + The value is an integer ranging from 1 to 65535. + tcp_MSS: + description: + - Maximum TCP MSS value used for TCP connection establishment for a peer. + The value is an integer ranging from 176 to 4096. + mpls_local_ifnet_disable: + description: + - If the value is true, peer create MPLS Local IFNET disable. + If the value is false, peer create MPLS Local IFNET enable. + choices: ['no_use','true','false'] + default: no_use + prepend_global_as: + description: + - Add the global AS number to the Update packets to be advertised. + choices: ['no_use','true','false'] + default: no_use + prepend_fake_as: + description: + - Add the Fake AS number to received Update packets. + choices: ['no_use','true','false'] + default: no_use + is_bfd_block: + description: + - If the value is true, peers are enabled to inherit the BFD function from the peer group. + If the value is false, peers are disabled to inherit the BFD function from the peer group. + choices: ['no_use','true','false'] + default: no_use + multiplier: + description: + - Specify the detection multiplier. The default value is 3. + The value is an integer ranging from 3 to 50. + is_bfd_enable: + description: + - If the value is true, BFD is enabled. + If the value is false, BFD is disabled. + choices: ['no_use','true','false'] + default: no_use + rx_interval: + description: + - Specify the minimum interval at which BFD packets are received. + The value is an integer ranging from 50 to 1000, in milliseconds. + tx_interval: + description: + - Specify the minimum interval at which BFD packets are sent. + The value is an integer ranging from 50 to 1000, in milliseconds. + is_single_hop: + description: + - If the value is true, the system is enabled to preferentially use the single-hop mode for + BFD session setup between IBGP peers. + If the value is false, the system is disabled from preferentially using the single-hop + mode for BFD session setup between IBGP peers. + choices: ['no_use','true','false'] + default: no_use +''' + +EXAMPLES = ''' + +- name: CloudEngine BGP neighbor test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config bgp peer" + ce_bgp_neighbor: + state: present + vrf_name: js + peer_addr: 192.168.10.10 + remote_as: 500 + provider: "{{ cli }}" + + - name: "Config bgp route id" + ce_bgp_neighbor: + state: absent + vrf_name: js + peer_addr: 192.168.10.10 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"peer_addr": "192.168.10.10", "remote_as": "500", "state": "present", "vrf_name": "js"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"bgp peer": []} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"bgp peer": [["192.168.10.10", "500"]]} +updates: + description: command sent to the device + returned: always + type: list + sample: ["peer 192.168.10.10 as-number 500"] +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + + +# get bgp peer +CE_GET_BGP_PEER_HEADER = """ + + + + + + %s + + + %s +""" +CE_GET_BGP_PEER_TAIL = """ + + + + + + + +""" + +# merge bgp peer +CE_MERGE_BGP_PEER_HEADER = """ + + + + + + %s + + + %s +""" +CE_MERGE_BGP_PEER_TAIL = """ + + + + + + + +""" + +# create bgp peer +CE_CREATE_BGP_PEER_HEADER = """ + + + + + + %s + + + %s +""" +CE_CREATE_BGP_PEER_TAIL = """ + + + + + + + +""" + +# delete bgp peer +CE_DELETE_BGP_PEER_HEADER = """ + + + + + + %s + + + %s +""" +CE_DELETE_BGP_PEER_TAIL = """ + + + + + + + +""" + +# get peer bfd +CE_GET_PEER_BFD_HEADER = """ + + + + + + %s + + + %s + +""" +CE_GET_PEER_BFD_TAIL = """ + + + + + + + + +""" + +# merge peer bfd +CE_MERGE_PEER_BFD_HEADER = """ + + + + + + %s + + + %s + +""" +CE_MERGE_PEER_BFD_TAIL = """ + + + + + + + + +""" + +# delete peer bfd +CE_DELETE_PEER_BFD_HEADER = """ + + + + + + %s + + + %s + +""" +CE_DELETE_PEER_BFD_TAIL = """ + + + + + + + + +""" + + +class BgpNeighbor(object): + """ Manages BGP peer configuration """ + + def netconf_get_config(self, **kwargs): + """ netconf_get_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ netconf_set_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = set_nc_config(module, conf_str) + + return xml_str + + def check_bgp_peer_args(self, **kwargs): + """ check_bgp_peer_args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + + peer_addr = module.params['peer_addr'] + if peer_addr: + if not check_ip_addr(ipaddr=peer_addr): + module.fail_json( + msg='Error: The peer_addr %s is invalid.' % peer_addr) + + need_cfg = True + + remote_as = module.params['remote_as'] + if remote_as: + if len(remote_as) > 11 or len(remote_as) < 1: + module.fail_json( + msg='Error: The len of remote_as %s is out of [1 - 11].' % remote_as) + + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def check_bgp_peer_other_args(self, **kwargs): + """ check_bgp_peer_other_args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + peerip = module.params['peer_addr'] + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + + description = module.params['description'] + if description: + if len(description) > 80 or len(description) < 1: + module.fail_json( + msg='Error: The len of description %s is out of [1 - 80].' % description) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["description"] = re_find + if re_find[0] != description: + need_cfg = True + else: + need_cfg = True + + fake_as = module.params['fake_as'] + if fake_as: + if len(fake_as) > 11 or len(fake_as) < 1: + module.fail_json( + msg='Error: The len of fake_as %s is out of [1 - 11].' % fake_as) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["fake_as"] = re_find + if re_find[0] != fake_as: + need_cfg = True + else: + need_cfg = True + + dual_as = module.params['dual_as'] + if dual_as != 'no_use': + if not fake_as: + module.fail_json(msg='fake_as must exist.') + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["dual_as"] = re_find + if re_find[0] != dual_as: + need_cfg = True + else: + need_cfg = True + + conventional = module.params['conventional'] + if conventional != 'no_use': + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["conventional"] = re_find + if re_find[0] != conventional: + need_cfg = True + else: + need_cfg = True + + route_refresh = module.params['route_refresh'] + if route_refresh != 'no_use': + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["route_refresh"] = re_find + if re_find[0] != route_refresh: + need_cfg = True + else: + need_cfg = True + + four_byte_as = module.params['four_byte_as'] + if four_byte_as != 'no_use': + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["four_byte_as"] = re_find + if re_find[0] != four_byte_as: + need_cfg = True + else: + need_cfg = True + + is_ignore = module.params['is_ignore'] + if is_ignore != 'no_use': + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_ignore"] = re_find + if re_find[0] != is_ignore: + need_cfg = True + else: + need_cfg = True + + local_if_name = module.params['local_if_name'] + if local_if_name: + if len(local_if_name) > 63 or len(local_if_name) < 1: + module.fail_json( + msg='Error: The len of local_if_name %s is out of [1 - 63].' % local_if_name) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["local_if_name"] = re_find + if re_find[0].lower() != local_if_name.lower(): + need_cfg = True + else: + need_cfg = True + + ebgp_max_hop = module.params['ebgp_max_hop'] + if ebgp_max_hop: + if int(ebgp_max_hop) > 255 or int(ebgp_max_hop) < 1: + module.fail_json( + msg='Error: The value of ebgp_max_hop %s is out of [1 - 255].' % ebgp_max_hop) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ebgp_max_hop"] = re_find + if re_find[0] != ebgp_max_hop: + need_cfg = True + else: + need_cfg = True + + valid_ttl_hops = module.params['valid_ttl_hops'] + if valid_ttl_hops: + if int(valid_ttl_hops) > 255 or int(valid_ttl_hops) < 1: + module.fail_json( + msg='Error: The value of valid_ttl_hops %s is out of [1 - 255].' % valid_ttl_hops) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["valid_ttl_hops"] = re_find + if re_find[0] != valid_ttl_hops: + need_cfg = True + else: + need_cfg = True + + connect_mode = module.params['connect_mode'] + if connect_mode: + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["connect_mode"] = re_find + if re_find[0] != connect_mode: + need_cfg = True + else: + need_cfg = True + + is_log_change = module.params['is_log_change'] + if is_log_change != 'no_use': + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_log_change"] = re_find + if re_find[0] != is_log_change: + need_cfg = True + else: + need_cfg = True + + pswd_type = module.params['pswd_type'] + if pswd_type: + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["pswd_type"] = re_find + if re_find[0] != pswd_type: + need_cfg = True + else: + need_cfg = True + + pswd_cipher_text = module.params['pswd_cipher_text'] + if pswd_cipher_text: + if len(pswd_cipher_text) > 255 or len(pswd_cipher_text) < 1: + module.fail_json( + msg='Error: The len of pswd_cipher_text %s is out of [1 - 255].' % pswd_cipher_text) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["pswd_cipher_text"] = re_find + if re_find[0] != pswd_cipher_text: + need_cfg = True + else: + need_cfg = True + + keep_alive_time = module.params['keep_alive_time'] + if keep_alive_time: + if int(keep_alive_time) > 21845 or len(keep_alive_time) < 0: + module.fail_json( + msg='Error: The len of keep_alive_time %s is out of [0 - 21845].' % keep_alive_time) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["keep_alive_time"] = re_find + if re_find[0] != keep_alive_time: + need_cfg = True + else: + need_cfg = True + + hold_time = module.params['hold_time'] + if hold_time: + if int(hold_time) != 0 and (int(hold_time) > 65535 or int(hold_time) < 3): + module.fail_json( + msg='Error: The value of hold_time %s is out of [0 or 3 - 65535].' % hold_time) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["hold_time"] = re_find + if re_find[0] != hold_time: + need_cfg = True + else: + need_cfg = True + + min_hold_time = module.params['min_hold_time'] + if min_hold_time: + if int(min_hold_time) != 0 and (int(min_hold_time) > 65535 or int(min_hold_time) < 20): + module.fail_json( + msg='Error: The value of min_hold_time %s is out of [0 or 20 - 65535].' % min_hold_time) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["min_hold_time"] = re_find + if re_find[0] != min_hold_time: + need_cfg = True + else: + need_cfg = True + + key_chain_name = module.params['key_chain_name'] + if key_chain_name: + if len(key_chain_name) > 47 or len(key_chain_name) < 1: + module.fail_json( + msg='Error: The len of key_chain_name %s is out of [1 - 47].' % key_chain_name) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["key_chain_name"] = re_find + if re_find[0] != key_chain_name: + need_cfg = True + else: + need_cfg = True + + conn_retry_time = module.params['conn_retry_time'] + if conn_retry_time: + if int(conn_retry_time) > 65535 or int(conn_retry_time) < 1: + module.fail_json( + msg='Error: The value of conn_retry_time %s is out of [1 - 65535].' % conn_retry_time) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["conn_retry_time"] = re_find + if re_find[0] != conn_retry_time: + need_cfg = True + else: + need_cfg = True + + tcp_mss = module.params['tcp_MSS'] + if tcp_mss: + if int(tcp_mss) > 4096 or int(tcp_mss) < 176: + module.fail_json( + msg='Error: The value of tcp_mss %s is out of [176 - 4096].' % tcp_mss) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["tcp_MSS"] = re_find + if re_find[0] != tcp_mss: + need_cfg = True + else: + need_cfg = True + + mpls_local_ifnet_disable = module.params['mpls_local_ifnet_disable'] + if mpls_local_ifnet_disable != 'no_use': + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["mpls_local_ifnet_disable"] = re_find + if re_find[0] != mpls_local_ifnet_disable: + need_cfg = True + else: + need_cfg = True + + prepend_global_as = module.params['prepend_global_as'] + if prepend_global_as != 'no_use': + if not fake_as: + module.fail_json(msg='fake_as must exist.') + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["prepend_global_as"] = re_find + if re_find[0] != prepend_global_as: + need_cfg = True + else: + need_cfg = True + + prepend_fake_as = module.params['prepend_fake_as'] + if prepend_fake_as != 'no_use': + if not fake_as: + module.fail_json(msg='fake_as must exist.') + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["prepend_fake_as"] = re_find + if re_find[0] != prepend_fake_as: + need_cfg = True + else: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def check_peer_bfd_merge_args(self, **kwargs): + """ check_peer_bfd_merge_args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + state = module.params['state'] + if state == "absent": + result["need_cfg"] = need_cfg + return result + + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + + peer_addr = module.params['peer_addr'] + + is_bfd_block = module.params['is_bfd_block'] + if is_bfd_block != 'no_use': + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_bfd_block"] = re_find + if re_find[0] != is_bfd_block: + need_cfg = True + else: + need_cfg = True + + multiplier = module.params['multiplier'] + if multiplier: + if int(multiplier) > 50 or int(multiplier) < 3: + module.fail_json( + msg='Error: The value of multiplier %s is out of [3 - 50].' % multiplier) + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["multiplier"] = re_find + if re_find[0] != multiplier: + need_cfg = True + else: + need_cfg = True + + is_bfd_enable = module.params['is_bfd_enable'] + if is_bfd_enable != 'no_use': + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_bfd_enable"] = re_find + if re_find[0] != is_bfd_enable: + need_cfg = True + else: + need_cfg = True + + rx_interval = module.params['rx_interval'] + if rx_interval: + if int(rx_interval) > 1000 or int(rx_interval) < 50: + module.fail_json( + msg='Error: The value of rx_interval %s is out of [50 - 1000].' % rx_interval) + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["rx_interval"] = re_find + if re_find[0] != rx_interval: + need_cfg = True + else: + need_cfg = True + + tx_interval = module.params['tx_interval'] + if tx_interval: + if int(tx_interval) > 1000 or int(tx_interval) < 50: + module.fail_json( + msg='Error: The value of tx_interval %s is out of [50 - 1000].' % tx_interval) + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["tx_interval"] = re_find + if re_find[0] != tx_interval: + need_cfg = True + else: + need_cfg = True + + is_single_hop = module.params['is_single_hop'] + if is_single_hop != 'no_use': + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_single_hop"] = re_find + if re_find[0] != is_single_hop: + need_cfg = True + else: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def check_peer_bfd_delete_args(self, **kwargs): + """ check_peer_bfd_delete_args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + state = module.params['state'] + if state == "present": + result["need_cfg"] = need_cfg + return result + + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + + peer_addr = module.params['peer_addr'] + + is_bfd_block = module.params['is_bfd_block'] + if is_bfd_block != 'no_use': + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_bfd_block"] = re_find + if re_find[0] == is_bfd_block: + need_cfg = True + + multiplier = module.params['multiplier'] + if multiplier: + if int(multiplier) > 50 or int(multiplier) < 3: + module.fail_json( + msg='Error: The value of multiplier %s is out of [3 - 50].' % multiplier) + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["multiplier"] = re_find + if re_find[0] == multiplier: + need_cfg = True + + is_bfd_enable = module.params['is_bfd_enable'] + if is_bfd_enable != 'no_use': + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_bfd_enable"] = re_find + if re_find[0] == is_bfd_enable: + need_cfg = True + + rx_interval = module.params['rx_interval'] + if rx_interval: + if int(rx_interval) > 1000 or int(rx_interval) < 50: + module.fail_json( + msg='Error: The value of rx_interval %s is out of [50 - 1000].' % rx_interval) + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["rx_interval"] = re_find + if re_find[0] == rx_interval: + need_cfg = True + + tx_interval = module.params['tx_interval'] + if tx_interval: + if int(tx_interval) > 1000 or int(tx_interval) < 50: + module.fail_json( + msg='Error: The value of tx_interval %s is out of [50 - 1000].' % tx_interval) + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["tx_interval"] = re_find + if re_find[0] == tx_interval: + need_cfg = True + + is_single_hop = module.params['is_single_hop'] + if is_single_hop != 'no_use': + + conf_str = CE_GET_PEER_BFD_HEADER % ( + vrf_name, peer_addr) + "" + CE_GET_PEER_BFD_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_single_hop"] = re_find + if re_find[0] == is_single_hop: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def get_bgp_peer(self, **kwargs): + """ get_bgp_peer """ + + module = kwargs["module"] + peerip = module.params['peer_addr'] + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + \ + "" + CE_GET_BGP_PEER_TAIL + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*\s.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def get_bgp_del_peer(self, **kwargs): + """ get_bgp_del_peer """ + + module = kwargs["module"] + peerip = module.params['peer_addr'] + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + + conf_str = CE_GET_BGP_PEER_HEADER % (vrf_name, peerip) + CE_GET_BGP_PEER_TAIL + + xml_str = self.netconf_get_config(module=module, conf_str=conf_str) + + result = list() + + if "" in xml_str: + return result + else: + re_find = re.findall( + r'.*(.*).*', xml_str) + + if re_find: + return re_find + else: + return result + + def merge_bgp_peer(self, **kwargs): + """ merge_bgp_peer """ + + module = kwargs["module"] + vrf_name = module.params['vrf_name'] + peer_addr = module.params['peer_addr'] + remote_as = module.params['remote_as'] + + conf_str = CE_MERGE_BGP_PEER_HEADER % ( + vrf_name, peer_addr) + "%s" % remote_as + CE_MERGE_BGP_PEER_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp peer failed.') + + cmds = [] + cmd = "peer %s as-number %s" % (peer_addr, remote_as) + cmds.append(cmd) + + return cmds + + def create_bgp_peer(self, **kwargs): + """ create_bgp_peer """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + + peer_addr = module.params['peer_addr'] + remote_as = module.params['remote_as'] + + conf_str = CE_CREATE_BGP_PEER_HEADER % ( + vrf_name, peer_addr) + "%s" % remote_as + CE_CREATE_BGP_PEER_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create bgp peer failed.') + + cmds = [] + cmd = "peer %s as-number %s" % (peer_addr, remote_as) + cmds.append(cmd) + + return cmds + + def delete_bgp_peer(self, **kwargs): + """ delete_bgp_peer """ + + module = kwargs["module"] + vrf_name = module.params['vrf_name'] + peer_addr = module.params['peer_addr'] + + conf_str = CE_DELETE_BGP_PEER_HEADER % ( + vrf_name, peer_addr) + CE_DELETE_BGP_PEER_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp peer failed.') + + cmds = [] + cmd = "undo peer %s" % peer_addr + cmds.append(cmd) + + return cmds + + def merge_bgp_peer_other(self, **kwargs): + """ merge_bgp_peer """ + + module = kwargs["module"] + vrf_name = module.params['vrf_name'] + peer_addr = module.params['peer_addr'] + + conf_str = CE_MERGE_BGP_PEER_HEADER % (vrf_name, peer_addr) + + cmds = [] + + description = module.params['description'] + if description: + conf_str += "%s" % description + + cmd = "peer %s description %s" % (peer_addr, description) + cmds.append(cmd) + + fake_as = module.params['fake_as'] + if fake_as: + conf_str += "%s" % fake_as + + cmd = "peer %s local-as %s" % (peer_addr, fake_as) + cmds.append(cmd) + + dual_as = module.params['dual_as'] + if dual_as != 'no_use': + conf_str += "%s" % dual_as + + if dual_as == "true": + cmd = "peer %s local-as %s dual-as" % (peer_addr, fake_as) + else: + cmd = "peer %s local-as %s" % (peer_addr, fake_as) + cmds.append(cmd) + + conventional = module.params['conventional'] + if conventional != 'no_use': + conf_str += "%s" % conventional + if conventional == "true": + cmd = "peer %s capability-advertise conventional" % peer_addr + else: + cmd = "undo peer %s capability-advertise conventional" % peer_addr + cmds.append(cmd) + + route_refresh = module.params['route_refresh'] + if route_refresh != 'no_use': + conf_str += "%s" % route_refresh + + if route_refresh == "true": + cmd = "peer %s capability-advertise route-refresh" % peer_addr + else: + cmd = "undo peer %s capability-advertise route-refresh" % peer_addr + cmds.append(cmd) + + four_byte_as = module.params['four_byte_as'] + if four_byte_as != 'no_use': + conf_str += "%s" % four_byte_as + + if four_byte_as == "true": + cmd = "peer %s capability-advertise 4-byte-as" % peer_addr + else: + cmd = "undo peer %s capability-advertise 4-byte-as" % peer_addr + cmds.append(cmd) + + is_ignore = module.params['is_ignore'] + if is_ignore != 'no_use': + conf_str += "%s" % is_ignore + + if is_ignore == "true": + cmd = "peer %s ignore" % peer_addr + else: + cmd = "undo peer %s ignore" % peer_addr + cmds.append(cmd) + + local_if_name = module.params['local_if_name'] + if local_if_name: + conf_str += "%s" % local_if_name + + cmd = "peer %s connect-interface %s" % (peer_addr, local_if_name) + cmds.append(cmd) + + ebgp_max_hop = module.params['ebgp_max_hop'] + if ebgp_max_hop: + conf_str += "%s" % ebgp_max_hop + + cmd = "peer %s ebgp-max-hop %s" % (peer_addr, ebgp_max_hop) + cmds.append(cmd) + + valid_ttl_hops = module.params['valid_ttl_hops'] + if valid_ttl_hops: + conf_str += "%s" % valid_ttl_hops + + cmd = "peer %s valid-ttl-hops %s" % (peer_addr, valid_ttl_hops) + cmds.append(cmd) + + connect_mode = module.params['connect_mode'] + if connect_mode: + + if connect_mode == "listenOnly": + cmd = "peer %s listen-only" % peer_addr + cmds.append(cmd) + elif connect_mode == "connectOnly": + cmd = "peer %s connect-only" % peer_addr + cmds.append(cmd) + elif connect_mode == "both": + connect_mode = "null" + cmd = "peer %s listen-only" % peer_addr + cmds.append(cmd) + cmd = "peer %s connect-only" % peer_addr + cmds.append(cmd) + conf_str += "%s" % connect_mode + + is_log_change = module.params['is_log_change'] + if is_log_change != 'no_use': + conf_str += "%s" % is_log_change + + if is_log_change == "true": + cmd = "peer %s log-change" % peer_addr + else: + cmd = "undo peer %s log-change" % peer_addr + cmds.append(cmd) + + pswd_type = module.params['pswd_type'] + if pswd_type: + conf_str += "%s" % pswd_type + + pswd_cipher_text = module.params['pswd_cipher_text'] + if pswd_cipher_text: + conf_str += "%s" % pswd_cipher_text + + if pswd_type == "cipher": + cmd = "peer %s password cipher %s" % ( + peer_addr, pswd_cipher_text) + elif pswd_type == "simple": + cmd = "peer %s password simple %s" % ( + peer_addr, pswd_cipher_text) + cmds.append(cmd) + + keep_alive_time = module.params['keep_alive_time'] + if keep_alive_time: + conf_str += "%s" % keep_alive_time + + cmd = "peer %s timer keepalive %s" % (peer_addr, keep_alive_time) + cmds.append(cmd) + + hold_time = module.params['hold_time'] + if hold_time: + conf_str += "%s" % hold_time + + cmd = "peer %s timer hold %s" % (peer_addr, hold_time) + cmds.append(cmd) + + min_hold_time = module.params['min_hold_time'] + if min_hold_time: + conf_str += "%s" % min_hold_time + + cmd = "peer %s timer min-holdtime %s" % (peer_addr, min_hold_time) + cmds.append(cmd) + + key_chain_name = module.params['key_chain_name'] + if key_chain_name: + conf_str += "%s" % key_chain_name + + cmd = "peer %s keychain %s" % (peer_addr, key_chain_name) + cmds.append(cmd) + + conn_retry_time = module.params['conn_retry_time'] + if conn_retry_time: + conf_str += "%s" % conn_retry_time + + cmd = "peer %s timer connect-retry %s" % ( + peer_addr, conn_retry_time) + cmds.append(cmd) + + tcp_mss = module.params['tcp_MSS'] + if tcp_mss: + conf_str += "%s" % tcp_mss + + cmd = "peer %s tcp-mss %s" % (peer_addr, tcp_mss) + cmds.append(cmd) + + mpls_local_ifnet_disable = module.params['mpls_local_ifnet_disable'] + if mpls_local_ifnet_disable != 'no_use': + conf_str += "%s" % mpls_local_ifnet_disable + + if mpls_local_ifnet_disable == "false": + cmd = "undo peer %s mpls-local-ifnet disable" % peer_addr + else: + cmd = "peer %s mpls-local-ifnet disable" % peer_addr + cmds.append(cmd) + + prepend_global_as = module.params['prepend_global_as'] + if prepend_global_as != 'no_use': + conf_str += "%s" % prepend_global_as + + if prepend_global_as == "true": + cmd = "peer %s local-as %s prepend-global-as" % (peer_addr, fake_as) + else: + cmd = "undo peer %s local-as %s prepend-global-as" % (peer_addr, fake_as) + cmds.append(cmd) + + prepend_fake_as = module.params['prepend_fake_as'] + if prepend_fake_as != 'no_use': + conf_str += "%s" % prepend_fake_as + + if prepend_fake_as == "true": + cmd = "peer %s local-as %s prepend-local-as" % (peer_addr, fake_as) + else: + cmd = "undo peer %s local-as %s prepend-local-as" % (peer_addr, fake_as) + cmds.append(cmd) + + conf_str += CE_MERGE_BGP_PEER_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp peer other failed.') + + return cmds + + def merge_peer_bfd(self, **kwargs): + """ merge_peer_bfd """ + + module = kwargs["module"] + vrf_name = module.params['vrf_name'] + peer_addr = module.params['peer_addr'] + + conf_str = CE_MERGE_PEER_BFD_HEADER % (vrf_name, peer_addr) + + cmds = [] + + is_bfd_block = module.params['is_bfd_block'] + if is_bfd_block != 'no_use': + conf_str += "%s" % is_bfd_block + + if is_bfd_block == "true": + cmd = "peer %s bfd block" % peer_addr + else: + cmd = "undo peer %s bfd block" % peer_addr + cmds.append(cmd) + + multiplier = module.params['multiplier'] + if multiplier: + conf_str += "%s" % multiplier + + cmd = "peer %s bfd detect-multiplier %s" % (peer_addr, multiplier) + cmds.append(cmd) + + is_bfd_enable = module.params['is_bfd_enable'] + if is_bfd_enable != 'no_use': + conf_str += "%s" % is_bfd_enable + + if is_bfd_enable == "true": + cmd = "peer %s bfd enable" % peer_addr + else: + cmd = "undo peer %s bfd enable" % peer_addr + cmds.append(cmd) + + rx_interval = module.params['rx_interval'] + if rx_interval: + conf_str += "%s" % rx_interval + + cmd = "peer %s bfd min-rx-interval %s" % (peer_addr, rx_interval) + cmds.append(cmd) + + tx_interval = module.params['tx_interval'] + if tx_interval: + conf_str += "%s" % tx_interval + + cmd = "peer %s bfd min-tx-interval %s" % (peer_addr, tx_interval) + cmds.append(cmd) + + is_single_hop = module.params['is_single_hop'] + if is_single_hop != 'no_use': + conf_str += "%s" % is_single_hop + + if is_single_hop == "true": + cmd = "peer %s bfd enable single-hop-prefer" % peer_addr + else: + cmd = "undo peer %s bfd enable single-hop-prefer" % peer_addr + cmds.append(cmd) + + conf_str += CE_MERGE_PEER_BFD_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge peer bfd failed.') + + return cmds + + def delete_peer_bfd(self, **kwargs): + """ delete_peer_bfd """ + + module = kwargs["module"] + vrf_name = module.params['vrf_name'] + peer_addr = module.params['peer_addr'] + + conf_str = CE_DELETE_PEER_BFD_HEADER % (vrf_name, peer_addr) + + cmds = [] + + is_bfd_block = module.params['is_bfd_block'] + if is_bfd_block != 'no_use': + conf_str += "%s" % is_bfd_block + + cmd = "undo peer %s bfd block" % peer_addr + cmds.append(cmd) + + multiplier = module.params['multiplier'] + if multiplier: + conf_str += "%s" % multiplier + + cmd = "undo peer %s bfd detect-multiplier %s" % ( + peer_addr, multiplier) + cmds.append(cmd) + + is_bfd_enable = module.params['is_bfd_enable'] + if is_bfd_enable != 'no_use': + conf_str += "%s" % is_bfd_enable + + cmd = "undo peer %s bfd enable" % peer_addr + cmds.append(cmd) + + rx_interval = module.params['rx_interval'] + if rx_interval: + conf_str += "%s" % rx_interval + + cmd = "undo peer %s bfd min-rx-interval %s" % ( + peer_addr, rx_interval) + cmds.append(cmd) + + tx_interval = module.params['tx_interval'] + if tx_interval: + conf_str += "%s" % tx_interval + + cmd = "undo peer %s bfd min-tx-interval %s" % ( + peer_addr, tx_interval) + cmds.append(cmd) + + is_single_hop = module.params['is_single_hop'] + if is_single_hop != 'no_use': + conf_str += "%s" % is_single_hop + + cmd = "undo peer %s bfd enable single-hop-prefer" % peer_addr + cmds.append(cmd) + + conf_str += CE_DELETE_PEER_BFD_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete peer bfd failed.') + + return cmds + + +def main(): + """ main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + vrf_name=dict(type='str', required=True), + peer_addr=dict(type='str', required=True), + remote_as=dict(type='str', required=True), + description=dict(type='str'), + fake_as=dict(type='str'), + dual_as=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + conventional=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + route_refresh=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + four_byte_as=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + is_ignore=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + local_if_name=dict(type='str'), + ebgp_max_hop=dict(type='str'), + valid_ttl_hops=dict(type='str'), + connect_mode=dict(choices=['listenOnly', 'connectOnly', 'both']), + is_log_change=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + pswd_type=dict(choices=['null', 'cipher', 'simple']), + pswd_cipher_text=dict(type='str', no_log=True), + keep_alive_time=dict(type='str'), + hold_time=dict(type='str'), + min_hold_time=dict(type='str'), + key_chain_name=dict(type='str'), + conn_retry_time=dict(type='str'), + tcp_MSS=dict(type='str'), + mpls_local_ifnet_disable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + prepend_global_as=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + prepend_fake_as=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + is_bfd_block=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + multiplier=dict(type='str'), + is_bfd_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + rx_interval=dict(type='str'), + tx_interval=dict(type='str'), + is_single_hop=dict(type='str', default='no_use', choices=['no_use', 'true', 'false'])) + + argument_spec.update(ce_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + state = module.params['state'] + vrf_name = module.params['vrf_name'] + peer_addr = module.params['peer_addr'] + remote_as = module.params['remote_as'] + description = module.params['description'] + fake_as = module.params['fake_as'] + dual_as = module.params['dual_as'] + conventional = module.params['conventional'] + route_refresh = module.params['route_refresh'] + four_byte_as = module.params['four_byte_as'] + is_ignore = module.params['is_ignore'] + local_if_name = module.params['local_if_name'] + ebgp_max_hop = module.params['ebgp_max_hop'] + valid_ttl_hops = module.params['valid_ttl_hops'] + connect_mode = module.params['connect_mode'] + is_log_change = module.params['is_log_change'] + pswd_type = module.params['pswd_type'] + pswd_cipher_text = module.params['pswd_cipher_text'] + keep_alive_time = module.params['keep_alive_time'] + hold_time = module.params['hold_time'] + min_hold_time = module.params['min_hold_time'] + key_chain_name = module.params['key_chain_name'] + conn_retry_time = module.params['conn_retry_time'] + tcp_mss = module.params['tcp_MSS'] + mpls_local_ifnet_disable = module.params['mpls_local_ifnet_disable'] + prepend_global_as = module.params['prepend_global_as'] + prepend_fake_as = module.params['prepend_fake_as'] + is_bfd_block = module.params['is_bfd_block'] + multiplier = module.params['multiplier'] + is_bfd_enable = module.params['is_bfd_enable'] + rx_interval = module.params['rx_interval'] + tx_interval = module.params['tx_interval'] + is_single_hop = module.params['is_single_hop'] + + ce_bgp_peer_obj = BgpNeighbor() + + # get proposed + proposed["state"] = state + if vrf_name: + proposed["vrf_name"] = vrf_name + if peer_addr: + proposed["peer_addr"] = peer_addr + if remote_as: + proposed["remote_as"] = remote_as + if description: + proposed["description"] = description + if fake_as: + proposed["fake_as"] = fake_as + if dual_as != 'no_use': + proposed["dual_as"] = dual_as + if conventional != 'no_use': + proposed["conventional"] = conventional + if route_refresh != 'no_use': + proposed["route_refresh"] = route_refresh + if four_byte_as != 'no_use': + proposed["four_byte_as"] = four_byte_as + if is_ignore != 'no_use': + proposed["is_ignore"] = is_ignore + if local_if_name: + proposed["local_if_name"] = local_if_name + if ebgp_max_hop: + proposed["ebgp_max_hop"] = ebgp_max_hop + if valid_ttl_hops: + proposed["valid_ttl_hops"] = valid_ttl_hops + if connect_mode: + proposed["connect_mode"] = connect_mode + if is_log_change != 'no_use': + proposed["is_log_change"] = is_log_change + if pswd_type: + proposed["pswd_type"] = pswd_type + if pswd_cipher_text: + proposed["pswd_cipher_text"] = pswd_cipher_text + if keep_alive_time: + proposed["keep_alive_time"] = keep_alive_time + if hold_time: + proposed["hold_time"] = hold_time + if min_hold_time: + proposed["min_hold_time"] = min_hold_time + if key_chain_name: + proposed["key_chain_name"] = key_chain_name + if conn_retry_time: + proposed["conn_retry_time"] = conn_retry_time + if tcp_mss: + proposed["tcp_MSS"] = tcp_mss + if mpls_local_ifnet_disable != 'no_use': + proposed["mpls_local_ifnet_disable"] = mpls_local_ifnet_disable + if prepend_global_as != 'no_use': + proposed["prepend_global_as"] = prepend_global_as + if prepend_fake_as != 'no_use': + proposed["prepend_fake_as"] = prepend_fake_as + if is_bfd_block != 'no_use': + proposed["is_bfd_block"] = is_bfd_block + if multiplier: + proposed["multiplier"] = multiplier + if is_bfd_enable != 'no_use': + proposed["is_bfd_enable"] = is_bfd_enable + if rx_interval: + proposed["rx_interval"] = rx_interval + if tx_interval: + proposed["tx_interval"] = tx_interval + if is_single_hop != 'no_use': + proposed["is_single_hop"] = is_single_hop + + if not ce_bgp_peer_obj: + module.fail_json(msg='Error: Init module failed.') + + need_bgp_peer_enable = ce_bgp_peer_obj.check_bgp_peer_args(module=module) + need_bgp_peer_other_rst = ce_bgp_peer_obj.check_bgp_peer_other_args( + module=module) + need_peer_bfd_merge_rst = ce_bgp_peer_obj.check_peer_bfd_merge_args( + module=module) + need_peer_bfd_del_rst = ce_bgp_peer_obj.check_peer_bfd_delete_args( + module=module) + + # bgp peer config + if need_bgp_peer_enable["need_cfg"]: + + if state == "present": + + if remote_as: + + bgp_peer_exist = ce_bgp_peer_obj.get_bgp_peer(module=module) + existing["bgp peer"] = bgp_peer_exist + + bgp_peer_new = (peer_addr, remote_as) + if len(bgp_peer_exist) == 0: + cmd = ce_bgp_peer_obj.create_bgp_peer(module=module) + changed = True + for item in cmd: + updates.append(item) + + elif bgp_peer_new in bgp_peer_exist: + pass + + else: + cmd = ce_bgp_peer_obj.merge_bgp_peer(module=module) + changed = True + for item in cmd: + updates.append(item) + + bgp_peer_end = ce_bgp_peer_obj.get_bgp_peer(module=module) + end_state["bgp peer"] = bgp_peer_end + + else: + + bgp_peer_exist = ce_bgp_peer_obj.get_bgp_del_peer(module=module) + existing["bgp peer"] = bgp_peer_exist + + bgp_peer_new = (peer_addr) + + if len(bgp_peer_exist) == 0: + pass + + elif bgp_peer_new in bgp_peer_exist: + cmd = ce_bgp_peer_obj.delete_bgp_peer(module=module) + changed = True + for item in cmd: + updates.append(item) + + bgp_peer_end = ce_bgp_peer_obj.get_bgp_del_peer(module=module) + end_state["bgp peer"] = bgp_peer_end + + # bgp peer other args + exist_tmp = dict() + for item in need_bgp_peer_other_rst: + if item != "need_cfg": + exist_tmp[item] = need_bgp_peer_other_rst[item] + if exist_tmp: + existing["bgp peer other"] = exist_tmp + + if need_bgp_peer_other_rst["need_cfg"]: + + if state == "present": + cmd = ce_bgp_peer_obj.merge_bgp_peer_other(module=module) + changed = True + for item in cmd: + updates.append(item) + + need_bgp_peer_other_rst = ce_bgp_peer_obj.check_bgp_peer_other_args( + module=module) + end_tmp = dict() + for item in need_bgp_peer_other_rst: + if item != "need_cfg": + end_tmp[item] = need_bgp_peer_other_rst[item] + if end_tmp: + end_state["bgp peer other"] = end_tmp + + # peer bfd args + if state == "present": + exist_tmp = dict() + for item in need_peer_bfd_merge_rst: + if item != "need_cfg": + exist_tmp[item] = need_peer_bfd_merge_rst[item] + if exist_tmp: + existing["peer bfd"] = exist_tmp + + if need_peer_bfd_merge_rst["need_cfg"]: + cmd = ce_bgp_peer_obj.merge_peer_bfd(module=module) + changed = True + for item in cmd: + updates.append(item) + + need_peer_bfd_merge_rst = ce_bgp_peer_obj.check_peer_bfd_merge_args( + module=module) + end_tmp = dict() + for item in need_peer_bfd_merge_rst: + if item != "need_cfg": + end_tmp[item] = need_peer_bfd_merge_rst[item] + if end_tmp: + end_state["peer bfd"] = end_tmp + else: + exist_tmp = dict() + for item in need_peer_bfd_del_rst: + if item != "need_cfg": + exist_tmp[item] = need_peer_bfd_del_rst[item] + if exist_tmp: + existing["peer bfd"] = exist_tmp + + # has already delete with bgp peer + + need_peer_bfd_del_rst = ce_bgp_peer_obj.check_peer_bfd_delete_args( + module=module) + end_tmp = dict() + for item in need_peer_bfd_del_rst: + if item != "need_cfg": + end_tmp[item] = need_peer_bfd_del_rst[item] + if end_tmp: + end_state["peer bfd"] = end_tmp + + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_bgp_neighbor_af.py b/plugins/modules/ce_bgp_neighbor_af.py new file mode 100644 index 0000000..1b39113 --- /dev/null +++ b/plugins/modules/ce_bgp_neighbor_af.py @@ -0,0 +1,2676 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_bgp_neighbor_af +short_description: Manages BGP neighbor Address-family configuration on HUAWEI CloudEngine switches. +description: + - Manages BGP neighbor Address-family configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + vrf_name: + description: + - Name of a BGP instance. The name is a case-sensitive string of characters. + The BGP instance can be used only after the corresponding VPN instance is created. + required: true + af_type: + description: + - Address family type of a BGP instance. + required: true + choices: ['ipv4uni', 'ipv4multi', 'ipv4vpn', 'ipv6uni', 'ipv6vpn', 'evpn'] + remote_address: + description: + - IPv4 or IPv6 peer connection address. + required: true + advertise_irb: + description: + - If the value is true, advertised IRB routes are distinguished. + If the value is false, advertised IRB routes are not distinguished. + default: no_use + choices: ['no_use','true','false'] + advertise_arp: + description: + - If the value is true, advertised ARP routes are distinguished. + If the value is false, advertised ARP routes are not distinguished. + default: no_use + choices: ['no_use','true','false'] + advertise_remote_nexthop: + description: + - If the value is true, the remote next-hop attribute is advertised to peers. + If the value is false, the remote next-hop attribute is not advertised to any peers. + default: no_use + choices: ['no_use','true','false'] + advertise_community: + description: + - If the value is true, the community attribute is advertised to peers. + If the value is false, the community attribute is not advertised to peers. + default: no_use + choices: ['no_use','true','false'] + advertise_ext_community: + description: + - If the value is true, the extended community attribute is advertised to peers. + If the value is false, the extended community attribute is not advertised to peers. + default: no_use + choices: ['no_use','true','false'] + discard_ext_community: + description: + - If the value is true, the extended community attribute in the peer route information is discarded. + If the value is false, the extended community attribute in the peer route information is not discarded. + default: no_use + choices: ['no_use','true','false'] + allow_as_loop_enable: + description: + - If the value is true, repetitive local AS numbers are allowed. + If the value is false, repetitive local AS numbers are not allowed. + default: no_use + choices: ['no_use','true','false'] + allow_as_loop_limit: + description: + - Set the maximum number of repetitive local AS number. + The value is an integer ranging from 1 to 10. + keep_all_routes: + description: + - If the value is true, the system stores all route update messages received from all peers (groups) + after BGP connection setup. + If the value is false, the system stores only BGP update messages that are received from peers + and pass the configured import policy. + default: no_use + choices: ['no_use','true','false'] + nexthop_configure: + description: + - null, The next hop is not changed. + local, The next hop is changed to the local IP address. + invariable, Prevent the device from changing the next hop of each imported IGP route + when advertising it to its BGP peers. + choices: ['null', 'local', 'invariable'] + preferred_value: + description: + - Assign a preferred value for the routes learned from a specified peer. + The value is an integer ranging from 0 to 65535. + public_as_only: + description: + - If the value is true, sent BGP update messages carry only the public AS number but do not carry + private AS numbers. + If the value is false, sent BGP update messages can carry private AS numbers. + default: no_use + choices: ['no_use','true','false'] + public_as_only_force: + description: + - If the value is true, sent BGP update messages carry only the public AS number but do not carry + private AS numbers. + If the value is false, sent BGP update messages can carry private AS numbers. + default: no_use + choices: ['no_use','true','false'] + public_as_only_limited: + description: + - Limited use public as number. + default: no_use + choices: ['no_use','true','false'] + public_as_only_replace: + description: + - Private as replaced by public as number. + default: no_use + choices: ['no_use','true','false'] + public_as_only_skip_peer_as: + description: + - Public as only skip peer as. + default: no_use + choices: ['no_use','true','false'] + route_limit: + description: + - Configure the maximum number of routes that can be accepted from a peer. + The value is an integer ranging from 1 to 4294967295. + route_limit_percent: + description: + - Specify the percentage of routes when a router starts to generate an alarm. + The value is an integer ranging from 1 to 100. + route_limit_type: + description: + - Noparameter, After the number of received routes exceeds the threshold and the timeout + timer expires,no action. + AlertOnly, An alarm is generated and no additional routes will be accepted if the maximum + number of routes allowed have been received. + IdleForever, The connection that is interrupted is not automatically re-established if the + maximum number of routes allowed have been received. + IdleTimeout, After the number of received routes exceeds the threshold and the timeout timer + expires, the connection that is interrupted is automatically re-established. + choices: ['noparameter', 'alertOnly', 'idleForever', 'idleTimeout'] + route_limit_idle_timeout: + description: + - Specify the value of the idle-timeout timer to automatically reestablish the connections after + they are cut off when the number of routes exceeds the set threshold. + The value is an integer ranging from 1 to 1200. + rt_updt_interval: + description: + - Specify the minimum interval at which Update packets are sent. The value is an integer, in seconds. + The value is an integer ranging from 0 to 600. + redirect_ip: + description: + - Redirect ip. + default: no_use + choices: ['no_use','true','false'] + redirect_ip_validation: + description: + - Redirect ip validation. + default: no_use + choices: ['no_use','true','false'] + aliases: ['redirect_ip_vaildation'] + reflect_client: + description: + - If the value is true, the local device functions as the route reflector and a peer functions + as a client of the route reflector. + If the value is false, the route reflector and client functions are not configured. + default: no_use + choices: ['no_use','true','false'] + substitute_as_enable: + description: + - If the value is true, the function to replace a specified peer's AS number in the AS-Path attribute with + the local AS number is enabled. + If the value is false, the function to replace a specified peer's AS number in the AS-Path attribute with + the local AS number is disabled. + default: no_use + choices: ['no_use','true','false'] + import_rt_policy_name: + description: + - Specify the filtering policy applied to the routes learned from a peer. + The value is a string of 1 to 40 characters. + export_rt_policy_name: + description: + - Specify the filtering policy applied to the routes to be advertised to a peer. + The value is a string of 1 to 40 characters. + import_pref_filt_name: + description: + - Specify the IPv4 filtering policy applied to the routes received from a specified peer. + The value is a string of 1 to 169 characters. + export_pref_filt_name: + description: + - Specify the IPv4 filtering policy applied to the routes to be advertised to a specified peer. + The value is a string of 1 to 169 characters. + import_as_path_filter: + description: + - Apply an AS_Path-based filtering policy to the routes received from a specified peer. + The value is an integer ranging from 1 to 256. + export_as_path_filter: + description: + - Apply an AS_Path-based filtering policy to the routes to be advertised to a specified peer. + The value is an integer ranging from 1 to 256. + import_as_path_name_or_num: + description: + - A routing strategy based on the AS path list for routing received by a designated peer. + export_as_path_name_or_num: + description: + - Application of a AS path list based filtering policy to the routing of a specified peer. + import_acl_name_or_num: + description: + - Apply an IPv4 ACL-based filtering policy to the routes received from a specified peer. + The value is a string of 1 to 32 characters. + export_acl_name_or_num: + description: + - Apply an IPv4 ACL-based filtering policy to the routes to be advertised to a specified peer. + The value is a string of 1 to 32 characters. + ipprefix_orf_enable: + description: + - If the value is true, the address prefix-based Outbound Route Filter (ORF) capability is + enabled for peers. + If the value is false, the address prefix-based Outbound Route Filter (ORF) capability is + disabled for peers. + default: no_use + choices: ['no_use','true','false'] + is_nonstd_ipprefix_mod: + description: + - If the value is true, Non-standard capability codes are used during capability negotiation. + If the value is false, RFC-defined standard ORF capability codes are used during capability negotiation. + default: no_use + choices: ['no_use','true','false'] + orftype: + description: + - ORF Type. + The value is an integer ranging from 0 to 65535. + orf_mode: + description: + - ORF mode. + null, Default value. + receive, ORF for incoming packets. + send, ORF for outgoing packets. + both, ORF for incoming and outgoing packets. + choices: ['null', 'receive', 'send', 'both'] + soostring: + description: + - Configure the Site-of-Origin (SoO) extended community attribute. + The value is a string of 3 to 21 characters. + default_rt_adv_enable: + description: + - If the value is true, the function to advertise default routes to peers is enabled. + If the value is false, the function to advertise default routes to peers is disabled. + default: no_use + choices: ['no_use','true', 'false'] + default_rt_adv_policy: + description: + - Specify the name of a used policy. The value is a string. + The value is a string of 1 to 40 characters. + default_rt_match_mode: + description: + - null, Null. + matchall, Advertise the default route if all matching conditions are met. + matchany, Advertise the default route if any matching condition is met. + choices: ['null', 'matchall', 'matchany'] + add_path_mode: + description: + - null, Null. + receive, Support receiving Add-Path routes. + send, Support sending Add-Path routes. + both, Support receiving and sending Add-Path routes. + choices: ['null', 'receive', 'send', 'both'] + adv_add_path_num: + description: + - The number of addPath advertise route. + The value is an integer ranging from 2 to 64. + origin_as_valid: + description: + - If the value is true, Application results of route announcement. + If the value is false, Routing application results are not notified. + default: no_use + choices: ['no_use','true', 'false'] + vpls_enable: + description: + - If the value is true, vpls enable. + If the value is false, vpls disable. + default: no_use + choices: ['no_use','true', 'false'] + vpls_ad_disable: + description: + - If the value is true, enable vpls-ad. + If the value is false, disable vpls-ad. + default: no_use + choices: ['no_use','true', 'false'] + update_pkt_standard_compatible: + description: + - If the value is true, When the vpnv4 multicast neighbor receives and updates the message, + the message has no label. + If the value is false, When the vpnv4 multicast neighbor receives and updates the message, + the message has label. + default: no_use + choices: ['no_use','true', 'false'] +''' + +EXAMPLES = ''' + +- name: CloudEngine BGP neighbor address family test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config BGP peer Address_Family" + ce_bgp_neighbor_af: + state: present + vrf_name: js + af_type: ipv4uni + remote_address: 192.168.10.10 + nexthop_configure: local + provider: "{{ cli }}" + + - name: "Undo BGP peer Address_Family" + ce_bgp_neighbor_af: + state: absent + vrf_name: js + af_type: ipv4uni + remote_address: 192.168.10.10 + nexthop_configure: local + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"af_type": "ipv4uni", "nexthop_configure": "local", + "remote_address": "192.168.10.10", + "state": "present", "vrf_name": "js"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"bgp neighbor af": {"af_type": "ipv4uni", "remote_address": "192.168.10.10", + "vrf_name": "js"}, + "bgp neighbor af other": {"af_type": "ipv4uni", "nexthop_configure": "null", + "vrf_name": "js"}} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"bgp neighbor af": {"af_type": "ipv4uni", "remote_address": "192.168.10.10", + "vrf_name": "js"}, + "bgp neighbor af other": {"af_type": "ipv4uni", "nexthop_configure": "local", + "vrf_name": "js"}} +updates: + description: command sent to the device + returned: always + type: list + sample: ["peer 192.168.10.10 next-hop-local"] +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + +# get bgp peer af +CE_GET_BGP_PEER_AF_HEADER = """ + + + + + + %s + + + %s + + + %s +""" +CE_GET_BGP_PEER_AF_TAIL = """ + + + + + + + + + +""" + +# merge bgp peer af +CE_MERGE_BGP_PEER_AF_HEADER = """ + + + + + + %s + + + %s + + + %s +""" +CE_MERGE_BGP_PEER_AF_TAIL = """ + + + + + + + + + +""" + +# create bgp peer af +CE_CREATE_BGP_PEER_AF = """ + + + + + + %s + + + %s + + + %s + + + + + + + + + +""" + +# delete bgp peer af +CE_DELETE_BGP_PEER_AF = """ + + + + + + %s + + + %s + + + %s + + + + + + + + + +""" + + +class BgpNeighborAf(object): + """ Manages BGP neighbor Address-family configuration """ + + def netconf_get_config(self, **kwargs): + """ netconf_get_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ netconf_set_config """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = set_nc_config(module, conf_str) + + return xml_str + + def check_bgp_neighbor_af_args(self, **kwargs): + """ check_bgp_neighbor_af_args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + vrf_name = module.params['vrf_name'] + if vrf_name: + if len(vrf_name) > 31 or len(vrf_name) == 0: + module.fail_json( + msg='Error: The len of vrf_name %s is out of [1 - 31].' % vrf_name) + + state = module.params['state'] + af_type = module.params['af_type'] + remote_address = module.params['remote_address'] + + if not check_ip_addr(ipaddr=remote_address): + module.fail_json( + msg='Error: The remote_address %s is invalid.' % remote_address) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if state == "present": + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + if re_find: + result["remote_address"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if remote_address not in re_find: + need_cfg = True + else: + need_cfg = True + else: + if "" in recv_xml: + pass + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["remote_address"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] == remote_address: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def check_bgp_neighbor_af_other(self, **kwargs): + """ check_bgp_neighbor_af_other """ + + module = kwargs["module"] + result = dict() + need_cfg = False + + state = module.params['state'] + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + remote_address = module.params['remote_address'] + + if state == "absent": + result["need_cfg"] = need_cfg + return result + + advertise_irb = module.params['advertise_irb'] + if advertise_irb != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall(r'.*%s\s*' + r'(.*).*' % remote_address, recv_xml) + if re_find: + result["advertise_irb"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != advertise_irb: + need_cfg = True + else: + need_cfg = True + + advertise_arp = module.params['advertise_arp'] + if advertise_arp != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall(r'.*%s\s*' + r'.*(.*).*' % remote_address, recv_xml) + + if re_find: + result["advertise_arp"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != advertise_arp: + need_cfg = True + else: + need_cfg = True + + advertise_remote_nexthop = module.params['advertise_remote_nexthop'] + if advertise_remote_nexthop != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["advertise_remote_nexthop"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != advertise_remote_nexthop: + need_cfg = True + else: + need_cfg = True + + advertise_community = module.params['advertise_community'] + if advertise_community != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["advertise_community"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != advertise_community: + need_cfg = True + else: + need_cfg = True + + advertise_ext_community = module.params['advertise_ext_community'] + if advertise_ext_community != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["advertise_ext_community"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != advertise_ext_community: + need_cfg = True + else: + need_cfg = True + + discard_ext_community = module.params['discard_ext_community'] + if discard_ext_community != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["discard_ext_community"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != discard_ext_community: + need_cfg = True + else: + need_cfg = True + + allow_as_loop_enable = module.params['allow_as_loop_enable'] + if allow_as_loop_enable != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["allow_as_loop_enable"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != allow_as_loop_enable: + need_cfg = True + else: + need_cfg = True + + allow_as_loop_limit = module.params['allow_as_loop_limit'] + if allow_as_loop_limit: + if int(allow_as_loop_limit) > 10 or int(allow_as_loop_limit) < 1: + module.fail_json( + msg='the value of allow_as_loop_limit %s is out of [1 - 10].' % allow_as_loop_limit) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["allow_as_loop_limit"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != allow_as_loop_limit: + need_cfg = True + else: + need_cfg = True + + keep_all_routes = module.params['keep_all_routes'] + if keep_all_routes != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["keep_all_routes"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != keep_all_routes: + need_cfg = True + else: + need_cfg = True + + nexthop_configure = module.params['nexthop_configure'] + if nexthop_configure: + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + self.exist_nexthop_configure = "null" + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + self.exist_nexthop_configure = re_find[0] + result["nexthop_configure"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != nexthop_configure: + need_cfg = True + else: + need_cfg = True + + preferred_value = module.params['preferred_value'] + if preferred_value: + if int(preferred_value) > 65535 or int(preferred_value) < 0: + module.fail_json( + msg='the value of preferred_value %s is out of [0 - 65535].' % preferred_value) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["preferred_value"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != preferred_value: + need_cfg = True + else: + need_cfg = True + + public_as_only = module.params['public_as_only'] + if public_as_only != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["public_as_only"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != public_as_only: + need_cfg = True + else: + need_cfg = True + + public_as_only_force = module.params['public_as_only_force'] + if public_as_only_force != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["public_as_only_force"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != public_as_only_force: + need_cfg = True + else: + need_cfg = True + + public_as_only_limited = module.params['public_as_only_limited'] + if public_as_only_limited != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["public_as_only_limited"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != public_as_only_limited: + need_cfg = True + else: + need_cfg = True + + public_as_only_replace = module.params['public_as_only_replace'] + if public_as_only_replace != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["public_as_only_replace"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != public_as_only_replace: + need_cfg = True + else: + need_cfg = True + + public_as_only_skip_peer_as = module.params[ + 'public_as_only_skip_peer_as'] + if public_as_only_skip_peer_as != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["public_as_only_skip_peer_as"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != public_as_only_skip_peer_as: + need_cfg = True + else: + need_cfg = True + + route_limit = module.params['route_limit'] + if route_limit: + + if int(route_limit) < 1: + module.fail_json( + msg='the value of route_limit %s is out of [1 - 4294967295].' % route_limit) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["route_limit"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != route_limit: + need_cfg = True + else: + need_cfg = True + + route_limit_percent = module.params['route_limit_percent'] + if route_limit_percent: + + if int(route_limit_percent) < 1 or int(route_limit_percent) > 100: + module.fail_json( + msg='Error: The value of route_limit_percent %s is out of [1 - 100].' % route_limit_percent) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["route_limit_percent"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != route_limit_percent: + need_cfg = True + else: + need_cfg = True + + route_limit_type = module.params['route_limit_type'] + if route_limit_type: + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["route_limit_type"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != route_limit_type: + need_cfg = True + else: + need_cfg = True + + route_limit_idle_timeout = module.params['route_limit_idle_timeout'] + if route_limit_idle_timeout: + + if int(route_limit_idle_timeout) < 1 or int(route_limit_idle_timeout) > 1200: + module.fail_json( + msg='Error: The value of route_limit_idle_timeout %s is out of ' + '[1 - 1200].' % route_limit_idle_timeout) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["route_limit_idle_timeout"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != route_limit_idle_timeout: + need_cfg = True + else: + need_cfg = True + + rt_updt_interval = module.params['rt_updt_interval'] + if rt_updt_interval: + + if int(rt_updt_interval) < 0 or int(rt_updt_interval) > 600: + module.fail_json( + msg='Error: The value of rt_updt_interval %s is out of [0 - 600].' % rt_updt_interval) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["rt_updt_interval"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != rt_updt_interval: + need_cfg = True + else: + need_cfg = True + + redirect_ip = module.params['redirect_ip'] + if redirect_ip != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["redirect_ip"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != redirect_ip: + need_cfg = True + else: + need_cfg = True + + redirect_ip_validation = module.params['redirect_ip_validation'] + if redirect_ip_validation != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["redirect_ip_validation"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != redirect_ip_validation: + need_cfg = True + else: + need_cfg = True + + reflect_client = module.params['reflect_client'] + if reflect_client != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["reflect_client"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != reflect_client: + need_cfg = True + else: + need_cfg = True + + substitute_as_enable = module.params['substitute_as_enable'] + if substitute_as_enable != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["substitute_as_enable"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != substitute_as_enable: + need_cfg = True + else: + need_cfg = True + + import_rt_policy_name = module.params['import_rt_policy_name'] + if import_rt_policy_name: + + if len(import_rt_policy_name) < 1 or len(import_rt_policy_name) > 40: + module.fail_json( + msg='Error: The len of import_rt_policy_name %s is out of [1 - 40].' % import_rt_policy_name) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["import_rt_policy_name"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != import_rt_policy_name: + need_cfg = True + else: + need_cfg = True + + export_rt_policy_name = module.params['export_rt_policy_name'] + if export_rt_policy_name: + + if len(export_rt_policy_name) < 1 or len(export_rt_policy_name) > 40: + module.fail_json( + msg='Error: The len of export_rt_policy_name %s is out of [1 - 40].' % export_rt_policy_name) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["export_rt_policy_name"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != export_rt_policy_name: + need_cfg = True + else: + need_cfg = True + + import_pref_filt_name = module.params['import_pref_filt_name'] + if import_pref_filt_name: + + if len(import_pref_filt_name) < 1 or len(import_pref_filt_name) > 169: + module.fail_json( + msg='Error: The len of import_pref_filt_name %s is out of [1 - 169].' % import_pref_filt_name) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["import_pref_filt_name"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != import_pref_filt_name: + need_cfg = True + else: + need_cfg = True + + export_pref_filt_name = module.params['export_pref_filt_name'] + if export_pref_filt_name: + + if len(export_pref_filt_name) < 1 or len(export_pref_filt_name) > 169: + module.fail_json( + msg='Error: The len of export_pref_filt_name %s is out of [1 - 169].' % export_pref_filt_name) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["export_pref_filt_name"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != export_pref_filt_name: + need_cfg = True + else: + need_cfg = True + + import_as_path_filter = module.params['import_as_path_filter'] + if import_as_path_filter: + + if int(import_as_path_filter) < 1 or int(import_as_path_filter) > 256: + module.fail_json( + msg='Error: The value of import_as_path_filter %s is out of [1 - 256].' % import_as_path_filter) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["import_as_path_filter"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != import_as_path_filter: + need_cfg = True + else: + need_cfg = True + + export_as_path_filter = module.params['export_as_path_filter'] + if export_as_path_filter: + + if int(export_as_path_filter) < 1 or int(export_as_path_filter) > 256: + module.fail_json( + msg='Error: The value of export_as_path_filter %s is out of [1 - 256].' % export_as_path_filter) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["export_as_path_filter"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != export_as_path_filter: + need_cfg = True + else: + need_cfg = True + + import_as_path_name_or_num = module.params[ + 'import_as_path_name_or_num'] + if import_as_path_name_or_num: + + if len(import_as_path_name_or_num) < 1 or len(import_as_path_name_or_num) > 51: + module.fail_json( + msg='Error: The len of import_as_path_name_or_num %s is out ' + 'of [1 - 51].' % import_as_path_name_or_num) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["import_as_path_name_or_num"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != import_as_path_name_or_num: + need_cfg = True + else: + need_cfg = True + + export_as_path_name_or_num = module.params[ + 'export_as_path_name_or_num'] + if export_as_path_name_or_num: + + if len(export_as_path_name_or_num) < 1 or len(export_as_path_name_or_num) > 51: + module.fail_json( + msg='Error: The len of export_as_path_name_or_num %s is out ' + 'of [1 - 51].' % export_as_path_name_or_num) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["export_as_path_name_or_num"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != export_as_path_name_or_num: + need_cfg = True + else: + need_cfg = True + + import_acl_name_or_num = module.params['import_acl_name_or_num'] + if import_acl_name_or_num: + + if len(import_acl_name_or_num) < 1 or len(import_acl_name_or_num) > 32: + module.fail_json( + msg='Error: The len of import_acl_name_or_num %s is out of [1 - 32].' % import_acl_name_or_num) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["import_acl_name_or_num"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != import_acl_name_or_num: + need_cfg = True + else: + need_cfg = True + + export_acl_name_or_num = module.params['export_acl_name_or_num'] + if export_acl_name_or_num: + + if len(export_acl_name_or_num) < 1 or len(export_acl_name_or_num) > 32: + module.fail_json( + msg='Error: The len of export_acl_name_or_num %s is out of [1 - 32].' % export_acl_name_or_num) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["export_acl_name_or_num"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != export_acl_name_or_num: + need_cfg = True + else: + need_cfg = True + + ipprefix_orf_enable = module.params['ipprefix_orf_enable'] + if ipprefix_orf_enable != 'no_use': + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["ipprefix_orf_enable"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != ipprefix_orf_enable: + need_cfg = True + else: + need_cfg = True + + is_nonstd_ipprefix_mod = module.params['is_nonstd_ipprefix_mod'] + if is_nonstd_ipprefix_mod != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["is_nonstd_ipprefix_mod"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != is_nonstd_ipprefix_mod: + need_cfg = True + else: + need_cfg = True + + orftype = module.params['orftype'] + if orftype: + + if int(orftype) < 0 or int(orftype) > 65535: + module.fail_json( + msg='Error: The value of orftype %s is out of [0 - 65535].' % orftype) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["orftype"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != orftype: + need_cfg = True + else: + need_cfg = True + + orf_mode = module.params['orf_mode'] + if orf_mode: + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["orf_mode"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != orf_mode: + need_cfg = True + else: + need_cfg = True + + soostring = module.params['soostring'] + if soostring: + + if len(soostring) < 3 or len(soostring) > 21: + module.fail_json( + msg='Error: The len of soostring %s is out of [3 - 21].' % soostring) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["soostring"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != soostring: + need_cfg = True + else: + need_cfg = True + + default_rt_adv_enable = module.params['default_rt_adv_enable'] + if default_rt_adv_enable != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_rt_adv_enable"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != default_rt_adv_enable: + need_cfg = True + else: + need_cfg = True + + default_rt_adv_policy = module.params['default_rt_adv_policy'] + if default_rt_adv_policy: + + if len(default_rt_adv_policy) < 1 or len(default_rt_adv_policy) > 40: + module.fail_json( + msg='Error: The len of default_rt_adv_policy %s is out of [1 - 40].' % default_rt_adv_policy) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_rt_adv_policy"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != default_rt_adv_policy: + need_cfg = True + else: + need_cfg = True + + default_rt_match_mode = module.params['default_rt_match_mode'] + if default_rt_match_mode: + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["default_rt_match_mode"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != default_rt_match_mode: + need_cfg = True + else: + need_cfg = True + + add_path_mode = module.params['add_path_mode'] + if add_path_mode: + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["add_path_mode"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != add_path_mode: + need_cfg = True + else: + need_cfg = True + + adv_add_path_num = module.params['adv_add_path_num'] + if adv_add_path_num: + + if int(adv_add_path_num) < 2 or int(adv_add_path_num) > 64: + module.fail_json( + msg='Error: The value of adv_add_path_num %s is out of [2 - 64].' % adv_add_path_num) + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["adv_add_path_num"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != adv_add_path_num: + need_cfg = True + else: + need_cfg = True + + origin_as_valid = module.params['origin_as_valid'] + if origin_as_valid != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["origin_as_valid"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != origin_as_valid: + need_cfg = True + else: + need_cfg = True + + vpls_enable = module.params['vpls_enable'] + if vpls_enable != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["vpls_enable"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != vpls_enable: + need_cfg = True + else: + need_cfg = True + + vpls_ad_disable = module.params['vpls_ad_disable'] + if vpls_ad_disable != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["vpls_ad_disable"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != vpls_ad_disable: + need_cfg = True + else: + need_cfg = True + + update_pkt_standard_compatible = module.params[ + 'update_pkt_standard_compatible'] + if update_pkt_standard_compatible != 'no_use': + + conf_str = CE_GET_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + "" + \ + CE_GET_BGP_PEER_AF_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + need_cfg = True + else: + re_find = re.findall( + r'.*(.*).*', recv_xml) + + if re_find: + result["update_pkt_standard_compatible"] = re_find + result["vrf_name"] = vrf_name + result["af_type"] = af_type + if re_find[0] != update_pkt_standard_compatible: + need_cfg = True + else: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def merge_bgp_peer_af(self, **kwargs): + """ merge_bgp_peer_af """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + remote_address = module.params['remote_address'] + + conf_str = CE_MERGE_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + CE_MERGE_BGP_PEER_AF_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp peer address family failed.') + + cmds = [] + cmd = af_type + if af_type == "ipv4uni": + if vrf_name == "_public_": + cmd = "ipv4-family unicast" + else: + cmd = "ipv4-family vpn-instance %s" % vrf_name + elif af_type == "ipv4multi": + cmd = "ipv4-family multicast" + elif af_type == "ipv6uni": + if vrf_name == "_public_": + cmd = "ipv6-family unicast" + else: + cmd = "ipv6-family vpn-instance %s" % vrf_name + elif af_type == "evpn": + cmd = "l2vpn-family evpn" + elif af_type == "ipv4vpn": + cmd = "ipv4-family vpnv4" + elif af_type == "ipv6vpn": + cmd = "ipv6-family vpnv6" + cmds.append(cmd) + if vrf_name == "_public_": + cmd = "peer %s enable" % remote_address + else: + cmd = "peer %s" % remote_address + cmds.append(cmd) + + return cmds + + def create_bgp_peer_af(self, **kwargs): + """ create_bgp_peer_af """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + remote_address = module.params['remote_address'] + + conf_str = CE_CREATE_BGP_PEER_AF % (vrf_name, af_type, remote_address) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create bgp peer address family failed.') + + cmds = [] + cmd = af_type + if af_type == "ipv4uni": + if vrf_name == "_public_": + cmd = "ipv4-family unicast" + else: + cmd = "ipv4-family vpn-instance %s" % vrf_name + elif af_type == "ipv4multi": + cmd = "ipv4-family multicast" + elif af_type == "ipv6uni": + if vrf_name == "_public_": + cmd = "ipv6-family unicast" + else: + cmd = "ipv6-family vpn-instance %s" % vrf_name + elif af_type == "evpn": + cmd = "l2vpn-family evpn" + elif af_type == "ipv4vpn": + cmd = "ipv4-family vpnv4" + elif af_type == "ipv6vpn": + cmd = "ipv6-family vpnv6" + cmds.append(cmd) + if vrf_name == "_public_": + cmd = "peer %s enable" % remote_address + else: + cmd = "peer %s" % remote_address + cmds.append(cmd) + + return cmds + + def delete_bgp_peer_af(self, **kwargs): + """ delete_bgp_peer_af """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + remote_address = module.params['remote_address'] + + conf_str = CE_DELETE_BGP_PEER_AF % (vrf_name, af_type, remote_address) + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete bgp peer address family failed.') + + cmds = [] + cmd = af_type + if af_type == "ipv4uni": + if vrf_name == "_public_": + cmd = "ipv4-family unicast" + else: + cmd = "ipv4-family vpn-instance %s" % vrf_name + elif af_type == "ipv4multi": + cmd = "ipv4-family multicast" + elif af_type == "ipv6uni": + if vrf_name == "_public_": + cmd = "ipv6-family unicast" + else: + cmd = "ipv6-family vpn-instance %s" % vrf_name + elif af_type == "evpn": + cmd = "l2vpn-family evpn" + elif af_type == "ipv4vpn": + cmd = "ipv4-family vpnv4" + elif af_type == "ipv6vpn": + cmd = "ipv6-family vpnv6" + cmds.append(cmd) + if vrf_name == "_public_": + cmd = "undo peer %s enable" % remote_address + else: + cmd = "undo peer %s" % remote_address + cmds.append(cmd) + + return cmds + + def merge_bgp_peer_af_other(self, **kwargs): + """ merge_bgp_peer_af_other """ + + module = kwargs["module"] + + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + remote_address = module.params['remote_address'] + + conf_str = CE_MERGE_BGP_PEER_AF_HEADER % ( + vrf_name, af_type, remote_address) + + cmds = [] + + advertise_irb = module.params['advertise_irb'] + if advertise_irb != 'no_use': + conf_str += "%s" % advertise_irb + + if advertise_irb == "true": + cmd = "peer %s advertise irb" % remote_address + else: + cmd = "undo peer %s advertise irb" % remote_address + cmds.append(cmd) + + advertise_arp = module.params['advertise_arp'] + if advertise_arp != 'no_use': + conf_str += "%s" % advertise_arp + + if advertise_arp == "true": + cmd = "peer %s advertise arp" % remote_address + else: + cmd = "undo peer %s advertise arp" % remote_address + cmds.append(cmd) + + advertise_remote_nexthop = module.params['advertise_remote_nexthop'] + if advertise_remote_nexthop != 'no_use': + conf_str += "%s" % advertise_remote_nexthop + + if advertise_remote_nexthop == "true": + cmd = "peer %s advertise remote-nexthop" % remote_address + else: + cmd = "undo peer %s advertise remote-nexthop" % remote_address + cmds.append(cmd) + + advertise_community = module.params['advertise_community'] + if advertise_community != 'no_use': + conf_str += "%s" % advertise_community + + if advertise_community == "true": + cmd = "peer %s advertise-community" % remote_address + else: + cmd = "undo peer %s advertise-community" % remote_address + cmds.append(cmd) + + advertise_ext_community = module.params['advertise_ext_community'] + if advertise_ext_community != 'no_use': + conf_str += "%s" % advertise_ext_community + + if advertise_ext_community == "true": + cmd = "peer %s advertise-ext-community" % remote_address + else: + cmd = "undo peer %s advertise-ext-community" % remote_address + cmds.append(cmd) + + discard_ext_community = module.params['discard_ext_community'] + if discard_ext_community != 'no_use': + conf_str += "%s" % discard_ext_community + + if discard_ext_community == "true": + cmd = "peer %s discard-ext-community" % remote_address + else: + cmd = "undo peer %s discard-ext-community" % remote_address + cmds.append(cmd) + + allow_as_loop_enable = module.params['allow_as_loop_enable'] + if allow_as_loop_enable != 'no_use': + conf_str += "%s" % allow_as_loop_enable + + if allow_as_loop_enable == "true": + cmd = "peer %s allow-as-loop" % remote_address + else: + cmd = "undo peer %s allow-as-loop" % remote_address + cmds.append(cmd) + + allow_as_loop_limit = module.params['allow_as_loop_limit'] + if allow_as_loop_limit: + conf_str += "%s" % allow_as_loop_limit + + if allow_as_loop_enable == "true": + cmd = "peer %s allow-as-loop %s" % (remote_address, allow_as_loop_limit) + else: + cmd = "undo peer %s allow-as-loop" % remote_address + cmds.append(cmd) + + keep_all_routes = module.params['keep_all_routes'] + if keep_all_routes != 'no_use': + conf_str += "%s" % keep_all_routes + + if keep_all_routes == "true": + cmd = "peer %s keep-all-routes" % remote_address + else: + cmd = "undo peer %s keep-all-routes" % remote_address + cmds.append(cmd) + + nexthop_configure = module.params['nexthop_configure'] + if nexthop_configure: + conf_str += "%s" % nexthop_configure + + if nexthop_configure == "local": + cmd = "peer %s next-hop-local" % remote_address + cmds.append(cmd) + elif nexthop_configure == "invariable": + cmd = "peer %s next-hop-invariable" % remote_address + cmds.append(cmd) + else: + if self.exist_nexthop_configure != "null": + if self.exist_nexthop_configure == "local": + cmd = "undo peer %s next-hop-local" % remote_address + cmds.append(cmd) + elif self.exist_nexthop_configure == "invariable": + cmd = "undo peer %s next-hop-invariable" % remote_address + cmds.append(cmd) + preferred_value = module.params['preferred_value'] + if preferred_value: + conf_str += "%s" % preferred_value + + cmd = "peer %s preferred-value %s" % (remote_address, preferred_value) + cmds.append(cmd) + + public_as_only = module.params['public_as_only'] + if public_as_only != 'no_use': + conf_str += "%s" % public_as_only + + if public_as_only == "true": + cmd = "peer %s public-as-only" % remote_address + else: + cmd = "undo peer %s public-as-only" % remote_address + cmds.append(cmd) + + public_as_only_force = module.params['public_as_only_force'] + if public_as_only_force != 'no_use': + conf_str += "%s" % public_as_only_force + + if public_as_only_force == "true": + cmd = "peer %s public-as-only force" % remote_address + else: + cmd = "undo peer %s public-as-only force" % remote_address + cmds.append(cmd) + + public_as_only_limited = module.params['public_as_only_limited'] + if public_as_only_limited != 'no_use': + conf_str += "%s" % public_as_only_limited + + if public_as_only_limited == "true": + cmd = "peer %s public-as-only limited" % remote_address + else: + cmd = "undo peer %s public-as-only limited" % remote_address + cmds.append(cmd) + + public_as_only_replace = module.params['public_as_only_replace'] + if public_as_only_replace != 'no_use': + conf_str += "%s" % public_as_only_replace + + if public_as_only_replace == "true": + if public_as_only_force != "no_use": + cmd = "peer %s public-as-only force replace" % remote_address + if public_as_only_limited != "no_use": + cmd = "peer %s public-as-only limited replace" % remote_address + else: + if public_as_only_force != "no_use": + cmd = "undo peer %s public-as-only force replace" % remote_address + if public_as_only_limited != "no_use": + cmd = "undo peer %s public-as-only limited replace" % remote_address + cmds.append(cmd) + + public_as_only_skip_peer_as = module.params[ + 'public_as_only_skip_peer_as'] + if public_as_only_skip_peer_as != 'no_use': + conf_str += "%s" % public_as_only_skip_peer_as + + if public_as_only_skip_peer_as == "true": + if public_as_only_force != "no_use": + cmd = "peer %s public-as-only force include-peer-as" % remote_address + if public_as_only_limited != "no_use": + cmd = "peer %s public-as-only limited include-peer-as" % remote_address + else: + if public_as_only_force != "no_use": + cmd = "undo peer %s public-as-only force include-peer-as" % remote_address + if public_as_only_limited != "no_use": + cmd = "undo peer %s public-as-only limited include-peer-as" % remote_address + cmds.append(cmd) + + route_limit_sign = "route-limit" + if af_type == "evpn": + route_limit_sign = "mac-limit" + route_limit = module.params['route_limit'] + if route_limit: + conf_str += "%s" % route_limit + + cmd = "peer %s %s %s" % (remote_address, route_limit_sign, route_limit) + cmds.append(cmd) + + route_limit_percent = module.params['route_limit_percent'] + if route_limit_percent: + conf_str += "%s" % route_limit_percent + + cmd = "peer %s %s %s %s" % (remote_address, route_limit_sign, route_limit, route_limit_percent) + cmds.append(cmd) + + route_limit_type = module.params['route_limit_type'] + if route_limit_type: + conf_str += "%s" % route_limit_type + + if route_limit_type == "alertOnly": + cmd = "peer %s %s %s %s alert-only" % (remote_address, route_limit_sign, route_limit, route_limit_percent) + cmds.append(cmd) + elif route_limit_type == "idleForever": + cmd = "peer %s %s %s %s idle-forever" % (remote_address, route_limit_sign, route_limit, route_limit_percent) + cmds.append(cmd) + elif route_limit_type == "idleTimeout": + cmd = "peer %s %s %s %s idle-timeout" % (remote_address, route_limit_sign, route_limit, route_limit_percent) + cmds.append(cmd) + + route_limit_idle_timeout = module.params['route_limit_idle_timeout'] + if route_limit_idle_timeout: + conf_str += "%s" % route_limit_idle_timeout + + cmd = "peer %s %s %s %s idle-timeout %s" % (remote_address, route_limit_sign, route_limit, route_limit_percent, route_limit_idle_timeout) + cmds.append(cmd) + + rt_updt_interval = module.params['rt_updt_interval'] + if rt_updt_interval: + conf_str += "%s" % rt_updt_interval + + cmd = "peer %s route-update-interval %s" % (remote_address, rt_updt_interval) + cmds.append(cmd) + + redirect_ip = module.params['redirect_ip'] + if redirect_ip != 'no_use': + conf_str += "%s" % redirect_ip + + redirect_ip_validation = module.params['redirect_ip_validation'] + if redirect_ip_validation != 'no_use': + conf_str += "%s" % redirect_ip_validation + + reflect_client = module.params['reflect_client'] + if reflect_client != 'no_use': + conf_str += "%s" % reflect_client + + if reflect_client == "true": + cmd = "peer %s reflect-client" % remote_address + else: + cmd = "undo peer %s reflect-client" % remote_address + cmds.append(cmd) + + substitute_as_enable = module.params['substitute_as_enable'] + if substitute_as_enable != 'no_use': + conf_str += "%s" % substitute_as_enable + + if substitute_as_enable == "true": + cmd = "peer %s substitute-as" % remote_address + else: + cmd = "undo peer %s substitute-as" % remote_address + cmds.append(cmd) + + import_rt_policy_name = module.params['import_rt_policy_name'] + if import_rt_policy_name: + conf_str += "%s" % import_rt_policy_name + + cmd = "peer %s route-policy %s import" % (remote_address, import_rt_policy_name) + cmds.append(cmd) + + export_rt_policy_name = module.params['export_rt_policy_name'] + if export_rt_policy_name: + conf_str += "%s" % export_rt_policy_name + + cmd = "peer %s route-policy %s export" % (remote_address, export_rt_policy_name) + cmds.append(cmd) + + import_pref_filt_name = module.params['import_pref_filt_name'] + if import_pref_filt_name: + conf_str += "%s" % import_pref_filt_name + + cmd = "peer %s ip-prefix %s import" % (remote_address, import_pref_filt_name) + cmds.append(cmd) + + export_pref_filt_name = module.params['export_pref_filt_name'] + if export_pref_filt_name: + conf_str += "%s" % export_pref_filt_name + + cmd = "peer %s ip-prefix %s export" % (remote_address, export_pref_filt_name) + cmds.append(cmd) + + import_as_path_filter = module.params['import_as_path_filter'] + if import_as_path_filter: + conf_str += "%s" % import_as_path_filter + + cmd = "peer %s as-path-filter %s import" % (remote_address, import_as_path_filter) + cmds.append(cmd) + + export_as_path_filter = module.params['export_as_path_filter'] + if export_as_path_filter: + conf_str += "%s" % export_as_path_filter + + cmd = "peer %s as-path-filter %s export" % (remote_address, export_as_path_filter) + cmds.append(cmd) + + import_as_path_name_or_num = module.params[ + 'import_as_path_name_or_num'] + if import_as_path_name_or_num: + conf_str += "%s" % import_as_path_name_or_num + + cmd = "peer %s as-path-filter %s import" % (remote_address, import_as_path_name_or_num) + cmds.append(cmd) + + export_as_path_name_or_num = module.params[ + 'export_as_path_name_or_num'] + if export_as_path_name_or_num: + conf_str += "%s" % export_as_path_name_or_num + + cmd = "peer %s as-path-filter %s export" % (remote_address, export_as_path_name_or_num) + cmds.append(cmd) + + import_acl_name_or_num = module.params['import_acl_name_or_num'] + if import_acl_name_or_num: + conf_str += "%s" % import_acl_name_or_num + if import_acl_name_or_num.isdigit(): + cmd = "peer %s filter-policy %s import" % (remote_address, import_acl_name_or_num) + else: + cmd = "peer %s filter-policy acl-name %s import" % (remote_address, import_acl_name_or_num) + cmds.append(cmd) + + export_acl_name_or_num = module.params['export_acl_name_or_num'] + if export_acl_name_or_num: + conf_str += "%s" % export_acl_name_or_num + if export_acl_name_or_num.isdigit(): + cmd = "peer %s filter-policy %s export" % (remote_address, export_acl_name_or_num) + else: + cmd = "peer %s filter-policy acl-name %s export" % (remote_address, export_acl_name_or_num) + cmds.append(cmd) + + ipprefix_orf_enable = module.params['ipprefix_orf_enable'] + if ipprefix_orf_enable != 'no_use': + conf_str += "%s" % ipprefix_orf_enable + + if ipprefix_orf_enable == "true": + cmd = "peer %s capability-advertise orf ip-prefix" % remote_address + else: + cmd = "undo peer %s capability-advertise orf ip-prefix" % remote_address + cmds.append(cmd) + + is_nonstd_ipprefix_mod = module.params['is_nonstd_ipprefix_mod'] + if is_nonstd_ipprefix_mod != 'no_use': + conf_str += "%s" % is_nonstd_ipprefix_mod + + if is_nonstd_ipprefix_mod == "true": + if ipprefix_orf_enable == "true": + cmd = "peer %s capability-advertise orf non-standard-compatible" % remote_address + else: + cmd = "undo peer %s capability-advertise orf non-standard-compatible" % remote_address + cmds.append(cmd) + else: + if ipprefix_orf_enable == "true": + cmd = "peer %s capability-advertise orf" % remote_address + else: + cmd = "undo peer %s capability-advertise orf" % remote_address + cmds.append(cmd) + + orftype = module.params['orftype'] + if orftype: + conf_str += "%s" % orftype + + orf_mode = module.params['orf_mode'] + if orf_mode: + conf_str += "%s" % orf_mode + + if ipprefix_orf_enable == "true": + cmd = "peer %s capability-advertise orf ip-prefix %s" % (remote_address, orf_mode) + else: + cmd = "undo peer %s capability-advertise orf ip-prefix %s" % (remote_address, orf_mode) + cmds.append(cmd) + + soostring = module.params['soostring'] + if soostring: + conf_str += "%s" % soostring + + cmd = "peer %s soo %s" % (remote_address, soostring) + cmds.append(cmd) + + cmd = "" + default_rt_adv_enable = module.params['default_rt_adv_enable'] + if default_rt_adv_enable != 'no_use': + conf_str += "%s" % default_rt_adv_enable + + if default_rt_adv_enable == "true": + cmd += "peer %s default-route-advertise" % remote_address + else: + cmd += "undo peer %s default-route-advertise" % remote_address + + default_rt_adv_policy = module.params['default_rt_adv_policy'] + if default_rt_adv_policy: + conf_str += "%s" % default_rt_adv_policy + cmd += " route-policy %s" % default_rt_adv_policy + + default_rt_match_mode = module.params['default_rt_match_mode'] + if default_rt_match_mode: + conf_str += "%s" % default_rt_match_mode + + if default_rt_match_mode == "matchall": + cmd += " conditional-route-match-all" + elif default_rt_match_mode == "matchany": + cmd += " conditional-route-match-any" + + if cmd: + cmds.append(cmd) + + add_path_mode = module.params['add_path_mode'] + if add_path_mode: + conf_str += "%s" % add_path_mode + if add_path_mode == "receive": + cmd = "peer %s capability-advertise add-path receive" % remote_address + elif add_path_mode == "send": + cmd = "peer %s capability-advertise add-path send" % remote_address + elif add_path_mode == "both": + cmd = "peer %s capability-advertise add-path both" % remote_address + cmds.append(cmd) + + adv_add_path_num = module.params['adv_add_path_num'] + if adv_add_path_num: + conf_str += "%s" % adv_add_path_num + cmd = "peer %s advertise add-path path-number %s" % (remote_address, adv_add_path_num) + cmds.append(cmd) + origin_as_valid = module.params['origin_as_valid'] + if origin_as_valid != 'no_use': + conf_str += "%s" % origin_as_valid + + vpls_enable = module.params['vpls_enable'] + if vpls_enable != 'no_use': + conf_str += "%s" % vpls_enable + + vpls_ad_disable = module.params['vpls_ad_disable'] + if vpls_ad_disable != 'no_use': + conf_str += "%s" % vpls_ad_disable + + update_pkt_standard_compatible = module.params[ + 'update_pkt_standard_compatible'] + if update_pkt_standard_compatible != 'no_use': + conf_str += "%s" % update_pkt_standard_compatible + + conf_str += CE_MERGE_BGP_PEER_AF_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge bgp peer address family other failed.') + + return cmds + + +def main(): + """ main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + vrf_name=dict(type='str', required=True), + af_type=dict(choices=['ipv4uni', 'ipv4multi', 'ipv4vpn', + 'ipv6uni', 'ipv6vpn', 'evpn'], required=True), + remote_address=dict(type='str', required=True), + advertise_irb=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + advertise_arp=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + advertise_remote_nexthop=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + advertise_community=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + advertise_ext_community=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + discard_ext_community=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + allow_as_loop_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + allow_as_loop_limit=dict(type='str'), + keep_all_routes=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + nexthop_configure=dict(choices=['null', 'local', 'invariable']), + preferred_value=dict(type='str'), + public_as_only=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + public_as_only_force=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + public_as_only_limited=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + public_as_only_replace=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + public_as_only_skip_peer_as=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + route_limit=dict(type='str'), + route_limit_percent=dict(type='str'), + route_limit_type=dict( + choices=['noparameter', 'alertOnly', 'idleForever', 'idleTimeout']), + route_limit_idle_timeout=dict(type='str'), + rt_updt_interval=dict(type='str'), + redirect_ip=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + redirect_ip_validation=dict( + type='str', default='no_use', + choices=['no_use', 'true', 'false'], aliases=['redirect_ip_vaildation']), + reflect_client=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + substitute_as_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + import_rt_policy_name=dict(type='str'), + export_rt_policy_name=dict(type='str'), + import_pref_filt_name=dict(type='str'), + export_pref_filt_name=dict(type='str'), + import_as_path_filter=dict(type='str'), + export_as_path_filter=dict(type='str'), + import_as_path_name_or_num=dict(type='str'), + export_as_path_name_or_num=dict(type='str'), + import_acl_name_or_num=dict(type='str'), + export_acl_name_or_num=dict(type='str'), + ipprefix_orf_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + is_nonstd_ipprefix_mod=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + orftype=dict(type='str'), + orf_mode=dict(choices=['null', 'receive', 'send', 'both']), + soostring=dict(type='str'), + default_rt_adv_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + default_rt_adv_policy=dict(type='str'), + default_rt_match_mode=dict(choices=['null', 'matchall', 'matchany']), + add_path_mode=dict(choices=['null', 'receive', 'send', 'both']), + adv_add_path_num=dict(type='str'), + origin_as_valid=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + vpls_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + vpls_ad_disable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + update_pkt_standard_compatible=dict(type='str', default='no_use', choices=['no_use', 'true', 'false'])) + + argument_spec.update(ce_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + state = module.params['state'] + vrf_name = module.params['vrf_name'] + af_type = module.params['af_type'] + remote_address = module.params['remote_address'] + advertise_irb = module.params['advertise_irb'] + advertise_arp = module.params['advertise_arp'] + advertise_remote_nexthop = module.params['advertise_remote_nexthop'] + advertise_community = module.params['advertise_community'] + advertise_ext_community = module.params['advertise_ext_community'] + discard_ext_community = module.params['discard_ext_community'] + allow_as_loop_enable = module.params['allow_as_loop_enable'] + allow_as_loop_limit = module.params['allow_as_loop_limit'] + keep_all_routes = module.params['keep_all_routes'] + nexthop_configure = module.params['nexthop_configure'] + preferred_value = module.params['preferred_value'] + public_as_only = module.params['public_as_only'] + public_as_only_force = module.params['public_as_only_force'] + public_as_only_limited = module.params['public_as_only_limited'] + public_as_only_replace = module.params['public_as_only_replace'] + public_as_only_skip_peer_as = module.params['public_as_only_skip_peer_as'] + route_limit = module.params['route_limit'] + route_limit_percent = module.params['route_limit_percent'] + route_limit_type = module.params['route_limit_type'] + route_limit_idle_timeout = module.params['route_limit_idle_timeout'] + rt_updt_interval = module.params['rt_updt_interval'] + redirect_ip = module.params['redirect_ip'] + redirect_ip_validation = module.params['redirect_ip_validation'] + reflect_client = module.params['reflect_client'] + substitute_as_enable = module.params['substitute_as_enable'] + import_rt_policy_name = module.params['import_rt_policy_name'] + export_rt_policy_name = module.params['export_rt_policy_name'] + import_pref_filt_name = module.params['import_pref_filt_name'] + export_pref_filt_name = module.params['export_pref_filt_name'] + import_as_path_filter = module.params['import_as_path_filter'] + export_as_path_filter = module.params['export_as_path_filter'] + import_as_path_name_or_num = module.params['import_as_path_name_or_num'] + export_as_path_name_or_num = module.params['export_as_path_name_or_num'] + import_acl_name_or_num = module.params['import_acl_name_or_num'] + export_acl_name_or_num = module.params['export_acl_name_or_num'] + ipprefix_orf_enable = module.params['ipprefix_orf_enable'] + is_nonstd_ipprefix_mod = module.params['is_nonstd_ipprefix_mod'] + orftype = module.params['orftype'] + orf_mode = module.params['orf_mode'] + soostring = module.params['soostring'] + default_rt_adv_enable = module.params['default_rt_adv_enable'] + default_rt_adv_policy = module.params['default_rt_adv_policy'] + default_rt_match_mode = module.params['default_rt_match_mode'] + add_path_mode = module.params['add_path_mode'] + adv_add_path_num = module.params['adv_add_path_num'] + origin_as_valid = module.params['origin_as_valid'] + vpls_enable = module.params['vpls_enable'] + vpls_ad_disable = module.params['vpls_ad_disable'] + update_pkt_standard_compatible = module.params[ + 'update_pkt_standard_compatible'] + + ce_bgp_peer_af_obj = BgpNeighborAf() + + # get proposed + proposed["state"] = state + if vrf_name: + proposed["vrf_name"] = vrf_name + if af_type: + proposed["af_type"] = af_type + if remote_address: + proposed["remote_address"] = remote_address + if advertise_irb != 'no_use': + proposed["advertise_irb"] = advertise_irb + if advertise_arp != 'no_use': + proposed["advertise_arp"] = advertise_arp + if advertise_remote_nexthop != 'no_use': + proposed["advertise_remote_nexthop"] = advertise_remote_nexthop + if advertise_community != 'no_use': + proposed["advertise_community"] = advertise_community + if advertise_ext_community != 'no_use': + proposed["advertise_ext_community"] = advertise_ext_community + if discard_ext_community != 'no_use': + proposed["discard_ext_community"] = discard_ext_community + if allow_as_loop_enable != 'no_use': + proposed["allow_as_loop_enable"] = allow_as_loop_enable + if allow_as_loop_limit: + proposed["allow_as_loop_limit"] = allow_as_loop_limit + if keep_all_routes != 'no_use': + proposed["keep_all_routes"] = keep_all_routes + if nexthop_configure: + proposed["nexthop_configure"] = nexthop_configure + if preferred_value: + proposed["preferred_value"] = preferred_value + if public_as_only != 'no_use': + proposed["public_as_only"] = public_as_only + if public_as_only_force != 'no_use': + proposed["public_as_only_force"] = public_as_only_force + if public_as_only_limited != 'no_use': + proposed["public_as_only_limited"] = public_as_only_limited + if public_as_only_replace != 'no_use': + proposed["public_as_only_replace"] = public_as_only_replace + if public_as_only_skip_peer_as != 'no_use': + proposed["public_as_only_skip_peer_as"] = public_as_only_skip_peer_as + if route_limit: + proposed["route_limit"] = route_limit + if route_limit_percent: + proposed["route_limit_percent"] = route_limit_percent + if route_limit_type: + proposed["route_limit_type"] = route_limit_type + if route_limit_idle_timeout: + proposed["route_limit_idle_timeout"] = route_limit_idle_timeout + if rt_updt_interval: + proposed["rt_updt_interval"] = rt_updt_interval + if redirect_ip != 'no_use': + proposed["redirect_ip"] = redirect_ip + if redirect_ip_validation != 'no_use': + proposed["redirect_ip_validation"] = redirect_ip_validation + if reflect_client != 'no_use': + proposed["reflect_client"] = reflect_client + if substitute_as_enable != 'no_use': + proposed["substitute_as_enable"] = substitute_as_enable + if import_rt_policy_name: + proposed["import_rt_policy_name"] = import_rt_policy_name + if export_rt_policy_name: + proposed["export_rt_policy_name"] = export_rt_policy_name + if import_pref_filt_name: + proposed["import_pref_filt_name"] = import_pref_filt_name + if export_pref_filt_name: + proposed["export_pref_filt_name"] = export_pref_filt_name + if import_as_path_filter: + proposed["import_as_path_filter"] = import_as_path_filter + if export_as_path_filter: + proposed["export_as_path_filter"] = export_as_path_filter + if import_as_path_name_or_num: + proposed["import_as_path_name_or_num"] = import_as_path_name_or_num + if export_as_path_name_or_num: + proposed["export_as_path_name_or_num"] = export_as_path_name_or_num + if import_acl_name_or_num: + proposed["import_acl_name_or_num"] = import_acl_name_or_num + if export_acl_name_or_num: + proposed["export_acl_name_or_num"] = export_acl_name_or_num + if ipprefix_orf_enable != 'no_use': + proposed["ipprefix_orf_enable"] = ipprefix_orf_enable + if is_nonstd_ipprefix_mod != 'no_use': + proposed["is_nonstd_ipprefix_mod"] = is_nonstd_ipprefix_mod + if orftype: + proposed["orftype"] = orftype + if orf_mode: + proposed["orf_mode"] = orf_mode + if soostring: + proposed["soostring"] = soostring + if default_rt_adv_enable != 'no_use': + proposed["default_rt_adv_enable"] = default_rt_adv_enable + if default_rt_adv_policy: + proposed["default_rt_adv_policy"] = default_rt_adv_policy + if default_rt_match_mode: + proposed["default_rt_match_mode"] = default_rt_match_mode + if add_path_mode: + proposed["add_path_mode"] = add_path_mode + if adv_add_path_num: + proposed["adv_add_path_num"] = adv_add_path_num + if origin_as_valid != 'no_use': + proposed["origin_as_valid"] = origin_as_valid + if vpls_enable != 'no_use': + proposed["vpls_enable"] = vpls_enable + if vpls_ad_disable != 'no_use': + proposed["vpls_ad_disable"] = vpls_ad_disable + if update_pkt_standard_compatible != 'no_use': + proposed["update_pkt_standard_compatible"] = update_pkt_standard_compatible + + if not ce_bgp_peer_af_obj: + module.fail_json(msg='Error: Init module failed.') + + bgp_peer_af_rst = ce_bgp_peer_af_obj.check_bgp_neighbor_af_args( + module=module) + bgp_peer_af_other_rst = ce_bgp_peer_af_obj.check_bgp_neighbor_af_other( + module=module) + + # state exist bgp peer address family config + exist_tmp = dict() + for item in bgp_peer_af_rst: + if item != "need_cfg": + exist_tmp[item] = bgp_peer_af_rst[item] + if exist_tmp: + existing["bgp neighbor af"] = exist_tmp + # state exist bgp peer address family other config + exist_tmp = dict() + for item in bgp_peer_af_other_rst: + if item != "need_cfg": + exist_tmp[item] = bgp_peer_af_other_rst[item] + if exist_tmp: + existing["bgp neighbor af other"] = exist_tmp + + if state == "present": + if bgp_peer_af_rst["need_cfg"]: + if "remote_address" in bgp_peer_af_rst.keys(): + cmd = ce_bgp_peer_af_obj.merge_bgp_peer_af(module=module) + changed = True + for item in cmd: + updates.append(item) + else: + cmd = ce_bgp_peer_af_obj.create_bgp_peer_af(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_peer_af_other_rst["need_cfg"]: + cmd = ce_bgp_peer_af_obj.merge_bgp_peer_af_other(module=module) + changed = True + for item in cmd: + updates.append(item) + + else: + if bgp_peer_af_rst["need_cfg"]: + cmd = ce_bgp_peer_af_obj.delete_bgp_peer_af(module=module) + changed = True + for item in cmd: + updates.append(item) + + if bgp_peer_af_other_rst["need_cfg"]: + pass + + # state end bgp peer address family config + bgp_peer_af_rst = ce_bgp_peer_af_obj.check_bgp_neighbor_af_args( + module=module) + end_tmp = dict() + for item in bgp_peer_af_rst: + if item != "need_cfg": + end_tmp[item] = bgp_peer_af_rst[item] + if end_tmp: + end_state["bgp neighbor af"] = end_tmp + # state end bgp peer address family other config + bgp_peer_af_other_rst = ce_bgp_peer_af_obj.check_bgp_neighbor_af_other( + module=module) + end_tmp = dict() + for item in bgp_peer_af_other_rst: + if item != "need_cfg": + end_tmp[item] = bgp_peer_af_other_rst[item] + if end_tmp: + end_state["bgp neighbor af other"] = end_tmp + if end_state == existing: + changed = False + updates = list() + + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_command.py b/plugins/modules/ce_command.py new file mode 100644 index 0000000..69a72ff --- /dev/null +++ b/plugins/modules/ce_command.py @@ -0,0 +1,260 @@ +#!/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 . +# + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- + +module: ce_command +author: "JackyGao2016 (@CloudEngine-Ansible)" +short_description: Run arbitrary command on HUAWEI CloudEngine devices. +description: + - Sends an arbitrary command to an HUAWEI CloudEngine node and returns + the results read from the device. The ce_command 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. +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + commands: + description: + - The commands to send to the remote HUAWEI CloudEngine device + over the configured provider. 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. + required: true + 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. See examples. + 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. + default: all + retries: + description: + - Specifies the number of retries a command should by 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 + 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 +''' + +EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. + +- name: CloudEngine command test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: "Run display version on remote devices" + ce_command: + commands: display version + provider: "{{ cli }}" + + - name: "Run display version and check to see if output contains HUAWEI" + ce_command: + commands: display version + wait_for: result[0] contains HUAWEI + provider: "{{ cli }}" + + - name: "Run multiple commands on remote nodes" + ce_command: + commands: + - display version + - display device + provider: "{{ cli }}" + + - name: "Run multiple commands and evaluate the output" + ce_command: + commands: + - display version + - display device + wait_for: + - result[0] contains HUAWEI + - result[1] contains Device + provider: "{{ cli }}" +""" + +RETURN = """ +stdout: + description: the set of responses from the commands + returned: always + type: list + sample: ['...', '...'] + +stdout_lines: + description: The value of stdout split into a list + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] + +failed_conditions: + description: the conditionals that failed + returned: failed + type: list + sample: ['...', '...'] +""" + + +import time +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, check_args +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import run_commands +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 ComplexList +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_native + + +def to_lines(stdout): + lines = list() + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + lines.append(item) + return lines + + +def parse_commands(module, warnings): + transform = ComplexList(dict( + command=dict(key=True), + output=dict(), + prompt=dict(), + answer=dict() + ), module) + + commands = transform(module.params['commands']) + + for _, item in enumerate(commands): + if module.check_mode and not item['command'].startswith('dis'): + warnings.append( + 'Only display commands are supported when using check_mode, not ' + 'executing %s' % item['command'] + ) + + return commands + + +def to_cli(obj): + cmd = obj['command'] + return cmd + + +def main(): + """entry point for module execution + """ + argument_spec = dict( + # { command: , output: , prompt: , response: } + commands=dict(type='list', required=True), + + wait_for=dict(type='list'), + match=dict(default='all', choices=['any', 'all']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + argument_spec.update(ce_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + result = {'changed': False} + + warnings = list() + check_args(module, warnings) + commands = parse_commands(module, warnings) + result['warnings'] = 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_native(exc), exception=traceback.format_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': to_lines(responses) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_config.py b/plugins/modules/ce_config.py new file mode 100644 index 0000000..6a2b675 --- /dev/null +++ b/plugins/modules/ce_config.py @@ -0,0 +1,493 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_config +author: "QijunPan (@QijunPan)" +short_description: Manage Huawei CloudEngine configuration sections. +description: + - Huawei CloudEngine configurations use a simple block indent file syntax + for segmenting configuration into sections. This module provides + an implementation for working with CloudEngine configuration sections in + a deterministic way. This module works with CLI transports. +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +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 current-configuration. Be sure to note the configuration + command syntax as some commands are automatically modified by the + device config parser. + 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. + 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. + 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. + 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. + 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 current-configuration on the remote device. + 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. + default: line + choices: ['line', 'block'] + backup: + description: + - This argument will cause the module to create a full backup of + the current C(current-configuration) 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. If the directory does not exist, it is created. + type: bool + default: 'no' + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current current-configuration 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-configuration 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. + defaults: + description: + - The I(defaults) argument will influence how the current-configuration + is collected from the device. When the value is set to true, + the command used to collect the current-configuration 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: + description: + - The C(save) argument instructs the module to save the + current-configuration to saved-configuration. This operation is performed + after any changes are made to the current running config. If + no changes are made, the configuration is still saved to the + startup config. This option will always cause the module to + return changed. + type: bool + default: 'no' + 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 _config.@ + 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 +''' + +EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. + +- name: CloudEngine config test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: "Configure top level configuration and save it" + ce_config: + lines: sysname {{ inventory_hostname }} + save: yes + provider: "{{ cli }}" + + - name: "Configure acl configuration and save it" + ce_config: + lines: + - rule 10 permit source 1.1.1.1 32 + - rule 20 permit source 2.2.2.2 32 + - rule 30 permit source 3.3.3.3 32 + - rule 40 permit source 4.4.4.4 32 + - rule 50 permit source 5.5.5.5 32 + parents: acl 2000 + before: undo acl 2000 + match: exact + provider: "{{ cli }}" + + - name: "Configure acl configuration and save it" + ce_config: + lines: + - rule 10 permit source 1.1.1.1 32 + - rule 20 permit source 2.2.2.2 32 + - rule 30 permit source 3.3.3.3 32 + - rule 40 permit source 4.4.4.4 32 + parents: acl 2000 + before: undo acl 2000 + replace: block + provider: "{{ cli }}" + + - name: configurable backup path + ce_config: + lines: sysname {{ inventory_hostname }} + provider: "{{ cli }}" + backup: yes + backup_options: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: Only when lines is specified. + type: list + sample: ['...', '...'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/ce_config.2016-07-16@22:28:34 +""" +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import ConnectionError, Connection +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig as _NetworkConfig +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import dumps, ConfigLine, ignore_line +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_config, run_commands, exec_command, cli_err_msg +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import check_args as ce_check_args +import re + + +def check_args(module, warnings): + ce_check_args(module, warnings) + + +def not_user_view(prompt): + return prompt is not None and prompt.strip().startswith("[") + + +def command_level(command): + regex_level = re.search(r"^(\s*)\S+", command) + if regex_level is not None: + level = str(regex_level.group(1)) + return len(level) + return 0 + + +def _load_config(module, config): + """Sends configuration commands to the remote device + """ + connection = Connection(module._socket_path) + rc, out, err = exec_command(module, 'mmi-mode enable') + if rc != 0: + module.fail_json(msg='unable to set mmi-mode enable', output=err) + rc, out, err = exec_command(module, 'system-view immediately') + if rc != 0: + module.fail_json(msg='unable to enter system-view', output=err) + current_view_prompt = system_view_prompt = connection.get_prompt() + + for index, cmd in enumerate(config): + level = command_level(cmd) + current_view_prompt = connection.get_prompt() + rc, out, err = exec_command(module, cmd) + if rc != 0: + print_msg = cli_err_msg(cmd.strip(), err) + # re-try command max 3 times + for i in (1, 2, 3): + current_view_prompt = connection.get_prompt() + if current_view_prompt != system_view_prompt and not_user_view(current_view_prompt): + exec_command(module, "quit") + current_view_prompt = connection.get_prompt() + # if current view is system-view, break. + if current_view_prompt == system_view_prompt and level > 0: + break + elif current_view_prompt == system_view_prompt or not not_user_view(current_view_prompt): + break + rc, out, err = exec_command(module, cmd) + if rc == 0: + print_msg = None + break + if print_msg is not None: + module.fail_json(msg=print_msg) + + +def get_running_config(module): + contents = module.params['config'] + if not contents: + command = "display current-configuration " + if module.params['defaults']: + command += 'include-default' + resp = run_commands(module, command) + contents = resp[0] + return NetworkConfig(indent=1, contents=contents) + + +def get_candidate(module): + candidate = NetworkConfig(indent=1) + if module.params['src']: + config = module.params['src'] + candidate.load(config) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) + return candidate + + +def run(module, result): + match = module.params['match'] + replace = module.params['replace'] + + candidate = get_candidate(module) + + if match != 'none': + before = get_running_config(module) + path = module.params['parents'] + configobjs = candidate.difference(before, match=match, replace=replace, path=path) + else: + configobjs = candidate.items + + if configobjs: + out_type = "commands" + if module.params["src"] is not None: + out_type = "raw" + commands = dumps(configobjs, out_type).split('\n') + + if module.params['lines']: + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + command_display = [] + for per_command in commands: + if per_command.strip() not in ['quit', 'return', 'system-view']: + command_display.append(per_command) + + result['commands'] = command_display + result['updates'] = command_display + + if not module.check_mode: + if module.params['parents'] is not None: + load_config(module, commands) + else: + _load_config(module, commands) + if match != "none": + after = get_running_config(module) + path = module.params["parents"] + if path is not None and match != 'line': + before_objs = before.get_block(path) + after_objs = after.get_block(path) + update = [] + if len(before_objs) == len(after_objs): + for b_item, a_item in zip(before_objs, after_objs): + if b_item != a_item: + update.append(a_item.text) + else: + update = [item.text for item in after_objs] + if len(update) == 0: + result["changed"] = False + result['updates'] = [] + else: + result["changed"] = True + result['updates'] = update + else: + configobjs = after.difference(before, match=match, replace=replace, path=path) + if len(configobjs) > 0: + result["changed"] = True + else: + result["changed"] = False + result['updates'] = [] + else: + result['changed'] = True + + +class NetworkConfig(_NetworkConfig): + + def add(self, lines, parents=None): + ancestors = list() + offset = 0 + obj = None + + # global config command + if not parents: + for line in lines: + # handle ignore lines + if ignore_line(line): + continue + + item = ConfigLine(line) + item.raw = line + self.items.append(item) + + else: + for index, p in enumerate(parents): + try: + i = index + 1 + obj = self.get_block(parents[:i])[0] + ancestors.append(obj) + + except ValueError: + # add parent to config + offset = index * self._indent + obj = ConfigLine(p) + obj.raw = p.rjust(len(p) + offset) + if ancestors: + obj._parents = list(ancestors) + ancestors[-1]._children.append(obj) + self.items.append(obj) + ancestors.append(obj) + + # add child objects + for line in lines: + # handle ignore lines + if ignore_line(line): + continue + + # check if child already exists + for child in ancestors[-1]._children: + if child.text == line: + break + else: + offset = len(parents) * self._indent + item = ConfigLine(line) + item.raw = line.rjust(len(line) + offset) + item._parents = ancestors + ancestors[-1]._children.append(item) + self.items.append(item) + + +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'), + parents=dict(type='list'), + + before=dict(type='list'), + after=dict(type='list'), + + match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), + config=dict(), + defaults=dict(type='bool', default=False), + + backup=dict(type='bool', default=False), + backup_options=dict(type='dict', options=backup_spec), + save=dict(type='bool', default=False), + ) + + argument_spec.update(ce_argument_spec) + + mutually_exclusive = [('lines', 'src'), + ('parents', 'src')] + + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines'])] + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_if=required_if, + supports_check_mode=True) + + warnings = list() + check_args(module, warnings) + + result = dict(changed=False, warnings=warnings) + + if module.params['backup']: + result['__backup__'] = get_config(module) + + if any((module.params['src'], module.params['lines'])): + run(module, result) + + if module.params['save']: + if not module.check_mode: + run_commands(module, ['return', 'mmi-mode enable', 'save']) + result["changed"] = True + run_commands(module, ['return', 'undo mmi-mode enable']) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_dldp.py b/plugins/modules/ce_dldp.py new file mode 100644 index 0000000..baa26bc --- /dev/null +++ b/plugins/modules/ce_dldp.py @@ -0,0 +1,550 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- + +module: ce_dldp +short_description: Manages global DLDP configuration on HUAWEI CloudEngine switches. +description: + - Manages global DLDP configuration on HUAWEI CloudEngine switches. +author: + - Zhijin Zhou (@QijunPan) +notes: + - The relevant configurations will be deleted if DLDP is disabled using enable=disable. + - When using auth_mode=none, it will restore the default DLDP authentication mode. By default, + DLDP packets are not authenticated. + - By default, the working mode of DLDP is enhance, so you are advised to use work_mode=enhance to restore default + DLDP working mode. + - The default interval for sending Advertisement packets is 5 seconds, so you are advised to use time_interval=5 to + restore default DLDP interval. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + enable: + description: + - Set global DLDP enable state. + choices: ['enable', 'disable'] + work_mode: + description: + - Set global DLDP work-mode. + choices: ['enhance', 'normal'] + time_internal: + description: + - Specifies the interval for sending Advertisement packets. + The value is an integer ranging from 1 to 100, in seconds. + The default interval for sending Advertisement packets is 5 seconds. + auth_mode: + description: + - Specifies authentication algorithm of DLDP. + choices: ['md5', 'simple', 'sha', 'hmac-sha256', 'none'] + auth_pwd: + description: + - Specifies authentication password. + The value is a string of 1 to 16 case-sensitive plaintexts or 24/32/48/108/128 case-sensitive encrypted + characters. The string excludes a question mark (?). + reset: + description: + - Specify whether reset DLDP state of disabled interfaces. + choices: ['enable', 'disable'] +''' + +EXAMPLES = ''' +- name: DLDP test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Configure global DLDP enable state" + ce_dldp: + enable: enable + provider: "{{ cli }}" + + - name: "Configure DLDP work-mode and ensure global DLDP state is already enabled" + ce_dldp: + enable: enable + work_mode: normal + provider: "{{ cli }}" + + - name: "Configure advertisement message time interval in seconds and ensure global DLDP state is already enabled" + ce_dldp: + enable: enable + time_interval: 6 + provider: "{{ cli }}" + + - name: "Configure a DLDP authentication mode and ensure global DLDP state is already enabled" + ce_dldp: + enable: enable + auth_mode: md5 + auth_pwd: abc + provider: "{{ cli }}" + + - name: "Reset DLDP state of disabled interfaces and ensure global DLDP state is already enabled" + ce_dldp: + enable: enable + reset: enable + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "enable": "enable", + "reset": "enable", + "time_internal": "12", + "work_mode": "normal" + } +existing: + description: k/v pairs of existing global DLDP configuration + returned: always + type: dict + sample: { + "enable": "disable", + "reset": "disable", + "time_internal": "5", + "work_mode": "enhance" + } +end_state: + description: k/v pairs of global DLDP configuration after module execution + returned: always + type: dict + sample: { + "enable": "enable", + "reset": "enable", + "time_internal": "12", + "work_mode": "normal" + } +updates: + description: command sent to the device + returned: always + type: list + sample: [ + "dldp enable", + "dldp work-mode normal", + "dldp interval 12", + "dldp reset" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import copy +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, set_nc_config, get_nc_config, execute_nc_action + +CE_NC_ACTION_RESET_DLDP = """ + + + + + +""" + +CE_NC_GET_GLOBAL_DLDP_CONFIG = """ + + + + + + + + + + +""" + +CE_NC_MERGE_DLDP_GLOBAL_CONFIG_HEAD = """ + + + + %s + %s + %s +""" + +CE_NC_MERGE_DLDP_GLOBAL_CONFIG_TAIL = """ + + + +""" + + +class Dldp(object): + """Manage global dldp configuration""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # DLDP global configuration info + self.enable = self.module.params['enable'] or None + self.work_mode = self.module.params['work_mode'] or None + self.internal = self.module.params['time_interval'] or None + self.reset = self.module.params['reset'] or None + self.auth_mode = self.module.params['auth_mode'] + self.auth_pwd = self.module.params['auth_pwd'] + + self.dldp_conf = dict() + self.same_conf = False + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = list() + self.end_state = list() + + def check_config_if_same(self): + """Judge whether current config is the same as what we excepted""" + + if self.enable and self.enable != self.dldp_conf['dldpEnable']: + return False + + if self.internal and self.internal != self.dldp_conf['dldpInterval']: + return False + + work_mode = 'normal' + if self.dldp_conf['dldpWorkMode'] == 'dldpEnhance': + work_mode = 'enhance' + if self.work_mode and self.work_mode != work_mode: + return False + + if self.auth_mode: + if self.auth_mode != 'none': + return False + + if self.auth_mode == 'none' and self.dldp_conf['dldpAuthMode'] != 'dldpAuthNone': + return False + + if self.reset and self.reset == 'enable': + return False + + return True + + def check_params(self): + """Check all input params""" + + if (self.auth_mode and self.auth_mode != 'none' and not self.auth_pwd) \ + or (self.auth_pwd and not self.auth_mode): + self.module.fail_json(msg="Error: auth_mode and auth_pwd must both exist or not exist.") + + if self.dldp_conf['dldpEnable'] == 'disable' and not self.enable: + if self.work_mode or self.reset or self.internal or self.auth_mode: + self.module.fail_json(msg="Error: when DLDP is already disabled globally, " + "work_mode, time_internal auth_mode and reset parameters are not " + "expected to configure.") + + if self.enable == 'disable' and (self.work_mode or self.internal or self.reset or self.auth_mode): + self.module.fail_json(msg="Error: when using enable=disable, work_mode, " + "time_internal auth_mode and reset parameters are not expected " + "to configure.") + + if self.internal: + if not self.internal.isdigit(): + self.module.fail_json( + msg='Error: time_interval must be digit.') + + if int(self.internal) < 1 or int(self.internal) > 100: + self.module.fail_json( + msg='Error: The value of time_internal should be between 1 and 100.') + + if self.auth_pwd: + if '?' in self.auth_pwd: + self.module.fail_json( + msg='Error: The auth_pwd string excludes a question mark (?).') + if (len(self.auth_pwd) != 24) and (len(self.auth_pwd) != 32) and (len(self.auth_pwd) != 48) and \ + (len(self.auth_pwd) != 108) and (len(self.auth_pwd) != 128): + if (len(self.auth_pwd) < 1) or (len(self.auth_pwd) > 16): + self.module.fail_json( + msg='Error: The value is a string of 1 to 16 case-sensitive plaintexts or 24/32/48/108/128 ' + 'case-sensitive encrypted characters.') + + def init_module(self): + """Init module object""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_dldp_exist_config(self): + """Get current dldp existed configuration""" + + dldp_conf = dict() + xml_str = CE_NC_GET_GLOBAL_DLDP_CONFIG + con_obj = get_nc_config(self.module, xml_str) + if "" in con_obj: + return dldp_conf + + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get global DLDP info + root = ElementTree.fromstring(xml_str) + topo = root.find("dldp/dldpSys") + if not topo: + self.module.fail_json( + msg="Error: Get current DLDP configuration failed.") + + for eles in topo: + if eles.tag in ["dldpEnable", "dldpInterval", "dldpWorkMode", "dldpAuthMode"]: + if eles.tag == 'dldpEnable': + if eles.text == 'true': + value = 'enable' + else: + value = 'disable' + else: + value = eles.text + dldp_conf[eles.tag] = value + + return dldp_conf + + def config_global_dldp(self): + """Config global dldp""" + + if self.same_conf: + return + + enable = self.enable + if not self.enable: + enable = self.dldp_conf['dldpEnable'] + if enable == 'enable': + enable = 'true' + else: + enable = 'false' + + internal = self.internal + if not self.internal: + internal = self.dldp_conf['dldpInterval'] + + work_mode = self.work_mode + if not self.work_mode: + work_mode = self.dldp_conf['dldpWorkMode'] + + if work_mode == 'enhance' or work_mode == 'dldpEnhance': + work_mode = 'dldpEnhance' + else: + work_mode = 'dldpNormal' + + auth_mode = self.auth_mode + if not self.auth_mode: + auth_mode = self.dldp_conf['dldpAuthMode'] + if auth_mode == 'md5': + auth_mode = 'dldpAuthMD5' + elif auth_mode == 'simple': + auth_mode = 'dldpAuthSimple' + elif auth_mode == 'sha': + auth_mode = 'dldpAuthSHA' + elif auth_mode == 'hmac-sha256': + auth_mode = 'dldpAuthHMAC-SHA256' + elif auth_mode == 'none': + auth_mode = 'dldpAuthNone' + + xml_str = CE_NC_MERGE_DLDP_GLOBAL_CONFIG_HEAD % ( + enable, internal, work_mode) + if self.auth_mode: + if self.auth_mode == 'none': + xml_str += "dldpAuthNone" + else: + xml_str += "%s" % auth_mode + xml_str += "%s" % self.auth_pwd + + xml_str += CE_NC_MERGE_DLDP_GLOBAL_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "MERGE_DLDP_GLOBAL_CONFIG") + + if self.reset == 'enable': + xml_str = CE_NC_ACTION_RESET_DLDP + ret_xml = execute_nc_action(self.module, xml_str) + self.check_response(ret_xml, "ACTION_RESET_DLDP") + + self.changed = True + + def get_existing(self): + """Get existing info""" + + dldp_conf = dict() + + dldp_conf['enable'] = self.dldp_conf.get('dldpEnable', None) + dldp_conf['time_interval'] = self.dldp_conf.get('dldpInterval', None) + work_mode = self.dldp_conf.get('dldpWorkMode', None) + if work_mode == 'dldpEnhance': + dldp_conf['work_mode'] = 'enhance' + else: + dldp_conf['work_mode'] = 'normal' + + auth_mode = self.dldp_conf.get('dldpAuthMode', None) + if auth_mode == 'dldpAuthNone': + dldp_conf['auth_mode'] = 'none' + elif auth_mode == 'dldpAuthSimple': + dldp_conf['auth_mode'] = 'simple' + elif auth_mode == 'dldpAuthMD5': + dldp_conf['auth_mode'] = 'md5' + elif auth_mode == 'dldpAuthSHA': + dldp_conf['auth_mode'] = 'sha' + else: + dldp_conf['auth_mode'] = 'hmac-sha256' + + dldp_conf['reset'] = 'disable' + + self.existing = copy.deepcopy(dldp_conf) + + def get_proposed(self): + """Get proposed result""" + + self.proposed = dict(enable=self.enable, work_mode=self.work_mode, + time_interval=self.internal, reset=self.reset, + auth_mode=self.auth_mode, auth_pwd=self.auth_pwd) + + def get_update_cmd(self): + """Get update commands""" + if self.same_conf: + return + + if self.enable and self.enable != self.dldp_conf['dldpEnable']: + if self.enable == 'enable': + self.updates_cmd.append("dldp enable") + elif self.enable == 'disable': + self.updates_cmd.append("undo dldp enable") + return + + work_mode = 'normal' + if self.dldp_conf['dldpWorkMode'] == 'dldpEnhance': + work_mode = 'enhance' + if self.work_mode and self.work_mode != work_mode: + if self.work_mode == 'enhance': + self.updates_cmd.append("dldp work-mode enhance") + else: + self.updates_cmd.append("dldp work-mode normal") + + if self.internal and self.internal != self.dldp_conf['dldpInterval']: + self.updates_cmd.append("dldp interval %s" % self.internal) + + if self.auth_mode: + if self.auth_mode == 'none': + self.updates_cmd.append("undo dldp authentication-mode") + else: + self.updates_cmd.append("dldp authentication-mode %s %s" % (self.auth_mode, self.auth_pwd)) + + if self.reset and self.reset == 'enable': + self.updates_cmd.append('dldp reset') + + def get_end_state(self): + """Get end state info""" + + dldp_conf = dict() + self.dldp_conf = self.get_dldp_exist_config() + + dldp_conf['enable'] = self.dldp_conf.get('dldpEnable', None) + dldp_conf['time_interval'] = self.dldp_conf.get('dldpInterval', None) + work_mode = self.dldp_conf.get('dldpWorkMode', None) + if work_mode == 'dldpEnhance': + dldp_conf['work_mode'] = 'enhance' + else: + dldp_conf['work_mode'] = 'normal' + + auth_mode = self.dldp_conf.get('dldpAuthMode', None) + if auth_mode == 'dldpAuthNone': + dldp_conf['auth_mode'] = 'none' + elif auth_mode == 'dldpAuthSimple': + dldp_conf['auth_mode'] = 'simple' + elif auth_mode == 'dldpAuthMD5': + dldp_conf['auth_mode'] = 'md5' + elif auth_mode == 'dldpAuthSHA': + dldp_conf['auth_mode'] = 'sha' + else: + dldp_conf['auth_mode'] = 'hmac-sha256' + + dldp_conf['reset'] = 'disable' + if self.reset == 'enable': + dldp_conf['reset'] = 'enable' + self.end_state = copy.deepcopy(dldp_conf) + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def work(self): + """Worker""" + + self.dldp_conf = self.get_dldp_exist_config() + self.check_params() + self.same_conf = self.check_config_if_same() + self.get_existing() + self.get_proposed() + self.config_global_dldp() + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + enable=dict(choices=['enable', 'disable'], type='str'), + work_mode=dict(choices=['enhance', 'normal'], type='str'), + time_interval=dict(type='str'), + reset=dict(choices=['enable', 'disable'], type='str'), + auth_mode=dict(choices=['md5', 'simple', 'sha', 'hmac-sha256', 'none'], type='str'), + auth_pwd=dict(type='str', no_log=True), + ) + argument_spec.update(ce_argument_spec) + dldp_obj = Dldp(argument_spec) + dldp_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_dldp_interface.py b/plugins/modules/ce_dldp_interface.py new file mode 100644 index 0000000..fb1e40e --- /dev/null +++ b/plugins/modules/ce_dldp_interface.py @@ -0,0 +1,659 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- + +module: ce_dldp_interface +short_description: Manages interface DLDP configuration on HUAWEI CloudEngine switches. +description: + - Manages interface DLDP configuration on HUAWEI CloudEngine switches. +author: + - Zhou Zhijin (@QijunPan) +notes: + - If C(state=present, enable=disable), interface DLDP enable will be turned off and + related interface DLDP configuration will be cleared. + - If C(state=absent), only local_mac is supported to configure. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - Must be fully qualified interface name, i.e. GE1/0/1, 10GE1/0/1, 40GE1/0/22, 100GE1/0/1. + required: true + enable: + description: + - Set interface DLDP enable state. + choices: ['enable', 'disable'] + mode_enable: + description: + - Set DLDP compatible-mode enable state. + choices: ['enable', 'disable'] + local_mac: + description: + - Set the source MAC address for DLDP packets sent in the DLDP-compatible mode. + The value of MAC address is in H-H-H format. H contains 1 to 4 hexadecimal digits. + reset: + description: + - Specify whether reseting interface DLDP state. + choices: ['enable', 'disable'] + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: DLDP interface test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Configure interface DLDP enable state and ensure global dldp enable is turned on" + ce_dldp_interface: + interface: 40GE2/0/1 + enable: enable + provider: "{{ cli }}" + + - name: "Configuire interface DLDP compatible-mode enable state and ensure interface DLDP state is already enabled" + ce_dldp_interface: + interface: 40GE2/0/1 + enable: enable + mode_enable: enable + provider: "{{ cli }}" + + - name: "Configuire the source MAC address for DLDP packets sent in the DLDP-compatible mode and + ensure interface DLDP state and compatible-mode enable state is already enabled" + ce_dldp_interface: + interface: 40GE2/0/1 + enable: enable + mode_enable: enable + local_mac: aa-aa-aa + provider: "{{ cli }}" + + - name: "Reset DLDP state of specified interface and ensure interface DLDP state is already enabled" + ce_dldp_interface: + interface: 40GE2/0/1 + enable: enable + reset: enable + provider: "{{ cli }}" + + - name: "Unconfigure interface DLDP local mac address when C(state=absent)" + ce_dldp_interface: + interface: 40GE2/0/1 + state: absent + local_mac: aa-aa-aa + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "enable": "enalbe", + "interface": "40GE2/0/22", + "local_mac": "aa-aa-aa", + "mode_enable": "enable", + "reset": "enable" + } +existing: + description: k/v pairs of existing interface DLDP configuration + returned: always + type: dict + sample: { + "enable": "disable", + "interface": "40GE2/0/22", + "local_mac": null, + "mode_enable": null, + "reset": "disable" + } +end_state: + description: k/v pairs of interface DLDP configuration after module execution + returned: always + type: dict + sample: { + "enable": "enable", + "interface": "40GE2/0/22", + "local_mac": "00aa-00aa-00aa", + "mode_enable": "enable", + "reset": "enable" + } +updates: + description: command sent to the device + returned: always + type: list + sample: [ + "dldp enable", + "dldp compatible-mode enable", + "dldp compatible-mode local-mac aa-aa-aa", + "dldp reset" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import copy +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, set_nc_config, get_nc_config, execute_nc_action + + +CE_NC_ACTION_RESET_INTF_DLDP = """ + + + + %s + + + +""" + +CE_NC_GET_INTF_DLDP_CONFIG = """ + + + + + %s + + + + + + + +""" + +CE_NC_MERGE_DLDP_INTF_CONFIG = """ + + + + + %s + %s + %s + %s + + + + +""" + +CE_NC_CREATE_DLDP_INTF_CONFIG = """ + + + + + %s + %s + %s + %s + + + + +""" + +CE_NC_DELETE_DLDP_INTF_CONFIG = """ + + + + + %s + + + + +""" + + +def judge_is_mac_same(mac1, mac2): + """Judge whether two macs are the same""" + + if mac1 == mac2: + return True + + list1 = re.findall(r'([0-9A-Fa-f]+)', mac1) + list2 = re.findall(r'([0-9A-Fa-f]+)', mac2) + if len(list1) != len(list2): + return False + + for index, value in enumerate(list1, start=0): + if value.lstrip('0').lower() != list2[index].lstrip('0').lower(): + return False + + return True + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class DldpInterface(object): + """Manage interface dldp configuration""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # DLDP interface configuration info + self.interface = self.module.params['interface'] + self.enable = self.module.params['enable'] or None + self.reset = self.module.params['reset'] or None + self.mode_enable = self.module.params['mode_enable'] or None + self.local_mac = self.module.params['local_mac'] or None + self.state = self.module.params['state'] + + self.dldp_intf_conf = dict() + self.same_conf = False + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = list() + self.end_state = list() + + def check_config_if_same(self): + """Judge whether current config is the same as what we excepted""" + + if self.state == 'absent': + return False + else: + if self.enable and self.enable != self.dldp_intf_conf['dldpEnable']: + return False + + if self.mode_enable and self.mode_enable != self.dldp_intf_conf['dldpCompatibleEnable']: + return False + + if self.local_mac: + flag = judge_is_mac_same( + self.local_mac, self.dldp_intf_conf['dldpLocalMac']) + if not flag: + return False + + if self.reset and self.reset == 'enable': + return False + return True + + def check_macaddr(self): + """Check mac-address whether valid""" + + valid_char = '0123456789abcdef-' + mac = self.local_mac + + if len(mac) > 16: + return False + + mac_list = re.findall(r'([0-9a-fA-F]+)', mac) + if len(mac_list) != 3: + return False + + if mac.count('-') != 2: + return False + + for _, value in enumerate(mac, start=0): + if value.lower() not in valid_char: + return False + + return True + + def check_params(self): + """Check all input params""" + + if not self.interface: + self.module.fail_json(msg='Error: Interface name cannot be empty.') + + if self.interface: + intf_type = get_interface_type(self.interface) + if not intf_type: + self.module.fail_json( + msg='Error: Interface name of %s ' + 'is error.' % self.interface) + + if (self.state == 'absent') and (self.reset or self.mode_enable or self.enable): + self.module.fail_json(msg="Error: It's better to use state=present when " + "configuring or unconfiguring enable, mode_enable " + "or using reset flag. state=absent is just for " + "when using local_mac param.") + + if self.state == 'absent' and not self.local_mac: + self.module.fail_json( + msg="Error: Please specify local_mac parameter.") + + if self.state == 'present': + if (self.dldp_intf_conf['dldpEnable'] == 'disable' and not self.enable and + (self.mode_enable or self.local_mac or self.reset)): + self.module.fail_json(msg="Error: when DLDP is already disabled on this port, " + "mode_enable, local_mac and reset parameters are not " + "expected to configure.") + + if self.enable == 'disable' and (self.mode_enable or self.local_mac or self.reset): + self.module.fail_json(msg="Error: when using enable=disable, " + "mode_enable, local_mac and reset parameters " + "are not expected to configure.") + + if self.local_mac and (self.mode_enable == 'disable' or + (self.dldp_intf_conf['dldpCompatibleEnable'] == 'disable' and self.mode_enable != 'enable')): + self.module.fail_json(msg="Error: when DLDP compatible-mode is disabled on this port, " + "Configuring local_mac is not allowed.") + + if self.local_mac: + if not self.check_macaddr(): + self.module.fail_json( + msg="Error: local_mac has invalid value %s." % self.local_mac) + + def init_module(self): + """Init module object""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_dldp_intf_exist_config(self): + """Get current dldp existed config""" + + dldp_conf = dict() + xml_str = CE_NC_GET_INTF_DLDP_CONFIG % self.interface + con_obj = get_nc_config(self.module, xml_str) + if "" in con_obj: + dldp_conf['dldpEnable'] = 'disable' + dldp_conf['dldpCompatibleEnable'] = "" + dldp_conf['dldpLocalMac'] = "" + return dldp_conf + + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get global DLDP info + root = ElementTree.fromstring(xml_str) + topo = root.find("dldp/dldpInterfaces/dldpInterface") + if topo is None: + self.module.fail_json( + msg="Error: Get current DLDP configuration failed.") + for eles in topo: + if eles.tag in ["dldpEnable", "dldpCompatibleEnable", "dldpLocalMac"]: + if not eles.text: + dldp_conf[eles.tag] = "" + else: + if eles.tag == "dldpEnable" or eles.tag == "dldpCompatibleEnable": + if eles.text == 'true': + value = 'enable' + else: + value = 'disable' + else: + value = eles.text + dldp_conf[eles.tag] = value + + return dldp_conf + + def config_intf_dldp(self): + """Config global dldp""" + + if self.same_conf: + return + + if self.state == "present": + enable = self.enable + if not self.enable: + enable = self.dldp_intf_conf['dldpEnable'] + if enable == 'enable': + enable = 'true' + else: + enable = 'false' + + mode_enable = self.mode_enable + if not self.mode_enable: + mode_enable = self.dldp_intf_conf['dldpCompatibleEnable'] + if mode_enable == 'enable': + mode_enable = 'true' + else: + mode_enable = 'false' + + local_mac = self.local_mac + if not self.local_mac: + local_mac = self.dldp_intf_conf['dldpLocalMac'] + + if self.enable == 'disable' and self.enable != self.dldp_intf_conf['dldpEnable']: + xml_str = CE_NC_DELETE_DLDP_INTF_CONFIG % self.interface + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "DELETE_DLDP_INTF_CONFIG") + elif self.dldp_intf_conf['dldpEnable'] == 'disable' and self.enable == 'enable': + xml_str = CE_NC_CREATE_DLDP_INTF_CONFIG % ( + self.interface, 'true', mode_enable, local_mac) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "CREATE_DLDP_INTF_CONFIG") + elif self.dldp_intf_conf['dldpEnable'] == 'enable': + if mode_enable == 'false': + local_mac = '' + xml_str = CE_NC_MERGE_DLDP_INTF_CONFIG % ( + self.interface, enable, mode_enable, local_mac) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "MERGE_DLDP_INTF_CONFIG") + + if self.reset == 'enable': + xml_str = CE_NC_ACTION_RESET_INTF_DLDP % self.interface + ret_xml = execute_nc_action(self.module, xml_str) + self.check_response(ret_xml, "ACTION_RESET_INTF_DLDP") + + self.changed = True + else: + if self.local_mac and judge_is_mac_same(self.local_mac, self.dldp_intf_conf['dldpLocalMac']): + if self.dldp_intf_conf['dldpEnable'] == 'enable': + dldp_enable = 'true' + else: + dldp_enable = 'false' + if self.dldp_intf_conf['dldpCompatibleEnable'] == 'enable': + dldp_compat_enable = 'true' + else: + dldp_compat_enable = 'false' + xml_str = CE_NC_MERGE_DLDP_INTF_CONFIG % (self.interface, dldp_enable, dldp_compat_enable, '') + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "UNDO_DLDP_INTF_LOCAL_MAC_CONFIG") + self.changed = True + + def get_existing(self): + """Get existing info""" + + dldp_conf = dict() + + dldp_conf['interface'] = self.interface + dldp_conf['enable'] = self.dldp_intf_conf.get('dldpEnable', None) + dldp_conf['mode_enable'] = self.dldp_intf_conf.get( + 'dldpCompatibleEnable', None) + dldp_conf['local_mac'] = self.dldp_intf_conf.get('dldpLocalMac', None) + dldp_conf['reset'] = 'disable' + + self.existing = copy.deepcopy(dldp_conf) + + def get_proposed(self): + """Get proposed result """ + + self.proposed = dict(interface=self.interface, enable=self.enable, + mode_enable=self.mode_enable, local_mac=self.local_mac, + reset=self.reset, state=self.state) + + def get_update_cmd(self): + """Get updated commands""" + + if self.same_conf: + return + + if self.state == "present": + if self.enable and self.enable != self.dldp_intf_conf['dldpEnable']: + if self.enable == 'enable': + self.updates_cmd.append("dldp enable") + elif self.enable == 'disable': + self.updates_cmd.append("undo dldp enable") + + if self.mode_enable and self.mode_enable != self.dldp_intf_conf['dldpCompatibleEnable']: + if self.mode_enable == 'enable': + self.updates_cmd.append("dldp compatible-mode enable") + else: + self.updates_cmd.append("undo dldp compatible-mode enable") + + if self.local_mac: + flag = judge_is_mac_same( + self.local_mac, self.dldp_intf_conf['dldpLocalMac']) + if not flag: + self.updates_cmd.append( + "dldp compatible-mode local-mac %s" % self.local_mac) + + if self.reset and self.reset == 'enable': + self.updates_cmd.append('dldp reset') + else: + if self.changed: + self.updates_cmd.append("undo dldp compatible-mode local-mac") + + def get_end_state(self): + """Get end state info""" + + dldp_conf = dict() + self.dldp_intf_conf = self.get_dldp_intf_exist_config() + + dldp_conf['interface'] = self.interface + dldp_conf['enable'] = self.dldp_intf_conf.get('dldpEnable', None) + dldp_conf['mode_enable'] = self.dldp_intf_conf.get( + 'dldpCompatibleEnable', None) + dldp_conf['local_mac'] = self.dldp_intf_conf.get('dldpLocalMac', None) + dldp_conf['reset'] = 'disable' + if self.reset == 'enable': + dldp_conf['reset'] = 'enable' + + self.end_state = copy.deepcopy(dldp_conf) + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def work(self): + """Execute task""" + + self.dldp_intf_conf = self.get_dldp_intf_exist_config() + self.check_params() + self.same_conf = self.check_config_if_same() + self.get_existing() + self.get_proposed() + self.config_intf_dldp() + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + interface=dict(required=True, type='str'), + enable=dict(choices=['enable', 'disable'], type='str'), + reset=dict(choices=['enable', 'disable'], type='str'), + mode_enable=dict(choices=['enable', 'disable'], type='str'), + local_mac=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + argument_spec.update(ce_argument_spec) + dldp_intf_obj = DldpInterface(argument_spec) + dldp_intf_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_eth_trunk.py b/plugins/modules/ce_eth_trunk.py new file mode 100644 index 0000000..7d38b85 --- /dev/null +++ b/plugins/modules/ce_eth_trunk.py @@ -0,0 +1,673 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_eth_trunk +short_description: Manages Eth-Trunk interfaces on HUAWEI CloudEngine switches. +description: + - Manages Eth-Trunk specific configuration parameters on HUAWEI CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - C(state=absent) removes the Eth-Trunk config and interface if it + already exists. If members to be removed are not explicitly + passed, all existing members (if any), are removed, + and Eth-Trunk removed. + - Members must be a list. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + trunk_id: + description: + - Eth-Trunk interface number. + The value is an integer. + The value range depends on the assign forward eth-trunk mode command. + When 256 is specified, the value ranges from 0 to 255. + When 512 is specified, the value ranges from 0 to 511. + When 1024 is specified, the value ranges from 0 to 1023. + required: true + mode: + description: + - Specifies the working mode of an Eth-Trunk interface. + choices: ['manual','lacp-dynamic','lacp-static'] + min_links: + description: + - Specifies the minimum number of Eth-Trunk member links in the Up state. + The value is an integer ranging from 1 to the maximum number of interfaces + that can be added to a Eth-Trunk interface. + hash_type: + description: + - Hash algorithm used for load balancing among Eth-Trunk member interfaces. + choices: ['src-dst-ip', 'src-dst-mac', 'enhanced', 'dst-ip', 'dst-mac', 'src-ip', 'src-mac'] + members: + description: + - List of interfaces that will be managed in a given Eth-Trunk. + The interface name must be full name. + force: + description: + - When true it forces Eth-Trunk members to match what is + declared in the members param. This can be used to remove + members. + type: bool + default: 'no' + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +- name: eth_trunk module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Ensure Eth-Trunk100 is created, add two members, and set to mode lacp-static + ce_eth_trunk: + trunk_id: 100 + members: ['10GE1/0/24','10GE1/0/25'] + mode: 'lacp-static' + state: present + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"trunk_id": "100", "members": ['10GE1/0/24','10GE1/0/25'], "mode": "lacp-static"} +existing: + description: k/v pairs of existing Eth-Trunk + returned: always + type: dict + sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [ + {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}], + "min_links": "1", "mode": "manual"} +end_state: + description: k/v pairs of Eth-Trunk info after module execution + returned: always + type: dict + sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [ + {"memberIfName": "10GE1/0/24", "memberIfState": "Down"}, + {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}], + "min_links": "1", "mode": "lacp-static"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface Eth-Trunk 100", + "mode lacp-static", + "interface 10GE1/0/25", + "eth-trunk 100"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_TRUNK = """ + + + + + Eth-Trunk%s + + + + + + + + + + + + + + + + + +""" + +CE_NC_XML_BUILD_TRUNK_CFG = """ + + + %s + + +""" + +CE_NC_XML_DELETE_TRUNK = """ + + Eth-Trunk%s + +""" + +CE_NC_XML_CREATE_TRUNK = """ + + Eth-Trunk%s + +""" + +CE_NC_XML_MERGE_MINUPNUM = """ + + Eth-Trunk%s + %s + +""" + +CE_NC_XML_MERGE_HASHTYPE = """ + + Eth-Trunk%s + %s + +""" + +CE_NC_XML_MERGE_WORKMODE = """ + + Eth-Trunk%s + %s + +""" + +CE_NC_XML_BUILD_MEMBER_CFG = """ + + Eth-Trunk%s + %s + +""" + +CE_NC_XML_MERGE_MEMBER = """ + + %s + +""" + +CE_NC_XML_DELETE_MEMBER = """ + + %s + +""" + +MODE_XML2CLI = {"Manual": "manual", "Dynamic": "lacp-dynamic", "Static": "lacp-static"} +MODE_CLI2XML = {"manual": "Manual", "lacp-dynamic": "Dynamic", "lacp-static": "Static"} +HASH_XML2CLI = {"IP": "src-dst-ip", "MAC": "src-dst-mac", "Enhanced": "enhanced", + "Desip": "dst-ip", "Desmac": "dst-mac", "Sourceip": "src-ip", "Sourcemac": "src-mac"} +HASH_CLI2XML = {"src-dst-ip": "IP", "src-dst-mac": "MAC", "enhanced": "Enhanced", + "dst-ip": "Desip", "dst-mac": "Desmac", "src-ip": "Sourceip", "src-mac": "Sourcemac"} + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +def mode_xml_to_cli_str(mode): + """convert mode to cli format string""" + + if not mode: + return "" + + return MODE_XML2CLI.get(mode) + + +def hash_type_xml_to_cli_str(hash_type): + """convert trunk hash type netconf xml to cli format string""" + + if not hash_type: + return "" + + return HASH_XML2CLI.get(hash_type) + + +class EthTrunk(object): + """ + Manages Eth-Trunk interfaces. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.trunk_id = self.module.params['trunk_id'] + self.mode = self.module.params['mode'] + self.min_links = self.module.params['min_links'] + self.hash_type = self.module.params['hash_type'] + self.members = self.module.params['members'] + self.state = self.module.params['state'] + self.force = self.module.params['force'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + # interface info + self.trunk_info = dict() + + def __init_module__(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def netconf_set_config(self, xml_str, xml_name): + """ netconf set config """ + + recv_xml = set_nc_config(self.module, xml_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_trunk_dict(self, trunk_id): + """ get one interface attributes dict.""" + + trunk_info = dict() + conf_str = CE_NC_GET_TRUNK % trunk_id + recv_xml = get_nc_config(self.module, conf_str) + + if "" in recv_xml: + return trunk_info + + # get trunk base info + base = re.findall( + r'.*(.*).*\s*' + r'(.*).*\s*' + r'(.*).*\s*' + r'(.*).*\s*' + r'(.*).*\s*' + r'(.*).*\s*' + r'(.*).*\s*' + r'(.*).*', recv_xml) + + if base: + trunk_info = dict(ifName=base[0][0], + trunkId=base[0][0].lower().replace("eth-trunk", "").replace(" ", ""), + minUpNum=base[0][1], + maxUpNum=base[0][2], + trunkType=base[0][3], + hashType=base[0][4], + workMode=base[0][5], + upMemberIfNum=base[0][6], + memberIfNum=base[0][7]) + + # get trunk member interface info + member = re.findall( + r'.*(.*).*\s*' + r'(.*).*', recv_xml) + trunk_info["TrunkMemberIfs"] = list() + + for mem in member: + trunk_info["TrunkMemberIfs"].append( + dict(memberIfName=mem[0], memberIfState=mem[1])) + + return trunk_info + + def is_member_exist(self, ifname): + """is trunk member exist""" + + if not self.trunk_info["TrunkMemberIfs"]: + return False + + for mem in self.trunk_info["TrunkMemberIfs"]: + if ifname.replace(" ", "").upper() == mem["memberIfName"].replace(" ", "").upper(): + return True + + return False + + def get_mode_xml_str(self): + """trunk mode netconf xml format string""" + + return MODE_CLI2XML.get(self.mode) + + def get_hash_type_xml_str(self): + """trunk hash type netconf xml format string""" + + return HASH_CLI2XML.get(self.hash_type) + + def create_eth_trunk(self): + """Create Eth-Trunk interface""" + + xml_str = CE_NC_XML_CREATE_TRUNK % self.trunk_id + self.updates_cmd.append("interface Eth-Trunk %s" % self.trunk_id) + + if self.hash_type: + self.updates_cmd.append("load-balance %s" % self.hash_type) + xml_str += CE_NC_XML_MERGE_HASHTYPE % (self.trunk_id, self.get_hash_type_xml_str()) + + if self.mode: + self.updates_cmd.append("mode %s" % self.mode) + xml_str += CE_NC_XML_MERGE_WORKMODE % (self.trunk_id, self.get_mode_xml_str()) + + if self.min_links: + self.updates_cmd.append("least active-linknumber %s" % self.min_links) + xml_str += CE_NC_XML_MERGE_MINUPNUM % (self.trunk_id, self.min_links) + + if self.members: + mem_xml = "" + for mem in self.members: + mem_xml += CE_NC_XML_MERGE_MEMBER % mem.upper() + self.updates_cmd.append("interface %s" % mem) + self.updates_cmd.append("eth-trunk %s" % self.trunk_id) + xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml) + cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str + self.netconf_set_config(cfg_xml, "CREATE_TRUNK") + self.changed = True + + def delete_eth_trunk(self): + """Delete Eth-Trunk interface and remove all member""" + + if not self.trunk_info: + return + + xml_str = "" + mem_str = "" + if self.trunk_info["TrunkMemberIfs"]: + for mem in self.trunk_info["TrunkMemberIfs"]: + mem_str += CE_NC_XML_DELETE_MEMBER % mem["memberIfName"] + self.updates_cmd.append("interface %s" % mem["memberIfName"]) + self.updates_cmd.append("undo eth-trunk") + if mem_str: + xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_str) + + xml_str += CE_NC_XML_DELETE_TRUNK % self.trunk_id + self.updates_cmd.append("undo interface Eth-Trunk %s" % self.trunk_id) + cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str + self.netconf_set_config(cfg_xml, "DELETE_TRUNK") + self.changed = True + + def remove_member(self): + """delete trunk member""" + + if not self.members: + return + + change = False + mem_xml = "" + xml_str = "" + for mem in self.members: + if self.is_member_exist(mem): + mem_xml += CE_NC_XML_DELETE_MEMBER % mem.upper() + self.updates_cmd.append("interface %s" % mem) + self.updates_cmd.append("undo eth-trunk") + if mem_xml: + xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml) + change = True + + if not change: + return + + cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str + self.netconf_set_config(cfg_xml, "REMOVE_TRUNK_MEMBER") + self.changed = True + + def merge_eth_trunk(self): + """Create or merge Eth-Trunk""" + + change = False + xml_str = "" + self.updates_cmd.append("interface Eth-Trunk %s" % self.trunk_id) + if self.hash_type and self.get_hash_type_xml_str() != self.trunk_info["hashType"]: + self.updates_cmd.append("load-balance %s" % + self.hash_type) + xml_str += CE_NC_XML_MERGE_HASHTYPE % ( + self.trunk_id, self.get_hash_type_xml_str()) + change = True + if self.min_links and self.min_links != self.trunk_info["minUpNum"]: + self.updates_cmd.append( + "least active-linknumber %s" % self.min_links) + xml_str += CE_NC_XML_MERGE_MINUPNUM % ( + self.trunk_id, self.min_links) + change = True + if self.mode and self.get_mode_xml_str() != self.trunk_info["workMode"]: + self.updates_cmd.append("mode %s" % self.mode) + xml_str += CE_NC_XML_MERGE_WORKMODE % ( + self.trunk_id, self.get_mode_xml_str()) + change = True + + if not change: + self.updates_cmd.pop() # remove 'interface Eth-Trunk' command + + # deal force: + # When true it forces Eth-Trunk members to match + # what is declared in the members param. + if self.force and self.trunk_info["TrunkMemberIfs"]: + mem_xml = "" + for mem in self.trunk_info["TrunkMemberIfs"]: + if not self.members or mem["memberIfName"].replace(" ", "").upper() not in self.members: + mem_xml += CE_NC_XML_DELETE_MEMBER % mem["memberIfName"] + self.updates_cmd.append("interface %s" % mem["memberIfName"]) + self.updates_cmd.append("undo eth-trunk") + if mem_xml: + xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml) + change = True + + if self.members: + mem_xml = "" + for mem in self.members: + if not self.is_member_exist(mem): + mem_xml += CE_NC_XML_MERGE_MEMBER % mem.upper() + self.updates_cmd.append("interface %s" % mem) + self.updates_cmd.append("eth-trunk %s" % self.trunk_id) + if mem_xml: + xml_str += CE_NC_XML_BUILD_MEMBER_CFG % ( + self.trunk_id, mem_xml) + change = True + + if not change: + return + + cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str + self.netconf_set_config(cfg_xml, "MERGE_TRUNK") + self.changed = True + + def check_params(self): + """Check all input params""" + + # trunk_id check + if not self.trunk_id.isdigit(): + self.module.fail_json(msg='The parameter of trunk_id is invalid.') + + # min_links check + if self.min_links and not self.min_links.isdigit(): + self.module.fail_json(msg='The parameter of min_links is invalid.') + + # members check and convert members to upper + if self.members: + for mem in self.members: + if not get_interface_type(mem.replace(" ", "")): + self.module.fail_json( + msg='The parameter of members is invalid.') + + for mem_id in range(len(self.members)): + self.members[mem_id] = self.members[mem_id].replace(" ", "").upper() + + def get_proposed(self): + """get proposed info""" + + self.proposed["trunk_id"] = self.trunk_id + self.proposed["mode"] = self.mode + if self.min_links: + self.proposed["min_links"] = self.min_links + self.proposed["hash_type"] = self.hash_type + if self.members: + self.proposed["members"] = self.members + self.proposed["state"] = self.state + self.proposed["force"] = self.force + + def get_existing(self): + """get existing info""" + + if not self.trunk_info: + return + + self.existing["trunk_id"] = self.trunk_info["trunkId"] + self.existing["min_links"] = self.trunk_info["minUpNum"] + self.existing["hash_type"] = hash_type_xml_to_cli_str(self.trunk_info["hashType"]) + self.existing["mode"] = mode_xml_to_cli_str(self.trunk_info["workMode"]) + self.existing["members_detail"] = self.trunk_info["TrunkMemberIfs"] + + def get_end_state(self): + """get end state info""" + + trunk_info = self.get_trunk_dict(self.trunk_id) + if not trunk_info: + return + + self.end_state["trunk_id"] = trunk_info["trunkId"] + self.end_state["min_links"] = trunk_info["minUpNum"] + self.end_state["hash_type"] = hash_type_xml_to_cli_str(trunk_info["hashType"]) + self.end_state["mode"] = mode_xml_to_cli_str(trunk_info["workMode"]) + self.end_state["members_detail"] = trunk_info["TrunkMemberIfs"] + + def work(self): + """worker""" + + self.check_params() + self.trunk_info = self.get_trunk_dict(self.trunk_id) + self.get_existing() + self.get_proposed() + + # deal present or absent + if self.state == "present": + if not self.trunk_info: + # create + self.create_eth_trunk() + else: + # merge trunk + self.merge_eth_trunk() + else: + if self.trunk_info: + if not self.members: + # remove all members and delete trunk + self.delete_eth_trunk() + else: + # remove some trunk members + self.remove_member() + else: + self.module.fail_json(msg='Error: Eth-Trunk does not exist.') + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + trunk_id=dict(required=True), + mode=dict(required=False, + choices=['manual', 'lacp-dynamic', 'lacp-static'], + type='str'), + min_links=dict(required=False, type='str'), + hash_type=dict(required=False, + choices=['src-dst-ip', 'src-dst-mac', 'enhanced', + 'dst-ip', 'dst-mac', 'src-ip', 'src-mac'], + type='str'), + members=dict(required=False, default=None, type='list'), + force=dict(required=False, default=False, type='bool'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = EthTrunk(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_evpn_bd_vni.py b/plugins/modules/ce_evpn_bd_vni.py new file mode 100644 index 0000000..e274b17 --- /dev/null +++ b/plugins/modules/ce_evpn_bd_vni.py @@ -0,0 +1,1054 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_evpn_bd_vni +short_description: Manages EVPN VXLAN Network Identifier (VNI) on HUAWEI CloudEngine switches. +description: + - Manages Ethernet Virtual Private Network (EVPN) VXLAN Network + Identifier (VNI) configurations on HUAWEI CloudEngine switches. +author: Zhijin Zhou (@QijunPan) +notes: + - Ensure that EVPN has been configured to serve as the VXLAN control plane when state is present. + - Ensure that a bridge domain (BD) has existed when state is present. + - Ensure that a VNI has been created and associated with a broadcast domain (BD) when state is present. + - If you configure evpn:false to delete an EVPN instance, all configurations in the EVPN instance are deleted. + - After an EVPN instance has been created in the BD view, you can configure an RD using route_distinguisher + parameter in BD-EVPN instance view. + - Before configuring VPN targets for a BD EVPN instance, ensure that an RD has been configured + for the BD EVPN instance + - If you unconfigure route_distinguisher, all VPN target attributes for the BD EVPN instance will be removed at the same time. + - When using state:absent, evpn is not supported and it will be ignored. + - When using state:absent to delete VPN target attributes, ensure the configuration of VPN target attributes has + existed and otherwise it will report an error. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + bridge_domain_id: + description: + - Specify an existed bridge domain (BD).The value is an integer ranging from 1 to 16777215. + required: true + evpn: + description: + - Create or delete an EVPN instance for a VXLAN in BD view. + choices: ['enable','disable'] + default: 'enable' + route_distinguisher: + description: + - Configures a route distinguisher (RD) for a BD EVPN instance. + The format of an RD can be as follows + - 1) 2-byte AS number:4-byte user-defined number, for example, 1:3. An AS number is an integer ranging from + 0 to 65535, and a user-defined number is an integer ranging from 0 to 4294967295. The AS and user-defined + numbers cannot be both 0s. This means that an RD cannot be 0:0. + - 2) Integral 4-byte AS number:2-byte user-defined number, for example, 65537:3. An AS number is an integer + ranging from 65536 to 4294967295, and a user-defined number is an integer ranging from 0 to 65535. + - 3) 4-byte AS number in dotted notation:2-byte user-defined number, for example, 0.0:3 or 0.1:0. A 4-byte + AS number in dotted notation is in the format of x.y, where x and y are integers ranging from 0 to 65535. + - 4) A user-defined number is an integer ranging from 0 to 65535. The AS and user-defined numbers cannot be + both 0s. This means that an RD cannot be 0.0:0. + - 5) 32-bit IP address:2-byte user-defined number. For example, 192.168.122.15:1. An IP address ranges from + 0.0.0.0 to 255.255.255.255, and a user-defined number is an integer ranging from 0 to 65535. + - 6) 'auto' specifies the RD that is automatically generated. + vpn_target_both: + description: + - Add VPN targets to both the import and export VPN target lists of a BD EVPN instance. + The format is the same as route_distinguisher. + vpn_target_import: + description: + - Add VPN targets to the import VPN target list of a BD EVPN instance. + The format is the same as route_distinguisher. + required: true + vpn_target_export: + description: + - Add VPN targets to the export VPN target list of a BD EVPN instance. + The format is the same as route_distinguisher. + state: + description: + - Manage the state of the resource. + choices: ['present','absent'] + default: 'present' +''' + +EXAMPLES = ''' +- name: EVPN BD VNI test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Configure an EVPN instance for a VXLAN in BD view" + ce_evpn_bd_vni: + bridge_domain_id: 20 + evpn: enable + provider: "{{ cli }}" + + - name: "Configure a route distinguisher (RD) for a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + route_distinguisher: '22:22' + provider: "{{ cli }}" + + - name: "Configure VPN targets to both the import and export VPN target lists of a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + vpn_target_both: 22:100,22:101 + provider: "{{ cli }}" + + - name: "Configure VPN targets to the import VPN target list of a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + vpn_target_import: 22:22,22:23 + provider: "{{ cli }}" + + - name: "Configure VPN targets to the export VPN target list of a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + vpn_target_export: 22:38,22:39 + provider: "{{ cli }}" + + - name: "Unconfigure VPN targets to both the import and export VPN target lists of a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + vpn_target_both: '22:100' + state: absent + provider: "{{ cli }}" + + - name: "Unconfigure VPN targets to the import VPN target list of a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + vpn_target_import: '22:22' + state: absent + provider: "{{ cli }}" + + - name: "Unconfigure VPN targets to the export VPN target list of a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + vpn_target_export: '22:38' + state: absent + provider: "{{ cli }}" + + - name: "Unconfigure a route distinguisher (RD) of a BD EVPN instance" + ce_evpn_bd_vni: + bridge_domain_id: 20 + route_distinguisher: '22:22' + state: absent + provider: "{{ cli }}" + + - name: "Unconfigure an EVPN instance for a VXLAN in BD view" + ce_evpn_bd_vni: + bridge_domain_id: 20 + evpn: disable + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "bridge_domain_id": "2", + "evpn": "enable", + "route_distinguisher": "22:22", + "state": "present", + "vpn_target_both": [ + "22:100", + "22:101" + ], + "vpn_target_export": [ + "22:38", + "22:39" + ], + "vpn_target_import": [ + "22:22", + "22:23" + ] + } +existing: + description: k/v pairs of existing attributes on the device + returned: always + type: dict + sample: { + "bridge_domain_id": "2", + "evpn": "disable", + "route_distinguisher": null, + "vpn_target_both": [], + "vpn_target_export": [], + "vpn_target_import": [] + } +end_state: + description: k/v pairs of end attributes on the device + returned: always + type: dict + sample: { + "bridge_domain_id": "2", + "evpn": "enable", + "route_distinguisher": "22:22", + "vpn_target_both": [ + "22:100", + "22:101" + ], + "vpn_target_export": [ + "22:38", + "22:39" + ], + "vpn_target_import": [ + "22:22", + "22:23" + ] + } +updates: + description: command list sent to the device + returned: always + type: list + sample: [ + "bridge-domain 2", + " evpn", + " route-distinguisher 22:22", + " vpn-target 22:38 export-extcommunity", + " vpn-target 22:39 export-extcommunity", + " vpn-target 22:100 export-extcommunity", + " vpn-target 22:101 export-extcommunity", + " vpn-target 22:22 import-extcommunity", + " vpn-target 22:23 import-extcommunity", + " vpn-target 22:100 import-extcommunity", + " vpn-target 22:101 import-extcommunity" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +import copy +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_VNI_BD = """ + + + + + + + + + + +""" + +CE_NC_GET_EVPN_CONFIG = """ + + + + + %s + %s + + + + + + + + + + + + + + + + + +""" + +CE_NC_DELETE_EVPN_CONFIG = """ + + + + + %s + %s + + + + +""" + +CE_NC_DELETE_EVPN_CONFIG_HEAD = """ + + + + + %s + %s +""" + +CE_NC_MERGE_EVPN_CONFIG_HEAD = """ + + + + + %s + %s +""" + +CE_NC_MERGE_EVPN_AUTORTS_HEAD = """ + +""" + +CE_NC_MERGE_EVPN_AUTORTS_TAIL = """ + +""" + +CE_NC_DELETE_EVPN_AUTORTS_CONTEXT = """ + + %s + +""" + +CE_NC_MERGE_EVPN_AUTORTS_CONTEXT = """ + + %s + +""" + +CE_NC_MERGE_EVPN_RTS_HEAD = """ + +""" + +CE_NC_MERGE_EVPN_RTS_TAIL = """ + +""" + +CE_NC_DELETE_EVPN_RTS_CONTEXT = """ + + %s + %s + +""" + +CE_NC_MERGE_EVPN_RTS_CONTEXT = """ + + %s + %s + +""" + +CE_NC_MERGE_EVPN_CONFIG_TAIL = """ + + + + +""" + + +def is_valid_value(vrf_targe_value): + """check whether VPN target value is valid""" + + each_num = None + if len(vrf_targe_value) > 21 or len(vrf_targe_value) < 3: + return False + if vrf_targe_value.find(':') == -1: + return False + elif vrf_targe_value == '0:0': + return False + elif vrf_targe_value == '0.0:0': + return False + else: + value_list = vrf_targe_value.split(':') + if value_list[0].find('.') != -1: + if not value_list[1].isdigit(): + return False + if int(value_list[1]) > 65535: + return False + value = value_list[0].split('.') + if len(value) == 4: + for each_num in value: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + elif len(value) == 2: + for each_num in value: + if not each_num.isdigit(): + return False + if int(each_num) > 65535: + return False + return True + else: + return False + elif not value_list[0].isdigit(): + return False + elif not value_list[1].isdigit(): + return False + elif int(value_list[0]) < 65536 and int(value_list[1]) < 4294967296: + return True + elif int(value_list[0]) > 65535 and int(value_list[0]) < 4294967296: + return bool(int(value_list[1]) < 65536) + else: + return False + + +class EvpnBd(object): + """Manage evpn instance in BD view""" + + def __init__(self, argument_spec, ): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # EVPN instance info + self.bridge_domain_id = self.module.params['bridge_domain_id'] + self.evpn = self.module.params['evpn'] + self.route_distinguisher = self.module.params['route_distinguisher'] + self.vpn_target_both = self.module.params['vpn_target_both'] or list() + self.vpn_target_import = self.module.params[ + 'vpn_target_import'] or list() + self.vpn_target_export = self.module.params[ + 'vpn_target_export'] or list() + self.state = self.module.params['state'] + self.__string_to_lowercase__() + + self.commands = list() + self.evpn_info = dict() + self.conf_exist = False + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """Init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def __check_response__(self, xml_str, xml_name): + """Check if response message is already succeed""" + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def __string_to_lowercase__(self): + """Convert string to lowercase""" + + if self.route_distinguisher: + self.route_distinguisher = self.route_distinguisher.lower() + + if self.vpn_target_export: + for index, ele in enumerate(self.vpn_target_export): + self.vpn_target_export[index] = ele.lower() + + if self.vpn_target_import: + for index, ele in enumerate(self.vpn_target_import): + self.vpn_target_import[index] = ele.lower() + + if self.vpn_target_both: + for index, ele in enumerate(self.vpn_target_both): + self.vpn_target_both[index] = ele.lower() + + def get_all_evpn_rts(self, evpn_rts): + """Get all EVPN RTS""" + + rts = evpn_rts.findall("evpnRT") + if not rts: + return + + for ele in rts: + vrf_rttype = ele.find('vrfRTType') + vrf_rtvalue = ele.find('vrfRTValue') + + if vrf_rttype.text == 'export_extcommunity': + self.evpn_info['vpn_target_export'].append(vrf_rtvalue.text) + elif vrf_rttype.text == 'import_extcommunity': + self.evpn_info['vpn_target_import'].append(vrf_rtvalue.text) + + def get_all_evpn_autorts(self, evpn_autorts): + """"Get all EVPN AUTORTS""" + + autorts = evpn_autorts.findall("evpnAutoRT") + if not autorts: + return + + for autort in autorts: + vrf_rttype = autort.find('vrfRTType') + + if vrf_rttype.text == 'export_extcommunity': + self.evpn_info['vpn_target_export'].append('auto') + elif vrf_rttype.text == 'import_extcommunity': + self.evpn_info['vpn_target_import'].append('auto') + + def process_rts_info(self): + """Process RTS information""" + + if not self.evpn_info['vpn_target_export'] or\ + not self.evpn_info['vpn_target_import']: + return + + vpn_target_export = copy.deepcopy(self.evpn_info['vpn_target_export']) + for ele in vpn_target_export: + if ele in self.evpn_info['vpn_target_import']: + self.evpn_info['vpn_target_both'].append(ele) + self.evpn_info['vpn_target_export'].remove(ele) + self.evpn_info['vpn_target_import'].remove(ele) + + def get_evpn_instance_info(self): + """Get current EVPN instance information""" + + if not self.bridge_domain_id: + self.module.fail_json(msg='Error: The value of bridge_domain_id cannot be empty.') + + self.evpn_info['route_distinguisher'] = None + self.evpn_info['vpn_target_import'] = list() + self.evpn_info['vpn_target_export'] = list() + self.evpn_info['vpn_target_both'] = list() + self.evpn_info['evpn_inst'] = 'enable' + + xml_str = CE_NC_GET_EVPN_CONFIG % ( + self.bridge_domain_id, self.bridge_domain_id) + xml_str = get_nc_config(self.module, xml_str) + if "" in xml_str: + self.evpn_info['evpn_inst'] = 'disable' + return + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + evpn_inst = root.find("evpn/evpnInstances/evpnInstance") + if evpn_inst: + for eles in evpn_inst: + if eles.tag in ["evpnAutoRD", "evpnRD", "evpnRTs", "evpnAutoRTs"]: + if eles.tag == 'evpnAutoRD' and eles.text == 'true': + self.evpn_info['route_distinguisher'] = 'auto' + elif eles.tag == 'evpnRD' and self.evpn_info['route_distinguisher'] != 'auto': + self.evpn_info['route_distinguisher'] = eles.text + elif eles.tag == 'evpnRTs': + self.get_all_evpn_rts(eles) + elif eles.tag == 'evpnAutoRTs': + self.get_all_evpn_autorts(eles) + self.process_rts_info() + + def get_existing(self): + """Get existing config""" + + self.existing = dict(bridge_domain_id=self.bridge_domain_id, + evpn=self.evpn_info['evpn_inst'], + route_distinguisher=self.evpn_info[ + 'route_distinguisher'], + vpn_target_both=self.evpn_info['vpn_target_both'], + vpn_target_import=self.evpn_info[ + 'vpn_target_import'], + vpn_target_export=self.evpn_info['vpn_target_export']) + + def get_proposed(self): + """Get proposed config""" + + self.proposed = dict(bridge_domain_id=self.bridge_domain_id, + evpn=self.evpn, + route_distinguisher=self.route_distinguisher, + vpn_target_both=self.vpn_target_both, + vpn_target_import=self.vpn_target_import, + vpn_target_export=self.vpn_target_export, + state=self.state) + + def get_end_state(self): + """Get end config""" + + self.get_evpn_instance_info() + self.end_state = dict(bridge_domain_id=self.bridge_domain_id, + evpn=self.evpn_info['evpn_inst'], + route_distinguisher=self.evpn_info[ + 'route_distinguisher'], + vpn_target_both=self.evpn_info[ + 'vpn_target_both'], + vpn_target_import=self.evpn_info[ + 'vpn_target_import'], + vpn_target_export=self.evpn_info['vpn_target_export']) + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def judge_if_vpn_target_exist(self, vpn_target_type): + """Judge whether proposed vpn target has existed""" + + vpn_target = list() + if vpn_target_type == 'vpn_target_import': + vpn_target.extend(self.existing['vpn_target_both']) + vpn_target.extend(self.existing['vpn_target_import']) + return set(self.proposed['vpn_target_import']).issubset(vpn_target) + elif vpn_target_type == 'vpn_target_export': + vpn_target.extend(self.existing['vpn_target_both']) + vpn_target.extend(self.existing['vpn_target_export']) + return set(self.proposed['vpn_target_export']).issubset(vpn_target) + + return False + + def judge_if_config_exist(self): + """Judge whether configuration has existed""" + + if self.state == 'absent': + if self.route_distinguisher or self.vpn_target_import or self.vpn_target_export or self.vpn_target_both: + return False + else: + return True + + if self.evpn_info['evpn_inst'] != self.evpn: + return False + + if self.evpn == 'disable' and self.evpn_info['evpn_inst'] == 'disable': + return True + + if self.proposed['bridge_domain_id'] != self.existing['bridge_domain_id']: + return False + + if self.proposed['route_distinguisher']: + if self.proposed['route_distinguisher'] != self.existing['route_distinguisher']: + return False + + if self.proposed['vpn_target_both']: + if not self.existing['vpn_target_both']: + return False + if not set(self.proposed['vpn_target_both']).issubset(self.existing['vpn_target_both']): + return False + + if self.proposed['vpn_target_import']: + if not self.judge_if_vpn_target_exist('vpn_target_import'): + return False + + if self.proposed['vpn_target_export']: + if not self.judge_if_vpn_target_exist('vpn_target_export'): + return False + + return True + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def unconfig_evpn_instance(self): + """Unconfigure EVPN instance""" + + self.updates_cmd.append("bridge-domain %s" % self.bridge_domain_id) + xml_str = CE_NC_MERGE_EVPN_CONFIG_HEAD % ( + self.bridge_domain_id, self.bridge_domain_id) + self.updates_cmd.append(" evpn") + + # unconfigure RD + if self.route_distinguisher: + if self.route_distinguisher.lower() == 'auto': + xml_str += 'false' + self.updates_cmd.append(" undo route-distinguisher auto") + else: + xml_str += '' + self.updates_cmd.append( + " undo route-distinguisher %s" % self.route_distinguisher) + xml_str += CE_NC_MERGE_EVPN_CONFIG_TAIL + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "UNDO_EVPN_BD_RD") + self.changed = True + return + + # process VPN target list + vpn_target_export = copy.deepcopy(self.vpn_target_export) + vpn_target_import = copy.deepcopy(self.vpn_target_import) + if self.vpn_target_both: + for ele in self.vpn_target_both: + if ele not in vpn_target_export: + vpn_target_export.append(ele) + if ele not in vpn_target_import: + vpn_target_import.append(ele) + + # unconfig EVPN auto RTS + head_flag = False + if vpn_target_export: + for ele in vpn_target_export: + if ele.lower() == 'auto': + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD + head_flag = True + xml_str += CE_NC_DELETE_EVPN_AUTORTS_CONTEXT % ( + 'export_extcommunity') + self.updates_cmd.append( + " undo vpn-target auto export-extcommunity") + if vpn_target_import: + for ele in vpn_target_import: + if ele.lower() == 'auto': + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD + head_flag = True + xml_str += CE_NC_DELETE_EVPN_AUTORTS_CONTEXT % ( + 'import_extcommunity') + self.updates_cmd.append( + " undo vpn-target auto import-extcommunity") + + if head_flag: + xml_str += CE_NC_MERGE_EVPN_AUTORTS_TAIL + + # unconfig EVPN RTS + head_flag = False + if vpn_target_export: + for ele in vpn_target_export: + if ele.lower() != 'auto': + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_RTS_HEAD + head_flag = True + xml_str += CE_NC_DELETE_EVPN_RTS_CONTEXT % ( + 'export_extcommunity', ele) + self.updates_cmd.append( + " undo vpn-target %s export-extcommunity" % ele) + + if vpn_target_import: + for ele in vpn_target_import: + if ele.lower() != 'auto': + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_RTS_HEAD + head_flag = True + xml_str += CE_NC_DELETE_EVPN_RTS_CONTEXT % ( + 'import_extcommunity', ele) + self.updates_cmd.append( + " undo vpn-target %s import-extcommunity" % ele) + + if head_flag: + xml_str += CE_NC_MERGE_EVPN_RTS_TAIL + + xml_str += CE_NC_MERGE_EVPN_CONFIG_TAIL + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "MERGE_EVPN_BD_VPN_TARGET_CONFIG") + self.changed = True + + def config_evpn_instance(self): + """Configure EVPN instance""" + + self.updates_cmd.append("bridge-domain %s" % self.bridge_domain_id) + + if self.evpn == 'disable': + xml_str = CE_NC_DELETE_EVPN_CONFIG % ( + self.bridge_domain_id, self.bridge_domain_id) + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "MERGE_EVPN_BD_CONFIG") + self.updates_cmd.append(" undo evpn") + self.changed = True + return + + xml_str = CE_NC_MERGE_EVPN_CONFIG_HEAD % ( + self.bridge_domain_id, self.bridge_domain_id) + self.updates_cmd.append(" evpn") + + # configure RD + if self.route_distinguisher: + if not self.existing['route_distinguisher']: + if self.route_distinguisher.lower() == 'auto': + xml_str += 'true' + self.updates_cmd.append(" route-distinguisher auto") + else: + xml_str += '%s' % self.route_distinguisher + self.updates_cmd.append( + " route-distinguisher %s" % self.route_distinguisher) + + # process VPN target list + vpn_target_export = copy.deepcopy(self.vpn_target_export) + vpn_target_import = copy.deepcopy(self.vpn_target_import) + if self.vpn_target_both: + for ele in self.vpn_target_both: + if ele not in vpn_target_export: + vpn_target_export.append(ele) + if ele not in vpn_target_import: + vpn_target_import.append(ele) + + # config EVPN auto RTS + head_flag = False + if vpn_target_export: + for ele in vpn_target_export: + if ele.lower() == 'auto' and \ + (not self.is_vpn_target_exist('export_extcommunity', ele.lower())): + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD + head_flag = True + xml_str += CE_NC_MERGE_EVPN_AUTORTS_CONTEXT % ( + 'export_extcommunity') + self.updates_cmd.append( + " vpn-target auto export-extcommunity") + if vpn_target_import: + for ele in vpn_target_import: + if ele.lower() == 'auto' and \ + (not self.is_vpn_target_exist('import_extcommunity', ele.lower())): + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD + head_flag = True + xml_str += CE_NC_MERGE_EVPN_AUTORTS_CONTEXT % ( + 'import_extcommunity') + self.updates_cmd.append( + " vpn-target auto import-extcommunity") + + if head_flag: + xml_str += CE_NC_MERGE_EVPN_AUTORTS_TAIL + + # config EVPN RTS + head_flag = False + if vpn_target_export: + for ele in vpn_target_export: + if ele.lower() != 'auto' and \ + (not self.is_vpn_target_exist('export_extcommunity', ele.lower())): + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_RTS_HEAD + head_flag = True + xml_str += CE_NC_MERGE_EVPN_RTS_CONTEXT % ( + 'export_extcommunity', ele) + self.updates_cmd.append( + " vpn-target %s export-extcommunity" % ele) + + if vpn_target_import: + for ele in vpn_target_import: + if ele.lower() != 'auto' and \ + (not self.is_vpn_target_exist('import_extcommunity', ele.lower())): + if not head_flag: + xml_str += CE_NC_MERGE_EVPN_RTS_HEAD + head_flag = True + xml_str += CE_NC_MERGE_EVPN_RTS_CONTEXT % ( + 'import_extcommunity', ele) + self.updates_cmd.append( + " vpn-target %s import-extcommunity" % ele) + + if head_flag: + xml_str += CE_NC_MERGE_EVPN_RTS_TAIL + + xml_str += CE_NC_MERGE_EVPN_CONFIG_TAIL + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "MERGE_EVPN_BD_CONFIG") + self.changed = True + + def is_vpn_target_exist(self, target_type, value): + """Judge whether VPN target has existed""" + + if target_type == 'export_extcommunity': + if (value not in self.existing['vpn_target_export']) and\ + (value not in self.existing['vpn_target_both']): + return False + return True + + if target_type == 'import_extcommunity': + if (value not in self.existing['vpn_target_import']) and\ + (value not in self.existing['vpn_target_both']): + return False + return True + + return False + + def config_evnp_bd(self): + """Configure EVPN in BD view""" + + if not self.conf_exist: + if self.state == 'present': + self.config_evpn_instance() + else: + self.unconfig_evpn_instance() + + def process_input_params(self): + """Process input parameters""" + + if self.state == 'absent': + self.evpn = None + else: + if self.evpn == 'disable': + return + + if self.vpn_target_both: + for ele in self.vpn_target_both: + if ele in self.vpn_target_export: + self.vpn_target_export.remove(ele) + if ele in self.vpn_target_import: + self.vpn_target_import.remove(ele) + + if self.vpn_target_export and self.vpn_target_import: + vpn_target_export = copy.deepcopy(self.vpn_target_export) + for ele in vpn_target_export: + if ele in self.vpn_target_import: + self.vpn_target_both.append(ele) + self.vpn_target_import.remove(ele) + self.vpn_target_export.remove(ele) + + def check_vpn_target_para(self): + """Check whether VPN target value is valid""" + + if self.route_distinguisher: + if self.route_distinguisher.lower() != 'auto' and\ + not is_valid_value(self.route_distinguisher): + self.module.fail_json( + msg='Error: Route distinguisher has invalid value %s.' % self.route_distinguisher) + + if self.vpn_target_export: + for ele in self.vpn_target_export: + if ele.lower() != 'auto' and not is_valid_value(ele): + self.module.fail_json( + msg='Error: VPN target extended community attribute has invalid value %s.' % ele) + + if self.vpn_target_import: + for ele in self.vpn_target_import: + if ele.lower() != 'auto' and not is_valid_value(ele): + self.module.fail_json( + msg='Error: VPN target extended community attribute has invalid value %s.' % ele) + + if self.vpn_target_both: + for ele in self.vpn_target_both: + if ele.lower() != 'auto' and not is_valid_value(ele): + self.module.fail_json( + msg='Error: VPN target extended community attribute has invalid value %s.' % ele) + + def check_undo_params_if_exist(self): + """Check whether all undo parameters is existed""" + + if self.vpn_target_import: + for ele in self.vpn_target_import: + if ele not in self.evpn_info['vpn_target_import'] and ele not in self.evpn_info['vpn_target_both']: + self.module.fail_json( + msg='Error: VPN target import attribute value %s does not exist.' % ele) + + if self.vpn_target_export: + for ele in self.vpn_target_export: + if ele not in self.evpn_info['vpn_target_export'] and ele not in self.evpn_info['vpn_target_both']: + self.module.fail_json( + msg='Error: VPN target export attribute value %s does not exist.' % ele) + + if self.vpn_target_both: + for ele in self.vpn_target_both: + if ele not in self.evpn_info['vpn_target_both']: + self.module.fail_json( + msg='Error: VPN target export and import attribute value %s does not exist.' % ele) + + def check_params(self): + """Check all input params""" + + # bridge_domain_id check + if self.bridge_domain_id: + if not self.bridge_domain_id.isdigit(): + self.module.fail_json( + msg='Error: The parameter of bridge domain id is invalid.') + if int(self.bridge_domain_id) > 16777215 or int(self.bridge_domain_id) < 1: + self.module.fail_json( + msg='Error: The bridge domain id must be an integer between 1 and 16777215.') + + if self.state == 'absent': + self.check_undo_params_if_exist() + + # check bd whether binding the vxlan vni + self.check_vni_bd() + self.check_vpn_target_para() + + if self.state == 'absent': + if self.route_distinguisher: + if not self.evpn_info['route_distinguisher']: + self.module.fail_json( + msg='Error: Route distinguisher has not been configured.') + else: + if self.route_distinguisher != self.evpn_info['route_distinguisher']: + self.module.fail_json( + msg='Error: Current route distinguisher value is %s.' % + self.evpn_info['route_distinguisher']) + + if self.state == 'present': + if self.route_distinguisher: + if self.evpn_info['route_distinguisher'] and\ + self.route_distinguisher != self.evpn_info['route_distinguisher']: + self.module.fail_json( + msg='Error: Route distinguisher has already been configured.') + + def check_vni_bd(self): + """Check whether vxlan vni is configured in BD view""" + + xml_str = CE_NC_GET_VNI_BD + xml_str = get_nc_config(self.module, xml_str) + if "" in xml_str or not re.findall(r'\S+\s+%s' % self.bridge_domain_id, xml_str): + self.module.fail_json( + msg='Error: The vxlan vni is not configured or the bridge domain id is invalid.') + + def work(self): + """Execute task""" + + self.get_evpn_instance_info() + self.process_input_params() + self.check_params() + self.get_existing() + self.get_proposed() + self.conf_exist = self.judge_if_config_exist() + + self.config_evnp_bd() + + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + bridge_domain_id=dict(required=True, type='str'), + evpn=dict(required=False, type='str', + default='enable', choices=['enable', 'disable']), + route_distinguisher=dict(required=False, type='str'), + vpn_target_both=dict(required=False, type='list'), + vpn_target_import=dict(required=False, type='list'), + vpn_target_export=dict(required=False, type='list'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + evpn_bd = EvpnBd(argument_spec) + evpn_bd.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_evpn_bgp.py b/plugins/modules/ce_evpn_bgp.py new file mode 100644 index 0000000..d689426 --- /dev/null +++ b/plugins/modules/ce_evpn_bgp.py @@ -0,0 +1,727 @@ +#!/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 . +# +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_evpn_bgp +short_description: Manages BGP EVPN configuration on HUAWEI CloudEngine switches. +description: + - This module offers the ability to configure a BGP EVPN peer relationship on HUAWEI CloudEngine switches. +author: + - Li Yanfeng (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + bgp_instance: + description: + - Name of a BGP instance. The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + required: True + as_number: + description: + - Specifies integral AS number. The value is an integer ranging from 1 to 4294967295. + peer_address: + description: + - Specifies the IPv4 address of a BGP EVPN peer. The value is in dotted decimal notation. + peer_group_name: + description: + - Specify the name of a peer group that BGP peers need to join. + The value is a string of 1 to 47 case-sensitive characters, spaces not supported. + peer_enable: + description: + - Enable or disable a BGP device to exchange routes with a specified peer or peer group in the address + family view. + choices: ['true','false'] + advertise_router_type: + description: + - Configures a device to advertise routes to its BGP EVPN peers. + choices: ['arp','irb'] + vpn_name: + description: + - Associates a specified VPN instance with the IPv4 address family. + The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + advertise_l2vpn_evpn: + description: + - Enable or disable a device to advertise IP routes imported to a VPN instance to its EVPN instance. + choices: ['enable','disable'] + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +- name: evpn bgp module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Enable peer address. + ce_evpn_bgp: + bgp_instance: 100 + peer_address: 1.1.1.1 + as_number: 100 + peer_enable: true + provider: "{{ cli }}" + + - name: Enable peer group arp. + ce_evpn_bgp: + bgp_instance: 100 + peer_group_name: aaa + advertise_router_type: arp + provider: "{{ cli }}" + + - name: Enable advertise l2vpn evpn. + ce_evpn_bgp: + bgp_instance: 100 + vpn_name: aaa + advertise_l2vpn_evpn: enable + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"advertise_router_type": "arp", "bgp_instance": "100", "peer_group_name": "aaa", "state": "present"} +existing: + description: k/v pairs of existing rollback + returned: always + type: dict + sample: {"bgp_instance": "100", "peer_group_advertise_type": []} + +updates: + description: command sent to the device + returned: always + type: list + sample: ["peer 1.1.1.1 enable", + "peer aaa advertise arp"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"advertise_l2vpn_evpn": "enable", "bgp_instance": "100", "vpn_name": "aaa"} +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + + +def is_config_exist(cmp_cfg, test_cfg): + """check configuration is exist""" + + if not cmp_cfg or not test_cfg: + return False + + return bool(test_cfg in cmp_cfg) + + +def is_valid_address(address): + """check ip-address is valid""" + + if address.find('.') != -1: + addr_list = address.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +def is_valid_as_number(as_number): + """check as-number is valid""" + + if as_number.isdigit(): + if int(as_number) > 4294967295 or int(as_number) < 1: + return False + return True + else: + if as_number.find('.') != -1: + number_list = as_number.split('.') + if len(number_list) != 2: + return False + if number_list[1] == 0: + return False + for each_num in number_list: + if not each_num.isdigit(): + return False + if int(each_num) > 65535: + return False + return True + + return False + + +class EvpnBgp(object): + """ + Manages evpn bgp configuration. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.netconf = None + self.init_module() + + # module input info + self.as_number = self.module.params['as_number'] + self.bgp_instance = self.module.params['bgp_instance'] + self.peer_address = self.module.params['peer_address'] + self.peer_group_name = self.module.params['peer_group_name'] + self.peer_enable = self.module.params['peer_enable'] + self.advertise_router_type = self.module.params[ + 'advertise_router_type'] + self.vpn_name = self.module.params['vpn_name'] + self.advertise_l2vpn_evpn = self.module.params['advertise_l2vpn_evpn'] + self.state = self.module.params['state'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.config = "" + self.config_list = list() + self.l2vpn_evpn_exist = False + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + def init_module(self): + """ init module """ + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def get_evpn_overlay_config(self): + """get evpn-overlay enable configuration""" + + cmd = "display current-configuration | include ^evpn-overlay enable" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + return out + + def get_current_config(self): + """get current configuration""" + + cmd = "display current-configuration | section include bgp %s" % self.bgp_instance + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + return out + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) # show updates result + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def check_params(self): + """Check all input params""" + + # as_number check + if not self.bgp_instance: + self.module.fail_json( + msg='Error: The bgp_instance can not be none.') + if not self.peer_enable and not self.advertise_router_type and not self.advertise_l2vpn_evpn: + self.module.fail_json( + msg='Error: The peer_enable, advertise_router_type, advertise_l2vpn_evpn ' + 'can not be none at the same time.') + if self.as_number: + if not is_valid_as_number(self.as_number): + self.module.fail_json( + msg='Error: The parameter of as_number %s is invalid.' % self.as_number) + # bgp_instance check + if self.bgp_instance: + if not is_valid_as_number(self.bgp_instance): + self.module.fail_json( + msg='Error: The parameter of bgp_instance %s is invalid.' % self.bgp_instance) + + # peer_address check + if self.peer_address: + if not is_valid_address(self.peer_address): + self.module.fail_json( + msg='Error: The %s is not a valid ip address.' % self.peer_address) + + # peer_group_name check + if self.peer_group_name: + if len(self.peer_group_name) > 47 \ + or len(self.peer_group_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: peer group name is not in the range from 1 to 47.') + + # vpn_name check + if self.vpn_name: + if len(self.vpn_name) > 31 \ + or len(self.vpn_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: peer group name is not in the range from 1 to 31.') + + def get_proposed(self): + """get proposed info""" + + if self.as_number: + self.proposed["as_number"] = self.as_number + if self.bgp_instance: + self.proposed["bgp_instance"] = self.bgp_instance + if self.peer_address: + self.proposed["peer_address"] = self.peer_address + if self.peer_group_name: + self.proposed["peer_group_name"] = self.peer_group_name + if self.peer_enable: + self.proposed["peer_enable"] = self.peer_enable + if self.advertise_router_type: + self.proposed["advertise_router_type"] = self.advertise_router_type + if self.vpn_name: + self.proposed["vpn_name"] = self.vpn_name + if self.advertise_l2vpn_evpn: + self.proposed["advertise_l2vpn_evpn"] = self.advertise_l2vpn_evpn + if not self.peer_enable or not self.advertise_l2vpn_evpn: + if self.state: + self.proposed["state"] = self.state + + def get_peers_enable(self): + """get evpn peer address enable list""" + + if len(self.config_list) != 2: + return None + self.config_list = self.config.split('l2vpn-family evpn') + get = re.findall( + r"peer ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\s?as-number\s?(\S*)", self.config_list[0]) + if not get: + return None + else: + peers = list() + for item in get: + cmd = "peer %s enable" % item[0] + exist = is_config_exist(self.config_list[1], cmd) + if exist: + peers.append( + dict(peer_address=item[0], as_number=item[1], peer_enable='true')) + else: + peers.append(dict(peer_address=item[0], as_number=item[1], peer_enable='false')) + return peers + + def get_peers_advertise_type(self): + """get evpn peer address advertise type list""" + + if len(self.config_list) != 2: + return None + self.config_list = self.config.split('l2vpn-family evpn') + get = re.findall( + r"peer ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\s?as-number\s?(\S*)", self.config_list[0]) + if not get: + return None + else: + peers = list() + for item in get: + cmd = "peer %s advertise arp" % item[0] + exist1 = is_config_exist(self.config_list[1], cmd) + cmd = "peer %s advertise irb" % item[0] + exist2 = is_config_exist(self.config_list[1], cmd) + if exist1: + peers.append(dict(peer_address=item[0], as_number=item[1], advertise_type='arp')) + if exist2: + peers.append(dict(peer_address=item[0], as_number=item[1], advertise_type='irb')) + return peers + + def get_peers_group_enable(self): + """get evpn peer group name enable list""" + + if len(self.config_list) != 2: + return None + self.config_list = self.config.split('l2vpn-family evpn') + get1 = re.findall( + r"group (\S+) external", self.config_list[0]) + + get2 = re.findall( + r"group (\S+) internal", self.config_list[0]) + + if not get1 and not get2: + return None + else: + peer_groups = list() + for item in get1: + cmd = "peer %s enable" % item + exist = is_config_exist(self.config_list[1], cmd) + if exist: + peer_groups.append( + dict(peer_group_name=item, peer_enable='true')) + else: + peer_groups.append( + dict(peer_group_name=item, peer_enable='false')) + + for item in get2: + cmd = "peer %s enable" % item + exist = is_config_exist(self.config_list[1], cmd) + if exist: + peer_groups.append( + dict(peer_group_name=item, peer_enable='true')) + else: + peer_groups.append( + dict(peer_group_name=item, peer_enable='false')) + return peer_groups + + def get_peer_groups_advertise_type(self): + """get evpn peer group name advertise type list""" + + if len(self.config_list) != 2: + return None + self.config_list = self.config.split('l2vpn-family evpn') + get1 = re.findall( + r"group (\S+) external", self.config_list[0]) + + get2 = re.findall( + r"group (\S+) internal", self.config_list[0]) + if not get1 and not get2: + return None + else: + peer_groups = list() + for item in get1: + cmd = "peer %s advertise arp" % item + exist1 = is_config_exist(self.config_list[1], cmd) + cmd = "peer %s advertise irb" % item + exist2 = is_config_exist(self.config_list[1], cmd) + if exist1: + peer_groups.append( + dict(peer_group_name=item, advertise_type='arp')) + if exist2: + peer_groups.append( + dict(peer_group_name=item, advertise_type='irb')) + + for item in get2: + cmd = "peer %s advertise arp" % item + exist1 = is_config_exist(self.config_list[1], cmd) + cmd = "peer %s advertise irb" % item + exist2 = is_config_exist(self.config_list[1], cmd) + if exist1: + peer_groups.append( + dict(peer_group_name=item, advertise_type='arp')) + if exist2: + peer_groups.append( + dict(peer_group_name=item, advertise_type='irb')) + return peer_groups + + def get_existing(self): + """get existing info""" + + if not self.config: + return + if self.bgp_instance: + self.existing["bgp_instance"] = self.bgp_instance + + if self.peer_address and self.peer_enable: + if self.l2vpn_evpn_exist: + self.existing["peer_address_enable"] = self.get_peers_enable() + + if self.peer_group_name and self.peer_enable: + if self.l2vpn_evpn_exist: + self.existing[ + "peer_group_enable"] = self.get_peers_group_enable() + + if self.peer_address and self.advertise_router_type: + if self.l2vpn_evpn_exist: + self.existing[ + "peer_address_advertise_type"] = self.get_peers_advertise_type() + + if self.peer_group_name and self.advertise_router_type: + if self.l2vpn_evpn_exist: + self.existing[ + "peer_group_advertise_type"] = self.get_peer_groups_advertise_type() + + if self.advertise_l2vpn_evpn and self.vpn_name: + cmd = " ipv4-family vpn-instance %s" % self.vpn_name + exist = is_config_exist(self.config, cmd) + if exist: + self.existing["vpn_name"] = self.vpn_name + l2vpn_cmd = "advertise l2vpn evpn" + l2vpn_exist = is_config_exist(self.config, l2vpn_cmd) + if l2vpn_exist: + self.existing["advertise_l2vpn_evpn"] = 'enable' + else: + self.existing["advertise_l2vpn_evpn"] = 'disable' + + def get_end_state(self): + """get end state info""" + + self.config = self.get_current_config() + if not self.config: + return + + self.config_list = self.config.split('l2vpn-family evpn') + if len(self.config_list) == 2: + self.l2vpn_evpn_exist = True + + if self.bgp_instance: + self.end_state["bgp_instance"] = self.bgp_instance + + if self.peer_address and self.peer_enable: + if self.l2vpn_evpn_exist: + self.end_state["peer_address_enable"] = self.get_peers_enable() + + if self.peer_group_name and self.peer_enable: + if self.l2vpn_evpn_exist: + self.end_state[ + "peer_group_enable"] = self.get_peers_group_enable() + + if self.peer_address and self.advertise_router_type: + if self.l2vpn_evpn_exist: + self.end_state[ + "peer_address_advertise_type"] = self.get_peers_advertise_type() + + if self.peer_group_name and self.advertise_router_type: + if self.l2vpn_evpn_exist: + self.end_state[ + "peer_group_advertise_type"] = self.get_peer_groups_advertise_type() + + if self.advertise_l2vpn_evpn and self.vpn_name: + cmd = " ipv4-family vpn-instance %s" % self.vpn_name + exist = is_config_exist(self.config, cmd) + if exist: + self.end_state["vpn_name"] = self.vpn_name + l2vpn_cmd = "advertise l2vpn evpn" + l2vpn_exist = is_config_exist(self.config, l2vpn_cmd) + if l2vpn_exist: + self.end_state["advertise_l2vpn_evpn"] = 'enable' + else: + self.end_state["advertise_l2vpn_evpn"] = 'disable' + + def config_peer(self): + """configure evpn bgp peer command""" + + if self.as_number and self.peer_address: + cmd = "peer %s as-number %s" % (self.peer_address, self.as_number) + exist = is_config_exist(self.config, cmd) + if not exist: + self.module.fail_json( + msg='Error: The peer session %s does not exist or the peer already ' + 'exists in another as-number.' % self.peer_address) + cmd = "bgp %s" % self.bgp_instance + self.cli_add_command(cmd) + cmd = "l2vpn-family evpn" + self.cli_add_command(cmd) + exist_l2vpn = is_config_exist(self.config, cmd) + if self.peer_enable: + cmd = "peer %s enable" % self.peer_address + if exist_l2vpn: + exist = is_config_exist(self.config_list[1], cmd) + if self.peer_enable == "true" and not exist: + self.cli_add_command(cmd) + self.changed = True + elif self.peer_enable == "false" and exist: + self.cli_add_command(cmd, undo=True) + self.changed = True + else: + self.cli_add_command(cmd) + self.changed = True + + if self.advertise_router_type: + cmd = "peer %s advertise %s" % ( + self.peer_address, self.advertise_router_type) + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + self.cli_add_command(cmd) + self.changed = True + elif self.state == "absent" and exist: + self.cli_add_command(cmd, undo=True) + self.changed = True + elif self.peer_group_name: + cmd_1 = "group %s external" % self.peer_group_name + exist_1 = is_config_exist(self.config, cmd_1) + cmd_2 = "group %s internal" % self.peer_group_name + exist_2 = is_config_exist(self.config, cmd_2) + exist = False + if exist_1: + exist = True + if exist_2: + exist = True + if not exist: + self.module.fail_json( + msg='Error: The peer-group %s does not exist.' % self.peer_group_name) + cmd = "bgp %s" % self.bgp_instance + self.cli_add_command(cmd) + cmd = "l2vpn-family evpn" + self.cli_add_command(cmd) + exist_l2vpn = is_config_exist(self.config, cmd) + if self.peer_enable: + cmd = "peer %s enable" % self.peer_group_name + if exist_l2vpn: + exist = is_config_exist(self.config_list[1], cmd) + if self.peer_enable == "true" and not exist: + self.cli_add_command(cmd) + self.changed = True + elif self.peer_enable == "false" and exist: + self.cli_add_command(cmd, undo=True) + self.changed = True + else: + self.cli_add_command(cmd) + self.changed = True + + if self.advertise_router_type: + cmd = "peer %s advertise %s" % ( + self.peer_group_name, self.advertise_router_type) + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + self.cli_add_command(cmd) + self.changed = True + elif self.state == "absent" and exist: + self.cli_add_command(cmd, undo=True) + self.changed = True + + def config_advertise_l2vpn_evpn(self): + """configure advertise l2vpn evpn""" + + cmd = "ipv4-family vpn-instance %s" % self.vpn_name + exist = is_config_exist(self.config, cmd) + if not exist: + self.module.fail_json( + msg='Error: The VPN instance name %s does not exist.' % self.vpn_name) + config_vpn_list = self.config.split(cmd) + cmd = "ipv4-family vpn-instance" + exist_vpn = is_config_exist(config_vpn_list[1], cmd) + cmd_l2vpn = "advertise l2vpn evpn" + if exist_vpn: + config_vpn = config_vpn_list[1].split('ipv4-family vpn-instance') + exist_l2vpn = is_config_exist(config_vpn[0], cmd_l2vpn) + else: + exist_l2vpn = is_config_exist(config_vpn_list[1], cmd_l2vpn) + cmd = "advertise l2vpn evpn" + if self.advertise_l2vpn_evpn == "enable" and not exist_l2vpn: + cmd = "bgp %s" % self.bgp_instance + self.cli_add_command(cmd) + cmd = "ipv4-family vpn-instance %s" % self.vpn_name + self.cli_add_command(cmd) + cmd = "advertise l2vpn evpn" + self.cli_add_command(cmd) + self.changed = True + elif self.advertise_l2vpn_evpn == "disable" and exist_l2vpn: + cmd = "bgp %s" % self.bgp_instance + self.cli_add_command(cmd) + cmd = "ipv4-family vpn-instance %s" % self.vpn_name + self.cli_add_command(cmd) + cmd = "advertise l2vpn evpn" + self.cli_add_command(cmd, undo=True) + self.changed = True + + def work(self): + """worker""" + + self.check_params() + evpn_config = self.get_evpn_overlay_config() + if not evpn_config: + self.module.fail_json( + msg="Error: evpn-overlay enable is not configured.") + self.config = self.get_current_config() + if not self.config: + self.module.fail_json( + msg="Error: Bgp instance %s does not exist." % self.bgp_instance) + + self.config_list = self.config.split('l2vpn-family evpn') + if len(self.config_list) == 2: + self.l2vpn_evpn_exist = True + self.get_existing() + self.get_proposed() + + if self.peer_enable or self.advertise_router_type: + self.config_peer() + + if self.advertise_l2vpn_evpn: + self.config_advertise_l2vpn_evpn() + if self.commands: + self.cli_load_config(self.commands) + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + bgp_instance=dict(required=True, type='str'), + as_number=dict(required=False, type='str'), + peer_address=dict(required=False, type='str'), + peer_group_name=dict(required=False, type='str'), + peer_enable=dict(required=False, type='str', choices=[ + 'true', 'false']), + advertise_router_type=dict(required=False, type='str', choices=[ + 'arp', 'irb']), + + vpn_name=dict(required=False, type='str'), + advertise_l2vpn_evpn=dict(required=False, type='str', choices=[ + 'enable', 'disable']), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = EvpnBgp(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_evpn_bgp_rr.py b/plugins/modules/ce_evpn_bgp_rr.py new file mode 100644 index 0000000..fe738e5 --- /dev/null +++ b/plugins/modules/ce_evpn_bgp_rr.py @@ -0,0 +1,528 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_evpn_bgp_rr +short_description: Manages RR for the VXLAN Network on HUAWEI CloudEngine switches. +description: + - Configure an RR in BGP-EVPN address family view on HUAWEI CloudEngine switches. +author: Zhijin Zhou (@QijunPan) +notes: + - Ensure that BGP view is existed. + - The peer, peer_type, and reflect_client arguments must all exist or not exist. + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + as_number: + description: + - Specifies the number of the AS, in integer format. + The value is an integer that ranges from 1 to 4294967295. + required: true + bgp_instance: + description: + - Specifies the name of a BGP instance. + The value of instance-name can be an integer 1 or a string of 1 to 31. + bgp_evpn_enable: + description: + - Enable or disable the BGP-EVPN address family. + choices: ['enable','disable'] + default: 'enable' + peer_type: + description: + - Specify the peer type. + choices: ['group_name','ipv4_address'] + peer: + description: + - Specifies the IPv4 address or the group name of a peer. + reflect_client: + description: + - Configure the local device as the route reflector and the peer or peer group as the client of the route reflector. + choices: ['enable','disable'] + policy_vpn_target: + description: + - Enable or disable the VPN-Target filtering. + choices: ['enable','disable'] +''' + +EXAMPLES = ''' +- name: BGP RR test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Configure BGP-EVPN address family view and ensure that BGP view has existed." + ce_evpn_bgp_rr: + as_number: 20 + bgp_evpn_enable: enable + provider: "{{ cli }}" + + - name: "Configure reflect client and ensure peer has existed." + ce_evpn_bgp_rr: + as_number: 20 + peer_type: ipv4_address + peer: 192.8.3.3 + reflect_client: enable + provider: "{{ cli }}" + + - name: "Configure the VPN-Target filtering." + ce_evpn_bgp_rr: + as_number: 20 + policy_vpn_target: enable + provider: "{{ cli }}" + + - name: "Configure an RR in BGP-EVPN address family view." + ce_evpn_bgp_rr: + as_number: 20 + bgp_evpn_enable: enable + peer_type: ipv4_address + peer: 192.8.3.3 + reflect_client: enable + policy_vpn_target: disable + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "as_number": "20", + "bgp_evpn_enable": "enable", + "bgp_instance": null, + "peer": "192.8.3.3", + "peer_type": "ipv4_address", + "policy_vpn_target": "disable", + "reflect_client": "enable" + } +existing: + description: k/v pairs of existing attributes on the device + returned: always + type: dict + sample: { + "as_number": "20", + "bgp_evpn_enable": "disable", + "bgp_instance": null, + "peer": null, + "peer_type": null, + "policy_vpn_target": "disable", + "reflect_client": "disable" + } +end_state: + description: k/v pairs of end attributes on the device + returned: always + type: dict + sample: { + "as_number": "20", + "bgp_evpn_enable": "enable", + "bgp_instance": null, + "peer": "192.8.3.3", + "peer_type": "ipv4_address", + "policy_vpn_target": "disable", + "reflect_client": "enable" + } +updates: + description: command list sent to the device + returned: always + type: list + sample: [ + "bgp 20", + " l2vpn-family evpn", + " peer 192.8.3.3 enable", + " peer 192.8.3.3 reflect-client", + " undo policy vpn-target" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config, ce_argument_spec + + +def is_config_exist(cmp_cfg, test_cfg): + """is configuration exist""" + + if not cmp_cfg or not test_cfg: + return False + + return bool(test_cfg in cmp_cfg) + + +class EvpnBgpRr(object): + """Manage RR in BGP-EVPN address family view""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # RR configuration parameters + self.as_number = self.module.params['as_number'] + self.bgp_instance = self.module.params['bgp_instance'] + self.peer_type = self.module.params['peer_type'] + self.peer = self.module.params['peer'] + self.bgp_evpn_enable = self.module.params['bgp_evpn_enable'] + self.reflect_client = self.module.params['reflect_client'] + self.policy_vpn_target = self.module.params['policy_vpn_target'] + + self.commands = list() + self.config = None + self.bgp_evpn_config = "" + self.cur_config = dict() + self.conf_exist = False + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """Init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def cli_load_config(self, commands): + """Load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def is_bgp_view_exist(self): + """Judge whether BGP view has existed""" + + if self.bgp_instance: + view_cmd = "bgp %s instance %s" % ( + self.as_number, self.bgp_instance) + else: + view_cmd = "bgp %s" % self.as_number + + return is_config_exist(self.config, view_cmd) + + def is_l2vpn_family_evpn_exist(self): + """Judge whether BGP-EVPN address family view has existed""" + + view_cmd = "l2vpn-family evpn" + return is_config_exist(self.config, view_cmd) + + def is_reflect_client_exist(self): + """Judge whether reflect client is configured""" + + view_cmd = "peer %s reflect-client" % self.peer + return is_config_exist(self.bgp_evpn_config, view_cmd) + + def is_policy_vpn_target_exist(self): + """Judge whether the VPN-Target filtering is enabled""" + + view_cmd = "undo policy vpn-target" + if is_config_exist(self.bgp_evpn_config, view_cmd): + return False + else: + return True + + def get_config_in_bgp_view(self): + """Get configuration in BGP view""" + + cmd = "display current-configuration | section include" + if self.as_number: + if self.bgp_instance: + cmd += " bgp %s instance %s" % (self.as_number, + self.bgp_instance) + else: + cmd += " bgp %s" % self.as_number + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + config = out.strip() if out else "" + if cmd == config: + return '' + + return config + + def get_config_in_bgp_evpn_view(self): + """Get configuration in BGP_EVPN view""" + + self.bgp_evpn_config = "" + if not self.config: + return "" + + index = self.config.find("l2vpn-family evpn") + if index == -1: + return "" + + return self.config[index:] + + def get_current_config(self): + """Get current configuration""" + + if not self.as_number: + self.module.fail_json(msg='Error: The value of as-number cannot be empty.') + + self.cur_config['bgp_exist'] = False + self.cur_config['bgp_evpn_enable'] = 'disable' + self.cur_config['reflect_client'] = 'disable' + self.cur_config['policy_vpn_target'] = 'disable' + self.cur_config['peer_type'] = None + self.cur_config['peer'] = None + + self.config = self.get_config_in_bgp_view() + + if not self.is_bgp_view_exist(): + return + self.cur_config['bgp_exist'] = True + + if not self.is_l2vpn_family_evpn_exist(): + return + self.cur_config['bgp_evpn_enable'] = 'enable' + + self.bgp_evpn_config = self.get_config_in_bgp_evpn_view() + if self.is_reflect_client_exist(): + self.cur_config['reflect_client'] = 'enable' + self.cur_config['peer_type'] = self.peer_type + self.cur_config['peer'] = self.peer + + if self.is_policy_vpn_target_exist(): + self.cur_config['policy_vpn_target'] = 'enable' + + def get_existing(self): + """Get existing config""" + + self.existing = dict(as_number=self.as_number, + bgp_instance=self.bgp_instance, + peer_type=self.cur_config['peer_type'], + peer=self.cur_config['peer'], + bgp_evpn_enable=self.cur_config[ + 'bgp_evpn_enable'], + reflect_client=self.cur_config['reflect_client'], + policy_vpn_target=self.cur_config[ + 'policy_vpn_target']) + + def get_proposed(self): + """Get proposed config""" + + self.proposed = dict(as_number=self.as_number, + bgp_instance=self.bgp_instance, + peer_type=self.peer_type, + peer=self.peer, + bgp_evpn_enable=self.bgp_evpn_enable, + reflect_client=self.reflect_client, + policy_vpn_target=self.policy_vpn_target) + + def get_end_state(self): + """Get end config""" + + self.get_current_config() + self.end_state = dict(as_number=self.as_number, + bgp_instance=self.bgp_instance, + peer_type=self.cur_config['peer_type'], + peer=self.cur_config['peer'], + bgp_evpn_enable=self.cur_config[ + 'bgp_evpn_enable'], + reflect_client=self.cur_config['reflect_client'], + policy_vpn_target=self.cur_config['policy_vpn_target']) + if self.end_state == self.existing: + self.changed = False + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def judge_if_config_exist(self): + """Judge whether configuration has existed""" + + if self.bgp_evpn_enable and self.bgp_evpn_enable != self.cur_config['bgp_evpn_enable']: + return False + + if self.bgp_evpn_enable == 'disable' and self.cur_config['bgp_evpn_enable'] == 'disable': + return True + + if self.reflect_client and self.reflect_client == 'enable': + if self.peer_type and self.peer_type != self.cur_config['peer_type']: + return False + if self.peer and self.peer != self.cur_config['peer']: + return False + if self.reflect_client and self.reflect_client != self.cur_config['reflect_client']: + return False + + if self.policy_vpn_target and self.policy_vpn_target != self.cur_config['policy_vpn_target']: + return False + + return True + + def cli_add_command(self, command, undo=False): + """Add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) # show updates result + + def config_rr(self): + """Configure RR""" + + if self.conf_exist: + return + + if self.bgp_instance: + view_cmd = "bgp %s instance %s" % ( + self.as_number, self.bgp_instance) + else: + view_cmd = "bgp %s" % self.as_number + self.cli_add_command(view_cmd) + + if self.bgp_evpn_enable == 'disable': + self.cli_add_command("undo l2vpn-family evpn") + else: + self.cli_add_command("l2vpn-family evpn") + if self.reflect_client and self.reflect_client != self.cur_config['reflect_client']: + if self.reflect_client == 'enable': + self.cli_add_command("peer %s enable" % self.peer) + self.cli_add_command( + "peer %s reflect-client" % self.peer) + else: + self.cli_add_command( + "undo peer %s reflect-client" % self.peer) + self.cli_add_command("undo peer %s enable" % self.peer) + if self.cur_config['bgp_evpn_enable'] == 'enable': + if self.policy_vpn_target and self.policy_vpn_target != self.cur_config['policy_vpn_target']: + if self.policy_vpn_target == 'enable': + self.cli_add_command("policy vpn-target") + else: + self.cli_add_command("undo policy vpn-target") + else: + if self.policy_vpn_target and self.policy_vpn_target == 'disable': + self.cli_add_command("undo policy vpn-target") + + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + def check_is_ipv4_addr(self): + """Check ipaddress validate""" + + rule1 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.' + rule2 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])' + ipv4_regex = '%s%s%s%s%s%s' % ('^', rule1, rule1, rule1, rule2, '$') + + return bool(re.match(ipv4_regex, self.peer)) + + def check_params(self): + """Check all input params""" + + if self.cur_config['bgp_exist'] == 'false': + self.module.fail_json(msg="Error: BGP view does not exist.") + + if self.bgp_instance: + if len(self.bgp_instance) < 1 or len(self.bgp_instance) > 31: + self.module.fail_json( + msg="Error: The length of BGP instance-name must be between 1 or a string of 1 to and 31.") + + if self.as_number: + if len(self.as_number) > 11 or len(self.as_number) == 0: + self.module.fail_json( + msg='Error: The len of as_number %s is out of [1 - 11].' % self.as_number) + + tmp_dict1 = dict(peer_type=self.peer_type, + peer=self.peer, + reflect_client=self.reflect_client) + tmp_dict2 = dict((k, v) + for k, v in tmp_dict1.items() if v is not None) + if len(tmp_dict2) != 0 and len(tmp_dict2) != 3: + self.module.fail_json( + msg='Error: The peer, peer_type, and reflect_client arguments must all exist or not exist.') + + if self.peer_type: + if self.peer_type == 'ipv4_address' and not self.check_is_ipv4_addr(): + self.module.fail_json(msg='Error: Illegal IPv4 address.') + elif self.peer_type == 'group_name' and self.check_is_ipv4_addr(): + self.module.fail_json( + msg='Error: Ip address cannot be configured as group-name.') + + def work(self): + """Execute task""" + + self.get_current_config() + self.check_params() + self.get_existing() + self.get_proposed() + self.conf_exist = self.judge_if_config_exist() + + self.config_rr() + + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + as_number=dict(required=True, type='str'), + bgp_instance=dict(required=False, type='str'), + bgp_evpn_enable=dict(required=False, type='str', + default='enable', choices=['enable', 'disable']), + peer_type=dict(required=False, type='str', choices=[ + 'group_name', 'ipv4_address']), + peer=dict(required=False, type='str'), + reflect_client=dict(required=False, type='str', + choices=['enable', 'disable']), + policy_vpn_target=dict(required=False, choices=['enable', 'disable']), + ) + argument_spec.update(ce_argument_spec) + evpn_bgp_rr = EvpnBgpRr(argument_spec) + evpn_bgp_rr.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_evpn_global.py b/plugins/modules/ce_evpn_global.py new file mode 100644 index 0000000..4335e8b --- /dev/null +++ b/plugins/modules/ce_evpn_global.py @@ -0,0 +1,237 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_evpn_global +short_description: Manages global configuration of EVPN on HUAWEI CloudEngine switches. +description: + - Manages global configuration of EVPN on HUAWEI CloudEngine switches. +author: Zhijin Zhou (@QijunPan) +notes: + - Before configuring evpn_overlay_enable=disable, delete other EVPN configurations. + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + evpn_overlay_enable: + description: + - Configure EVPN as the VXLAN control plane. + required: true + choices: ['enable','disable'] +''' + +EXAMPLES = ''' +- name: evpn global module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configure EVPN as the VXLAN control plan + ce_evpn_global: + evpn_overlay_enable: enable + provider: "{{ cli }}" + + - name: Undo EVPN as the VXLAN control plan + ce_evpn_global: + evpn_overlay_enable: disable + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "evpn_overlay_enable": "enable" + } +existing: + description: k/v pairs of existing attributes on the device + returned: always + type: dict + sample: { + "evpn_overlay_enable": "disable" + } +end_state: + description: k/v pairs of end attributes on the interface + returned: always + type: dict + sample: { + "evpn_overlay_enable": "enable" + } +updates: + description: command list sent to the device + returned: always + type: list + sample: [ + "evpn-overlay enable", + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + + +class EvpnGlobal(object): + """Manage global configuration of EVPN""" + + def __init__(self, argument_spec, ): + self.spec = argument_spec + self.module = None + self.init_module() + + # EVPN global configuration parameters + self.overlay_enable = self.module.params['evpn_overlay_enable'] + + self.commands = list() + self.global_info = dict() + self.conf_exist = False + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init_module""" + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def cli_load_config(self, commands): + """load config by cli""" + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) # show updates result + + def get_evpn_global_info(self): + """ get current EVPN global configuration""" + + self.global_info['evpnOverLay'] = 'disable' + cmd = "display current-configuration | include ^evpn-overlay enable" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + if out: + self.global_info['evpnOverLay'] = 'enable' + + def get_existing(self): + """get existing config""" + self.existing = dict( + evpn_overlay_enable=self.global_info['evpnOverLay']) + + def get_proposed(self): + """get proposed config""" + self.proposed = dict(evpn_overlay_enable=self.overlay_enable) + + def get_end_state(self): + """get end config""" + self.get_evpn_global_info() + self.end_state = dict( + evpn_overlay_enable=self.global_info['evpnOverLay']) + + def show_result(self): + """ show result""" + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def judge_if_config_exist(self): + """ judge whether configuration has existed""" + if self.overlay_enable == self.global_info['evpnOverLay']: + return True + + return False + + def config_evnp_global(self): + """ set global EVPN configuration""" + if not self.conf_exist: + if self.overlay_enable == 'enable': + self.cli_add_command('evpn-overlay enable') + else: + self.cli_add_command('evpn-overlay enable', True) + + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + def work(self): + """execute task""" + self.get_evpn_global_info() + self.get_existing() + self.get_proposed() + self.conf_exist = self.judge_if_config_exist() + + self.config_evnp_global() + + self.get_end_state() + self.show_result() + + +def main(): + """main function entry""" + + argument_spec = dict( + evpn_overlay_enable=dict( + required=True, type='str', choices=['enable', 'disable']), + ) + argument_spec.update(ce_argument_spec) + evpn_global = EvpnGlobal(argument_spec) + evpn_global.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_facts.py b/plugins/modules/ce_facts.py new file mode 100644 index 0000000..d0e9ac3 --- /dev/null +++ b/plugins/modules/ce_facts.py @@ -0,0 +1,415 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_facts +author: "wangdezhuang (@QijunPan)" +short_description: Gets facts about HUAWEI CloudEngine switches. +description: + - Collects facts from CloudEngine devices running the CloudEngine + operating system. Fact collection is supported over Cli + transport. This module prepends all of the base network fact keys + with C(ansible_net_). The facts module will always collect a + base set of facts from the device and can enable or disable + collection of additional facts. +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all, hardware, config, and interfaces. Can specify a + list of values to include a larger subset. Values can also be used + with an initial C(M(!)) to specify that a specific subset should + not be collected. + required: false + default: '!config' +''' + +EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. + +- name: CloudEngine facts test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Gather_subset is all" + ce_facts: + gather_subset: all + provider: "{{ cli }}" + + - name: "Collect only the config facts" + ce_facts: + gather_subset: config + provider: "{{ cli }}" + + - name: "Do not collect hardware facts" + ce_facts: + gather_subset: "!hardware" + provider: "{{ cli }}" +""" + +RETURN = """ +gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list + +# default +BIOS Version: + description: The BIOS version running on the remote device + returned: always + type: str +Board Type: + description: The board type of the remote device + returned: always + type: str +CPLD1 Version: + description: The CPLD1 Version running the remote device + returned: always + type: str +CPLD2 Version: + description: The CPLD2 Version running the remote device + returned: always + type: str +MAB Version: + description: The MAB Version running the remote device + returned: always + type: str +PCB Version: + description: The PCB Version running the remote device + returned: always + type: str +hostname: + description: The hostname of the remote device + returned: always + type: str + +# hardware +FAN: + description: The fan state on the device + returned: when hardware is configured + type: str +PWR: + description: The power state on the device + returned: when hardware is configured + type: str +filesystems: + description: The filesystems on the device + returned: when hardware is configured + type: str +flash_free: + description: The flash free space on the device + returned: when hardware is configured + type: str +flash_total: + description: The flash total space on the device + returned: when hardware is configured + type: str +memory_free: + description: The memory free space on the remote device + returned: when hardware is configured + type: str +memory_total: + description: The memory total space on the remote device + returned: when hardware is configured + type: str + +# config +config: + description: The current system configuration on the device + returned: when config is configured + type: str + +# interfaces +all_ipv4_addresses: + description: All IPv4 addresses configured on the device + returned: when interfaces is configured + type: list +interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict +neighbors: + description: The list of LLDP neighbors from the remote device + returned: when interfaces is configured + type: dict +""" + +import re + +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import run_commands +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, check_args +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + + +class FactsBase(object): + + COMMANDS = frozenset() + + def __init__(self, module): + self.module = module + self.facts = dict() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, list(self.COMMANDS)) + + +class Default(FactsBase): + """ Class default """ + + COMMANDS = [ + 'display version', + 'display current-configuration | include sysname' + ] + + def populate(self): + """ Populate method """ + + super(Default, self).populate() + + data = self.responses[0] + if data: + version = data.split("\n") + for item in version: + if re.findall(r"^\d+\S\s+", item.strip()): + tmp_item = item.split() + tmp_key = tmp_item[1] + " " + tmp_item[2] + if len(tmp_item) > 5: + self.facts[tmp_key] = " ".join(tmp_item[4:]) + else: + self.facts[tmp_key] = tmp_item[4] + + data = self.responses[1] + if data: + tmp_value = re.findall(r'sysname (.*)', data) + self.facts['hostname'] = tmp_value[0] + + +class Config(FactsBase): + """ Class config """ + + COMMANDS = [ + 'display current-configuration configuration system' + ] + + def populate(self): + """ Populate method """ + + super(Config, self).populate() + + data = self.responses[0] + if data: + self.facts['config'] = data.split("\n") + + +class Hardware(FactsBase): + """ Class hardware """ + + COMMANDS = [ + 'dir', + 'display memory', + 'display device' + ] + + def populate(self): + """ Populate method """ + + super(Hardware, self).populate() + + data = self.responses[0] + if data: + self.facts['filesystems'] = re.findall(r'Directory of (.*)/', data)[0] + self.facts['flash_total'] = re.findall(r'(.*) total', data)[0].replace(",", "") + self.facts['flash_free'] = re.findall(r'total \((.*) free\)', data)[0].replace(",", "") + + data = self.responses[1] + if data: + memory_total = re.findall(r'Total Memory Used: (.*) Kbytes', data)[0] + use_percent = re.findall(r'Memory Using Percentage: (.*)%', data)[0] + memory_free = str(int(memory_total) - int(memory_total) * int(use_percent) / 100) + self.facts['memory_total'] = memory_total + " Kb" + self.facts['memory_free'] = memory_free + " Kb" + + data = self.responses[2] + if data: + device_info = data.split("\n") + tmp_device_info = device_info[4:-1] + for item in tmp_device_info: + tmp_item = item.split() + if len(tmp_item) == 8: + self.facts[tmp_item[2]] = tmp_item[6] + elif len(tmp_item) == 7: + self.facts[tmp_item[0]] = tmp_item[5] + + +class Interfaces(FactsBase): + """ Class interfaces """ + + COMMANDS = [ + 'display interface brief', + 'display ip interface brief', + 'display lldp neighbor brief' + ] + + def populate(self): + """ Populate method""" + + interface_dict = dict() + ipv4_addr_dict = dict() + neighbors_dict = dict() + + super(Interfaces, self).populate() + + data = self.responses[0] + begin = False + if data: + interface_info = data.split("\n") + for item in interface_info: + if begin: + tmp_item = item.split() + interface_dict[tmp_item[0]] = tmp_item[1] + + if re.findall(r"^Interface", item.strip()): + begin = True + + self.facts['interfaces'] = interface_dict + + data = self.responses[1] + if data: + ipv4_addr = data.split("\n") + tmp_ipv4 = ipv4_addr[11:] + for item in tmp_ipv4: + tmp_item = item.split() + ipv4_addr_dict[tmp_item[0]] = tmp_item[1] + self.facts['all_ipv4_addresses'] = ipv4_addr_dict + + data = self.responses[2] + if data: + neighbors = data.split("\n") + tmp_neighbors = neighbors[2:] + for item in tmp_neighbors: + tmp_item = item.split() + if len(tmp_item) > 3: + neighbors_dict[tmp_item[0]] = tmp_item[3] + else: + neighbors_dict[tmp_item[0]] = None + self.facts['neighbors'] = neighbors_dict + + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): + """ Module main """ + + spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + + spec.update(ce_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + + warnings = list() + check_args(module, warnings) + + gather_subset = module.params['gather_subset'] + + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset == 'all': + runable_subsets.update(VALID_SUBSETS) + continue + + if subset.startswith('!'): + subset = subset[1:] + if subset == 'all': + exclude_subsets.update(VALID_SUBSETS) + continue + exclude = True + else: + exclude = False + + if subset not in VALID_SUBSETS: + module.fail_json(msg='Bad subset') + + if exclude: + exclude_subsets.add(subset) + else: + runable_subsets.add(subset) + + if not runable_subsets: + runable_subsets.update(VALID_SUBSETS) + + runable_subsets.difference_update(exclude_subsets) + runable_subsets.add('default') + + facts = dict() + facts['gather_subset'] = list(runable_subsets) + + instances = list() + for key in runable_subsets: + instances.append(FACT_SUBSETS[key](module)) + + for inst in instances: + inst.populate() + facts.update(inst.facts) + + ansible_facts = dict() + for key, value in iteritems(facts): + # this is to maintain capability with nxos_facts 2.1 + if key.startswith('_'): + ansible_facts[key[1:]] = value + else: + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_file_copy.py b/plugins/modules/ce_file_copy.py new file mode 100644 index 0000000..0c13316 --- /dev/null +++ b/plugins/modules/ce_file_copy.py @@ -0,0 +1,413 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_file_copy +short_description: Copy a file to a remote cloudengine device over SCP on HUAWEI CloudEngine switches. +description: + - Copy a file to a remote cloudengine device over SCP on HUAWEI CloudEngine switches. +author: + - Zhou Zhijin (@QijunPan) +notes: + - The feature must be enabled with feature scp-server. + - If the file is already present, no transfer will take place. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +requirements: + - paramiko +options: + local_file: + description: + - Path to local file. Local directory must exist. + The maximum length of I(local_file) is C(4096). + required: true + remote_file: + description: + - Remote file path of the copy. Remote directories must exist. + If omitted, the name of the local file will be used. + The maximum length of I(remote_file) is C(4096). + file_system: + description: + - The remote file system of the device. If omitted, + devices that support a I(file_system) parameter will use + their default values. + File system indicates the storage medium and can be set to as follows, + 1) C(flash) is root directory of the flash memory on the master MPU. + 2) C(slave#flash) is root directory of the flash memory on the slave MPU. + If no slave MPU exists, this drive is unavailable. + 3) C(chassis ID/slot number#flash) is root directory of the flash memory on + a device in a stack. For example, C(1/5#flash) indicates the flash memory + whose chassis ID is 1 and slot number is 5. + default: 'flash:' +''' + +EXAMPLES = ''' +- name: File copy test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Copy a local file to remote device" + ce_file_copy: + local_file: /usr/vrpcfg.cfg + remote_file: /vrpcfg.cfg + file_system: 'flash:' + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +transfer_result: + description: information about transfer result. + returned: always + type: str + sample: 'The local file has been successfully transferred to the device.' +local_file: + description: The path of the local file. + returned: always + type: str + sample: '/usr/work/vrpcfg.zip' +remote_file: + description: The path of the remote file. + returned: always + type: str + sample: '/vrpcfg.zip' +''' + +import re +import os +import sys +import time +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, get_nc_config +from ansible.module_utils.connection import ConnectionError +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import validate_ip_v6_address + +try: + import paramiko + HAS_PARAMIKO = True +except ImportError: + HAS_PARAMIKO = False + +try: + from scp import SCPClient + HAS_SCP = True +except ImportError: + HAS_SCP = False + +CE_NC_GET_DISK_INFO = """ + + + + + + + + + + + + +""" + +CE_NC_GET_FILE_INFO = """ + + + + + %s + %s + + + + + +""" + +CE_NC_GET_SCP_ENABLE = """ + + + + +""" + + +def get_cli_exception(exc=None): + """Get cli exception message""" + + msg = list() + if not exc: + exc = sys.exc_info[1] + if exc: + errs = str(exc).split("\r\n") + for err in errs: + if not err: + continue + if "matched error in response:" in err: + continue + if " at '^' position" in err: + err = err.replace(" at '^' position", "") + if err.replace(" ", "") == "^": + continue + if len(err) > 2 and err[0] in ["<", "["] and err[-1] in [">", "]"]: + continue + if err[-1] == ".": + err = err[:-1] + if err.replace(" ", "") == "": + continue + msg.append(err) + else: + msg = ["Error: Fail to get cli exception message."] + + while msg[-1][-1] == ' ': + msg[-1] = msg[-1][:-1] + + if msg[-1][-1] != ".": + msg[-1] += "." + + return ", ".join(msg).capitalize() + + +class FileCopy(object): + """File copy function class""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # file copy parameters + self.local_file = self.module.params['local_file'] + self.remote_file = self.module.params['remote_file'] + self.file_system = self.module.params['file_system'] + self.host_is_ipv6 = validate_ip_v6_address(self.module.params['provider']['host']) + + # state + self.transfer_result = None + self.changed = False + + def init_module(self): + """Init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def remote_file_exists(self, dst, file_system='flash:'): + """Remote file whether exists""" + + full_path = file_system + dst + file_name = os.path.basename(full_path) + file_path = os.path.dirname(full_path) + file_path = file_path + '/' + xml_str = CE_NC_GET_FILE_INFO % (file_name, file_path) + ret_xml = get_nc_config(self.module, xml_str) + if "" in ret_xml: + return False, 0 + + xml_str = ret_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get file info + root = ElementTree.fromstring(xml_str) + topo = root.find("vfm/dirs/dir") + if topo is None: + return False, 0 + + for eles in topo: + if eles.tag in ["DirSize"]: + return True, int(eles.text.replace(',', '')) + + return False, 0 + + def local_file_exists(self): + """Local file whether exists""" + + return os.path.isfile(self.local_file) + + def enough_space(self): + """Whether device has enough space""" + + xml_str = CE_NC_GET_DISK_INFO + ret_xml = get_nc_config(self.module, xml_str) + if "" in ret_xml: + return + + xml_str = ret_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + topo = root.find("vfm/dfs/df/freeSize") + kbytes_free = topo.text + + file_size = os.path.getsize(self.local_file) + if int(kbytes_free) * 1024 > file_size: + return True + + return False + + def transfer_file(self, dest): + """Begin to transfer file by scp""" + + if not self.local_file_exists(): + self.module.fail_json( + msg='Could not transfer file. Local file doesn\'t exist.') + + if not self.enough_space(): + self.module.fail_json( + msg='Could not transfer file. Not enough space on device.') + + hostname = self.module.params['provider']['host'] + username = self.module.params['provider']['username'] + password = self.module.params['provider']['password'] + port = self.module.params['provider']['port'] + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname=hostname, username=username, password=password, port=port) + full_remote_path = '{0}{1}'.format(self.file_system, dest) + scp = SCPClient(ssh.get_transport()) + try: + scp.put(self.local_file, full_remote_path) + except Exception: + time.sleep(10) + file_exists, temp_size = self.remote_file_exists( + dest, self.file_system) + file_size = os.path.getsize(self.local_file) + if file_exists and int(temp_size) == int(file_size): + pass + else: + scp.close() + self.module.fail_json(msg='Could not transfer file. There was an error ' + 'during transfer. Please make sure the format of ' + 'input parameters is right.') + scp.close() + return True + + def get_scp_enable(self): + """Get scp enable state""" + + ret_xml = '' + try: + ret_xml = get_nc_config(self.module, CE_NC_GET_SCP_ENABLE) + except ConnectionError: + self.module.fail_json(msg='Error: The NETCONF API of scp_enable is not supported.') + + if "" in ret_xml: + return False + + xml_str = ret_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get file info + root = ElementTree.fromstring(xml_str) + topo1 = root.find("sshs/sshServer/scpEnable") + topo2 = root.find("sshs/sshServerEnable/scpIpv4Enable") + topo3 = root.find("sshs/sshServerEnable/scpIpv6Enable") + if topo1 is not None: + return str(topo1.text).strip().lower() == 'enable' + elif self.host_is_ipv6 and topo3 is not None: + return str(topo3.text).strip().lower() == 'enable' + elif topo2 is not None: + return str(topo2.text).strip().lower() == 'enable' + return False + + def work(self): + """Execute task """ + + if not HAS_SCP: + self.module.fail_json( + msg="'Error: No scp package, please install it.'") + + if not HAS_PARAMIKO: + self.module.fail_json( + msg="'Error: No paramiko package, please install it.'") + + if self.local_file and len(self.local_file) > 4096: + self.module.fail_json( + msg="'Error: The maximum length of local_file is 4096.'") + + if self.remote_file and len(self.remote_file) > 4096: + self.module.fail_json( + msg="'Error: The maximum length of remote_file is 4096.'") + + scp_enable = self.get_scp_enable() + if not scp_enable: + if self.host_is_ipv6: + self.module.fail_json( + msg="'Error: Please ensure ipv6 SCP server are enabled.'") + else: + self.module.fail_json( + msg="'Error: Please ensure ipv4 SCP server are enabled.'") + + if not os.path.isfile(self.local_file): + self.module.fail_json( + msg="Local file {0} not found".format(self.local_file)) + + dest = self.remote_file or ('/' + os.path.basename(self.local_file)) + remote_exists, file_size = self.remote_file_exists( + dest, file_system=self.file_system) + if remote_exists and (os.path.getsize(self.local_file) != file_size): + remote_exists = False + + if not remote_exists: + self.changed = True + file_exists = False + else: + file_exists = True + self.transfer_result = 'The local file already exists on the device.' + + if not file_exists: + self.transfer_file(dest) + self.transfer_result = 'The local file has been successfully transferred to the device.' + + if self.remote_file is None: + self.remote_file = '/' + os.path.basename(self.local_file) + + self.module.exit_json( + changed=self.changed, + transfer_result=self.transfer_result, + local_file=self.local_file, + remote_file=self.remote_file, + file_system=self.file_system) + + +def main(): + """Main function entry""" + + argument_spec = dict( + local_file=dict(required=True), + remote_file=dict(required=False), + file_system=dict(required=False, default='flash:') + ) + argument_spec.update(ce_argument_spec) + filecopy_obj = FileCopy(argument_spec) + filecopy_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_info_center_debug.py b/plugins/modules/ce_info_center_debug.py new file mode 100644 index 0000000..c6199ee --- /dev/null +++ b/plugins/modules/ce_info_center_debug.py @@ -0,0 +1,614 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_info_center_debug +short_description: Manages information center debug configuration on HUAWEI CloudEngine switches. +description: + - Manages information center debug configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] + debug_time_stamp: + description: + - Timestamp type of debugging information. + choices: ['date_boot', 'date_second', 'date_tenthsecond', 'date_millisecond', 'shortdate_second', + 'shortdate_tenthsecond', 'shortdate_millisecond', 'formatdate_second', 'formatdate_tenthsecond', + 'formatdate_millisecond'] + module_name: + description: + - Module name of the rule. + The value is a string of 1 to 31 case-insensitive characters. The default value is default. + Please use lower-case letter, such as [aaa, acl, arp, bfd]. + channel_id: + description: + - Number of a channel. + The value is an integer ranging from 0 to 9. The default value is 0. + debug_enable: + description: + - Whether a device is enabled to output debugging information. + default: no_use + choices: ['no_use','true','false'] + debug_level: + description: + - Debug level permitted to output. + choices: ['emergencies', 'alert', 'critical', 'error', 'warning', 'notification', + 'informational', 'debugging'] +''' + +EXAMPLES = ''' + +- name: CloudEngine info center debug test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config debug time stamp" + ce_info_center_debug: + state: present + debug_time_stamp: date_boot + provider: "{{ cli }}" + + - name: "Undo debug time stamp" + ce_info_center_debug: + state: absent + debug_time_stamp: date_boot + provider: "{{ cli }}" + + - name: "Config debug module log level" + ce_info_center_debug: + state: present + module_name: aaa + channel_id: 1 + debug_enable: true + debug_level: error + provider: "{{ cli }}" + + - name: "Undo debug module log level" + ce_info_center_debug: + state: absent + module_name: aaa + channel_id: 1 + debug_enable: true + debug_level: error + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"state": "present", "debug_time_stamp": "date_boot"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"debugTimeStamp": "DATE_MILLISECOND"} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"debugTimeStamp": "DATE_BOOT"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["info-center timestamp debugging boot"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +# get info center debug global +CE_GET_DEBUG_GLOBAL_HEADER = """ + + + +""" +CE_GET_DEBUG_GLOBAL_TAIL = """ + + + +""" +# merge info center debug global +CE_MERGE_DEBUG_GLOBAL_HEADER = """ + + + +""" +CE_MERGE_DEBUG_GLOBAL_TAIL = """ + + + +""" + +# get info center debug source +CE_GET_DEBUG_SOURCE_HEADER = """ + + + + +""" +CE_GET_DEBUG_SOURCE_TAIL = """ + + + + +""" +# merge info center debug source +CE_MERGE_DEBUG_SOURCE_HEADER = """ + + + + +""" +CE_MERGE_DEBUG_SOURCE_TAIL = """ + + + + +""" +# delete info center debug source +CE_DELETE_DEBUG_SOURCE_HEADER = """ + + + + +""" +CE_DELETE_DEBUG_SOURCE_TAIL = """ + + + + +""" + +TIME_STAMP_DICT = {"date_boot": "boot", + "date_second": "date precision-time second", + "date_tenthsecond": "date precision-time tenth-second", + "date_millisecond": "date precision-time millisecond", + "shortdate_second": "short-date precision-time second", + "shortdate_tenthsecond": "short-date precision-time tenth-second", + "shortdate_millisecond": "short-date precision-time millisecond", + "formatdate_second": "format-date precision-time second", + "formatdate_tenthsecond": "format-date precision-time tenth-second", + "formatdate_millisecond": "format-date precision-time millisecond"} + +CHANNEL_DEFAULT_DBG_STATE = {"0": "true", + "1": "true", + "2": "false", + "3": "false", + "4": "false", + "5": "false", + "6": "false", + "7": "false", + "8": "false", + "9": "false"} + +CHANNEL_DEFAULT_DBG_LEVEL = {"0": "debugging", + "1": "debugging", + "2": "debugging", + "3": "debugging", + "4": "debugging", + "5": "debugging", + "6": "debugging", + "7": "debugging", + "8": "debugging", + "9": "debugging"} + + +class InfoCenterDebug(object): + """ Manages info center debug configuration """ + + def __init__(self, **kwargs): + """ Init function """ + + # argument spec + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # module args + self.state = self.module.params['state'] + self.debug_time_stamp = self.module.params['debug_time_stamp'] or None + self.module_name = self.module.params['module_name'] or None + self.channel_id = self.module.params['channel_id'] or None + self.debug_enable = self.module.params['debug_enable'] + self.debug_level = self.module.params['debug_level'] or None + + # cur config + self.cur_global_cfg = dict() + self.cur_source_cfg = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def check_global_args(self): + """ Check global args """ + + need_cfg = False + find_flag = False + self.cur_global_cfg["global_cfg"] = [] + + if self.debug_time_stamp: + + conf_str = CE_GET_DEBUG_GLOBAL_HEADER + conf_str += "" + conf_str += CE_GET_DEBUG_GLOBAL_TAIL + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + find_flag = False + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + global_cfg = root.findall("syslog/globalParam") + if global_cfg: + for tmp in global_cfg: + tmp_dict = dict() + for site in tmp: + if site.tag in ["debugTimeStamp"]: + tmp_dict[site.tag] = site.text + + self.cur_global_cfg["global_cfg"].append(tmp_dict) + + if self.cur_global_cfg["global_cfg"]: + for tmp in self.cur_global_cfg["global_cfg"]: + find_flag = True + + if tmp.get("debugTimeStamp").lower() != self.debug_time_stamp: + find_flag = False + + if find_flag: + break + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + else: + need_cfg = bool(find_flag) + + self.cur_global_cfg["need_cfg"] = need_cfg + + def check_source_args(self): + """ Check source args """ + + need_cfg = False + find_flag = False + self.cur_source_cfg["source_cfg"] = [] + + if self.module_name: + if len(self.module_name) < 1 or len(self.module_name) > 31: + self.module.fail_json( + msg='Error: The module_name is out of [1 - 31].') + + if not self.channel_id: + self.module.fail_json( + msg='Error: Please input channel_id at the same time.') + + if self.channel_id: + if self.channel_id.isdigit(): + if int(self.channel_id) < 0 or int(self.channel_id) > 9: + self.module.fail_json( + msg='Error: The value of channel_id is out of [0 - 9].') + else: + self.module.fail_json( + msg='Error: The channel_id is not digit.') + + conf_str = CE_GET_DEBUG_SOURCE_HEADER + + if self.module_name != "default": + conf_str += "%s" % self.module_name.upper() + else: + conf_str += "default" + + if self.channel_id: + conf_str += "" + if self.debug_enable != 'no_use': + conf_str += "" + if self.debug_level: + conf_str += "" + + conf_str += CE_GET_DEBUG_SOURCE_TAIL + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + find_flag = False + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + source_cfg = root.findall("syslog/icSources/icSource") + if source_cfg: + for tmp in source_cfg: + tmp_dict = dict() + for site in tmp: + if site.tag in ["moduleName", "icChannelId", "dbgEnFlg", "dbgEnLevel"]: + tmp_dict[site.tag] = site.text + + self.cur_source_cfg["source_cfg"].append(tmp_dict) + + if self.cur_source_cfg["source_cfg"]: + for tmp in self.cur_source_cfg["source_cfg"]: + find_flag = True + + if self.module_name and tmp.get("moduleName").lower() != self.module_name.lower(): + find_flag = False + if self.channel_id and tmp.get("icChannelId") != self.channel_id: + find_flag = False + if self.debug_enable != 'no_use' and tmp.get("dbgEnFlg") != self.debug_enable: + find_flag = False + if self.debug_level and tmp.get("dbgEnLevel") != self.debug_level: + find_flag = False + + if find_flag: + break + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + else: + need_cfg = bool(find_flag) + + self.cur_source_cfg["need_cfg"] = need_cfg + + def get_proposed(self): + """ Get proposed """ + + self.proposed["state"] = self.state + + if self.debug_time_stamp: + self.proposed["debug_time_stamp"] = self.debug_time_stamp + if self.module_name: + self.proposed["module_name"] = self.module_name + if self.channel_id: + self.proposed["channel_id"] = self.channel_id + if self.debug_enable != 'no_use': + self.proposed["debug_enable"] = self.debug_enable + if self.debug_level: + self.proposed["debug_level"] = self.debug_level + + def get_existing(self): + """ Get existing """ + + if self.cur_global_cfg["global_cfg"]: + self.existing["global_cfg"] = self.cur_global_cfg["global_cfg"] + if self.cur_source_cfg["source_cfg"]: + self.existing["source_cfg"] = self.cur_source_cfg["source_cfg"] + + def get_end_state(self): + """ Get end state """ + + self.check_global_args() + if self.cur_global_cfg["global_cfg"]: + self.end_state["global_cfg"] = self.cur_global_cfg["global_cfg"] + + self.check_source_args() + if self.cur_source_cfg["source_cfg"]: + self.end_state["source_cfg"] = self.cur_source_cfg["source_cfg"] + + def merge_debug_global(self): + """ Merge debug global """ + + conf_str = CE_MERGE_DEBUG_GLOBAL_HEADER + + if self.debug_time_stamp: + conf_str += "%s" % self.debug_time_stamp.upper() + + conf_str += CE_MERGE_DEBUG_GLOBAL_TAIL + + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge debug global failed.') + + if self.debug_time_stamp: + cmd = "info-center timestamp debugging " + TIME_STAMP_DICT.get(self.debug_time_stamp) + self.updates_cmd.append(cmd) + + self.changed = True + + def delete_debug_global(self): + """ Delete debug global """ + + conf_str = CE_MERGE_DEBUG_GLOBAL_HEADER + + if self.debug_time_stamp: + conf_str += "DATE_MILLISECOND" + + conf_str += CE_MERGE_DEBUG_GLOBAL_TAIL + + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: delete debug global failed.') + + if self.debug_time_stamp: + cmd = "undo info-center timestamp debugging" + self.updates_cmd.append(cmd) + + self.changed = True + + def merge_debug_source(self): + """ Merge debug source """ + + conf_str = CE_MERGE_DEBUG_SOURCE_HEADER + + if self.module_name: + conf_str += "%s" % self.module_name + if self.channel_id: + conf_str += "%s" % self.channel_id + if self.debug_enable != 'no_use': + conf_str += "%s" % self.debug_enable + if self.debug_level: + conf_str += "%s" % self.debug_level + + conf_str += CE_MERGE_DEBUG_SOURCE_TAIL + + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge debug source failed.') + + cmd = "info-center source" + if self.module_name: + cmd += " %s" % self.module_name + if self.channel_id: + cmd += " channel %s" % self.channel_id + if self.debug_enable != 'no_use': + if self.debug_enable == "true": + cmd += " debug state on" + else: + cmd += " debug state off" + if self.debug_level: + cmd += " level %s" % self.debug_level + + self.updates_cmd.append(cmd) + self.changed = True + + def delete_debug_source(self): + """ Delete debug source """ + + if self.debug_enable == 'no_use' and not self.debug_level: + conf_str = CE_DELETE_DEBUG_SOURCE_HEADER + if self.module_name: + conf_str += "%s" % self.module_name + if self.channel_id: + conf_str += "%s" % self.channel_id + conf_str += CE_DELETE_DEBUG_SOURCE_TAIL + else: + conf_str = CE_MERGE_DEBUG_SOURCE_HEADER + if self.module_name: + conf_str += "%s" % self.module_name + if self.channel_id: + conf_str += "%s" % self.channel_id + if self.debug_enable != 'no_use': + conf_str += "%s" % CHANNEL_DEFAULT_DBG_STATE.get(self.channel_id) + if self.debug_level: + conf_str += "%s" % CHANNEL_DEFAULT_DBG_LEVEL.get(self.channel_id) + conf_str += CE_MERGE_DEBUG_SOURCE_TAIL + + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: Delete debug source failed.') + + cmd = "undo info-center source" + if self.module_name: + cmd += " %s" % self.module_name + if self.channel_id: + cmd += " channel %s" % self.channel_id + if self.debug_enable != 'no_use': + cmd += " debug state" + if self.debug_level: + cmd += " level" + + self.updates_cmd.append(cmd) + self.changed = True + + def work(self): + """ work function """ + + self.check_global_args() + self.check_source_args() + self.get_proposed() + self.get_existing() + + if self.state == "present": + if self.cur_global_cfg["need_cfg"]: + self.merge_debug_global() + if self.cur_source_cfg["need_cfg"]: + self.merge_debug_source() + + else: + if self.cur_global_cfg["need_cfg"]: + self.delete_debug_global() + if self.cur_source_cfg["need_cfg"]: + self.delete_debug_source() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + debug_time_stamp=dict(choices=['date_boot', 'date_second', 'date_tenthsecond', + 'date_millisecond', 'shortdate_second', 'shortdate_tenthsecond', + 'shortdate_millisecond', 'formatdate_second', 'formatdate_tenthsecond', + 'formatdate_millisecond']), + module_name=dict(type='str'), + channel_id=dict(type='str'), + debug_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + debug_level=dict(choices=['emergencies', 'alert', 'critical', 'error', 'warning', 'notification', + 'informational', 'debugging']) + ) + + argument_spec.update(ce_argument_spec) + module = InfoCenterDebug(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_info_center_global.py b/plugins/modules/ce_info_center_global.py new file mode 100644 index 0000000..7b5dd96 --- /dev/null +++ b/plugins/modules/ce_info_center_global.py @@ -0,0 +1,1722 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_info_center_global +short_description: Manages outputting logs on HUAWEI CloudEngine switches. +description: + - This module offers the ability to be output to the log buffer, log file, console, terminal, or log host on HUAWEI CloudEngine switches. +author: + - Li Yanfeng (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + info_center_enable: + description: + - Whether the info-center function is enabled. The value is of the Boolean type. + choices: ['true','false'] + packet_priority: + description: + - Set the priority of the syslog packet.The value is an integer ranging from 0 to 7. The default value is 0. + suppress_enable: + description: + - Whether a device is enabled to suppress duplicate statistics. The value is of the Boolean type. + choices: [ 'false', 'true' ] + logfile_max_num: + description: + - Maximum number of log files of the same type. The default value is 200. + - The value range for log files is[3, 500], for security files is [1, 3],and for operation files is [1, 7]. + logfile_max_size: + description: + - Maximum size (in MB) of a log file. The default value is 32. + - The value range for log files is [4, 8, 16, 32], for security files is [1, 4], + - and for operation files is [1, 4]. + default: 32 + choices: ['4', '8', '16', '32'] + channel_id: + description: + - Number for channel. The value is an integer ranging from 0 to 9. The default value is 0. + channel_cfg_name: + description: + - Channel name.The value is a string of 1 to 30 case-sensitive characters. The default value is console. + default: console + channel_out_direct: + description: + - Direction of information output. + choices: ['console','monitor','trapbuffer','logbuffer','snmp','logfile'] + filter_feature_name: + description: + - Feature name of the filtered log. The value is a string of 1 to 31 case-insensitive characters. + filter_log_name: + description: + - Name of the filtered log. The value is a string of 1 to 63 case-sensitive characters. + ip_type: + description: + - Log server address type, IPv4 or IPv6. + choices: ['ipv4','ipv6'] + server_ip: + description: + - Log server address, IPv4 or IPv6 type. The value is a string of 0 to 255 characters. + The value can be an valid IPv4 or IPv6 address. + server_domain: + description: + - Server name. The value is a string of 1 to 255 case-sensitive characters. + is_default_vpn: + description: + - Use the default VPN or not. + type: bool + default: 'no' + vrf_name: + description: + - VPN name on a log server. The value is a string of 1 to 31 case-sensitive characters. + The default value is _public_. + level: + description: + - Level of logs saved on a log server. + choices: ['emergencies','alert','critical','error','warning','notification','informational','debugging'] + server_port: + description: + - Number of a port sending logs.The value is an integer ranging from 1 to 65535. + For UDP, the default value is 514. For TCP, the default value is 601. For TSL, the default value is 6514. + facility: + description: + - Log record tool. + choices: ['local0','local1','local2','local3','local4','local5','local6','local7'] + channel_name: + description: + - Channel name. The value is a string of 1 to 30 case-sensitive characters. + timestamp: + description: + - Log server timestamp. The value is of the enumerated type and case-sensitive. + choices: ['UTC', 'localtime'] + transport_mode: + description: + - Transport mode. The value is of the enumerated type and case-sensitive. + choices: ['tcp','udp'] + ssl_policy_name: + description: + - SSL policy name. The value is a string of 1 to 23 case-sensitive characters. + source_ip: + description: + - Log source ip address, IPv4 or IPv6 type. The value is a string of 0 to 255. + The value can be an valid IPv4 or IPv6 address. + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +- name: info center global module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Config info-center enable + ce_info_center_global: + info_center_enable: true + state: present + provider: "{{ cli }}" + + - name: Config statistic-suppress enable + ce_info_center_global: + suppress_enable: true + state: present + provider: "{{ cli }}" + + - name: Config info-center syslog packet-priority 1 + ce_info_center_global: + packet_priority: 2 + state: present + provider: "{{ cli }}" + + - name: Config info-center channel 1 name aaa + ce_info_center_global: + channel_id: 1 + channel_cfg_name: aaa + state: present + provider: "{{ cli }}" + + - name: Config info-center logfile size 10 + ce_info_center_global: + logfile_max_num: 10 + state: present + provider: "{{ cli }}" + + - name: Config info-center console channel 1 + ce_info_center_global: + channel_out_direct: console + channel_id: 1 + state: present + provider: "{{ cli }}" + + - name: Config info-center filter-id bymodule-alias snmp snmp_ipunlock + ce_info_center_global: + filter_feature_name: SNMP + filter_log_name: SNMP_IPLOCK + state: present + provider: "{{ cli }}" + + + - name: Config info-center max-logfile-number 16 + ce_info_center_global: + logfile_max_size: 16 + state: present + provider: "{{ cli }}" + + - name: Config syslog loghost domain. + ce_info_center_global: + server_domain: aaa + vrf_name: aaa + channel_id: 1 + transport_mode: tcp + facility: local4 + server_port: 100 + level: alert + timestamp: UTC + state: present + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"channel_id": "1", "facility": "local4", "is_default_vpn": True, "level": "alert", "server_domain": "aaa", + "server_port": "100", "state": "present", "timestamp": "localtime", "transport_mode": "tcp"} +existing: + description: k/v pairs of existing rollback + returned: always + type: dict + sample: + "server_domain_info": [ + { + "chnlId": "1", + "chnlName": "monitor", + "facility": "local4", + "isBriefFmt": "false", + "isDefaultVpn": "false", + "level": "alert", + "serverDomain": "aaa", + "serverPort": "100", + "sourceIP": "0.0.0.0", + "sslPolicyName": "gmc", + "timestamp": "UTC", + "transportMode": "tcp", + "vrfName": "aaa" + } + ] +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: + "server_domain_info": [ + { + "chnlId": "1", + "chnlName": "monitor", + "facility": "local4", + "isBriefFmt": "false", + "isDefaultVpn": "true", + "level": "alert", + "serverDomain": "aaa", + "serverPort": "100", + "sourceIP": "0.0.0.0", + "sslPolicyName": null, + "timestamp": "localtime", + "transportMode": "tcp", + "vrfName": "_public_" + }, + { + "chnlId": "1", + "chnlName": "monitor", + "facility": "local4", + "isBriefFmt": "false", + "isDefaultVpn": "false", + "level": "alert", + "serverDomain": "aaa", + "serverPort": "100", + "sourceIP": "0.0.0.0", + "sslPolicyName": "gmc", + "timestamp": "UTC", + "transportMode": "tcp", + "vrfName": "aaa" + } + ] +updates: + description: command sent to the device + returned: always + type: list + sample: ["info-center loghost domain aaa level alert port 100 facility local4 channel 1 localtime transport tcp"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, get_nc_config, set_nc_config, check_ip_addr + + +CE_NC_GET_CENTER_GLOBAL_INFO_HEADER = """ + + + +""" +CE_NC_GET_CENTER_GLOBAL_INFO_TAIL = """ + + + +""" + +CE_NC_MERGE_CENTER_GLOBAL_INFO_HEADER = """ + + + +""" + +CE_NC_MERGE_CENTER_GLOBAL_INFO_TAIL = """ + + + +""" + +CE_NC_GET_LOG_FILE_INFO_HEADER = """ + + + + +""" +CE_NC_GET_LOG_FILE_INFO_TAIL = """ + + + + +""" + +CE_NC_MERGE_LOG_FILE_INFO_HEADER = """ + + + + +""" + +CE_NC_MERGE_LOG_FILE_INFO_TAIL = """ + + + + +""" + + +CE_NC_GET_CHANNEL_INFO = """ + + + + + %s + + + + + +""" + +CE_NC_MERGE_CHANNEL_INFO_HEADER = """ + + + + +""" +CE_NC_MERGE_CHANNEL_INFO_TAIL = """ + + + + +""" + +CE_NC_GET_CHANNEL_DIRECT_INFO = """ + + + + + %s + + + + + +""" +CE_NC_MERGE_CHANNEL_DIRECT_HEADER = """ + + + + +""" + +CE_NC_MERGE_CHANNEL_DIRECT_TAIL = """ + + + + +""" + +CE_NC_GET_FILTER_INFO = """ + + + + + + + + + + +""" + +CE_NC_CREATE_CHANNEL_FILTER_HEADER = """ + + + + + +""" +CE_NC_CREATE_CHANNEL_FILTER_TAIL = """ + + + + +""" +CE_NC_DELETE_CHANNEL_FILTER_HEADER = """ + + + + + +""" +CE_NC_DELETE_CHANNEL_FILTER_TAIL = """ + + + + +""" + +CE_NC_GET_SERVER_IP_INFO_HEADER = """ + + + + + %s + %s + %s + %s +""" +CE_NC_GET_SERVER_IP_INFO_TAIL = """ + + + + +""" +CE_NC_MERGE_SERVER_IP_INFO_HEADER = """ + + + + + %s + %s + %s + %s +""" +CE_NC_MERGE_SERVER_IP_INFO_TAIL = """ + + + + +""" +CE_NC_DELETE_SERVER_IP_INFO_HEADER = """ + + + + + %s + %s + %s + %s +""" +CE_NC_DELETE_SERVER_IP_INFO_TAIL = """ + + + + +""" +CE_NC_GET_SERVER_DNS_INFO_HEADER = """ + + + + +""" + +CE_NC_GET_SERVER_DNS_INFO_TAIL = """ + + + + +""" + +CE_NC_MERGE_SERVER_DNS_INFO_HEADER = """ + + + + + %s + %s + %s +""" +CE_NC_MERGE_SERVER_DNS_INFO_TAIL = """ + + + + +""" + +CE_NC_DELETE_SERVER_DNS_INFO_HEADER = """ + + + + + %s + %s + %s +""" +CE_NC_DELETE_SERVER_DNS_INFO_TAIL = """ + + + + +""" + + +def get_out_direct_default(out_direct): + """get default out direct""" + + outdict = {"console": "1", "monitor": "2", "trapbuffer": "3", + "logbuffer": "4", "snmp": "5", "logfile": "6"} + channel_id_default = outdict.get(out_direct) + return channel_id_default + + +def get_channel_name_default(channel_id): + """get default out direct""" + + channel_dict = {"0": "console", "1": "monitor", "2": "loghost", "3": "trapbuffer", "4": "logbuffer", + "5": "snmpagent", "6": "channel6", "7": "channel7", "8": "channel8", "9": "channel9"} + channel_name_default = channel_dict.get(channel_id) + return channel_name_default + + +class InfoCenterGlobal(object): + """ + Manages info center global configuration. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.info_center_enable = self.module.params['info_center_enable'] or None + self.packet_priority = self.module.params['packet_priority'] or None + self.suppress_enable = self.module.params['suppress_enable'] or None + self.logfile_max_num = self.module.params['logfile_max_num'] or None + self.logfile_max_size = self.module.params['logfile_max_size'] or None + self.channel_id = self.module.params['channel_id'] or None + self.channel_cfg_name = self.module.params['channel_cfg_name'] or None + self.channel_out_direct = self.module.params['channel_out_direct'] or None + self.filter_feature_name = self.module.params['filter_feature_name'] or None + self.filter_log_name = self.module.params['filter_log_name'] or None + self.ip_type = self.module.params['ip_type'] or None + self.server_ip = self.module.params['server_ip'] or None + self.server_domain = self.module.params['server_domain'] or None + self.is_default_vpn = self.module.params['is_default_vpn'] or None + self.vrf_name = self.module.params['vrf_name'] or None + self.level = self.module.params['level'] or None + self.server_port = self.module.params['server_port'] or None + self.facility = self.module.params['facility'] or None + self.channel_name = self.module.params['channel_name'] or None + self.timestamp = self.module.params['timestamp'] or None + self.transport_mode = self.module.params['transport_mode'] or None + self.ssl_policy_name = self.module.params['ssl_policy_name'] or None + self.source_ip = self.module.params['source_ip'] or None + self.state = self.module.params['state'] or None + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + # syslog info + self.cur_global_info = None + self.cur_logfile_info = None + self.channel_info = None + self.channel_direct_info = None + self.filter_info = None + self.server_ip_info = None + self.server_domain_info = None + + def init_module(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, con_obj, xml_name): + """Check if response message is already succeed.""" + + xml_str = con_obj.xml + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_channel_dict(self): + """ get channel attributes dict.""" + + channel_info = dict() + # get channel info + conf_str = CE_NC_GET_CHANNEL_INFO % self.channel_id + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return channel_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + channel_info["channelInfos"] = list() + channels = root.findall("syslog/icChannels/icChannel") + if channels: + for channel in channels: + channel_dict = dict() + for ele in channel: + if ele.tag in ["icChnlId", "icChnlCfgName"]: + channel_dict[ele.tag] = ele.text + channel_info["channelInfos"].append(channel_dict) + return channel_info + + def is_exist_channel_id_name(self, channel_id, channel_name): + """if channel id exist""" + + if not self.channel_info: + return False + + for id2name in self.channel_info["channelInfos"]: + if id2name["icChnlId"] == channel_id and id2name["icChnlCfgName"] == channel_name: + return True + return False + + def config_merge_syslog_channel(self, channel_id, channel_name): + """config channel id""" + + if not self.is_exist_channel_id_name(channel_id, channel_name): + conf_str = CE_NC_MERGE_CHANNEL_INFO_HEADER + if channel_id: + conf_str += "%s" % channel_id + if channel_name: + conf_str += "%s" % channel_name + + conf_str += CE_NC_MERGE_CHANNEL_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge syslog channel id failed.') + + self.updates_cmd.append( + "info-center channel %s name %s" % (channel_id, channel_name)) + self.changed = True + + def delete_merge_syslog_channel(self, channel_id, channel_name): + """delete channel id""" + + change_flag = False + + if channel_name: + for id2name in self.channel_info["channelInfos"]: + channel_default_name = get_channel_name_default( + id2name["icChnlId"]) + if id2name["icChnlId"] == channel_id and id2name["icChnlCfgName"] == channel_name: + channel_name = channel_default_name + change_flag = True + + if not channel_name: + for id2name in self.channel_info["channelInfos"]: + channel_default_name = get_channel_name_default( + id2name["icChnlId"]) + if id2name["icChnlId"] == channel_id and id2name["icChnlCfgName"] != channel_default_name: + channel_name = channel_default_name + change_flag = True + if change_flag: + conf_str = CE_NC_MERGE_CHANNEL_INFO_HEADER + if channel_id: + conf_str += "%s" % channel_id + if channel_name: + conf_str += "%s" % channel_name + + conf_str += CE_NC_MERGE_CHANNEL_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge syslog channel id failed.') + + self.updates_cmd.append("undo info-center channel %s" % channel_id) + self.changed = True + + def get_channel_direct_dict(self): + """ get channel direct attributes dict.""" + + channel_direct_info = dict() + # get channel direct info + conf_str = CE_NC_GET_CHANNEL_DIRECT_INFO % self.channel_out_direct + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return channel_direct_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + channel_direct_info["channelDirectInfos"] = list() + dir_channels = root.findall("syslog/icDirChannels/icDirChannel") + if dir_channels: + for ic_dir_channel in dir_channels: + channel_direct_dict = dict() + for ele in ic_dir_channel: + if ele.tag in ["icOutDirect", "icCfgChnlId"]: + channel_direct_dict[ele.tag] = ele.text + channel_direct_info["channelDirectInfos"].append( + channel_direct_dict) + return channel_direct_info + + def is_exist_out_direct(self, out_direct, channel_id): + """if channel out direct exist""" + + if not self.channel_direct_info: + return False + + for id2name in self.channel_direct_info["channelDirectInfos"]: + if id2name["icOutDirect"] == out_direct and id2name["icCfgChnlId"] == channel_id: + return True + return False + + def config_merge_out_direct(self, out_direct, channel_id): + """config out direct""" + + if not self.is_exist_out_direct(out_direct, channel_id): + conf_str = CE_NC_MERGE_CHANNEL_DIRECT_HEADER + if out_direct: + conf_str += "%s" % out_direct + if channel_id: + conf_str += "%s" % channel_id + + conf_str += CE_NC_MERGE_CHANNEL_DIRECT_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge syslog channel out direct failed.') + + self.updates_cmd.append( + "info-center %s channel %s" % (out_direct, channel_id)) + self.changed = True + + def delete_merge_out_direct(self, out_direct, channel_id): + """delete out direct""" + + change_flag = False + channel_id_default = get_out_direct_default(out_direct) + if channel_id: + for id2name in self.channel_direct_info["channelDirectInfos"]: + if id2name["icOutDirect"] == out_direct and id2name["icCfgChnlId"] == channel_id: + if channel_id != channel_id_default: + channel_id = channel_id_default + change_flag = True + + if not channel_id: + for id2name in self.channel_direct_info["channelDirectInfos"]: + if id2name["icOutDirect"] == out_direct and id2name["icCfgChnlId"] != channel_id_default: + channel_id = channel_id_default + change_flag = True + + if change_flag: + conf_str = CE_NC_MERGE_CHANNEL_DIRECT_HEADER + if out_direct: + conf_str += "%s" % out_direct + if channel_id: + conf_str += "%s" % channel_id + + conf_str += CE_NC_MERGE_CHANNEL_DIRECT_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge syslog channel out direct failed.') + + self.updates_cmd.append("undo info-center logfile channel") + self.changed = True + + def get_filter_dict(self): + """ get syslog filter attributes dict.""" + + filter_info = dict() + # get filter info + conf_str = CE_NC_GET_FILTER_INFO + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return filter_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + filter_info["filterInfos"] = list() + ic_filters = root.findall("syslog/icFilters/icFilter") + if ic_filters: + for ic_filter in ic_filters: + filter_dict = dict() + for ele in ic_filter: + if ele.tag in ["icFeatureName", "icFilterLogName"]: + filter_dict[ele.tag] = ele.text + filter_info["filterInfos"].append(filter_dict) + return filter_info + + def is_exist_filter(self, filter_feature_name, filter_log_name): + """if filter info exist""" + + if not self.filter_info: + return False + for id2name in self.filter_info["filterInfos"]: + if id2name["icFeatureName"] == filter_feature_name and id2name["icFilterLogName"] == filter_log_name: + return True + return False + + def config_merge_filter(self, filter_feature_name, filter_log_name): + """config filter""" + + if not self.is_exist_filter(filter_feature_name, filter_log_name): + conf_str = CE_NC_CREATE_CHANNEL_FILTER_HEADER + conf_str += "true" + if filter_feature_name: + conf_str += "%s" % filter_feature_name + if filter_log_name: + conf_str += "%s" % filter_log_name + + conf_str += CE_NC_CREATE_CHANNEL_FILTER_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge syslog filter failed.') + + self.updates_cmd.append("info-center filter-id bymodule-alias %s %s" + % (filter_feature_name, filter_log_name)) + self.changed = True + + def delete_merge_filter(self, filter_feature_name, filter_log_name): + """delete filter""" + + change_flag = False + if self.is_exist_filter(filter_feature_name, filter_log_name): + for id2name in self.filter_info["filterInfos"]: + if id2name["icFeatureName"] == filter_feature_name and id2name["icFilterLogName"] == filter_log_name: + change_flag = True + if change_flag: + conf_str = CE_NC_DELETE_CHANNEL_FILTER_HEADER + conf_str += "true" + if filter_feature_name: + conf_str += "%s" % filter_feature_name + if filter_log_name: + conf_str += "%s" % filter_log_name + + conf_str += CE_NC_DELETE_CHANNEL_FILTER_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge syslog channel out direct failed.') + self.updates_cmd.append("undo info-center filter-id bymodule-alias %s %s" + % (filter_feature_name, filter_log_name)) + self.changed = True + + def get_server_ip_dict(self): + """ get server ip attributes dict.""" + + server_ip_info = dict() + # get server ip info + is_default_vpn = "false" + if not self.is_default_vpn: + self.is_default_vpn = False + if self.is_default_vpn is True: + is_default_vpn = "true" + if not self.vrf_name: + self.vrf_name = "_public_" + conf_str = CE_NC_GET_SERVER_IP_INFO_HEADER % ( + self.ip_type, self.server_ip, self.vrf_name, is_default_vpn) + conf_str += CE_NC_GET_SERVER_IP_INFO_TAIL + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return server_ip_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + server_ip_info["serverIpInfos"] = list() + syslog_servers = root.findall("syslog/syslogServers/syslogServer") + if syslog_servers: + for syslog_server in syslog_servers: + server_dict = dict() + for ele in syslog_server: + if ele.tag in ["ipType", "serverIp", "vrfName", "level", "serverPort", "facility", "chnlId", + "chnlName", "timestamp", "transportMode", "sslPolicyName", "isDefaultVpn", + "sourceIP", "isBriefFmt"]: + server_dict[ele.tag] = ele.text + server_ip_info["serverIpInfos"].append(server_dict) + return server_ip_info + + def config_merge_loghost(self): + """config loghost ip or dns""" + + conf_str = "" + is_default_vpn = "false" + if self.is_default_vpn is True: + is_default_vpn = "true" + if self.ip_type: + conf_str = CE_NC_MERGE_SERVER_IP_INFO_HEADER % (self.ip_type, self.server_ip, self.vrf_name, + is_default_vpn) + elif self.server_domain: + conf_str = CE_NC_MERGE_SERVER_DNS_INFO_HEADER % ( + self.server_domain, self.vrf_name, is_default_vpn) + if self.level: + conf_str += "%s" % self.level + if self.server_port: + conf_str += "%s" % self.server_port + if self.facility: + conf_str += "%s" % self.facility + if self.channel_id: + conf_str += "%s" % self.channel_id + if self.channel_name: + conf_str += "%s" % self.channel_name + if self.timestamp: + conf_str += "%s" % self.timestamp + if self.transport_mode: + conf_str += "%s" % self.transport_mode + if self.ssl_policy_name: + conf_str += "%s" % self.ssl_policy_name + if self.source_ip: + conf_str += "%s" % self.source_ip + if self.ip_type: + conf_str += CE_NC_MERGE_SERVER_IP_INFO_TAIL + elif self.server_domain: + conf_str += CE_NC_MERGE_SERVER_DNS_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge server loghost failed.') + + cmd = "info-center loghost" + if self.ip_type == "ipv4" and self.server_ip: + cmd += " %s" % self.server_ip + if self.ip_type == "ipv6" and self.server_ip: + cmd += " ipv6 %s" % self.server_ip + if self.server_domain: + cmd += " domain %s" % self.server_domain + if self.channel_id: + cmd += " channel %s" % self.channel_id + if self.channel_name: + cmd += " channel %s" % self.channel_name + if self.vrf_name: + if self.vrf_name != "_public_": + cmd += " vpn-instance %s" % self.vrf_name + if self.source_ip: + cmd += " source-ip %s" % self.source_ip + if self.facility: + cmd += " facility %s" % self.facility + if self.server_port: + cmd += " port %s" % self.server_port + if self.level: + cmd += " level %s" % self.level + if self.timestamp: + if self.timestamp == "localtime": + cmd += " local-time" + else: + cmd += " utc" + if self.transport_mode: + cmd += " transport %s" % self.transport_mode + if self.ssl_policy_name: + cmd += " ssl-policy %s" % self.ssl_policy_name + self.updates_cmd.append(cmd) + self.changed = True + + def delete_merge_loghost(self): + """delete loghost ip or dns""" + + conf_str = "" + is_default_vpn = "false" + if self.is_default_vpn is True: + is_default_vpn = "true" + if self.ip_type: + conf_str = CE_NC_DELETE_SERVER_IP_INFO_HEADER % (self.ip_type, self.server_ip, self.vrf_name, + is_default_vpn) + elif self.server_domain: + conf_str = CE_NC_DELETE_SERVER_DNS_INFO_HEADER % ( + self.server_domain, self.vrf_name, is_default_vpn) + if self.level: + conf_str += "%s" % self.level + if self.server_port: + conf_str += "%s" % self.server_port + if self.facility: + conf_str += "%s" % self.facility + if self.channel_id: + conf_str += "%s" % self.channel_id + if self.channel_name: + conf_str += "%s" % self.channel_name + if self.timestamp: + conf_str += "%s" % self.timestamp + if self.transport_mode: + conf_str += "%s" % self.transport_mode + if self.ssl_policy_name: + conf_str += "%s" % self.ssl_policy_name + if self.source_ip: + conf_str += "%s" % self.source_ip + if self.ip_type: + conf_str += CE_NC_DELETE_SERVER_IP_INFO_TAIL + elif self.server_domain: + conf_str += CE_NC_DELETE_SERVER_DNS_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge server loghost failed.') + + cmd = "undo info-center loghost" + if self.ip_type == "ipv4" and self.server_ip: + cmd += " %s" % self.server_ip + if self.ip_type == "ipv6" and self.server_ip: + cmd += " ipv6 %s" % self.server_ip + if self.server_domain: + cmd += " domain %s" % self.server_domain + if self.vrf_name: + if self.vrf_name != "_public_": + cmd += " vpn-instance %s" % self.vrf_name + self.updates_cmd.append(cmd) + self.changed = True + + def get_server_domain_dict(self): + """ get server domain attributes dict""" + + server_domain_info = dict() + # get server domain info + if not self.is_default_vpn: + self.is_default_vpn = False + if not self.vrf_name: + self.vrf_name = "_public_" + conf_str = CE_NC_GET_SERVER_DNS_INFO_HEADER + conf_str += CE_NC_GET_SERVER_DNS_INFO_TAIL + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return server_domain_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + server_domain_info["serverAddressInfos"] = list() + syslog_dnss = root.findall("syslog/syslogDNSs/syslogDNS") + if syslog_dnss: + for syslog_dns in syslog_dnss: + dns_dict = dict() + for ele in syslog_dns: + if ele.tag in ["serverDomain", "vrfName", "level", "serverPort", "facility", "chnlId", + "chnlName", "timestamp", "transportMode", "sslPolicyName", "isDefaultVpn", + "sourceIP", "isBriefFmt"]: + dns_dict[ele.tag] = ele.text + server_domain_info["serverAddressInfos"].append(dns_dict) + + return server_domain_info + + def check_need_loghost_cfg(self): + """ check need cfg""" + + need_cfg = False + find_flag = False + if self.ip_type and self.server_ip: + if self.server_ip_info: + for tmp in self.server_ip_info["serverIpInfos"]: + find_flag = True + if self.ip_type and tmp.get("ipType") != self.ip_type: + find_flag = False + if self.server_ip and tmp.get("serverIp") != self.server_ip: + find_flag = False + if self.vrf_name and tmp.get("vrfName") != self.vrf_name: + find_flag = False + if self.level and tmp.get("level") != self.level: + find_flag = False + if self.server_port and tmp.get("serverPort") != self.server_port: + find_flag = False + if self.facility and tmp.get("facility") != self.facility: + find_flag = False + if self.channel_id and tmp.get("chnlId") != self.channel_id: + find_flag = False + if self.channel_name and tmp.get("chnlName") != self.channel_name: + find_flag = False + if self.timestamp and tmp.get("timestamp") != self.timestamp: + find_flag = False + if self.transport_mode and tmp.get("transportMode") != self.transport_mode: + find_flag = False + if self.ssl_policy_name and tmp.get("sslPolicyName") != self.ssl_policy_name: + find_flag = False + if self.source_ip and tmp.get("sourceIP") != self.source_ip: + find_flag = False + if find_flag: + break + elif self.server_domain: + if self.server_domain_info: + for tmp in self.server_domain_info["serverAddressInfos"]: + find_flag = True + if self.server_domain and tmp.get("serverDomain") != self.server_domain: + find_flag = False + if self.vrf_name and tmp.get("vrfName") != self.vrf_name: + find_flag = False + if self.level and tmp.get("level") != self.level: + find_flag = False + if self.server_port and tmp.get("serverPort") != self.server_port: + find_flag = False + if self.facility and tmp.get("facility") != self.facility: + find_flag = False + if self.channel_id and tmp.get("chnlId") != self.channel_id: + find_flag = False + if self.channel_name and tmp.get("chnlName") != self.channel_name: + find_flag = False + if self.timestamp and tmp.get("timestamp") != self.timestamp: + find_flag = False + if self.transport_mode and tmp.get("transportMode") != self.transport_mode: + find_flag = False + if self.ssl_policy_name and tmp.get("sslPolicyName") != self.ssl_policy_name: + find_flag = False + if self.source_ip and tmp.get("sourceIP") != self.source_ip: + find_flag = False + if find_flag: + break + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + elif self.state == "absent": + need_cfg = bool(find_flag) + return need_cfg + + def get_syslog_global(self): + """get syslog global attributes""" + + cur_global_info = dict() + conf_str = CE_NC_GET_CENTER_GLOBAL_INFO_HEADER + if self.info_center_enable: + conf_str += "" + if self.packet_priority: + conf_str += "" + if self.suppress_enable: + conf_str += "" + conf_str += CE_NC_GET_CENTER_GLOBAL_INFO_TAIL + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return cur_global_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + global_info = root.findall( + "syslog/globalParam") + + if global_info: + for tmp in global_info: + for site in tmp: + if site.tag in ["icEnable", "packetPriority", "suppressEnable"]: + cur_global_info[site.tag] = site.text + return cur_global_info + + def merge_syslog_global(self): + """config global""" + + conf_str = CE_NC_MERGE_CENTER_GLOBAL_INFO_HEADER + if self.info_center_enable: + conf_str += "%s" % self.info_center_enable + if self.packet_priority: + if self.state == "present": + packet_priority = self.packet_priority + else: + packet_priority = 0 + conf_str += "%s" % packet_priority + if self.suppress_enable: + conf_str += "%s" % self.suppress_enable + + conf_str += CE_NC_MERGE_CENTER_GLOBAL_INFO_TAIL + + if self.info_center_enable == "true" and self.cur_global_info["icEnable"] != self.info_center_enable: + cmd = "info-center enable" + self.updates_cmd.append(cmd) + self.changed = True + if self.suppress_enable == "true" and self.cur_global_info["suppressEnable"] != self.suppress_enable: + cmd = "info-center statistic-suppress enable" + self.updates_cmd.append(cmd) + self.changed = True + if self.info_center_enable == "false" and self.cur_global_info["icEnable"] != self.info_center_enable: + cmd = "undo info-center enable" + self.updates_cmd.append(cmd) + self.changed = True + if self.suppress_enable == "false" and self.cur_global_info["suppressEnable"] != self.suppress_enable: + cmd = "undo info-center statistic-suppress enable" + self.updates_cmd.append(cmd) + self.changed = True + + if self.state == "present": + if self.packet_priority: + if self.cur_global_info["packetPriority"] != self.packet_priority: + cmd = "info-center syslog packet-priority %s" % self.packet_priority + self.updates_cmd.append(cmd) + self.changed = True + if self.state == "absent": + if self.packet_priority: + if self.cur_global_info["packetPriority"] == self.packet_priority: + cmd = "undo info-center syslog packet-priority %s" % self.packet_priority + self.updates_cmd.append(cmd) + self.changed = True + if self.changed: + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge syslog global failed.') + + def get_syslog_logfile(self): + """get syslog logfile""" + + cur_logfile_info = dict() + conf_str = CE_NC_GET_LOG_FILE_INFO_HEADER + conf_str += "log" + if self.logfile_max_num: + conf_str += "" + if self.logfile_max_size: + conf_str += "" + conf_str += CE_NC_GET_LOG_FILE_INFO_TAIL + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return cur_logfile_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + logfile_info = root.findall( + "syslog/icLogFileInfos/icLogFileInfo") + if logfile_info: + for tmp in logfile_info: + for site in tmp: + if site.tag in ["maxFileNum", "maxFileSize"]: + cur_logfile_info[site.tag] = site.text + return cur_logfile_info + + def merge_syslog_logfile(self): + """config logfile""" + + logfile_max_num = "200" + conf_str = CE_NC_MERGE_LOG_FILE_INFO_HEADER + if self.logfile_max_num: + if self.state == "present": + logfile_max_num = self.logfile_max_num + else: + if self.logfile_max_num != "200" and self.cur_logfile_info["maxFileNum"] == self.logfile_max_num: + logfile_max_num = "200" + conf_str += "%s" % logfile_max_num + + if self.logfile_max_size: + logfile_max_size = "32" + if self.state == "present": + logfile_max_size = self.logfile_max_size + else: + if self.logfile_max_size != "32" and self.cur_logfile_info["maxFileSize"] == self.logfile_max_size: + logfile_max_size = "32" + conf_str += "%s" % logfile_max_size + + conf_str += "log" + conf_str += CE_NC_MERGE_LOG_FILE_INFO_TAIL + + if self.state == "present": + if self.logfile_max_num: + if self.cur_logfile_info["maxFileNum"] != self.logfile_max_num: + cmd = "info-center max-logfile-number %s" % self.logfile_max_num + self.updates_cmd.append(cmd) + self.changed = True + if self.logfile_max_size: + if self.cur_logfile_info["maxFileSize"] != self.logfile_max_size: + cmd = "info-center logfile size %s" % self.logfile_max_size + self.updates_cmd.append(cmd) + self.changed = True + if self.state == "absent": + if self.logfile_max_num and self.logfile_max_num != "200": + if self.cur_logfile_info["maxFileNum"] == self.logfile_max_num: + cmd = "undo info-center max-logfile-number" + self.updates_cmd.append(cmd) + self.changed = True + if self.logfile_max_size and self.logfile_max_size != "32": + if self.cur_logfile_info["maxFileSize"] == self.logfile_max_size: + cmd = "undo info-center logfile size" + self.updates_cmd.append(cmd) + self.changed = True + + if self.changed: + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge syslog logfile failed.') + + def check_params(self): + """Check all input params""" + + # packet_priority check + if self.packet_priority: + if not self.packet_priority.isdigit(): + self.module.fail_json( + msg='Error: The parameter of packet priority is invalid.') + if int(self.packet_priority) > 7 or int(self.packet_priority) < 0: + self.module.fail_json( + msg='Error: The packet priority must be an integer between 0 and 7.') + + # logfile_max_num check + if self.logfile_max_num: + if not self.logfile_max_num.isdigit(): + self.module.fail_json( + msg='Error: The parameter of logfile_max_num is invalid.') + if int(self.logfile_max_num) > 500 or int(self.logfile_max_num) < 3: + self.module.fail_json( + msg='Error: The logfile_max_num must be an integer between 3 and 500.') + + # channel_id check + if self.channel_id: + if not self.channel_id.isdigit(): + self.module.fail_json( + msg='Error: The parameter of channel_id is invalid.') + if int(self.channel_id) > 9 or int(self.channel_id) < 0: + self.module.fail_json( + msg='Error: The channel_id must be an integer between 0 and 9.') + + # channel_cfg_name check + if self.channel_cfg_name: + if len(self.channel_cfg_name) > 30 \ + or len(self.channel_cfg_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: channel_cfg_name is not in the range from 1 to 30.') + + # filter_feature_name check + if self.filter_feature_name: + if len(self.filter_feature_name) > 31 \ + or len(self.filter_feature_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: filter_feature_name is not in the range from 1 to 31.') + + # filter_log_name check + if self.filter_log_name: + if len(self.filter_log_name) > 63 \ + or len(self.filter_log_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: filter_log_name is not in the range from 1 to 63.') + + # server_ip check + if self.server_ip: + if not check_ip_addr(self.server_ip): + self.module.fail_json( + msg='Error: The %s is not a valid ip address' % self.server_ip) + # source_ip check + if self.source_ip: + if not check_ip_addr(self.source_ip): + self.module.fail_json( + msg='Error: The %s is not a valid ip address' % self.source_ip) + + # server_domain check + if self.server_domain: + if len(self.server_domain) > 255 \ + or len(self.server_domain.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: server_domain is not in the range from 1 to 255.') + + # vrf_name check + if self.vrf_name: + if len(self.vrf_name) > 31 \ + or len(self.vrf_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: vrf_name is not in the range from 1 to 31.') + + # server_port check + if self.server_port: + if not self.server_port.isdigit(): + self.module.fail_json( + msg='Error: The parameter of server_port is invalid.') + if int(self.server_port) > 65535 or int(self.server_port) < 1: + self.module.fail_json( + msg='Error: The server_port must be an integer between 1 and 65535.') + + # channel_name check + if self.channel_name: + if len(self.channel_name) > 31 \ + or len(self.channel_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: channel_name is not in the range from 1 to 30.') + + # ssl_policy_name check + if self.ssl_policy_name: + if len(self.ssl_policy_name) > 23 \ + or len(self.ssl_policy_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: ssl_policy_name is not in the range from 1 to 23.') + + def get_proposed(self): + """get proposed info""" + + if self.info_center_enable: + self.proposed["info_center_enable"] = self.info_center_enable + if self.packet_priority: + self.proposed["packet_priority"] = self.packet_priority + if self.suppress_enable: + self.proposed["suppress_enable"] = self.suppress_enable + if self.logfile_max_num: + self.proposed["logfile_max_num"] = self.logfile_max_num + if self.logfile_max_size: + self.proposed["logfile_max_size"] = self.logfile_max_size + if self.channel_id: + self.proposed["channel_id"] = self.channel_id + if self.channel_cfg_name: + self.proposed["channel_cfg_name"] = self.channel_cfg_name + if self.channel_out_direct: + self.proposed["channel_out_direct"] = self.channel_out_direct + if self.filter_feature_name: + self.proposed["filter_feature_name"] = self.filter_feature_name + if self.filter_log_name: + self.proposed["filter_log_name"] = self.filter_log_name + if self.ip_type: + self.proposed["ip_type"] = self.ip_type + if self.server_ip: + self.proposed["server_ip"] = self.server_ip + if self.server_domain: + self.proposed["server_domain"] = self.server_domain + if self.vrf_name: + self.proposed["vrf_name"] = self.vrf_name + if self.level: + self.proposed["level"] = self.level + if self.server_port: + self.proposed["server_port"] = self.server_port + if self.facility: + self.proposed["facility"] = self.facility + if self.channel_name: + self.proposed["channel_name"] = self.channel_name + if self.timestamp: + self.proposed["timestamp"] = self.timestamp + if self.ssl_policy_name: + self.proposed["ssl_policy_name"] = self.ssl_policy_name + if self.transport_mode: + self.proposed["transport_mode"] = self.transport_mode + if self.is_default_vpn: + self.proposed["is_default_vpn"] = self.is_default_vpn + if self.source_ip: + self.proposed["source_ip"] = self.source_ip + if self.state: + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if self.info_center_enable: + self.existing["info_center_enable"] = self.cur_global_info[ + "icEnable"] + if self.packet_priority: + self.existing["packet_priority"] = self.cur_global_info[ + "packetPriority"] + if self.suppress_enable: + self.existing["suppress_enable"] = self.cur_global_info[ + "suppressEnable"] + if self.logfile_max_num: + self.existing["logfile_max_num"] = self.cur_logfile_info[ + "maxFileNum"] + if self.logfile_max_size: + self.existing["logfile_max_size"] = self.cur_logfile_info[ + "maxFileSize"] + + if self.channel_id and self.channel_cfg_name: + if self.channel_info: + self.existing["channel_id_info"] = self.channel_info[ + "channelInfos"] + if self.channel_out_direct and self.channel_id: + if self.channel_direct_info: + self.existing["channel_out_direct_info"] = self.channel_direct_info[ + "channelDirectInfos"] + if self.filter_feature_name and self.filter_log_name: + if self.filter_info: + self.existing["filter_id_info"] = self.filter_info[ + "filterInfos"] + if self.ip_type: + if self.server_ip_info: + self.existing["server_ip_info"] = self.server_ip_info[ + "serverIpInfos"] + + if self.server_domain: + if self.server_domain_info: + self.existing["server_domain_info"] = self.server_domain_info[ + "serverAddressInfos"] + + def get_end_state(self): + """get end state info""" + + if self.info_center_enable or self.packet_priority or self.suppress_enable: + self.cur_global_info = self.get_syslog_global() + if self.logfile_max_num or self.logfile_max_size: + self.cur_logfile_info = self.get_syslog_logfile() + if self.channel_id and self.channel_cfg_name: + self.channel_info = self.get_channel_dict() + if self.channel_out_direct and self.channel_id: + self.channel_direct_info = self.get_channel_direct_dict() + if self.filter_feature_name and self.filter_log_name: + self.filter_info = self.get_filter_dict() + if self.ip_type: + self.server_ip_info = self.get_server_ip_dict() + if self.server_domain: + self.server_domain_info = self.get_server_domain_dict() + + if self.info_center_enable: + self.end_state[ + "info_center_enable"] = self.cur_global_info["icEnable"] + if self.packet_priority: + self.end_state["packet_priority"] = self.cur_global_info[ + "packetPriority"] + if self.suppress_enable: + self.end_state["suppress_enable"] = self.cur_global_info[ + "suppressEnable"] + if self.logfile_max_num: + self.end_state["logfile_max_num"] = self.cur_logfile_info[ + "maxFileNum"] + if self.logfile_max_size: + self.end_state["logfile_max_size"] = self.cur_logfile_info[ + "maxFileSize"] + + if self.channel_id and self.channel_cfg_name: + if self.channel_info: + self.end_state["channel_id_info"] = self.channel_info[ + "channelInfos"] + + if self.channel_out_direct and self.channel_id: + if self.channel_direct_info: + self.end_state["channel_out_direct_info"] = self.channel_direct_info[ + "channelDirectInfos"] + + if self.filter_feature_name and self.filter_log_name: + if self.filter_info: + self.end_state["filter_id_info"] = self.filter_info[ + "filterInfos"] + + if self.ip_type: + if self.server_ip_info: + self.end_state["server_ip_info"] = self.server_ip_info[ + "serverIpInfos"] + + if self.server_domain: + if self.server_domain_info: + self.end_state["server_domain_info"] = self.server_domain_info[ + "serverAddressInfos"] + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + if self.info_center_enable or self.packet_priority or self.suppress_enable: + self.cur_global_info = self.get_syslog_global() + if self.logfile_max_num or self.logfile_max_size: + self.cur_logfile_info = self.get_syslog_logfile() + if self.channel_id: + self.channel_info = self.get_channel_dict() + if self.channel_out_direct: + self.channel_direct_info = self.get_channel_direct_dict() + if self.filter_feature_name and self.filter_log_name: + self.filter_info = self.get_filter_dict() + if self.ip_type: + self.server_ip_info = self.get_server_ip_dict() + if self.server_domain: + self.server_domain_info = self.get_server_domain_dict() + self.get_existing() + self.get_proposed() + if self.info_center_enable or self.packet_priority or self.suppress_enable: + self.merge_syslog_global() + + if self.logfile_max_num or self.logfile_max_size: + self.merge_syslog_logfile() + + if self.server_ip: + if not self.ip_type: + self.module.fail_json( + msg='Error: ip_type and server_ip must be exist at the same time.') + if self.ip_type: + if not self.server_ip: + self.module.fail_json( + msg='Error: ip_type and server_ip must be exist at the same time.') + + if self.ip_type or self.server_domain or self.channel_id or self.filter_feature_name: + if self.ip_type and self.server_domain: + self.module.fail_json( + msg='Error: ip_type and server_domain can not be exist at the same time.') + if self.channel_id and self.channel_name: + self.module.fail_json( + msg='Error: channel_id and channel_name can not be exist at the same time.') + if self.ssl_policy_name: + if self.transport_mode == "udp": + self.module.fail_json( + msg='Error: transport_mode: udp does not support ssl_policy.') + if not self.transport_mode: + self.module.fail_json( + msg='Error: transport_mode, ssl_policy_name must be exist at the same time.') + if self.ip_type == "ipv6": + if self.vrf_name and self.vrf_name != "_public_": + self.module.fail_json( + msg='Error: ipType:ipv6 only support default vpn:_public_.') + if self.is_default_vpn is True: + if self.vrf_name: + if self.vrf_name != "_public_": + self.module.fail_json( + msg='Error: vrf_name should be _public_ when is_default_vpn is True.') + else: + self.vrf_name = "_public_" + else: + if self.vrf_name == "_public_": + self.module.fail_json( + msg='Error: The default vpn value is _public_, but is_default_vpn is False.') + if self.state == "present": + # info-center channel channel-number name channel-name + if self.channel_id and self.channel_cfg_name: + self.config_merge_syslog_channel( + self.channel_id, self.channel_cfg_name) + # info-center { console | logfile | monitor | snmp | logbuffer + # | trapbuffer } channel channel-number + if self.channel_out_direct and self.channel_id: + self.config_merge_out_direct( + self.channel_out_direct, self.channel_id) + # info-center filter-id bymodule-alias modname alias + if self.filter_feature_name and self.filter_log_name: + self.config_merge_filter( + self.filter_feature_name, self.filter_log_name) + if self.ip_type and self.server_ip: + if not self.vrf_name: + self.vrf_name = "_public_" + if self.check_need_loghost_cfg(): + self.config_merge_loghost() + if self.server_domain: + if not self.vrf_name: + self.vrf_name = "_public_" + if self.check_need_loghost_cfg(): + self.config_merge_loghost() + + elif self.state == "absent": + if self.channel_id: + self.delete_merge_syslog_channel( + self.channel_id, self.channel_cfg_name) + if self.channel_out_direct: + self.delete_merge_out_direct( + self.channel_out_direct, self.channel_id) + if self.filter_feature_name and self.filter_log_name: + self.delete_merge_filter( + self.filter_feature_name, self.filter_log_name) + if self.ip_type and self.server_ip: + if not self.vrf_name: + self.vrf_name = "_public_" + if self.check_need_loghost_cfg(): + self.delete_merge_loghost() + if self.server_domain: + if not self.vrf_name: + self.vrf_name = "_public_" + if self.check_need_loghost_cfg(): + self.delete_merge_loghost() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + info_center_enable=dict(choices=['true', 'false']), + packet_priority=dict(type='str'), + suppress_enable=dict(choices=['true', 'false']), + logfile_max_num=dict(type='str'), + logfile_max_size=dict(choices=['4', '8', '16', '32']), + channel_id=dict(type='str'), + channel_cfg_name=dict(type='str'), + channel_out_direct=dict(choices=['console', 'monitor', + 'trapbuffer', 'logbuffer', 'snmp', 'logfile']), + filter_feature_name=dict(type='str'), + filter_log_name=dict(type='str'), + ip_type=dict(choices=['ipv4', 'ipv6']), + server_ip=dict(type='str'), + server_domain=dict(type='str'), + is_default_vpn=dict(default=False, type='bool'), + vrf_name=dict(type='str'), + level=dict(choices=['emergencies', 'alert', 'critical', 'error', 'warning', 'notification', + 'informational', 'debugging']), + server_port=dict(type='str'), + facility=dict(choices=['local0', 'local1', 'local2', + 'local3', 'local4', 'local5', 'local6', 'local7']), + channel_name=dict(type='str'), + timestamp=dict(choices=['UTC', 'localtime']), + transport_mode=dict(choices=['tcp', 'udp']), + ssl_policy_name=dict(type='str'), + source_ip=dict(type='str'), + state=dict(choices=['present', 'absent'], default='present') + + ) + argument_spec.update(ce_argument_spec) + module = InfoCenterGlobal(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_info_center_log.py b/plugins/modules/ce_info_center_log.py new file mode 100644 index 0000000..4b44722 --- /dev/null +++ b/plugins/modules/ce_info_center_log.py @@ -0,0 +1,545 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_info_center_log +short_description: Manages information center log configuration on HUAWEI CloudEngine switches. +description: + - Setting the Timestamp Format of Logs. + Configuring the Device to Output Logs to the Log Buffer. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + log_time_stamp: + description: + - Sets the timestamp format of logs. + choices: ['date_boot', 'date_second', 'date_tenthsecond', 'date_millisecond', + 'shortdate_second', 'shortdate_tenthsecond', 'shortdate_millisecond', + 'formatdate_second', 'formatdate_tenthsecond', 'formatdate_millisecond'] + log_buff_enable: + description: + - Enables the Switch to send logs to the log buffer. + default: no_use + choices: ['no_use','true', 'false'] + log_buff_size: + description: + - Specifies the maximum number of logs in the log buffer. + The value is an integer that ranges from 0 to 10240. If logbuffer-size is 0, logs are not displayed. + module_name: + description: + - Specifies the name of a module. + The value is a module name in registration logs. + channel_id: + description: + - Specifies a channel ID. + The value is an integer ranging from 0 to 9. + log_enable: + description: + - Indicates whether log filtering is enabled. + default: no_use + choices: ['no_use','true', 'false'] + log_level: + description: + - Specifies a log severity. + choices: ['emergencies', 'alert', 'critical', 'error', + 'warning', 'notification', 'informational', 'debugging'] + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' + +- name: CloudEngine info center log test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Setting the timestamp format of logs" + ce_info_center_log: + log_time_stamp: date_tenthsecond + provider: "{{ cli }}" + + - name: "Enabled to output information to the log buffer" + ce_info_center_log: + log_buff_enable: true + provider: "{{ cli }}" + + - name: "Set the maximum number of logs in the log buffer" + ce_info_center_log: + log_buff_size: 100 + provider: "{{ cli }}" + + - name: "Set a rule for outputting logs to a channel" + ce_info_center_log: + module_name: aaa + channel_id: 1 + log_enable: true + log_level: critical + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"log_time_stamp": "date_tenthsecond", "state": "present"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"log_time_stamp": "date_second"} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"log_time_stamp": "date_tenthsecond"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["info-center timestamp log date precision-time tenth-second"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_LOG = """ + + + + + + + + + + %s + %s + + + + + + + +""" + +CE_NC_GET_LOG_GLOBAL = """ + + + + + + + + + +""" + +TIME_STAMP_DICT = {"date_boot": "boot", + "date_second": "date precision-time second", + "date_tenthsecond": "date precision-time tenth-second", + "date_millisecond": "date precision-time millisecond", + "shortdate_second": "short-date precision-time second", + "shortdate_tenthsecond": "short-date precision-time tenth-second", + "shortdate_millisecond": "short-date precision-time millisecond", + "formatdate_second": "format-date precision-time second", + "formatdate_tenthsecond": "format-date precision-time tenth-second", + "formatdate_millisecond": "format-date precision-time millisecond"} + +CHANNEL_DEFAULT_LOG_STATE = {"0": "true", + "1": "true", + "2": "true", + "3": "false", + "4": "true", + "5": "false", + "6": "true", + "7": "true", + "8": "true", + "9": "true"} + +CHANNEL_DEFAULT_LOG_LEVEL = {"0": "warning", + "1": "warning", + "2": "informational", + "3": "informational", + "4": "warning", + "5": "debugging", + "6": "debugging", + "7": "warning", + "8": "debugging", + "9": "debugging"} + + +class InfoCenterLog(object): + """ + Manages information center log configuration + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.log_time_stamp = self.module.params['log_time_stamp'] + self.log_buff_enable = self.module.params['log_buff_enable'] + self.log_buff_size = self.module.params['log_buff_size'] + self.module_name = self.module.params['module_name'] + self.channel_id = self.module.params['channel_id'] + self.log_enable = self.module.params['log_enable'] + self.log_level = self.module.params['log_level'] + self.state = self.module.params['state'] + + # state + self.log_dict = dict() + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init module""" + + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_log_dict(self): + """ log config dict""" + + log_dict = dict() + if self.module_name: + if self.module_name.lower() == "default": + conf_str = CE_NC_GET_LOG % (self.module_name.lower(), self.channel_id) + else: + conf_str = CE_NC_GET_LOG % (self.module_name.upper(), self.channel_id) + else: + conf_str = CE_NC_GET_LOG_GLOBAL + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return log_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get global param info + glb = root.find("syslog/globalParam") + if glb: + for attr in glb: + if attr.tag in ["bufferSize", "logTimeStamp", "icLogBuffEn"]: + log_dict[attr.tag] = attr.text + + # get info-center source info + log_dict["source"] = dict() + src = root.find("syslog/icSources/icSource") + if src: + for attr in src: + if attr.tag in ["moduleName", "icChannelId", "icChannelName", "logEnFlg", "logEnLevel"]: + log_dict["source"][attr.tag] = attr.text + + return log_dict + + def config_log_global(self): + """config log global param""" + + xml_str = '' + if self.log_time_stamp: + if self.state == "present" and self.log_time_stamp.upper() != self.log_dict.get("logTimeStamp"): + xml_str += '%s' % self.log_time_stamp.upper() + self.updates_cmd.append( + "info-center timestamp log %s" % TIME_STAMP_DICT.get(self.log_time_stamp)) + elif self.state == "absent" and self.log_time_stamp.upper() == self.log_dict.get("logTimeStamp"): + xml_str += 'DATE_SECOND' # set default + self.updates_cmd.append("undo info-center timestamp log") + else: + pass + + if self.log_buff_enable != 'no_use': + if self.log_dict.get("icLogBuffEn") != self.log_buff_enable: + xml_str += '%s' % self.log_buff_enable + if self.log_buff_enable == "true": + self.updates_cmd.append("info-center logbuffer") + else: + self.updates_cmd.append("undo info-center logbuffer") + + if self.log_buff_size: + if self.state == "present" and self.log_dict.get("bufferSize") != self.log_buff_size: + xml_str += '%s' % self.log_buff_size + self.updates_cmd.append( + "info-center logbuffer size %s" % self.log_buff_size) + elif self.state == "absent" and self.log_dict.get("bufferSize") == self.log_buff_size: + xml_str += '512' + self.updates_cmd.append("undo info-center logbuffer size") + + if xml_str == '': + return "" + else: + xml_str += '' + return xml_str + + def config_log_soruce(self): + """config info-center sources""" + + xml_str = '' + if not self.module_name or not self.channel_id: + return xml_str + + source = self.log_dict["source"] + if self.state == "present": + xml_str = '' + cmd = 'info-center source %s channel %s log' % ( + self.module_name, self.channel_id) + else: + if not source or self.module_name != source.get("moduleName").lower() or \ + self.channel_id != source.get("icChannelId"): + return '' + + if self.log_enable == 'no_use' and not self.log_level: + xml_str = '' + else: + xml_str = '' + cmd = 'undo info-center source %s channel %s log' % ( + self.module_name, self.channel_id) + + xml_str += '%s%s' % ( + self.module_name, self.channel_id) + + # log_enable + if self.log_enable != 'no_use': + if self.state == "present" and (not source or self.log_enable != source.get("logEnFlg")): + xml_str += '%s' % self.log_enable + if self.log_enable == "true": + cmd += ' state on' + else: + cmd += ' state off' + elif self.state == "absent" and source and self.log_level == source.get("logEnLevel"): + xml_str += '%s' % CHANNEL_DEFAULT_LOG_STATE.get(self.channel_id) + cmd += ' state' + + # log_level + if self.log_level: + if self.state == "present" and (not source or self.log_level != source.get("logEnLevel")): + xml_str += '%s' % self.log_level + cmd += ' level %s' % self.log_level + elif self.state == "absent" and source and self.log_level == source.get("logEnLevel"): + xml_str += '%s' % CHANNEL_DEFAULT_LOG_LEVEL.get(self.channel_id) + cmd += ' level' + + if xml_str.endswith(""): + if self.log_enable == 'no_use' and not self.log_level and self.state == "absent": + xml_str += '' + self.updates_cmd.append(cmd) + return xml_str + else: + return '' + else: + xml_str += '' + self.updates_cmd.append(cmd) + return xml_str + + def netconf_load_config(self, xml_str): + """load log config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + + recv_xml = set_nc_config(self.module, xml_cfg) + self.check_response(recv_xml, "SET_LOG") + self.changed = True + + def check_params(self): + """Check all input params""" + + # check log_buff_size ranges from 0 to 10240 + if self.log_buff_size: + if not self.log_buff_size.isdigit(): + self.module.fail_json( + msg="Error: log_buff_size is not digit.") + if int(self.log_buff_size) < 0 or int(self.log_buff_size) > 10240: + self.module.fail_json( + msg="Error: log_buff_size is not ranges from 0 to 10240.") + + # check channel_id ranging from 0 to 9 + if self.channel_id: + if not self.channel_id.isdigit(): + self.module.fail_json(msg="Error: channel_id is not digit.") + if int(self.channel_id) < 0 or int(self.channel_id) > 9: + self.module.fail_json( + msg="Error: channel_id is not ranges from 0 to 9.") + + # module_name and channel_id must be set at the same time + if bool(self.module_name) != bool(self.channel_id): + self.module.fail_json( + msg="Error: module_name and channel_id must be set at the same time.") + + def get_proposed(self): + """get proposed info""" + + if self.log_time_stamp: + self.proposed["log_time_stamp"] = self.log_time_stamp + if self.log_buff_enable != 'no_use': + self.proposed["log_buff_enable"] = self.log_buff_enable + if self.log_buff_size: + self.proposed["log_buff_size"] = self.log_buff_size + if self.module_name: + self.proposed["module_name"] = self.module_name + if self.channel_id: + self.proposed["channel_id"] = self.channel_id + if self.log_enable != 'no_use': + self.proposed["log_enable"] = self.log_enable + if self.log_level: + self.proposed["log_level"] = self.log_level + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.log_dict: + return + + if self.log_time_stamp: + self.existing["log_time_stamp"] = self.log_dict.get("logTimeStamp").lower() + if self.log_buff_enable != 'no_use': + self.existing["log_buff_enable"] = self.log_dict.get("icLogBuffEn") + if self.log_buff_size: + self.existing["log_buff_size"] = self.log_dict.get("bufferSize") + if self.module_name: + self.existing["source"] = self.log_dict.get("source") + + def get_end_state(self): + """get end state info""" + + log_dict = self.get_log_dict() + if not log_dict: + return + + if self.log_time_stamp: + self.end_state["log_time_stamp"] = log_dict.get("logTimeStamp").lower() + if self.log_buff_enable != 'no_use': + self.end_state["log_buff_enable"] = log_dict.get("icLogBuffEn") + if self.log_buff_size: + self.end_state["log_buff_size"] = log_dict.get("bufferSize") + if self.module_name: + self.end_state["source"] = log_dict.get("source") + + def work(self): + """worker""" + + self.check_params() + self.log_dict = self.get_log_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.log_time_stamp or self.log_buff_enable != 'no_use' or self.log_buff_size: + xml_str += self.config_log_global() + + if self.module_name: + xml_str += self.config_log_soruce() + + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + log_time_stamp=dict(required=False, type='str', + choices=['date_boot', 'date_second', 'date_tenthsecond', 'date_millisecond', + 'shortdate_second', 'shortdate_tenthsecond', 'shortdate_millisecond', + 'formatdate_second', 'formatdate_tenthsecond', 'formatdate_millisecond']), + log_buff_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + log_buff_size=dict(required=False, type='str'), + module_name=dict(required=False, type='str'), + channel_id=dict(required=False, type='str'), + log_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + log_level=dict(required=False, type='str', + choices=['emergencies', 'alert', 'critical', 'error', + 'warning', 'notification', 'informational', 'debugging']), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = InfoCenterLog(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_info_center_trap.py b/plugins/modules/ce_info_center_trap.py new file mode 100644 index 0000000..6e1c9df --- /dev/null +++ b/plugins/modules/ce_info_center_trap.py @@ -0,0 +1,694 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_info_center_trap +short_description: Manages information center trap configuration on HUAWEI CloudEngine switches. +description: + - Manages information center trap configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] + trap_time_stamp: + description: + - Timestamp format of alarm information. + choices: ['date_boot', 'date_second', 'date_tenthsecond', 'date_millisecond', 'shortdate_second', + 'shortdate_tenthsecond', 'shortdate_millisecond', 'formatdate_second', 'formatdate_tenthsecond', + 'formatdate_millisecond'] + trap_buff_enable: + description: + - Whether a trap buffer is enabled to output information. + default: no_use + choices: ['no_use','true','false'] + trap_buff_size: + description: + - Size of a trap buffer. + The value is an integer ranging from 0 to 1024. The default value is 256. + module_name: + description: + - Module name of the rule. + The value is a string of 1 to 31 case-insensitive characters. The default value is default. + Please use lower-case letter, such as [aaa, acl, arp, bfd]. + channel_id: + description: + - Number of a channel. + The value is an integer ranging from 0 to 9. The default value is 0. + trap_enable: + description: + - Whether a device is enabled to output alarms. + default: no_use + choices: ['no_use','true','false'] + trap_level: + description: + - Trap level permitted to output. + choices: ['emergencies', 'alert', 'critical', 'error', 'warning', 'notification', + 'informational', 'debugging'] +''' + +EXAMPLES = ''' + +- name: CloudEngine info center trap test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config trap buffer" + ce_info_center_trap: + state: present + trap_buff_enable: true + trap_buff_size: 768 + provider: "{{ cli }}" + + - name: "Undo trap buffer" + ce_info_center_trap: + state: absent + trap_buff_enable: true + trap_buff_size: 768 + provider: "{{ cli }}" + + - name: "Config trap module log level" + ce_info_center_trap: + state: present + module_name: aaa + channel_id: 1 + trap_enable: true + trap_level: error + provider: "{{ cli }}" + + - name: "Undo trap module log level" + ce_info_center_trap: + state: absent + module_name: aaa + channel_id: 1 + trap_enable: true + trap_level: error + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"state": "present", "trap_buff_enable": "true", "trap_buff_size": "768"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"icTrapBuffEn": "false", "trapBuffSize": "256"} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"icTrapBuffEn": "true", "trapBuffSize": "768"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["info-center trapbuffer", "info-center trapbuffer size 768"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +# get info center trap global +CE_GET_TRAP_GLOBAL_HEADER = """ + + + +""" +CE_GET_TRAP_GLOBAL_TAIL = """ + + + +""" +# merge info center trap global +CE_MERGE_TRAP_GLOBAL_HEADER = """ + + + +""" +CE_MERGE_TRAP_GLOBAL_TAIL = """ + + + +""" + +# get info center trap source +CE_GET_TRAP_SOURCE_HEADER = """ + + + + +""" +CE_GET_TRAP_SOURCE_TAIL = """ + + + + +""" +# merge info center trap source +CE_MERGE_TRAP_SOURCE_HEADER = """ + + + + +""" +CE_MERGE_TRAP_SOURCE_TAIL = """ + + + + +""" +# delete info center trap source +CE_DELETE_TRAP_SOURCE_HEADER = """ + + + + +""" +CE_DELETE_TRAP_SOURCE_TAIL = """ + + + + +""" + +TIME_STAMP_DICT = {"date_boot": "boot", + "date_second": "date precision-time second", + "date_tenthsecond": "date precision-time tenth-second", + "date_millisecond": "date precision-time millisecond", + "shortdate_second": "short-date precision-time second", + "shortdate_tenthsecond": "short-date precision-time tenth-second", + "shortdate_millisecond": "short-date precision-time millisecond", + "formatdate_second": "format-date precision-time second", + "formatdate_tenthsecond": "format-date precision-time tenth-second", + "formatdate_millisecond": "format-date precision-time millisecond"} + +CHANNEL_DEFAULT_TRAP_STATE = {"0": "true", + "1": "true", + "2": "true", + "3": "true", + "4": "false", + "5": "true", + "6": "true", + "7": "true", + "8": "true", + "9": "true"} + +CHANNEL_DEFAULT_TRAP_LEVEL = {"0": "debugging", + "1": "debugging", + "2": "debugging", + "3": "debugging", + "4": "debugging", + "5": "debugging", + "6": "debugging", + "7": "debugging", + "8": "debugging", + "9": "debugging"} + + +class InfoCenterTrap(object): + """ Manages info center trap configuration """ + + def __init__(self, **kwargs): + """ Init function """ + + # argument spec + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # module args + self.state = self.module.params['state'] + self.trap_time_stamp = self.module.params['trap_time_stamp'] or None + self.trap_buff_enable = self.module.params['trap_buff_enable'] + self.trap_buff_size = self.module.params['trap_buff_size'] or None + self.module_name = self.module.params['module_name'] or None + self.channel_id = self.module.params['channel_id'] or None + self.trap_enable = self.module.params['trap_enable'] + self.trap_level = self.module.params['trap_level'] or None + + # cur config + self.cur_global_cfg = dict() + self.cur_source_cfg = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def netconf_get_config(self, conf_str): + """ Netconf get config """ + + xml_str = get_nc_config(self.module, conf_str) + + return xml_str + + def netconf_set_config(self, conf_str): + """ Netconf set config """ + + xml_str = set_nc_config(self.module, conf_str) + + return xml_str + + def check_global_args(self): + """ Check global args """ + + need_cfg = False + find_flag = False + self.cur_global_cfg["global_cfg"] = [] + + if self.trap_time_stamp or self.trap_buff_enable != 'no_use' or self.trap_buff_size: + if self.trap_buff_size: + if self.trap_buff_size.isdigit(): + if int(self.trap_buff_size) < 0 or int(self.trap_buff_size) > 1024: + self.module.fail_json( + msg='Error: The value of trap_buff_size is out of [0 - 1024].') + else: + self.module.fail_json( + msg='Error: The trap_buff_size is not digit.') + + conf_str = CE_GET_TRAP_GLOBAL_HEADER + + if self.trap_time_stamp: + conf_str += "" + if self.trap_buff_enable != 'no_use': + conf_str += "" + if self.trap_buff_size: + conf_str += "" + + conf_str += CE_GET_TRAP_GLOBAL_TAIL + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + find_flag = False + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + global_cfg = root.findall("syslog/globalParam") + if global_cfg: + for tmp in global_cfg: + tmp_dict = dict() + for site in tmp: + if site.tag in ["trapTimeStamp", "icTrapBuffEn", "trapBuffSize"]: + tmp_dict[site.tag] = site.text + + self.cur_global_cfg["global_cfg"].append(tmp_dict) + + if self.cur_global_cfg["global_cfg"]: + for tmp in self.cur_global_cfg["global_cfg"]: + find_flag = True + + if self.trap_time_stamp and tmp.get("trapTimeStamp").lower() != self.trap_time_stamp: + find_flag = False + if self.trap_buff_enable != 'no_use' and tmp.get("icTrapBuffEn") != self.trap_buff_enable: + find_flag = False + if self.trap_buff_size and tmp.get("trapBuffSize") != self.trap_buff_size: + find_flag = False + + if find_flag: + break + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + else: + need_cfg = bool(find_flag) + + self.cur_global_cfg["need_cfg"] = need_cfg + + def check_source_args(self): + """ Check source args """ + + need_cfg = False + find_flag = False + self.cur_source_cfg["source_cfg"] = list() + + if self.module_name: + if len(self.module_name) < 1 or len(self.module_name) > 31: + self.module.fail_json( + msg='Error: The module_name is out of [1 - 31].') + + if not self.channel_id: + self.module.fail_json( + msg='Error: Please input channel_id at the same time.') + + if self.channel_id: + if self.channel_id.isdigit(): + if int(self.channel_id) < 0 or int(self.channel_id) > 9: + self.module.fail_json( + msg='Error: The value of channel_id is out of [0 - 9].') + else: + self.module.fail_json( + msg='Error: The channel_id is not digit.') + + conf_str = CE_GET_TRAP_SOURCE_HEADER + + if self.module_name != "default": + conf_str += "%s" % self.module_name.upper() + else: + conf_str += "default" + + if self.channel_id: + conf_str += "" + if self.trap_enable != 'no_use': + conf_str += "" + if self.trap_level: + conf_str += "" + + conf_str += CE_GET_TRAP_SOURCE_TAIL + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + find_flag = False + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + source_cfg = root.findall("syslog/icSources/icSource") + if source_cfg: + for tmp in source_cfg: + tmp_dict = dict() + for site in tmp: + if site.tag in ["moduleName", "icChannelId", "trapEnFlg", "trapEnLevel"]: + tmp_dict[site.tag] = site.text + + self.cur_source_cfg["source_cfg"].append(tmp_dict) + + if self.cur_source_cfg["source_cfg"]: + for tmp in self.cur_source_cfg["source_cfg"]: + find_flag = True + + if self.module_name and tmp.get("moduleName").lower() != self.module_name.lower(): + find_flag = False + if self.channel_id and tmp.get("icChannelId") != self.channel_id: + find_flag = False + if self.trap_enable != 'no_use' and tmp.get("trapEnFlg") != self.trap_enable: + find_flag = False + if self.trap_level and tmp.get("trapEnLevel") != self.trap_level: + find_flag = False + + if find_flag: + break + else: + find_flag = False + + if self.state == "present": + need_cfg = bool(not find_flag) + else: + need_cfg = bool(find_flag) + + self.cur_source_cfg["need_cfg"] = need_cfg + + def get_proposed(self): + """ Get proposed """ + + self.proposed["state"] = self.state + + if self.trap_time_stamp: + self.proposed["trap_time_stamp"] = self.trap_time_stamp + if self.trap_buff_enable != 'no_use': + self.proposed["trap_buff_enable"] = self.trap_buff_enable + if self.trap_buff_size: + self.proposed["trap_buff_size"] = self.trap_buff_size + if self.module_name: + self.proposed["module_name"] = self.module_name + if self.channel_id: + self.proposed["channel_id"] = self.channel_id + if self.trap_enable != 'no_use': + self.proposed["trap_enable"] = self.trap_enable + if self.trap_level: + self.proposed["trap_level"] = self.trap_level + + def get_existing(self): + """ Get existing """ + + if self.cur_global_cfg["global_cfg"]: + self.existing["global_cfg"] = self.cur_global_cfg["global_cfg"] + if self.cur_source_cfg["source_cfg"]: + self.existing["source_cfg"] = self.cur_source_cfg["source_cfg"] + + def get_end_state(self): + """ Get end state """ + + self.check_global_args() + if self.cur_global_cfg["global_cfg"]: + self.end_state["global_cfg"] = self.cur_global_cfg["global_cfg"] + + self.check_source_args() + if self.cur_source_cfg["source_cfg"]: + self.end_state["source_cfg"] = self.cur_source_cfg["source_cfg"] + + def merge_trap_global(self): + """ Merge trap global """ + + conf_str = CE_MERGE_TRAP_GLOBAL_HEADER + + if self.trap_time_stamp: + conf_str += "%s" % self.trap_time_stamp.upper() + if self.trap_buff_enable != 'no_use': + conf_str += "%s" % self.trap_buff_enable + if self.trap_buff_size: + conf_str += "%s" % self.trap_buff_size + + conf_str += CE_MERGE_TRAP_GLOBAL_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge trap global failed.') + + if self.trap_time_stamp: + cmd = "info-center timestamp trap " + TIME_STAMP_DICT.get(self.trap_time_stamp) + self.updates_cmd.append(cmd) + if self.trap_buff_enable != 'no_use': + if self.trap_buff_enable == "true": + cmd = "info-center trapbuffer" + else: + cmd = "undo info-center trapbuffer" + self.updates_cmd.append(cmd) + if self.trap_buff_size: + cmd = "info-center trapbuffer size %s" % self.trap_buff_size + self.updates_cmd.append(cmd) + + self.changed = True + + def delete_trap_global(self): + """ Delete trap global """ + + conf_str = CE_MERGE_TRAP_GLOBAL_HEADER + + if self.trap_time_stamp: + conf_str += "DATE_SECOND" + if self.trap_buff_enable != 'no_use': + conf_str += "false" + if self.trap_buff_size: + conf_str += "256" + + conf_str += CE_MERGE_TRAP_GLOBAL_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: delete trap global failed.') + + if self.trap_time_stamp: + cmd = "undo info-center timestamp trap" + self.updates_cmd.append(cmd) + if self.trap_buff_enable != 'no_use': + cmd = "undo info-center trapbuffer" + self.updates_cmd.append(cmd) + if self.trap_buff_size: + cmd = "undo info-center trapbuffer size" + self.updates_cmd.append(cmd) + + self.changed = True + + def merge_trap_source(self): + """ Merge trap source """ + + conf_str = CE_MERGE_TRAP_SOURCE_HEADER + + if self.module_name: + conf_str += "%s" % self.module_name + if self.channel_id: + conf_str += "%s" % self.channel_id + if self.trap_enable != 'no_use': + conf_str += "%s" % self.trap_enable + if self.trap_level: + conf_str += "%s" % self.trap_level + + conf_str += CE_MERGE_TRAP_SOURCE_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge trap source failed.') + + cmd = "info-center source" + if self.module_name: + cmd += " %s" % self.module_name + if self.channel_id: + cmd += " channel %s" % self.channel_id + if self.trap_enable != 'no_use': + if self.trap_enable == "true": + cmd += " trap state on" + else: + cmd += " trap state off" + if self.trap_level: + cmd += " level %s" % self.trap_level + + self.updates_cmd.append(cmd) + self.changed = True + + def delete_trap_source(self): + """ Delete trap source """ + + if self.trap_enable == 'no_use' and not self.trap_level: + conf_str = CE_DELETE_TRAP_SOURCE_HEADER + if self.module_name: + conf_str += "%s" % self.module_name + if self.channel_id: + conf_str += "%s" % self.channel_id + conf_str += CE_DELETE_TRAP_SOURCE_TAIL + else: + conf_str = CE_MERGE_TRAP_SOURCE_HEADER + if self.module_name: + conf_str += "%s" % self.module_name + if self.channel_id: + conf_str += "%s" % self.channel_id + if self.trap_enable != 'no_use': + conf_str += "%s" % CHANNEL_DEFAULT_TRAP_STATE.get(self.channel_id) + if self.trap_level: + conf_str += "%s" % CHANNEL_DEFAULT_TRAP_LEVEL.get(self.channel_id) + conf_str += CE_MERGE_TRAP_SOURCE_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Delete trap source failed.') + + cmd = "undo info-center source" + if self.module_name: + cmd += " %s" % self.module_name + if self.channel_id: + cmd += " channel %s" % self.channel_id + if self.trap_enable != 'no_use': + cmd += " trap state" + if self.trap_level: + cmd += " level" + + self.updates_cmd.append(cmd) + self.changed = True + + def work(self): + """ work function """ + + self.check_global_args() + self.check_source_args() + self.get_proposed() + self.get_existing() + + if self.state == "present": + if self.cur_global_cfg["need_cfg"]: + self.merge_trap_global() + if self.cur_source_cfg["need_cfg"]: + self.merge_trap_source() + + else: + if self.cur_global_cfg["need_cfg"]: + self.delete_trap_global() + if self.cur_source_cfg["need_cfg"]: + self.delete_trap_source() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + trap_time_stamp=dict(choices=['date_boot', 'date_second', 'date_tenthsecond', + 'date_millisecond', 'shortdate_second', 'shortdate_tenthsecond', + 'shortdate_millisecond', 'formatdate_second', 'formatdate_tenthsecond', + 'formatdate_millisecond']), + trap_buff_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + trap_buff_size=dict(type='str'), + module_name=dict(type='str'), + channel_id=dict(type='str'), + trap_enable=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + trap_level=dict(choices=['emergencies', 'alert', 'critical', 'error', 'warning', 'notification', + 'informational', 'debugging']) + ) + + argument_spec.update(ce_argument_spec) + module = InfoCenterTrap(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_interface.py b/plugins/modules/ce_interface.py new file mode 100644 index 0000000..a2ca00b --- /dev/null +++ b/plugins/modules/ce_interface.py @@ -0,0 +1,892 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_interface +short_description: Manages physical attributes of interfaces on HUAWEI CloudEngine switches. +description: + - Manages physical attributes of interfaces on HUAWEI CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - This module is also used to create logical interfaces such as + vlanif and loopbacks. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - Full name of interface, i.e. 40GE1/0/10, Tunnel1. + interface_type: + description: + - Interface type to be configured from the device. + choices: ['ge', '10ge', '25ge', '4x10ge', '40ge', '100ge', 'vlanif', 'loopback', 'meth', + 'eth-trunk', 'nve', 'tunnel', 'ethernet', 'fcoe-port', 'fabric-port', 'stack-port', 'null'] + admin_state: + description: + - Specifies the interface management status. + The value is an enumerated type. + up, An interface is in the administrative Up state. + down, An interface is in the administrative Down state. + choices: ['up', 'down'] + description: + description: + - Specifies an interface description. + The value is a string of 1 to 242 case-sensitive characters, + spaces supported but question marks (?) not supported. + mode: + description: + - Manage Layer 2 or Layer 3 state of the interface. + choices: ['layer2', 'layer3'] + l2sub: + description: + - Specifies whether the interface is a Layer 2 sub-interface. + type: bool + default: 'no' + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present', 'absent', 'default'] +''' + +EXAMPLES = ''' +- name: interface module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Ensure an interface is a Layer 3 port and that it has the proper description + ce_interface: + interface: 10GE1/0/22 + description: 'Configured by Ansible' + mode: layer3 + provider: '{{ cli }}' + + - name: Admin down an interface + ce_interface: + interface: 10GE1/0/22 + admin_state: down + provider: '{{ cli }}' + + - name: Remove all tunnel interfaces + ce_interface: + interface_type: tunnel + state: absent + provider: '{{ cli }}' + + - name: Remove all logical interfaces + ce_interface: + interface_type: '{{ item }}' + state: absent + provider: '{{ cli }}' + with_items: + - loopback + - eth-trunk + - nve + + - name: Admin up all 10GE interfaces + ce_interface: + interface_type: 10GE + admin_state: up + provider: '{{ cli }}' +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"interface": "10GE1/0/10", "admin_state": "down"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {"admin_state": "up", "description": "None", + "interface": "10GE1/0/10", "mode": "layer2"} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"admin_state": "down", "description": "None", + "interface": "10GE1/0/10", "mode": "layer2"} +updates: + description: command list sent to the device + returned: always + type: list + sample: ["interface 10GE1/0/10", "shutdown"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + + +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_INTFS = """ + + + + + + %s + + + + + + + + + +""" + + +CE_NC_GET_INTF = """ + + + + + %s + + + + + + + + + + +""" + +CE_NC_XML_CREATE_INTF = """ + + + + %s + %s + + + +""" + +CE_NC_XML_CREATE_INTF_L2SUB = """ + + + + %s + %s + true + + + +""" + +CE_NC_XML_DELETE_INTF = """ + + + + %s + + + +""" + + +CE_NC_XML_MERGE_INTF_DES = """ + + + + %s + %s + + + +""" +CE_NC_XML_MERGE_INTF_STATUS = """ + + + + %s + %s + + + +""" + +CE_NC_XML_MERGE_INTF_L2ENABLE = """ + + + + %s + %s + + + +""" + +ADMIN_STATE_TYPE = ('ge', '10ge', '25ge', '4x10ge', '40ge', '100ge', + 'vlanif', 'meth', 'eth-trunk', 'vbdif', 'tunnel', + 'ethernet', 'stack-port') + +SWITCH_PORT_TYPE = ('ge', '10ge', '25ge', + '4x10ge', '40ge', '100ge', 'eth-trunk') + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + if interface.upper().startswith('GE'): + return 'ge' + elif interface.upper().startswith('10GE'): + return '10ge' + elif interface.upper().startswith('25GE'): + return '25ge' + elif interface.upper().startswith('4X10GE'): + return '4x10ge' + elif interface.upper().startswith('40GE'): + return '40ge' + elif interface.upper().startswith('100GE'): + return '100ge' + elif interface.upper().startswith('VLANIF'): + return 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + return 'loopback' + elif interface.upper().startswith('METH'): + return 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + return 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + return 'vbdif' + elif interface.upper().startswith('NVE'): + return 'nve' + elif interface.upper().startswith('TUNNEL'): + return 'tunnel' + elif interface.upper().startswith('ETHERNET'): + return 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + return 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + return 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + return 'stack-port' + elif interface.upper().startswith('NULL'): + return 'null' + else: + return None + + +def is_admin_state_enable(iftype): + """admin state disable: loopback nve""" + + return bool(iftype in ADMIN_STATE_TYPE) + + +def is_portswitch_enalbe(iftype): + """"is portswitch? """ + + return bool(iftype in SWITCH_PORT_TYPE) + + +class Interface(object): + """Manages physical attributes of interfaces.""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # interface info + self.interface = self.module.params['interface'] + self.interface_type = self.module.params['interface_type'] + self.admin_state = self.module.params['admin_state'] + self.description = self.module.params['description'] + self.mode = self.module.params['mode'] + self.l2sub = self.module.params['l2sub'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + self.intfs_info = dict() # all type interface info + self.intf_info = dict() # one interface info + self.intf_type = None # loopback tunnel ... + + def init_module(self): + """init_module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_interfaces_dict(self): + """ get interfaces attributes dict.""" + + intfs_info = dict() + conf_str = CE_NC_GET_INTFS % self.interface_type + recv_xml = get_nc_config(self.module, conf_str) + + if "" in recv_xml: + return intfs_info + + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + intfs = root.findall("ifm/interfaces/") + if intfs: + for intf in intfs: + intf_type = intf.find("ifPhyType").text.lower() + if intf_type: + if not intfs_info.get(intf_type): + intfs_info[intf_type] = list() + intf_info = dict() + for tmp in intf: + intf_info[tmp.tag] = tmp.text + intfs_info[intf_type].append(intf_info) + return intfs_info + + def get_interface_dict(self, ifname): + """ get one interface attributes dict.""" + + intf_info = dict() + conf_str = CE_NC_GET_INTF % ifname + recv_xml = get_nc_config(self.module, conf_str) + + if "" in recv_xml: + return intf_info + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + intfs = root.findall("ifm/interfaces/interface/") + if intfs: + for intf in intfs: + intf_info[intf.tag] = intf.text + return intf_info + + def create_interface(self, ifname, description, admin_state, mode, l2sub): + """Create interface.""" + + if l2sub: + self.updates_cmd.append("interface %s mode l2" % ifname) + else: + self.updates_cmd.append("interface %s" % ifname) + + if not description: + description = '' + else: + self.updates_cmd.append("description %s" % description) + + if l2sub: + xmlstr = CE_NC_XML_CREATE_INTF_L2SUB % (ifname, description) + else: + xmlstr = CE_NC_XML_CREATE_INTF % (ifname, description) + if admin_state and is_admin_state_enable(self.intf_type): + xmlstr += CE_NC_XML_MERGE_INTF_STATUS % (ifname, admin_state) + if admin_state == 'up': + self.updates_cmd.append("undo shutdown") + else: + self.updates_cmd.append("shutdown") + if mode and is_portswitch_enalbe(self.intf_type): + if mode == "layer2": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % (ifname, 'enable') + self.updates_cmd.append('portswitch') + elif mode == "layer3": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % (ifname, 'disable') + self.updates_cmd.append('undo portswitch') + + conf_str = ' ' + xmlstr + ' ' + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "CREATE_INTF") + self.changed = True + + def delete_interface(self, ifname): + """ Delete interface.""" + + xmlstr = CE_NC_XML_DELETE_INTF % ifname + conf_str = ' ' + xmlstr + ' ' + self.updates_cmd.append('undo interface %s' % ifname) + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "DELETE_INTF") + self.changed = True + + def delete_interfaces(self, iftype): + """ Delete interfaces with type.""" + + xmlstr = '' + intfs_list = self.intfs_info.get(iftype.lower()) + if not intfs_list: + return + + for intf in intfs_list: + xmlstr += CE_NC_XML_DELETE_INTF % intf['ifName'] + self.updates_cmd.append('undo interface %s' % intf['ifName']) + + conf_str = ' ' + xmlstr + ' ' + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "DELETE_INTFS") + self.changed = True + + def merge_interface(self, ifname, description, admin_state, mode): + """ Merge interface attributes.""" + + xmlstr = '' + change = False + self.updates_cmd.append("interface %s" % ifname) + if description and self.intf_info["ifDescr"] != description: + xmlstr += CE_NC_XML_MERGE_INTF_DES % (ifname, description) + self.updates_cmd.append("description %s" % description) + change = True + + if admin_state and is_admin_state_enable(self.intf_type) \ + and self.intf_info["ifAdminStatus"] != admin_state: + xmlstr += CE_NC_XML_MERGE_INTF_STATUS % (ifname, admin_state) + change = True + if admin_state == "up": + self.updates_cmd.append("undo shutdown") + else: + self.updates_cmd.append("shutdown") + + if is_portswitch_enalbe(self.intf_type): + if mode == "layer2" and self.intf_info["isL2SwitchPort"] != "true": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % (ifname, 'enable') + self.updates_cmd.append("portswitch") + change = True + elif mode == "layer3" \ + and self.intf_info["isL2SwitchPort"] != "false": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % (ifname, 'disable') + self.updates_cmd.append("undo portswitch") + change = True + + if not change: + return + + conf_str = ' ' + xmlstr + ' ' + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "MERGE_INTF_ATTR") + self.changed = True + + def merge_interfaces(self, iftype, description, admin_state, mode): + """ Merge interface attributes by type.""" + + xmlstr = '' + change = False + intfs_list = self.intfs_info.get(iftype.lower()) + if not intfs_list: + return + + for intf in intfs_list: + if_change = False + self.updates_cmd.append("interface %s" % intf['ifName']) + if description and intf["ifDescr"] != description: + xmlstr += CE_NC_XML_MERGE_INTF_DES % ( + intf['ifName'], description) + self.updates_cmd.append("description %s" % description) + if_change = True + if admin_state and is_admin_state_enable(self.intf_type)\ + and intf["ifAdminStatus"] != admin_state: + xmlstr += CE_NC_XML_MERGE_INTF_STATUS % ( + intf['ifName'], admin_state) + if_change = True + if admin_state == "up": + self.updates_cmd.append("undo shutdown") + else: + self.updates_cmd.append("shutdown") + + if is_portswitch_enalbe(self.intf_type): + if mode == "layer2" \ + and intf["isL2SwitchPort"] != "true": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % ( + intf['ifName'], 'enable') + self.updates_cmd.append("portswitch") + if_change = True + elif mode == "layer3" \ + and intf["isL2SwitchPort"] != "false": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % ( + intf['ifName'], 'disable') + self.updates_cmd.append("undo portswitch") + if_change = True + + if if_change: + change = True + else: + self.updates_cmd.pop() + + if not change: + return + + conf_str = ' ' + xmlstr + ' ' + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "MERGE_INTFS_ATTR") + self.changed = True + + def default_interface(self, ifname): + """default_interface""" + + change = False + xmlstr = "" + self.updates_cmd.append("interface %s" % ifname) + # set description default + if self.intf_info["ifDescr"]: + xmlstr += CE_NC_XML_MERGE_INTF_DES % (ifname, '') + self.updates_cmd.append("undo description") + change = True + + # set admin_status default + if is_admin_state_enable(self.intf_type) \ + and self.intf_info["ifAdminStatus"] != 'up': + xmlstr += CE_NC_XML_MERGE_INTF_STATUS % (ifname, 'up') + self.updates_cmd.append("undo shutdown") + change = True + + # set portswitch default + if is_portswitch_enalbe(self.intf_type) \ + and self.intf_info["isL2SwitchPort"] != "true": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % (ifname, 'enable') + self.updates_cmd.append("portswitch") + change = True + + if not change: + return + + conf_str = ' ' + xmlstr + ' ' + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "SET_INTF_DEFAULT") + self.changed = True + + def default_interfaces(self, iftype): + """ Set interface config to default by type.""" + + change = False + xmlstr = '' + intfs_list = self.intfs_info.get(iftype.lower()) + if not intfs_list: + return + + for intf in intfs_list: + if_change = False + self.updates_cmd.append("interface %s" % intf['ifName']) + + # set description default + if intf['ifDescr']: + xmlstr += CE_NC_XML_MERGE_INTF_DES % (intf['ifName'], '') + self.updates_cmd.append("undo description") + if_change = True + + # set admin_status default + if is_admin_state_enable(self.intf_type) and intf["ifAdminStatus"] != 'up': + xmlstr += CE_NC_XML_MERGE_INTF_STATUS % (intf['ifName'], 'up') + self.updates_cmd.append("undo shutdown") + if_change = True + + # set portswitch default + if is_portswitch_enalbe(self.intf_type) and intf["isL2SwitchPort"] != "true": + xmlstr += CE_NC_XML_MERGE_INTF_L2ENABLE % (intf['ifName'], 'enable') + self.updates_cmd.append("portswitch") + if_change = True + + if if_change: + change = True + else: + self.updates_cmd.pop() + + if not change: + return + + conf_str = ' ' + xmlstr + ' ' + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "SET_INTFS_DEFAULT") + self.changed = True + + def check_params(self): + """Check all input params""" + + if not self.interface and not self.interface_type: + self.module.fail_json( + msg='Error: Interface or interface_type must be set.') + if self.interface and self.interface_type: + self.module.fail_json( + msg='Error: Interface or interface_type' + ' can not be set at the same time.') + + # interface type check + if self.interface: + self.intf_type = get_interface_type(self.interface) + if not self.intf_type: + self.module.fail_json( + msg='Error: interface name of %s' + ' is error.' % self.interface) + + elif self.interface_type: + self.intf_type = get_interface_type(self.interface_type) + if not self.intf_type or self.intf_type != self.interface_type.replace(" ", "").lower(): + self.module.fail_json( + msg='Error: interface type of %s' + ' is error.' % self.interface_type) + + if not self.intf_type: + self.module.fail_json( + msg='Error: interface or interface type %s is error.') + + # shutdown check + if not is_admin_state_enable(self.intf_type) \ + and self.state == "present" and self.admin_state == "down": + self.module.fail_json( + msg='Error: The %s interface can not' + ' be shutdown.' % self.intf_type) + + # port switch mode check + if not is_portswitch_enalbe(self.intf_type)\ + and self.mode and self.state == "present": + self.module.fail_json( + msg='Error: The %s interface can not manage' + ' Layer 2 or Layer 3 state.' % self.intf_type) + + # check description len + if self.description: + if len(self.description) > 242 \ + or len(self.description.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: interface description ' + 'is not in the range from 1 to 242.') + # check l2sub flag + if self.l2sub: + if not self.interface: + self.module.fail_json(msg='Error: L2sub flag can not be set when there no interface set with.') + if self.interface.count(".") != 1: + self.module.fail_json(msg='Error: Interface name is invalid, it is not sub-interface.') + + def get_proposed(self): + """get_proposed""" + + self.proposed['state'] = self.state + if self.interface: + self.proposed["interface"] = self.interface + if self.interface_type: + self.proposed["interface_type"] = self.interface_type + + if self.state == 'present': + if self.description: + self.proposed["description"] = self.description + if self.mode: + self.proposed["mode"] = self.mode + if self.admin_state: + self.proposed["admin_state"] = self.admin_state + self.proposed["l2sub"] = self.l2sub + + elif self.state == 'default': + if self.description: + self.proposed["description"] = "" + if is_admin_state_enable(self.intf_type) and self.admin_state: + self.proposed["admin_state"] = self.admin_state + if is_portswitch_enalbe(self.intf_type) and self.mode: + self.proposed["mode"] = self.mode + + def get_existing(self): + """get_existing""" + + if self.intf_info: + self.existing["interface"] = self.intf_info["ifName"] + if is_admin_state_enable(self.intf_type): + self.existing["admin_state"] = self.intf_info["ifAdminStatus"] + self.existing["description"] = self.intf_info["ifDescr"] + if is_portswitch_enalbe(self.intf_type): + if self.intf_info["isL2SwitchPort"] == "true": + self.existing["mode"] = "layer2" + else: + self.existing["mode"] = "layer3" + + if self.intfs_info: + intfs = self.intfs_info.get(self.intf_type.lower()) + for intf in intfs: + intf_para = dict() + if intf["ifAdminStatus"]: + intf_para["admin_state"] = intf["ifAdminStatus"] + intf_para["description"] = intf["ifDescr"] + + if intf["isL2SwitchPort"] == "true": + intf_para["mode"] = "layer2" + else: + intf_para["mode"] = "layer3" + self.existing[intf["ifName"]] = intf_para + + def get_end_state(self): + """get_end_state""" + if self.interface: + end_info = self.get_interface_dict(self.interface) + if end_info: + self.end_state["interface"] = end_info["ifName"] + if is_admin_state_enable(self.intf_type): + self.end_state["admin_state"] = end_info["ifAdminStatus"] + self.end_state["description"] = end_info["ifDescr"] + if is_portswitch_enalbe(self.intf_type): + if end_info["isL2SwitchPort"] == "true": + self.end_state["mode"] = "layer2" + else: + self.end_state["mode"] = "layer3" + + if self.interface_type: + end_info = self.get_interfaces_dict() + intfs = end_info.get(self.intf_type.lower()) + for intf in intfs: + intf_para = dict() + if intf["ifAdminStatus"]: + intf_para["admin_state"] = intf["ifAdminStatus"] + intf_para["description"] = intf["ifDescr"] + + if intf["isL2SwitchPort"] == "true": + intf_para["mode"] = "layer2" + else: + intf_para["mode"] = "layer3" + self.end_state[intf["ifName"]] = intf_para + + def work(self): + """worker""" + + self.check_params() + + # single interface config + if self.interface: + self.intf_info = self.get_interface_dict(self.interface) + self.get_existing() + if self.state == 'present': + if not self.intf_info: + # create interface + self.create_interface(self.interface, + self.description, + self.admin_state, + self.mode, + self.l2sub) + else: + # merge interface + if self.description or self.admin_state or self.mode: + self.merge_interface(self.interface, + self.description, + self.admin_state, + self.mode) + + elif self.state == 'absent': + if self.intf_info: + # delete interface + self.delete_interface(self.interface) + else: + # interface does not exist + self.module.fail_json( + msg='Error: interface does not exist.') + + else: # default + if not self.intf_info: + # error, interface does not exist + self.module.fail_json( + msg='Error: interface does not exist.') + else: + self.default_interface(self.interface) + + # interface type config + else: + self.intfs_info = self.get_interfaces_dict() + self.get_existing() + if self.state == 'present': + if self.intfs_info.get(self.intf_type.lower()): + if self.description or self.admin_state or self.mode: + self.merge_interfaces(self.intf_type, + self.description, + self.admin_state, + self.mode) + elif self.state == 'absent': + # delete all interface of this type + if self.intfs_info.get(self.intf_type.lower()): + self.delete_interfaces(self.intf_type) + + else: + # set interfaces config to default + if self.intfs_info.get(self.intf_type.lower()): + self.default_interfaces(self.intf_type) + else: + self.module.fail_json( + msg='Error: no interface in this type.') + + self.get_proposed() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """main""" + + argument_spec = dict( + interface=dict(required=False, type='str'), + admin_state=dict(choices=['up', 'down'], required=False), + description=dict(required=False, default=None), + mode=dict(choices=['layer2', 'layer3'], required=False), + interface_type=dict(required=False), + l2sub=dict(required=False, default=False, type='bool'), + state=dict(choices=['absent', 'present', 'default'], + default='present', required=False), + ) + + argument_spec.update(ce_argument_spec) + interface = Interface(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_interface_ospf.py b/plugins/modules/ce_interface_ospf.py new file mode 100644 index 0000000..e172b72 --- /dev/null +++ b/plugins/modules/ce_interface_ospf.py @@ -0,0 +1,794 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_interface_ospf +short_description: Manages configuration of an OSPF interface instanceon HUAWEI CloudEngine switches. +description: + - Manages configuration of an OSPF interface instanceon HUAWEI CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - Full name of interface, i.e. 40GE1/0/10. + required: true + process_id: + description: + - Specifies a process ID. + The value is an integer ranging from 1 to 4294967295. + required: true + area: + description: + - Ospf area associated with this ospf process. + Valid values are a string, formatted as an IP address + (i.e. "0.0.0.0") or as an integer between 1 and 4294967295. + required: true + cost: + description: + - The cost associated with this interface. + Valid values are an integer in the range from 1 to 65535. + hello_interval: + description: + - Time between sending successive hello packets. + Valid values are an integer in the range from 1 to 65535. + dead_interval: + description: + - Time interval an ospf neighbor waits for a hello + packet before tearing down adjacencies. Valid values are an + integer in the range from 1 to 235926000. + silent_interface: + description: + - Setting to true will prevent this interface from receiving + HELLO packets. Valid values are 'true' and 'false'. + type: bool + default: 'no' + auth_mode: + description: + - Specifies the authentication type. + choices: ['none', 'null', 'hmac-sha256', 'md5', 'hmac-md5', 'simple'] + auth_text_simple: + description: + - Specifies a password for simple authentication. + The value is a string of 1 to 8 characters. + auth_key_id: + description: + - Authentication key id when C(auth_mode) is 'hmac-sha256', 'md5' or 'hmac-md5. + Valid value is an integer is in the range from 1 to 255. + auth_text_md5: + description: + - Specifies a password for MD5, HMAC-MD5, or HMAC-SHA256 authentication. + The value is a string of 1 to 255 case-sensitive characters, spaces not supported. + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: eth_trunk module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Enables OSPF and sets the cost on an interface + ce_interface_ospf: + interface: 10GE1/0/30 + process_id: 1 + area: 100 + cost: 100 + provider: '{{ cli }}' + + - name: Sets the dead interval of the OSPF neighbor + ce_interface_ospf: + interface: 10GE1/0/30 + process_id: 1 + area: 100 + dead_interval: 100 + provider: '{{ cli }}' + + - name: Sets the interval for sending Hello packets on an interface + ce_interface_ospf: + interface: 10GE1/0/30 + process_id: 1 + area: 100 + hello_interval: 2 + provider: '{{ cli }}' + + - name: Disables an interface from receiving and sending OSPF packets + ce_interface_ospf: + interface: 10GE1/0/30 + process_id: 1 + area: 100 + silent_interface: true + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"process_id": "1", "area": "0.0.0.100", "interface": "10GE1/0/30", "cost": "100"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"process_id": "1", "area": "0.0.0.100"} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"process_id": "1", "area": "0.0.0.100", "interface": "10GE1/0/30", + "cost": "100", "dead_interval": "40", "hello_interval": "10", + "silent_interface": "false", "auth_mode": "none"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface 10GE1/0/30", + "ospf enable 1 area 0.0.0.100", + "ospf cost 100"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_OSPF = """ + + + + + + %s + + + + + %s + + + %s + + + + + + + + + + + + + + + + + + +""" + +CE_NC_XML_BUILD_PROCESS = """ + + + + + + %s + + + %s + %s + + + + + + + +""" + +CE_NC_XML_BUILD_MERGE_INTF = """ + + + %s + + +""" + +CE_NC_XML_BUILD_DELETE_INTF = """ + + + %s + + +""" +CE_NC_XML_SET_IF_NAME = """ + %s +""" + +CE_NC_XML_SET_HELLO = """ + %s +""" + +CE_NC_XML_SET_DEAD = """ + %s +""" + +CE_NC_XML_SET_SILENT = """ + %s +""" + +CE_NC_XML_SET_COST = """ + %s +""" + +CE_NC_XML_SET_AUTH_MODE = """ + %s +""" + + +CE_NC_XML_SET_AUTH_TEXT_SIMPLE = """ + %s +""" + +CE_NC_XML_SET_AUTH_MD5 = """ + %s + %s +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + if interface.upper().startswith('GE'): + return 'ge' + elif interface.upper().startswith('10GE'): + return '10ge' + elif interface.upper().startswith('25GE'): + return '25ge' + elif interface.upper().startswith('4X10GE'): + return '4x10ge' + elif interface.upper().startswith('40GE'): + return '40ge' + elif interface.upper().startswith('100GE'): + return '100ge' + elif interface.upper().startswith('VLANIF'): + return 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + return 'loopback' + elif interface.upper().startswith('METH'): + return 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + return 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + return 'vbdif' + elif interface.upper().startswith('NVE'): + return 'nve' + elif interface.upper().startswith('TUNNEL'): + return 'tunnel' + elif interface.upper().startswith('ETHERNET'): + return 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + return 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + return 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + return 'stack-port' + elif interface.upper().startswith('NULL'): + return 'null' + else: + return None + + +def is_valid_v4addr(addr): + """check is ipv4 addr is valid""" + + if not addr: + return False + + if addr.find('.') != -1: + addr_list = addr.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +class InterfaceOSPF(object): + """ + Manages configuration of an OSPF interface instance. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.interface = self.module.params['interface'] + self.process_id = self.module.params['process_id'] + self.area = self.module.params['area'] + self.cost = self.module.params['cost'] + self.hello_interval = self.module.params['hello_interval'] + self.dead_interval = self.module.params['dead_interval'] + self.silent_interface = self.module.params['silent_interface'] + self.auth_mode = self.module.params['auth_mode'] + self.auth_text_simple = self.module.params['auth_text_simple'] + self.auth_key_id = self.module.params['auth_key_id'] + self.auth_text_md5 = self.module.params['auth_text_md5'] + self.state = self.module.params['state'] + + # ospf info + self.ospf_info = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def netconf_set_config(self, xml_str, xml_name): + """netconf set config""" + + rcv_xml = set_nc_config(self.module, xml_str) + if "" not in rcv_xml: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_area_ip(self): + """convert integer to ip address""" + + if not self.area.isdigit(): + return self.area + + addr_int = ['0'] * 4 + addr_int[0] = str(((int(self.area) & 0xFF000000) >> 24) & 0xFF) + addr_int[1] = str(((int(self.area) & 0x00FF0000) >> 16) & 0xFF) + addr_int[2] = str(((int(self.area) & 0x0000FF00) >> 8) & 0XFF) + addr_int[3] = str(int(self.area) & 0xFF) + + return '.'.join(addr_int) + + def get_ospf_dict(self): + """ get one ospf attributes dict.""" + + ospf_info = dict() + conf_str = CE_NC_GET_OSPF % ( + self.process_id, self.get_area_ip(), self.interface) + rcv_xml = get_nc_config(self.module, conf_str) + + if "" in rcv_xml: + return ospf_info + + xml_str = rcv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get process base info + root = ElementTree.fromstring(xml_str) + ospfsite = root.find("ospfv2/ospfv2comm/ospfSites/ospfSite") + if not ospfsite: + self.module.fail_json(msg="Error: ospf process does not exist.") + + for site in ospfsite: + if site.tag in ["processId", "routerId", "vrfName"]: + ospf_info[site.tag] = site.text + + # get areas info + ospf_info["areaId"] = "" + areas = root.find( + "ospfv2/ospfv2comm/ospfSites/ospfSite/areas/area") + if areas: + for area in areas: + if area.tag == "areaId": + ospf_info["areaId"] = area.text + break + + # get interface info + ospf_info["interface"] = dict() + intf = root.find( + "ospfv2/ospfv2comm/ospfSites/ospfSite/areas/area/interfaces/interface") + if intf: + for attr in intf: + if attr.tag in ["ifName", "networkType", + "helloInterval", "deadInterval", + "silentEnable", "configCost", + "authenticationMode", "authTextSimple", + "keyId", "authTextMd5"]: + ospf_info["interface"][attr.tag] = attr.text + + return ospf_info + + def set_ospf_interface(self): + """set interface ospf enable, and set its ospf attributes""" + + xml_intf = CE_NC_XML_SET_IF_NAME % self.interface + + # ospf view + self.updates_cmd.append("ospf %s" % self.process_id) + self.updates_cmd.append("area %s" % self.get_area_ip()) + if self.silent_interface: + xml_intf += CE_NC_XML_SET_SILENT % str(self.silent_interface).lower() + if self.silent_interface: + self.updates_cmd.append("silent-interface %s" % self.interface) + else: + self.updates_cmd.append("undo silent-interface %s" % self.interface) + + # interface view + self.updates_cmd.append("interface %s" % self.interface) + self.updates_cmd.append("ospf enable %s area %s" % ( + self.process_id, self.get_area_ip())) + if self.cost: + xml_intf += CE_NC_XML_SET_COST % self.cost + self.updates_cmd.append("ospf cost %s" % self.cost) + if self.hello_interval: + xml_intf += CE_NC_XML_SET_HELLO % self.hello_interval + self.updates_cmd.append("ospf timer hello %s" % + self.hello_interval) + if self.dead_interval: + xml_intf += CE_NC_XML_SET_DEAD % self.dead_interval + self.updates_cmd.append("ospf timer dead %s" % self.dead_interval) + if self.auth_mode: + xml_intf += CE_NC_XML_SET_AUTH_MODE % self.auth_mode + if self.auth_mode == "none": + self.updates_cmd.append("undo ospf authentication-mode") + else: + self.updates_cmd.append("ospf authentication-mode %s" % self.auth_mode) + if self.auth_mode == "simple" and self.auth_text_simple: + xml_intf += CE_NC_XML_SET_AUTH_TEXT_SIMPLE % self.auth_text_simple + self.updates_cmd.pop() + self.updates_cmd.append("ospf authentication-mode %s %s" + % (self.auth_mode, self.auth_text_simple)) + elif self.auth_mode in ["hmac-sha256", "md5", "hmac-md5"] and self.auth_key_id: + xml_intf += CE_NC_XML_SET_AUTH_MD5 % ( + self.auth_key_id, self.auth_text_md5) + self.updates_cmd.pop() + self.updates_cmd.append("ospf authentication-mode %s %s %s" + % (self.auth_mode, self.auth_key_id, self.auth_text_md5)) + else: + pass + + xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, + self.get_area_ip(), + (CE_NC_XML_BUILD_MERGE_INTF % xml_intf)) + self.netconf_set_config(xml_str, "SET_INTERFACE_OSPF") + self.changed = True + + def merge_ospf_interface(self): + """merge interface ospf attributes""" + + intf_dict = self.ospf_info["interface"] + + # ospf view + xml_ospf = "" + if intf_dict.get("silentEnable") != str(self.silent_interface).lower(): + xml_ospf += CE_NC_XML_SET_SILENT % str(self.silent_interface).lower() + self.updates_cmd.append("ospf %s" % self.process_id) + self.updates_cmd.append("area %s" % self.get_area_ip()) + if self.silent_interface: + self.updates_cmd.append("silent-interface %s" % self.interface) + else: + self.updates_cmd.append("undo silent-interface %s" % self.interface) + + # interface view + xml_intf = "" + self.updates_cmd.append("interface %s" % self.interface) + if self.cost and intf_dict.get("configCost") != self.cost: + xml_intf += CE_NC_XML_SET_COST % self.cost + self.updates_cmd.append("ospf cost %s" % self.cost) + if self.hello_interval and intf_dict.get("helloInterval") != self.hello_interval: + xml_intf += CE_NC_XML_SET_HELLO % self.hello_interval + self.updates_cmd.append("ospf timer hello %s" % + self.hello_interval) + if self.dead_interval and intf_dict.get("deadInterval") != self.dead_interval: + xml_intf += CE_NC_XML_SET_DEAD % self.dead_interval + self.updates_cmd.append("ospf timer dead %s" % self.dead_interval) + if self.auth_mode: + # NOTE: for security, authentication config will always be update + xml_intf += CE_NC_XML_SET_AUTH_MODE % self.auth_mode + if self.auth_mode == "none": + self.updates_cmd.append("undo ospf authentication-mode") + else: + self.updates_cmd.append("ospf authentication-mode %s" % self.auth_mode) + if self.auth_mode == "simple" and self.auth_text_simple: + xml_intf += CE_NC_XML_SET_AUTH_TEXT_SIMPLE % self.auth_text_simple + self.updates_cmd.pop() + self.updates_cmd.append("ospf authentication-mode %s %s" + % (self.auth_mode, self.auth_text_simple)) + elif self.auth_mode in ["hmac-sha256", "md5", "hmac-md5"] and self.auth_key_id: + xml_intf += CE_NC_XML_SET_AUTH_MD5 % ( + self.auth_key_id, self.auth_text_md5) + self.updates_cmd.pop() + self.updates_cmd.append("ospf authentication-mode %s %s %s" + % (self.auth_mode, self.auth_key_id, self.auth_text_md5)) + else: + pass + if not xml_intf: + self.updates_cmd.pop() # remove command: interface + + if not xml_ospf and not xml_intf: + return + + xml_sum = CE_NC_XML_SET_IF_NAME % self.interface + xml_sum += xml_ospf + xml_intf + xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, + self.get_area_ip(), + (CE_NC_XML_BUILD_MERGE_INTF % xml_sum)) + self.netconf_set_config(xml_str, "MERGE_INTERFACE_OSPF") + self.changed = True + + def unset_ospf_interface(self): + """set interface ospf disable, and all its ospf attributes will be removed""" + + intf_dict = self.ospf_info["interface"] + xml_sum = "" + xml_intf = CE_NC_XML_SET_IF_NAME % self.interface + if intf_dict.get("silentEnable") == "true": + xml_sum += CE_NC_XML_BUILD_MERGE_INTF % ( + xml_intf + (CE_NC_XML_SET_SILENT % "false")) + self.updates_cmd.append("ospf %s" % self.process_id) + self.updates_cmd.append("area %s" % self.get_area_ip()) + self.updates_cmd.append( + "undo silent-interface %s" % self.interface) + + xml_sum += CE_NC_XML_BUILD_DELETE_INTF % xml_intf + xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, + self.get_area_ip(), + xml_sum) + self.netconf_set_config(xml_str, "DELETE_INTERFACE_OSPF") + self.updates_cmd.append("undo ospf cost") + self.updates_cmd.append("undo ospf timer hello") + self.updates_cmd.append("undo ospf timer dead") + self.updates_cmd.append("undo ospf authentication-mode") + self.updates_cmd.append("undo ospf enable %s area %s" % ( + self.process_id, self.get_area_ip())) + self.changed = True + + def check_params(self): + """Check all input params""" + + self.interface = self.interface.replace(" ", "").upper() + + # interface check + if not get_interface_type(self.interface): + self.module.fail_json(msg="Error: interface is invalid.") + + # process_id check + if not self.process_id.isdigit(): + self.module.fail_json(msg="Error: process_id is not digit.") + if int(self.process_id) < 1 or int(self.process_id) > 4294967295: + self.module.fail_json(msg="Error: process_id must be an integer between 1 and 4294967295.") + + # area check + if self.area.isdigit(): + if int(self.area) < 0 or int(self.area) > 4294967295: + self.module.fail_json(msg="Error: area id (Integer) must be between 0 and 4294967295.") + else: + if not is_valid_v4addr(self.area): + self.module.fail_json(msg="Error: area id is invalid.") + + # area authentication check + if self.state == "present": + if self.auth_mode: + if self.auth_mode == "simple": + if self.auth_text_simple and len(self.auth_text_simple) > 8: + self.module.fail_json( + msg="Error: auth_text_simple is not in the range from 1 to 8.") + if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: + if self.auth_key_id and not self.auth_text_md5: + self.module.fail_json( + msg='Error: auth_key_id and auth_text_md5 should be set at the same time.') + if not self.auth_key_id and self.auth_text_md5: + self.module.fail_json( + msg='Error: auth_key_id and auth_text_md5 should be set at the same time.') + if self.auth_key_id: + if not self.auth_key_id.isdigit(): + self.module.fail_json( + msg="Error: auth_key_id is not digit.") + if int(self.auth_key_id) < 1 or int(self.auth_key_id) > 255: + self.module.fail_json( + msg="Error: auth_key_id is not in the range from 1 to 255.") + if self.auth_text_md5 and len(self.auth_text_md5) > 255: + self.module.fail_json( + msg="Error: auth_text_md5 is not in the range from 1 to 255.") + # cost check + if self.cost: + if not self.cost.isdigit(): + self.module.fail_json(msg="Error: cost is not digit.") + if int(self.cost) < 1 or int(self.cost) > 65535: + self.module.fail_json( + msg="Error: cost is not in the range from 1 to 65535") + + # hello_interval check + if self.hello_interval: + if not self.hello_interval.isdigit(): + self.module.fail_json( + msg="Error: hello_interval is not digit.") + if int(self.hello_interval) < 1 or int(self.hello_interval) > 65535: + self.module.fail_json( + msg="Error: hello_interval is not in the range from 1 to 65535") + + # dead_interval check + if self.dead_interval: + if not self.dead_interval.isdigit(): + self.module.fail_json(msg="Error: dead_interval is not digit.") + if int(self.dead_interval) < 1 or int(self.dead_interval) > 235926000: + self.module.fail_json( + msg="Error: dead_interval is not in the range from 1 to 235926000") + + def get_proposed(self): + """get proposed info""" + + self.proposed["interface"] = self.interface + self.proposed["process_id"] = self.process_id + self.proposed["area"] = self.get_area_ip() + self.proposed["cost"] = self.cost + self.proposed["hello_interval"] = self.hello_interval + self.proposed["dead_interval"] = self.dead_interval + self.proposed["silent_interface"] = self.silent_interface + if self.auth_mode: + self.proposed["auth_mode"] = self.auth_mode + if self.auth_mode == "simple": + self.proposed["auth_text_simple"] = self.auth_text_simple + if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: + self.proposed["auth_key_id"] = self.auth_key_id + self.proposed["auth_text_md5"] = self.auth_text_md5 + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.ospf_info: + return + + if self.ospf_info["interface"]: + self.existing["interface"] = self.interface + self.existing["cost"] = self.ospf_info["interface"].get("configCost") + self.existing["hello_interval"] = self.ospf_info["interface"].get("helloInterval") + self.existing["dead_interval"] = self.ospf_info["interface"].get("deadInterval") + self.existing["silent_interface"] = self.ospf_info["interface"].get("silentEnable") + self.existing["auth_mode"] = self.ospf_info["interface"].get("authenticationMode") + self.existing["auth_text_simple"] = self.ospf_info["interface"].get("authTextSimple") + self.existing["auth_key_id"] = self.ospf_info["interface"].get("keyId") + self.existing["auth_text_md5"] = self.ospf_info["interface"].get("authTextMd5") + self.existing["process_id"] = self.ospf_info["processId"] + self.existing["area"] = self.ospf_info["areaId"] + + def get_end_state(self): + """get end state info""" + + ospf_info = self.get_ospf_dict() + if not ospf_info: + return + + if ospf_info["interface"]: + self.end_state["interface"] = self.interface + self.end_state["cost"] = ospf_info["interface"].get("configCost") + self.end_state["hello_interval"] = ospf_info["interface"].get("helloInterval") + self.end_state["dead_interval"] = ospf_info["interface"].get("deadInterval") + self.end_state["silent_interface"] = ospf_info["interface"].get("silentEnable") + self.end_state["auth_mode"] = ospf_info["interface"].get("authenticationMode") + self.end_state["auth_text_simple"] = ospf_info["interface"].get("authTextSimple") + self.end_state["auth_key_id"] = ospf_info["interface"].get("keyId") + self.end_state["auth_text_md5"] = ospf_info["interface"].get("authTextMd5") + self.end_state["process_id"] = ospf_info["processId"] + self.end_state["area"] = ospf_info["areaId"] + + def work(self): + """worker""" + + self.check_params() + self.ospf_info = self.get_ospf_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + if self.state == "present": + if not self.ospf_info or not self.ospf_info["interface"]: + # create ospf area and set interface config + self.set_ospf_interface() + else: + # merge interface ospf area config + self.merge_ospf_interface() + else: + if self.ospf_info and self.ospf_info["interface"]: + # delete interface ospf area config + self.unset_ospf_interface() + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + interface=dict(required=True, type='str'), + process_id=dict(required=True, type='str'), + area=dict(required=True, type='str'), + cost=dict(required=False, type='str'), + hello_interval=dict(required=False, type='str'), + dead_interval=dict(required=False, type='str'), + silent_interface=dict(required=False, default=False, type='bool'), + auth_mode=dict(required=False, + choices=['none', 'null', 'hmac-sha256', 'md5', 'hmac-md5', 'simple'], type='str'), + auth_text_simple=dict(required=False, type='str', no_log=True), + auth_key_id=dict(required=False, type='str'), + auth_text_md5=dict(required=False, type='str', no_log=True), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = InterfaceOSPF(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_ip_interface.py b/plugins/modules/ce_ip_interface.py new file mode 100644 index 0000000..84ddb47 --- /dev/null +++ b/plugins/modules/ce_ip_interface.py @@ -0,0 +1,736 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_ip_interface +short_description: Manages L3 attributes for IPv4 and IPv6 interfaces on HUAWEI CloudEngine switches. +description: + - Manages Layer 3 attributes for IPv4 and IPv6 interfaces on HUAWEI CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - Interface must already be a L3 port when using this module. + - Logical interfaces (loopback, vlanif) must be created first. + - C(mask) must be inserted in decimal format (i.e. 24) for + both IPv6 and IPv4. + - A single interface can have multiple IPv6 configured. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - Full name of interface, i.e. 40GE1/0/22, vlanif10. + required: true + addr: + description: + - IPv4 or IPv6 Address. + mask: + description: + - Subnet mask for IPv4 or IPv6 Address in decimal format. + version: + description: + - IP address version. + default: v4 + choices: ['v4','v6'] + ipv4_type: + description: + - Specifies an address type. + The value is an enumerated type. + main, primary IP address. + sub, secondary IP address. + default: main + choices: ['main','sub'] + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: ip_interface module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Ensure ipv4 address is configured on 10GE1/0/22 + ce_ip_interface: + interface: 10GE1/0/22 + version: v4 + state: present + addr: 20.20.20.20 + mask: 24 + provider: '{{ cli }}' + + - name: Ensure ipv4 secondary address is configured on 10GE1/0/22 + ce_ip_interface: + interface: 10GE1/0/22 + version: v4 + state: present + addr: 30.30.30.30 + mask: 24 + ipv4_type: sub + provider: '{{ cli }}' + + - name: Ensure ipv6 is enabled on 10GE1/0/22 + ce_ip_interface: + interface: 10GE1/0/22 + version: v6 + state: present + provider: '{{ cli }}' + + - name: Ensure ipv6 address is configured on 10GE1/0/22 + ce_ip_interface: + interface: 10GE1/0/22 + version: v6 + state: present + addr: 2001::db8:800:200c:cccb + mask: 64 + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"addr": "20.20.20.20", "interface": "10GE1/0/22", "mask": "24"} +existing: + description: k/v pairs of existing IP attributes on the interface + returned: always + type: dict + sample: {"ipv4": [{"ifIpAddr": "11.11.11.11", "subnetMask": "255.255.0.0", "addrType": "main"}], + "interface": "10GE1/0/22"} +end_state: + description: k/v pairs of IP attributes after module execution + returned: always + type: dict + sample: {"ipv4": [{"ifIpAddr": "20.20.20.20", "subnetMask": "255.255.255.0", "addrType": "main"}], + "interface": "10GE1/0/22"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface 10GE1/0/22", "ip address 20.20.20.20 24"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_INTF = """ + + + + + %s + + + + + + + + + +""" + +CE_NC_ADD_IPV4 = """ + + + + + %s + + + + %s + %s + %s + + + + + + + +""" + +CE_NC_MERGE_IPV4 = """ + + + + + %s + + + + %s + %s + main + + + %s + %s + main + + + + + + + +""" + + +CE_NC_DEL_IPV4 = """ + + + + + %s + + + + %s + %s + %s + + + + + + + +""" + +CE_NC_ADD_IPV6 = """ + + + + + %s + + + + %s + %s + global + + + + + + + +""" + +CE_NC_DEL_IPV6 = """ + + + + + %s + + + + %s + %s + global + + + + + + + +""" + +CE_NC_MERGE_IPV6_ENABLE = """ + + + + + %s + + %s + + + + + +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + if interface.upper().startswith('GE'): + return 'ge' + elif interface.upper().startswith('10GE'): + return '10ge' + elif interface.upper().startswith('25GE'): + return '25ge' + elif interface.upper().startswith('4X10GE'): + return '4x10ge' + elif interface.upper().startswith('40GE'): + return '40ge' + elif interface.upper().startswith('100GE'): + return '100ge' + elif interface.upper().startswith('VLANIF'): + return 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + return 'loopback' + elif interface.upper().startswith('METH'): + return 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + return 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + return 'vbdif' + elif interface.upper().startswith('NVE'): + return 'nve' + elif interface.upper().startswith('TUNNEL'): + return 'tunnel' + elif interface.upper().startswith('ETHERNET'): + return 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + return 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + return 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + return 'stack-port' + elif interface.upper().startswith('NULL'): + return 'null' + else: + return None + + +def is_valid_v4addr(addr): + """check is ipv4 addr is valid""" + + if not addr: + return False + + if addr.find('.') != -1: + addr_list = addr.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +class IpInterface(object): + """ + Manages L3 attributes for IPv4 and IPv6 interfaces. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info] + self.interface = self.module.params['interface'] + self.addr = self.module.params['addr'] + self.mask = self.module.params['mask'] + self.version = self.module.params['version'] + self.ipv4_type = self.module.params['ipv4_type'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + # interface info + self.intf_info = dict() + self.intf_type = None + + def __init_module__(self): + """ init module """ + + required_if = [("version", "v4", ("addr", "mask"))] + required_together = [("addr", "mask")] + self.module = AnsibleModule( + argument_spec=self.spec, + required_if=required_if, + required_together=required_together, + supports_check_mode=True + ) + + def netconf_set_config(self, xml_str, xml_name): + """ netconf set config """ + + rcv_xml = set_nc_config(self.module, xml_str) + if "" not in rcv_xml: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_interface_dict(self, ifname): + """ get one interface attributes dict.""" + + intf_info = dict() + conf_str = CE_NC_GET_INTF % ifname + rcv_xml = get_nc_config(self.module, conf_str) + + if "" in rcv_xml: + return intf_info + + # get interface base info + intf = re.findall( + r'.*(.*).*\s*' + r'(.*).*', rcv_xml) + + if intf: + intf_info = dict(ifName=intf[0][0], + isL2SwitchPort=intf[0][1]) + + # get interface ipv4 address info + ipv4_info = re.findall( + r'.*(.*).*\s*(.*)' + r'.*\s*(.*).*', rcv_xml) + intf_info["am4CfgAddr"] = list() + for info in ipv4_info: + intf_info["am4CfgAddr"].append( + dict(ifIpAddr=info[0], subnetMask=info[1], addrType=info[2])) + + # get interface ipv6 address info + ipv6_info = re.findall( + r'.*.*\s*(.*).*', rcv_xml) + if not ipv6_info: + self.module.fail_json(msg='Error: Fail to get interface %s IPv6 state.' % self.interface) + else: + intf_info["enableFlag"] = ipv6_info[0] + + # get interface ipv6 enable info + ipv6_info = re.findall( + r'.*(.*).*\s*(.*)' + r'.*\s*(.*).*', rcv_xml) + + intf_info["am6CfgAddr"] = list() + for info in ipv6_info: + intf_info["am6CfgAddr"].append( + dict(ifIp6Addr=info[0], addrPrefixLen=info[1], addrType6=info[2])) + + return intf_info + + def convert_len_to_mask(self, masklen): + """convert mask length to ip address mask, i.e. 24 to 255.255.255.0""" + + mask_int = ["0"] * 4 + length = int(masklen) + + if length > 32: + self.module.fail_json(msg='Error: IPv4 ipaddress mask length is invalid.') + if length < 8: + mask_int[0] = str(int((0xFF << (8 - length % 8)) & 0xFF)) + if length >= 8: + mask_int[0] = '255' + mask_int[1] = str(int((0xFF << (16 - (length % 16))) & 0xFF)) + if length >= 16: + mask_int[1] = '255' + mask_int[2] = str(int((0xFF << (24 - (length % 24))) & 0xFF)) + if length >= 24: + mask_int[2] = '255' + mask_int[3] = str(int((0xFF << (32 - (length % 32))) & 0xFF)) + if length == 32: + mask_int[3] = '255' + + return '.'.join(mask_int) + + def is_ipv4_exist(self, addr, maskstr, ipv4_type): + """"Check IPv4 address exist""" + + addrs = self.intf_info["am4CfgAddr"] + if not addrs: + return False + + for address in addrs: + if address["ifIpAddr"] == addr: + return address["subnetMask"] == maskstr and address["addrType"] == ipv4_type + return False + + def get_ipv4_main_addr(self): + """get IPv4 main address""" + + addrs = self.intf_info["am4CfgAddr"] + if not addrs: + return None + + for address in addrs: + if address["addrType"] == "main": + return address + + return None + + def is_ipv6_exist(self, addr, masklen): + """Check IPv6 address exist""" + + addrs = self.intf_info["am6CfgAddr"] + if not addrs: + return False + + for address in addrs: + if address["ifIp6Addr"] == addr.upper(): + if address["addrPrefixLen"] == masklen and address["addrType6"] == "global": + return True + else: + self.module.fail_json( + msg="Error: Input IPv6 address or mask is invalid.") + + return False + + def set_ipv4_addr(self, ifname, addr, mask, ipv4_type): + """Set interface IPv4 address""" + + if not addr or not mask or not type: + return + + maskstr = self.convert_len_to_mask(mask) + if self.state == "present": + if not self.is_ipv4_exist(addr, maskstr, ipv4_type): + # primary IP address + if ipv4_type == "main": + main_addr = self.get_ipv4_main_addr() + if not main_addr: + # no ipv4 main address in this interface + xml_str = CE_NC_ADD_IPV4 % (ifname, addr, maskstr, ipv4_type) + self.netconf_set_config(xml_str, "ADD_IPV4_ADDR") + else: + # remove old address and set new + xml_str = CE_NC_MERGE_IPV4 % (ifname, main_addr["ifIpAddr"], + main_addr["subnetMask"], + addr, maskstr) + self.netconf_set_config(xml_str, "MERGE_IPV4_ADDR") + # secondary IP address + else: + xml_str = CE_NC_ADD_IPV4 % (ifname, addr, maskstr, ipv4_type) + self.netconf_set_config(xml_str, "ADD_IPV4_ADDR") + + self.updates_cmd.append("interface %s" % ifname) + if ipv4_type == "main": + self.updates_cmd.append("ip address %s %s" % (addr, maskstr)) + else: + self.updates_cmd.append("ip address %s %s sub" % (addr, maskstr)) + self.changed = True + else: + if self.is_ipv4_exist(addr, maskstr, ipv4_type): + xml_str = CE_NC_DEL_IPV4 % (ifname, addr, maskstr, ipv4_type) + self.netconf_set_config(xml_str, "DEL_IPV4_ADDR") + self.updates_cmd.append("interface %s" % ifname) + if ipv4_type == "main": + self.updates_cmd.append("undo ip address %s %s" % (addr, maskstr)) + else: + self.updates_cmd.append("undo ip address %s %s sub" % (addr, maskstr)) + self.changed = True + + def set_ipv6_addr(self, ifname, addr, mask): + """Set interface IPv6 address""" + + if not addr or not mask: + return + + if self.state == "present": + self.updates_cmd.append("interface %s" % ifname) + if self.intf_info["enableFlag"] == "false": + xml_str = CE_NC_MERGE_IPV6_ENABLE % (ifname, "true") + self.netconf_set_config(xml_str, "SET_IPV6_ENABLE") + self.updates_cmd.append("ipv6 enable") + self.changed = True + + if not self.is_ipv6_exist(addr, mask): + xml_str = CE_NC_ADD_IPV6 % (ifname, addr, mask) + self.netconf_set_config(xml_str, "ADD_IPV6_ADDR") + + self.updates_cmd.append("ipv6 address %s %s" % (addr, mask)) + self.changed = True + + if not self.changed: + self.updates_cmd.pop() + else: + if self.is_ipv6_exist(addr, mask): + xml_str = CE_NC_DEL_IPV6 % (ifname, addr, mask) + self.netconf_set_config(xml_str, "DEL_IPV6_ADDR") + self.updates_cmd.append("interface %s" % ifname) + self.updates_cmd.append( + "undo ipv6 address %s %s" % (addr, mask)) + self.changed = True + + def set_ipv6_enable(self, ifname): + """Set interface IPv6 enable""" + + if self.state == "present": + if self.intf_info["enableFlag"] == "false": + xml_str = CE_NC_MERGE_IPV6_ENABLE % (ifname, "true") + self.netconf_set_config(xml_str, "SET_IPV6_ENABLE") + self.updates_cmd.append("interface %s" % ifname) + self.updates_cmd.append("ipv6 enable") + self.changed = True + else: + if self.intf_info["enableFlag"] == "true": + xml_str = CE_NC_MERGE_IPV6_ENABLE % (ifname, "false") + self.netconf_set_config(xml_str, "SET_IPV6_DISABLE") + self.updates_cmd.append("interface %s" % ifname) + self.updates_cmd.append("undo ipv6 enable") + self.changed = True + + def check_params(self): + """Check all input params""" + + # check interface type + if self.interface: + self.intf_type = get_interface_type(self.interface) + if not self.intf_type: + self.module.fail_json( + msg='Error: Interface name of %s ' + 'is error.' % self.interface) + + # ipv4 addr and mask check + if self.version == "v4": + if not is_valid_v4addr(self.addr): + self.module.fail_json( + msg='Error: The %s is not a valid address.' % self.addr) + if not self.mask.isdigit(): + self.module.fail_json(msg='Error: mask is invalid.') + if int(self.mask) > 32 or int(self.mask) < 1: + self.module.fail_json( + msg='Error: mask must be an integer between 1 and 32.') + + # ipv6 mask check + if self.version == "v6": + if self.addr: + if not self.mask.isdigit(): + self.module.fail_json(msg='Error: mask is invalid.') + if int(self.mask) > 128 or int(self.mask) < 1: + self.module.fail_json( + msg='Error: mask must be an integer between 1 and 128.') + + # interface and layer3 check + self.intf_info = self.get_interface_dict(self.interface) + if not self.intf_info: + self.module.fail_json(msg='Error: interface %s does not exist.' % self.interface) + + if self.intf_info["isL2SwitchPort"] == "true": + self.module.fail_json(msg='Error: interface %s is layer2.' % self.interface) + + def get_proposed(self): + """get proposed info""" + + self.proposed["state"] = self.state + self.proposed["addr"] = self.addr + self.proposed["mask"] = self.mask + self.proposed["ipv4_type"] = self.ipv4_type + self.proposed["version"] = self.version + self.proposed["interface"] = self.interface + + def get_existing(self): + """get existing info""" + + self.existing["interface"] = self.interface + self.existing["ipv4addr"] = self.intf_info["am4CfgAddr"] + self.existing["ipv6addr"] = self.intf_info["am6CfgAddr"] + self.existing["ipv6enalbe"] = self.intf_info["enableFlag"] + + def get_end_state(self): + """get end state info""" + + intf_info = self.get_interface_dict(self.interface) + self.end_state["interface"] = self.interface + self.end_state["ipv4addr"] = intf_info["am4CfgAddr"] + self.end_state["ipv6addr"] = intf_info["am6CfgAddr"] + self.end_state["ipv6enalbe"] = intf_info["enableFlag"] + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + + # deal present or absent + if self.version == "v4": + self.set_ipv4_addr(self.interface, self.addr, self.mask, self.ipv4_type) + else: + if not self.addr and not self.mask: + self.set_ipv6_enable(self.interface) + else: + self.set_ipv6_addr(self.interface, self.addr, self.mask) + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + interface=dict(required=True), + addr=dict(required=False), + version=dict(required=False, choices=['v4', 'v6'], + default='v4'), + mask=dict(type='str', required=False), + ipv4_type=dict(required=False, choices=['main', 'sub'], default='main'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = IpInterface(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_is_is_instance.py b/plugins/modules/ce_is_is_instance.py new file mode 100644 index 0000000..a1eb7ab --- /dev/null +++ b/plugins/modules/ce_is_is_instance.py @@ -0,0 +1,330 @@ +#!/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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: ce_is_is_instance +author: xuxiaowei0512 (@CloudEngine-Ansible) +short_description: Manages isis process id configuration on HUAWEI CloudEngine devices. +description: + - Manages isis process id, creates a isis instance id or deletes a process id on HUAWEI CloudEngine devices. +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - This module works with connection C(netconf). +options: + instance_id: + description: + - Specifies the id of a isis process.The value is a number of 1 to 4294967295. + required: true + type: int + vpn_name: + description: + - VPN Instance, associate the VPN instance with the corresponding IS-IS process. + type: str + state: + description: + - Determines whether the config should be present or not on the device. + default: present + type: str + choices: ['present', 'absent'] +''' + +EXAMPLES = r''' + - name: Set isis process + ce_is_is_instance: + instance_id: 3 + state: present + + - name: Unset isis process + ce_is_is_instance: + instance_id: 3 + state: absent + + - name: check isis process + ce_is_is_instance: + instance_id: 4294967296 + state: present + + - name: Set vpn name + ce_is_is_instance: + instance_id: 22 + vpn_name: vpn1 + state: present + + - name: check vpn name + ce_is_is_instance: + instance_id: 22 + vpn_name: vpn1234567896321452212221556asdasdasdasdsadvdv + state: present +''' + +RETURN = r''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "instance_id": 1, + "vpn_name": null + } +existing: + description: k/v pairs of existing configuration + returned: always + type: dict + sample: { + "session": {} + } +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: { + "session": { + "instance_id": 1, + "vpn_name": null + } + } +updates: + description: commands sent to the device + returned: always + type: list + sample: [ + "isis 1" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +CE_NC_GET_ISIS = """ + + + %s + + +""" + +CE_NC_GET_ISIS_INSTANCE = """ + + + %s + + + +""" + + +def is_valid_ip_vpn(vpname): + """check ip vpn""" + + if not vpname: + return False + + if vpname == "_public_": + return False + + if len(vpname) < 1 or len(vpname) > 31: + return False + + return True + + +class ISIS_Instance(object): + """Manages ISIS Instance""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.instance_id = self.module.params['instance_id'] + self.vpn_name = self.module.params['vpn_name'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.isis_dict = dict() + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def get_isis_dict(self): + """isis config dict""" + isis_dict = dict() + isis_dict["instance"] = dict() + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_INSTANCE % self.instance_id)) + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return isis_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get isis info + glb = root.find("isiscomm/isSites/isSite") + if glb: + for attr in glb: + isis_dict["instance"][attr.tag] = attr.text + + return isis_dict + + def config_session(self): + """configures isis""" + xml_str = "" + instance = self.isis_dict["instance"] + if not self.instance_id: + return xml_str + + if self.state == "present": + xml_str = "%s" % self.instance_id + self.updates_cmd.append("isis %s" % self.instance_id) + + if self.vpn_name: + xml_str += "%s" % self.vpn_name + self.updates_cmd.append("vpn-instance %s" % self.vpn_name) + else: + # absent + if self.instance_id and str(self.instance_id) == instance.get("instanceId"): + xml_str = "%s" % self.instance_id + self.updates_cmd.append("undo isis %s" % self.instance_id) + + if self.state == "present": + return '' + xml_str + '' + else: + if xml_str: + return '' + xml_str + '' + + def netconf_load_config(self, xml_str): + """load isis config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + set_nc_config(self.module, xml_cfg) + self.changed = True + + def check_params(self): + """Check all input params""" + + # check instance id + if not self.instance_id: + self.module.fail_json(msg="Error: Missing required arguments: instance_id.") + + if self.instance_id: + if self.instance_id < 1 or self.instance_id > 4294967295: + self.module.fail_json(msg="Error: Instance id is not ranges from 1 to 4294967295.") + + # check vpn_name + if self.vpn_name: + if not is_valid_ip_vpn(self.vpn_name): + self.module.fail_json(msg="Error: Session vpn_name is invalid.") + + def get_proposed(self): + """get proposed info""" + # base config + self.proposed["instance_id"] = self.instance_id + self.proposed["vpn_name"] = self.vpn_name + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.isis_dict: + self.existing["instance"] = None + + self.existing["instance"] = self.isis_dict.get("instance") + + def get_end_state(self): + """get end state info""" + + isis_dict = self.get_isis_dict() + if not isis_dict: + self.end_state["instance"] = None + + self.end_state["instance"] = isis_dict.get("instance") + + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + self.check_params() + self.isis_dict = self.get_isis_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.instance_id: + cfg_str = self.config_session() + if cfg_str: + xml_str += cfg_str + + # update to device + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + argument_spec = dict( + instance_id=dict(required=True, type='int'), + vpn_name=dict(required=False, type='str'), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + + module = ISIS_Instance(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_is_is_interface.py b/plugins/modules/ce_is_is_interface.py new file mode 100644 index 0000000..7127d87 --- /dev/null +++ b/plugins/modules/ce_is_is_interface.py @@ -0,0 +1,788 @@ +#!/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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_is_is_interface +author: xuxiaowei0512 (@CloudEngine-Ansible) +short_description: Manages isis interface configuration on HUAWEI CloudEngine devices. +description: + - Manages isis process id, creates a isis instance id or deletes a process id on HUAWEI CloudEngine devices. +notes: + - Interface must already be a L3 port when using this module. + - This module requires the netconf system service be enabled on the remote device being managed. + - This module works with connection C(netconf). +options: + instance_id: + description: + - Specifies the id of a isis process. + The value is a number of 1 to 4294967295. + required: true + type: int + ifname: + description: + - A L3 interface. + required: true + type: str + leveltype: + description: + - level type for three types. + type: str + choices: ['level_1', 'level_2', 'level_1_2'] + level1dispriority: + description: + - the dispriority of the level1. + The value is a number of 1 to 127. + type: int + level2dispriority: + description: + - the dispriority of the level1. + The value is a number of 1 to 127. + type: int + silentenable: + description: + - enable the interface can send isis message. + The value is a bool type. + type: bool + silentcost: + description: + - Specifies whether the routing cost of the silent interface is 0. + The value is a bool type. + type: bool + typep2penable: + description: + - Simulate the network type of the interface as P2P. + The value is a bool type. + type: bool + snpacheck: + description: + - Enable SNPA check for LSPs and SNPs. + The value is a bool type. + type: bool + p2pnegotiationmode: + description: + - Set the P2P neighbor negotiation type. + type: str + choices: ['2_way', '3_way', '3_wayonly'] + p2ppeeripignore: + description: + - When the P2P hello packet is received, no IP address check is performed. + The value is a bool type. + type: bool + ppposicpcheckenable: + description: + - Interface for setting PPP link protocol to check OSICP negotiation status. + The value is a bool type. + type: bool + level1cost: + description: + - Specifies the link cost of the interface when performing Level-1 SPF calculation. + The value is a number of 0 to 16777215. + type: int + level2cost: + description: + - Specifies the link cost of the interface when performing Level-2 SPF calculation. + The value is a number of 0 to 16777215. + type: int + bfdstaticen: + description: + - Configure static BFD on a specific interface enabled with ISIS. + The value is a bool type. + type: bool + bfdblocken: + description: + - Blocking interfaces to dynamically create BFD features. + The value is a bool type. + type: bool + state: + description: + - Determines whether the config should be present or not on the device. + type: str + default: 'present' + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' + - name: "create vlan and config vlanif" + ce_config: + lines: 'vlan {{ test_vlan_id }},quit,interface {{test_intf_vlanif}},ip address {{test_vlanif_ip}} 24' + match: none + + - name: "create eth-trunk and config eth-trunk" + ce_config: + lines: 'interface {{test_intf_trunk}},undo portswitch,ip address {{test_trunk_ip}} 24' + match: none + + - name: "create vpn instance" + ce_config: + lines: 'ip vpn-instance {{test_vpn}},ipv4-family' + match: none + + - name: Set isis circuit-level + ce_is_is_interface: + instance_id: 3 + ifname: Eth-Trunk10 + leveltype: level_1_2 + state: present + + - name: Set isis level1dispriority + ce_is_is_interface: + instance_id: 3 + ifname: Eth-Trunk10 + level1dispriority: 0 + state: present + + - name: Set isis level2dispriority + ce_is_is_interface: + instance_id: 3 + ifname: Eth-Trunk10 + level2dispriority: 0 + state: present + + - name: Set isis silentenable + ce_is_is_interface: + instance_id: 3 + ifname: Eth-Trunk10 + silentenable: true + state: present + + - name: Set vpn name + ce_is_is_instance: + instance_id: 22 + vpn_name: vpn1 + state: present +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "addr_type": null, + "create_type": null, + "dest_addr": null, + "out_if_name": "10GE1/0/1", + "session_name": "bfd_l2link", + "src_addr": null, + "state": "present", + "use_default_ip": true, + "vrf_name": null + } +existing: + description: k/v pairs of existing configuration + returned: always + type: dict + sample: { + "session": {} + } +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: { + "session": { + "addrType": "IPV4", + "createType": "SESS_STATIC", + "destAddr": null, + "outIfName": "10GE1/0/1", + "sessName": "bfd_l2link", + "srcAddr": null, + "useDefaultIp": "true", + "vrfName": null + } + } +updates: + description: commands sent to the device + returned: always + type: list + sample: [ + "bfd bfd_l2link bind peer-ip default-ip interface 10ge1/0/1" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import sys +import socket +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +CE_NC_GET_ISIS = """ + + + %s + + +""" + +CE_NC_GET_ISIS_INTERFACE = """ + + + %s + + + + + + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_INTERFACE = """ + + + %s + + + %s + + + + +""" + +CE_NC_DELETE_ISIS_INTERFACE = """ + + + %s + + + %s + + + + +""" + +CE_NC_GET_ISIS_BFDINTERFACE = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_BFDINTERFACE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_DELETE_ISIS_BFDINTERFACE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + + +def is_valid_ip_vpn(vpname): + """check ip vpn""" + + if not vpname: + return False + + if vpname == "_public_": + return False + + if len(vpname) < 1 or len(vpname) > 31: + return False + + return True + + +def check_ip_addr(ipaddr): + """check ip address, Supports IPv4 and IPv6""" + + if not ipaddr or '\x00' in ipaddr: + return False + + try: + res = socket.getaddrinfo(ipaddr, 0, socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, socket.AI_NUMERICHOST) + return bool(res) + except socket.gaierror: + err = sys.exc_info()[1] + if err.args[0] == socket.EAI_NONAME: + return False + raise + + return True + + +def check_default_ip(ipaddr): + """check the default multicast IP address""" + + # The value ranges from 224.0.0.107 to 224.0.0.250 + if not check_ip_addr(ipaddr): + return False + + if ipaddr.count(".") != 3: + return False + + ips = ipaddr.split(".") + if ips[0] != "224" or ips[1] != "0" or ips[2] != "0": + return False + + if not ips[3].isdigit() or int(ips[3]) < 107 or int(ips[3]) > 250: + return False + + return True + + +def get_interface_type(interface): + """get the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface.upper().startswith('GE'): + return 'ge' + elif interface.upper().startswith('10GE'): + return '10ge' + elif interface.upper().startswith('25GE'): + return '25ge' + elif interface.upper().startswith('4X10GE'): + return '4x10ge' + elif interface.upper().startswith('40GE'): + return '40ge' + elif interface.upper().startswith('100GE'): + return '100ge' + elif interface.upper().startswith('VLANIF'): + return 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + return 'loopback' + elif interface.upper().startswith('METH'): + return 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + return 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + return 'vbdif' + elif interface.upper().startswith('NVE'): + return 'nve' + elif interface.upper().startswith('TUNNEL'): + return 'tunnel' + elif interface.upper().startswith('ETHERNET'): + return 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + return 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + return 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + return 'stack-port' + elif interface.upper().startswith('NULL'): + return'null' + else: + return None + + +class ISIS_Instance(object): + """Manages ISIS Instance""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.instance_id = self.module.params['instance_id'] + self.ifname = self.module.params['ifname'] + self.leveltype = self.module.params['leveltype'] + self.level1dispriority = self.module.params['level1dispriority'] + self.level2dispriority = self.module.params['level2dispriority'] + self.silentenable = self.module.params['silentenable'] + self.silentcost = self.module.params['silentcost'] + self.typep2penable = self.module.params['typep2penable'] + self.snpacheck = self.module.params['snpacheck'] + self.p2pnegotiationmode = self.module.params['p2pnegotiationmode'] + self.p2ppeeripignore = self.module.params['p2ppeeripignore'] + self.ppposicpcheckenable = self.module.params['ppposicpcheckenable'] + self.level1cost = self.module.params['level1cost'] + self.level2cost = self.module.params['level2cost'] + self.bfdstaticen = self.module.params['bfdstaticen'] + self.bfdblocken = self.module.params['bfdblocken'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.isis_dict = dict() + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + mutually_exclusive = [["level1dispriority", "level2dispriority"], + ["level1cost", "level2cost"]] + self.module = AnsibleModule( + argument_spec=self.spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + def get_isis_dict(self): + """bfd config dict""" + + isis_dict = dict() + isis_dict["instance"] = dict() + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_INTERFACE % self.instance_id)) + if self.bfdstaticen or self.bfdblocken: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_BFDINTERFACE % self.instance_id)) + + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return isis_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # + glb = root.find("isiscomm/isSites/isSite/isCircuits/isCircuit") + if self.bfdstaticen or self.bfdblocken: + glb = root.find("isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isCircMts/isCircMt") + if glb: + for attr in glb: + isis_dict["instance"][attr.tag] = attr.text + + return isis_dict + + def config_session(self): + """configures bfd session""" + + xml_str = "" + instance = self.isis_dict["instance"] + if not self.instance_id: + return xml_str + if self.ifname: + xml_str = "%s" % self.ifname + self.updates_cmd.append("interface %s" % self.ifname) + if self.state == "present": + self.updates_cmd.append("isis enable %s" % self.instance_id) + + if self.leveltype: + if self.leveltype == "level_1": + xml_str += "level_1" + self.updates_cmd.append("isis circuit-level level-1") + elif self.leveltype == "level_2": + xml_str += "level_2" + self.updates_cmd.append("isis circuit-level level-2") + elif self.leveltype == "level_1_2": + xml_str += "level_1_2" + self.updates_cmd.append("isis circuit-level level-1-2") + if self.level1dispriority is not None: + xml_str += "%s" % self.level1dispriority + self.updates_cmd.append("isis dis-priority %s level-1" % self.level1dispriority) + if self.level2dispriority is not None: + xml_str += "%s" % self.level2dispriority + self.updates_cmd.append("isis dis-priority %s level-2" % self.level2dispriority) + if self.p2pnegotiationmode: + if self.p2pnegotiationmode == "2_way": + xml_str += "2_way" + self.updates_cmd.append("isis ppp-negotiation 2-way") + elif self.p2pnegotiationmode == "3_way": + xml_str += "3_way" + self.updates_cmd.append("isis ppp-negotiation 3-way") + elif self.p2pnegotiationmode == "3_wayonly": + xml_str += "3_wayonly" + self.updates_cmd.append("isis ppp-negotiation only") + if self.level1cost is not None: + xml_str += "%s" % self.level1cost + self.updates_cmd.append("isis cost %s level-1" % self.level1cost) + if self.level2cost is not None: + xml_str += "%s" % self.level2cost + self.updates_cmd.append("isis cost %s level-2" % self.level2cost) + + else: + # absent + self.updates_cmd.append("undo isis enable") + if self.leveltype and self.leveltype == instance.get("circuitLevelType"): + xml_str += "level_1_2" + self.updates_cmd.append("undo isis circuit-level") + if self.level1dispriority is not None and self.level1dispriority == instance.get("level1DisPriority"): + xml_str += "64" + self.updates_cmd.append("undo isis dis-priority %s level-1" % self.level1dispriority) + if self.level2dispriority is not None and self.level2dispriority == instance.get("level2dispriority"): + xml_str += "64" + self.updates_cmd.append("undo isis dis-priority %s level-2" % self.level2dispriority) + if self.p2pnegotiationmode and self.p2pnegotiationmode == instance.get("p2pNegotiationMode"): + xml_str += "" + self.updates_cmd.append("undo isis ppp-negotiation") + if self.level1cost is not None and self.level1cost == instance.get("level1Cost"): + xml_str += "" + self.updates_cmd.append("undo isis cost %s level-1" % self.level1cost) + if self.level2cost is not None and self.level2cost == instance.get("level2Cost"): + xml_str += "" + self.updates_cmd.append("undo isis cost %s level-2" % self.level2cost) + + if self.silentenable and instance.get("silentEnable", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis silent") + elif not self.silentenable and instance.get("silentEnable", "false") == "true": + xml_str += "false" + self.updates_cmd.append("undo isis silent") + + if self.silentcost and instance.get("silentCost", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis silent advertise-zero-cost") + elif not self.silentcost and instance.get("silentCost", "false") == "true": + xml_str += "false" + + if self.typep2penable and instance.get("typeP2pEnable", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis circuit-type p2p") + elif not self.typep2penable and instance.get("typeP2pEnable", "false") == "true": + xml_str += "false" + self.updates_cmd.append("undo isis circuit-type") + + if self.snpacheck and instance.get("snpaCheck", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis circuit-type p2p strict-snpa-check") + elif not self.snpacheck and instance.get("snpaCheck", "false") == "true": + xml_str += "false" + + if self.p2ppeeripignore and instance.get("p2pPeerIPIgnore", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis peer-ip-ignore") + elif not self.p2ppeeripignore and instance.get("p2pPeerIPIgnore", "false") == "true": + xml_str += "false" + self.updates_cmd.append("undo isis peer-ip-ignore") + + if self.ppposicpcheckenable and instance.get("pPPOsicpCheckEnable", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis ppp-osicp-check") + elif not self.ppposicpcheckenable and instance.get("pPPOsicpCheckEnable", "false") == "true": + xml_str += "false" + self.updates_cmd.append("undo isis ppp-osicp-check") + if self.bfdstaticen and instance.get("bfdStaticEn", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis bfd static") + elif not self.bfdstaticen and instance.get("bfdStaticEn", "false") == "true": + xml_str += "false" + self.updates_cmd.append("undo isis bfd static") + if self.bfdblocken and instance.get("bfdBlockEn", "false") == "false": + xml_str += "true" + self.updates_cmd.append("isis bfd block") + elif not self.bfdblocken and instance.get("bfdBlockEn", "false") == "true": + xml_str += "false" + self.updates_cmd.append("undo isis bfd block") + + if self.state == "present": + if self.bfdstaticen is not None or self.bfdblocken is not None: + return CE_NC_MERGE_ISIS_BFDINTERFACE % (self.instance_id, xml_str) + return CE_NC_MERGE_ISIS_INTERFACE % (self.instance_id, xml_str) + else: + if self.bfdstaticen is not None or self.bfdblocken is not None: + return CE_NC_DELETE_ISIS_BFDINTERFACE % (self.instance_id, xml_str) + return CE_NC_DELETE_ISIS_INTERFACE % (self.instance_id, xml_str) + + def netconf_load_config(self, xml_str): + """load bfd config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + set_nc_config(self.module, xml_cfg) + self.changed = True + + def check_params(self): + """Check all input params""" + + # check instance id + if not self.instance_id: + self.module.fail_json(msg="Error: Missing required arguments: instance_id.") + + if self.instance_id: + if self.instance_id < 1 or self.instance_id > 4294967295: + self.module.fail_json(msg="Error: Instance id is not ranges from 1 to 4294967295.") + + # check level1dispriority + if self.level1dispriority is not None: + if self.level1dispriority < 0 or self.level1dispriority > 127: + self.module.fail_json(msg="Error: level1dispriority is not ranges from 0 to 127.") + + if self.level2dispriority is not None: + if self.level2dispriority < 0 or self.level2dispriority > 127: + self.module.fail_json(msg="Error: level2dispriority is not ranges from 0 to 127.") + + if self.level1cost is not None: + if self.level1cost < 0 or self.level1cost > 16777215: + self.module.fail_json(msg="Error: level1cost is not ranges from 0 to 16777215.") + + if self.level2cost is not None: + if self.level2cost < 0 or self.level2cost > 16777215: + self.module.fail_json(msg="Error: level2cost is not ranges from 0 to 16777215.") + + def get_proposed(self): + """get proposed info""" + self.proposed["instance_id"] = self.instance_id + self.proposed["ifname"] = self.ifname + self.proposed["leveltype"] = self.leveltype + self.proposed["level1dispriority"] = self.level1dispriority + self.proposed["level2dispriority"] = self.level2dispriority + self.proposed["silentenable"] = self.silentenable + self.proposed["silentcost"] = self.silentcost + self.proposed["typep2penable"] = self.typep2penable + self.proposed["snpacheck"] = self.snpacheck + self.proposed["p2pnegotiationmode"] = self.p2pnegotiationmode + self.proposed["p2ppeeripignore"] = self.p2ppeeripignore + self.proposed["ppposicpcheckenable"] = self.ppposicpcheckenable + self.proposed["level1cost"] = self.level1cost + self.proposed["level2cost"] = self.level2cost + self.proposed["bfdstaticen"] = self.bfdstaticen + self.proposed["bfdblocken"] = self.bfdblocken + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.isis_dict: + self.existing["instance"] = None + else: + self.existing["instance"] = self.isis_dict.get("instance") + + def get_end_state(self): + """get end state info""" + + isis_dict = self.get_isis_dict() + if not isis_dict: + self.end_state["instance"] = None + else: + self.end_state["instance"] = isis_dict.get("instance") + if self.existing == self.end_state: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.isis_dict = self.get_isis_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.instance_id: + xml_str += self.config_session() + + # update to device + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + instance_id=dict(required=True, type='int'), + ifname=dict(required=True, type='str'), + leveltype=dict(required=False, type='str', choices=['level_1', 'level_2', 'level_1_2']), + level1dispriority=dict(required=False, type='int'), + level2dispriority=dict(required=False, type='int'), + silentenable=dict(required=False, type='bool'), + silentcost=dict(required=False, type='bool'), + typep2penable=dict(required=False, type='bool'), + snpacheck=dict(required=False, type='bool'), + p2pnegotiationmode=dict(required=False, type='str', choices=['2_way', '3_way', '3_wayonly']), + p2ppeeripignore=dict(required=False, type='bool'), + ppposicpcheckenable=dict(required=False, type='bool'), + level1cost=dict(required=False, type='int'), + level2cost=dict(required=False, type='int'), + bfdstaticen=dict(required=False, type='bool'), + bfdblocken=dict(required=False, type='bool'), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + + module = ISIS_Instance(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_is_is_view.py b/plugins/modules/ce_is_is_view.py new file mode 100644 index 0000000..cdd07da --- /dev/null +++ b/plugins/modules/ce_is_is_view.py @@ -0,0 +1,1955 @@ +#!/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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_is_is_view +author: xuxiaowei0512 (@CloudEngine-Ansible) +short_description: Manages isis view configuration on HUAWEI CloudEngine devices. +description: + - Manages isis process id, creates a isis instance id or deletes a process id + on HUAWEI CloudEngine devices. +options: + coststyle: + description: + - Specifies the cost style. + type: str + choices: ['narrow', 'wide', 'transition', 'ntransition', 'wtransition'] + cost_type: + description: + - Specifies the cost type. + type: str + choices: ['external', 'internal'] + defaultmode: + description: + - Specifies the default mode. + type: str + choices: ['always', 'matchDefault', 'matchAny'] + export_policytype: + description: + - Specifies the default mode. + type: str + choices: ['aclNumOrName', 'ipPrefix', 'routePolicy'] + export_protocol: + description: + - Specifies the export router protocol. + type: str + choices: ['direct', 'ospf', 'isis', 'static', 'rip', 'bgp', 'ospfv3', 'all'] + impotr_leveltype: + description: + - Specifies the export router protocol. + type: str + choices: ['level_1', 'level_2', 'level_1_2'] + islevel: + description: + - Specifies the isis level. + type: str + choices: ['level_1', 'level_2', 'level_1_2'] + level_type: + description: + - Specifies the isis level type. + type: str + choices: ['level_1', 'level_2', 'level_1_2'] + penetration_direct: + description: + - Specifies the penetration direct. + type: str + choices: ['level2-level1', 'level1-level2'] + protocol: + description: + - Specifies the protocol. + type: str + choices: ['direct', 'ospf', 'isis', 'static', 'rip', 'bgp', 'ospfv3', 'all'] + aclnum_or_name: + description: + - Specifies the acl number or name for isis. + type: str + allow_filter: + description: + - Specifies the alow filter or not. + type: bool + allow_up_down: + description: + - Specifies the alow up or down. + type: bool + autocostenable: + description: + - Specifies the alow auto cost enable. + type: bool + autocostenablecompatible: + description: + - Specifies the alow auto cost enable compatible. + type: bool + avoid_learning: + description: + - Specifies the alow avoid learning. + type: bool + bfd_min_tx: + description: + - Specifies the bfd min sent package. + type: int + bfd_min_rx: + description: + - Specifies the bfd min received package. + type: int + bfd_multiplier_num: + description: + - Specifies the bfd multiplier number. + type: int + cost: + description: + - Specifies the bfd cost. + type: int + description: + description: + - Specifies description of isis. + type: str + enablelevel1tolevel2: + description: + - Enable level1 to level2. + type: bool + export_aclnumorname: + description: + - Specifies export acl number or name. + type: str + export_ipprefix: + description: + - Specifies export ip prefix. + type: str + export_processid: + description: + - Specifies export process id. + type: int + export_routepolicyname: + description: + - Specifies export route policy name. + type: str + import_aclnumorname: + description: + - Specifies import acl number or name. + type: str + import_cost: + description: + - Specifies import cost. + type: int + import_ipprefix: + description: + - Specifies import ip prefix. + type: str + import_route_policy: + description: + - Specifies import route policy. + type: str + import_routepolicy_name: + description: + - Specifies import route policy name. + type: str + import_routepolicyname: + description: + - Specifies import route policy name. + type: str + import_tag: + description: + - Specifies import tag. + type: int + inheritcost: + description: + - Enable inherit cost. + type: bool + instance_id: + description: + - Specifies instance id. + type: int + ip_address: + description: + - Specifies ip address. + type: str + ip_prefix_name: + description: + - Specifies ip prefix name. + type: str + max_load: + description: + - Specifies route max load. + type: int + mode_routepolicyname: + description: + - Specifies the mode of route polic yname. + type: str + mode_tag: + description: + - Specifies the tag of mode. + type: int + netentity: + description: + - Specifies the netentity. + type: str + permitibgp: + description: + - Specifies the permitibgp. + type: bool + processid: + description: + - Specifies the process id. + type: int + relaxspfLimit: + description: + - Specifies enable the relax spf limit. + type: bool + route_policy_name: + description: + - Specifies the route policy name. + type: str + stdbandwidth: + description: + - Specifies the std band width. + type: int + stdlevel1cost: + description: + - Specifies the std level1 cost. + type: int + stdlevel2cost: + description: + - Specifies the std level2 cost. + type: int + tag: + description: + - Specifies the isis tag. + type: int + weight: + description: + - Specifies the isis weight. + type: int + preference_value: + description: + - Specifies the preference value. + type: int + state: + description: + - Determines whether the config should be present or not on the device. + default: present + type: str + choices: ['present', 'absent'] +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - This module works with connection C(netconf). +''' + +EXAMPLES = ''' + - name: Set isis description + ce_is_is_view: + instance_id: 3 + description: abcdeggfs + state: present + + - name: Set isis islevel + ce_is_is_view: + instance_id: 3 + islevel: level_1 + state: present + - name: Set isis coststyle + ce_is_is_view: + instance_id: 3 + coststyle: narrow + state: present + + - name: Set isis stdlevel1cost + ce_is_is_view: + instance_id: 3 + stdlevel1cost: 63 + state: present + + - name: set isis stdlevel2cost + ce_is_is_view: + instance_id: 3 + stdlevel2cost: 63 + state: present + + - name: set isis stdbandwidth + ce_is_is_view: + instance_id: 3 + stdbandwidth: 1 + state: present +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "state": "present" + } +existing: + description: k/v pairs of existing configuration + returned: always + type: dict + sample: { + "session": {} + } +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: { + "session": { + "addrType": "IPV4", + "createType": "SESS_STATIC", + "destAddr": null, + "outIfName": "10GE1/0/1", + "sessName": "bfd_l2link", + "srcAddr": null, + "useDefaultIp": "true", + "vrfName": null + } + } +updates: + description: commands sent to the device + returned: always + type: list + sample: [ + "bfd bfd_l2link bind peer-ip default-ip interface 10ge1/0/1" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import sys +import socket +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +CE_NC_GET_ISIS = """ + + + %s + + +""" + +CE_NC_GET_ISIS_INSTANCE = """ + + + %s + + + + + + + + + + + +""" + +CE_NC_GET_ISIS_ENTITY = """ + + + %s + + + + + + + +""" + +CE_NC_CREAT_ISIS_ENTITY = """ + + + %s + + + %s + + + + +""" + +CE_NC_DELATE_ISIS_ENTITY = """ + + + %s + + + %s + + + + +""" + +CE_NC_GET_ISIS_PREFERENCE = """ + + + %s + + + + + + + + + + + + +""" + +CE_NC_MREGE_ISIS_PREFERENCE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_DELETE_ISIS_PREFERENCE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_GET_ISIS_MAXLOAD = """ + + + %s + + + afIpv4 + 0 + + + + + +""" + +CE_NC_MERGE_ISIS_MAXLOAD = """ + + + %s + + + afIpv4 + 0 + %s + + + + +""" + +CE_NC_DELETE_ISIS_MAXLOAD = """ + + + %s + + + afIpv4 + 0 + 32 + + + + +""" + +CE_NC_GET_ISIS_NEXTHOP = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_NEXTHOP = """ + + + %s + + + afIpv4 + 0 + + + %s + %s + + + + + + +""" + +CE_NC_DELETE_ISIS_NEXTHOP = """ + + + %s + + + afIpv4 + 0 + + + %s + 1 + + + + + + +""" + +CE_NC_GET_ISIS_LEAKROUTELEVEL2 = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_LEAKROUTELEVEL2 = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_DELETE_ISIS_LEAKROUTELEVEL2 = """ + + + %s + + + afIpv4 + 0 + + + 0 + + + + false + + + + + + +""" + +CE_NC_GET_ISIS_LEAKROUTELEVEL1 = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_LEAKROUTELEVEL1 = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_DELETE_ISIS_LEAKROUTELEVEL1 = """ + + + %s + + + afIpv4 + 0 + + + 0 + + + + false + false + + + + + + +""" + +CE_NC_GET_ISIS_DEFAULTROUTE = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_DEFAULTROUTE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_DELETE_ISIS_DEFAULTROUTE = """ + + + %s + + + afIpv4 + 0 + + + always + 0 + 0 + level_2 + false + + + + + + +""" + +CE_NC_GET_ISIS_IMPORTROUTE = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_IMPORTROUTE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_GET_ISIS_EXPORTROUTE = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_EXPORTROUTE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_GET_ISIS_IMPORTIPROUTE = """ + + + %s + + + afIpv4 + 0 + + + + + + + + + + + + +""" + +CE_NC_MERGE_ISIS_IMPORTIPROUTE = """ + + + %s + + + afIpv4 + 0 + + + %s + + + + + + +""" + +CE_NC_GET_ISIS_BFDLINK = """ + + + %s + + + afIpv4 + 0 + + + + + + + +""" + +CE_NC_MERGE_ISIS_BFDLINK = """ + + + %s + + + afIpv4 + 0 + %s + + + + +""" + +CE_NC_DELETE_ISIS_BFDLINK = """ + + + %s + + + afIpv4 + 0 + 3 + 3 + 3 + + + + +""" + + +def is_valid_ip_vpn(vpname): + """check ip vpn""" + + if not vpname: + return False + + if vpname == "_public_": + return False + + if len(vpname) < 1 or len(vpname) > 31: + return False + + return True + + +def check_ip_addr(ipaddr): + """check ip address, Supports IPv4 and IPv6""" + + if not ipaddr or '\x00' in ipaddr: + return False + + try: + res = socket.getaddrinfo(ipaddr, 0, socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, socket.AI_NUMERICHOST) + return bool(res) + except socket.gaierror: + err = sys.exc_info()[1] + if err.args[0] == socket.EAI_NONAME: + return False + raise + + return True + + +def check_default_ip(ipaddr): + """check the default multicast IP address""" + + # The value ranges from 224.0.0.107 to 224.0.0.250 + if not check_ip_addr(ipaddr): + return False + + if ipaddr.count(".") != 3: + return False + + ips = ipaddr.split(".") + if ips[0] != "224" or ips[1] != "0" or ips[2] != "0": + return False + + if not ips[3].isdigit() or int(ips[3]) < 107 or int(ips[3]) > 250: + return False + + return True + + +def get_interface_type(interface): + """get the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class ISIS_View(object): + """Manages ISIS Instance""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.instance_id = self.module.params['instance_id'] + self.description = self.module.params['description'] + self.islevel = self.module.params['islevel'] + self.coststyle = self.module.params['coststyle'] + self.relaxspfLimit = self.module.params['relaxspfLimit'] + self.stdlevel1cost = self.module.params['stdlevel1cost'] + self.stdlevel2cost = self.module.params['stdlevel2cost'] + self.stdbandwidth = self.module.params['stdbandwidth'] + self.autocostenable = self.module.params['autocostenable'] + self.autocostenablecompatible = self.module.params['autocostenablecompatible'] + self.netentity = self.module.params['netentity'] + self.preference_value = self.module.params['preference_value'] + self.route_policy_name = self.module.params['route_policy_name'] + self.max_load = self.module.params['max_load'] + self.ip_address = self.module.params['ip_address'] + self.weight = self.module.params['weight'] + self.aclnum_or_name = self.module.params['aclnum_or_name'] + self.ip_prefix_name = self.module.params['ip_prefix_name'] + self.import_routepolicy_name = self.module.params['import_routepolicy_name'] + self.tag = self.module.params['tag'] + self.allow_filter = self.module.params['allow_filter'] + self.allow_up_down = self.module.params['allow_up_down'] + self.penetration_direct = self.module.params['penetration_direct'] + self.enablelevel1tolevel2 = self.module.params['enablelevel1tolevel2'] + self.defaultmode = self.module.params['defaultmode'] + self.mode_routepolicyname = self.module.params['mode_routepolicyname'] + self.cost = self.module.params['cost'] + self.mode_tag = self.module.params['mode_tag'] + self.level_type = self.module.params['level_type'] + self.avoid_learning = self.module.params['avoid_learning'] + self.protocol = self.module.params['protocol'] + self.processid = self.module.params['processid'] + self.cost_type = self.module.params['cost_type'] + self.import_cost = self.module.params['import_cost'] + self.import_tag = self.module.params['import_tag'] + self.impotr_leveltype = self.module.params['impotr_leveltype'] + self.import_route_policy = self.module.params['import_route_policy'] + self.inheritcost = self.module.params['inheritcost'] + self.permitibgp = self.module.params['permitibgp'] + self.avoid_learning = self.module.params['avoid_learning'] + self.export_protocol = self.module.params['export_protocol'] + self.export_policytype = self.module.params['export_policytype'] + self.export_processid = self.module.params['export_processid'] + self.export_aclnumorname = self.module.params['export_aclnumorname'] + self.export_ipprefix = self.module.params['export_ipprefix'] + self.export_routepolicyname = self.module.params['export_routepolicyname'] + self.import_aclnumorname = self.module.params['import_aclnumorname'] + self.import_ipprefix = self.module.params['import_ipprefix'] + self.import_routepolicyname = self.module.params['import_routepolicyname'] + self.bfd_min_rx = self.module.params['bfd_min_rx'] + self.bfd_min_tx = self.module.params['bfd_min_tx'] + self.bfd_multiplier_num = self.module.params['bfd_multiplier_num'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.isis_dict = dict() + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + mutually_exclusive = [["stdlevel1cost", "stdlevel2cost"], + ["aclnum_or_name", "ip_prefix_name", "import_routepolicy_name"], + ["export_aclnumorname", "import_ipprefix", "import_routepolicyname"]] + required_together = [('ip_address', 'weight')] + self.module = AnsibleModule( + argument_spec=self.spec, + mutually_exclusive=mutually_exclusive, + required_together=required_together, + supports_check_mode=True) + + def get_isis_dict(self): + """bfd config dict""" + + isis_dict = dict() + isis_dict["instance"] = dict() + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_INSTANCE % self.instance_id)) + + if self.netentity: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_ENTITY % self.instance_id)) + + if self.route_policy_name or self.preference_value: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_PREFERENCE % self.instance_id)) + if self.max_load: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_MAXLOAD % self.instance_id)) + if self.ip_address: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_NEXTHOP % self.instance_id)) + if self.penetration_direct and self.penetration_direct == "level2-level1": + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_LEAKROUTELEVEL2 % self.instance_id)) + elif self.penetration_direct and self.penetration_direct == "level1-level2": + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_LEAKROUTELEVEL1 % self.instance_id)) + elif self.defaultmode: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_DEFAULTROUTE % self.instance_id)) + elif self.protocol: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_IMPORTROUTE % self.instance_id)) + elif self.export_protocol: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_EXPORTROUTE % self.instance_id)) + elif self.bfd_min_rx or self.bfd_min_tx or self.bfd_multiplier_num: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_BFDLINK % self.instance_id)) + elif self.import_aclnumorname or self.import_ipprefix or self.import_ipprefix: + conf_str = CE_NC_GET_ISIS % ( + (CE_NC_GET_ISIS_IMPORTIPROUTE % self.instance_id)) + xml_str = get_nc_config(self.module, conf_str) + + if "" in xml_str: + return isis_dict + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get bfd global info + if self.netentity: + glb = root.find("isiscomm/isSites/isSite/isNetEntitys/isNetEntity") + elif self.route_policy_name or self.preference_value: + glb = root.find("isiscomm/isSites/isSite//isSiteMTs/isSiteMT/isPreferences/isPreference") + elif self.max_load: + glb = root.find("isiscomm/isSites/isSite/isSiteMTs/isSiteMT") + elif self.ip_address: + glb = root.find("isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isNextHopWeights/isNextHopWeight") + elif self.penetration_direct and self.penetration_direct == "level2-level1": + glb = root.find("isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isLeakRouteLevel2ToLevel1s/isLeakRouteLevel2ToLevel1") + elif self.penetration_direct and self.penetration_direct == "level1-level2": + glb = root.find( + "isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isLeakRouteLevel1ToLevel2s/isLeakRouteLevel1ToLevel2") + elif self.defaultmode: + glb = root.find( + "isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isDefaultRoutes/isDefaultRoute") + elif self.protocol: + glb = root.find( + "isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isImportRoutes/isImportRoute") + elif self.export_protocol: + glb = root.find( + "isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isFilterExports/isFilterExport") + elif self.bfd_min_rx or self.bfd_min_tx or self.bfd_multiplier_num: + glb = root.find( + "isiscomm/isSites/isSite/isSiteMTs/isSiteMT") + elif self.import_aclnumorname or self.import_ipprefix or self.import_ipprefix: + glb = root.find( + "isiscomm/isSites/isSite/isSiteMTs/isSiteMT/isFilterImports/isFilterImport") + else: + glb = root.find("isiscomm/isSites/isSite") + + if glb is not None: + for attr in glb: + isis_dict["instance"][attr.tag] = attr.text + + return isis_dict + + def config_session(self): + """configures bfd session""" + + xml_str = "" + instance = self.isis_dict["instance"] + if not self.instance_id: + return xml_str + xml_str = "%s" % self.instance_id + self.updates_cmd.append("isis %s" % self.instance_id) + cmd_list = list() + + if self.state == "present": + if self.description and self.description != instance.get("description"): + xml_str += "%s" % self.description + self.updates_cmd.append("description %s" % self.description) + + if self.islevel and self.islevel != instance.get("isLevel"): + xml_str += "%s" % self.islevel + self.updates_cmd.append("is-level %s" % self.islevel) + + if self.coststyle: + if self.coststyle != instance.get("costStyle"): + xml_str += "%s" % self.coststyle + self.updates_cmd.append("cost-style %s" % self.coststyle) + if self.relaxspfLimit and instance.get("relaxSpfLimit", "false") == "false": + xml_str += "true" + self.updates_cmd.append("cost-style %s relax-spf-limit" % self.coststyle) + elif not self.relaxspfLimit and instance.get("relaxSpfLimit", "false") == "true": + xml_str += "false" + self.updates_cmd.append("cost-style %s" % self.coststyle) + + if self.stdlevel1cost and str(self.stdlevel1cost) != instance.get("stdLevel1Cost"): + xml_str += "%s" % self.stdlevel1cost + self.updates_cmd.append("circuit-cost %s level-1" % self.stdlevel1cost) + + if self.stdlevel2cost and str(self.stdlevel2cost) != instance.get("stdLevel2Cost"): + xml_str += "%s" % self.stdlevel2cost + self.updates_cmd.append("circuit-cost %s level-2" % self.stdlevel2cost) + + if self.stdbandwidth and str(self.stdbandwidth) != instance.get("stdbandwidth"): + xml_str += "%s" % self.stdbandwidth + self.updates_cmd.append("bandwidth-reference %s" % self.stdbandwidth) + + if self.netentity and self.netentity != instance.get("netEntity"): + xml_str = CE_NC_CREAT_ISIS_ENTITY % (self.instance_id, self.netentity) + self.updates_cmd.append("network-entity %s" % self.netentity) + + if self.preference_value or self.route_policy_name: + xml_str = "" + cmd_session = "preference" + if self.preference_value and str(self.preference_value) != instance.get("preferenceValue"): + xml_str = "%s" % self.preference_value + cmd_session += " %s" % self.preference_value + if self.route_policy_name and self.route_policy_name != instance.get("routePolicyName"): + xml_str += "%s" % self.route_policy_name + cmd_session += " route-policy %s" % self.route_policy_name + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + xml_str = CE_NC_MREGE_ISIS_PREFERENCE % (self.instance_id, xml_str) + + if self.max_load and str(self.max_load) != instance.get("maxLoadBalancing"): + xml_str = CE_NC_MERGE_ISIS_MAXLOAD % (self.instance_id, self.max_load) + self.updates_cmd.append("maximum load-balancing %s" % self.max_load) + + if self.ip_address: + xml_str = CE_NC_MERGE_ISIS_NEXTHOP % (self.instance_id, self.ip_address, self.weight) + self.updates_cmd.append("nexthop %s weight %s" % (self.ip_address, self.weight)) + + if self.penetration_direct: + xml_str = "" + if self.penetration_direct == "level2-level1": + cmd_session = "import-route isis level-2 into level-1" + elif self.penetration_direct == "level1-level2": + cmd_session = "import-route isis level-1 into level-2" + if self.aclnum_or_name: + xml_str = "%s" % self.aclnum_or_name + xml_str += "aclNumOrName" + if isinstance(self.aclnum_or_name, int): + cmd_session += " filter-policy %s" % self.aclnum_or_name + elif isinstance(self.aclnum_or_name, str): + cmd_session += " filter-policy acl-name %s" % self.aclnum_or_name + if self.ip_prefix_name: + xml_str = "%s" % self.ip_prefix_name + xml_str += "ipPrefix" + cmd_session += " filter-policy ip-prefix %s" % self.ip_prefix_name + if self.import_routepolicy_name: + xml_str = "%s" % self.import_routepolicy_name + xml_str += "routePolicy" + cmd_session += " filter-policy route-policy %s" % self.import_routepolicy_name + if self.tag: + xml_str += "%s" % self.tag + cmd_session += " tag %s" % self.tag + if self.allow_filter or self.allow_up_down: + cmd_session += " direct" + if self.allow_filter: + xml_str += "true" + cmd_session += " allow-filter-policy" + if self.allow_up_down: + xml_str += "true" + cmd_session += " allow-up-down-bit" + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + if self.enablelevel1tolevel2: + xml_str += "true" + self.updates_cmd.append("undo import-route isis level-1 into level-2 disable") + + if self.defaultmode: + cmd_session = "default-route-advertise" + if self.defaultmode == "always": + xml_str = "always" + cmd_session += " always" + elif self.defaultmode == "matchDefault": + xml_str = "matchDefault" + cmd_session += " match default" + elif self.defaultmode == "matchAny": + xml_str = "matchAny" + xml_str += "routePolicy" + xml_str += "%s" % self.mode_routepolicyname + cmd_session += " route-policy %s" % self.mode_routepolicyname + if self.cost is not None: + xml_str += "%s" % self.cost + cmd_session += " cost %s" % self.cost + if self.mode_tag: + xml_str += "%s" % self.mode_tag + cmd_session += " tag %s" % self.mode_tag + if self.level_type: + if self.level_type == "level_1": + xml_str += "level_1" + cmd_session += " level-1" + elif self.level_type == "level_2": + xml_str += "level_2" + cmd_session += " level-2" + elif self.level_type == "level_1_2": + xml_str += "level_1_2" + cmd_session += " level-1-2" + if self.avoid_learning: + xml_str += "true" + cmd_session += " avoid-learning" + elif not self.avoid_learning: + xml_str += "false" + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + + if self.protocol: + cmd_session = "import-route" + if self.protocol == "rip": + xml_str = "rip" + cmd_session += " rip" + elif self.protocol == "isis": + xml_str = "isis" + cmd_session += " isis" + elif self.protocol == "ospf": + xml_str = "ospf" + cmd_session += " ospf" + elif self.protocol == "static": + xml_str = "static" + cmd_session += " static" + elif self.protocol == "direct": + xml_str = "direct" + cmd_session += " direct" + elif self.protocol == "bgp": + xml_str = "bgp" + cmd_session += " bgp" + if self.permitibgp: + xml_str += "true" + cmd_session += " permit-ibgp" + if self.protocol == "rip" or self.protocol == "isis" or self.protocol == "ospf": + xml_str += "%s" % self.processid + cmd_session += " %s" % self.processid + if self.inheritcost: + xml_str += "%s" % self.inheritcost + cmd_session += " inherit-cost" + if self.cost_type: + if self.cost_type == "external": + xml_str += "external" + cmd_session += " cost-type external" + elif self.cost_type == "internal": + xml_str += "internal" + cmd_session += " cost-type internal" + if self.import_cost: + xml_str += "%s" % self.import_cost + cmd_session += " cost %s" % self.import_cost + if self.import_tag: + xml_str += "%s" % self.import_tag + cmd_session += " tag %s" % self.import_tag + if self.import_route_policy: + xml_str += "routePolicy" + xml_str += "%s" % self.import_route_policy + cmd_session += " route-policy %s" % self.import_route_policy + if self.impotr_leveltype: + if self.impotr_leveltype == "level_1": + cmd_session += " level-1" + elif self.impotr_leveltype == "level_2": + cmd_session += " level-2" + elif self.impotr_leveltype == "level_1_2": + cmd_session += " level-1-2" + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + + if self.bfd_min_rx or self.bfd_min_tx or self.bfd_multiplier_num: + xml_str = "" + self.updates_cmd.append("bfd all-interfaces enable") + cmd_session = "bfd all-interfaces" + if self.bfd_min_rx: + xml_str += "%s" % self.bfd_min_rx + cmd_session += " min-rx-interval %s" % self.bfd_min_rx + if self.bfd_min_tx: + xml_str += "%s" % self.bfd_min_tx + cmd_session += " min-tx-interval %s" % self.bfd_min_tx + if self.bfd_multiplier_num: + xml_str += "%s" % self.bfd_multiplier_num + cmd_session += " detect-multiplier %s" % self.bfd_multiplier_num + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + + if self.export_protocol: + cmd_session = "filter-policy" + if self.export_aclnumorname: + xml_str = "aclNumOrName" + xml_str += "%s" % self.export_aclnumorname + if isinstance(self.export_aclnumorname, int): + cmd_session += " %s" % self.export_aclnumorname + elif isinstance(self.export_aclnumorname, str): + cmd_session += " acl-name %s" % self.export_aclnumorname + if self.export_ipprefix: + xml_str = "ipPrefix" + xml_str += "%s" % self.export_ipprefix + cmd_session += " ip-prefix %s" % self.export_ipprefix + if self.export_routepolicyname: + xml_str = "routePolicy" + xml_str += "%s" % self.export_routepolicyname + cmd_session += " route-policy %s" % self.export_routepolicyname + xml_str += "%s" % self.export_protocol + cmd_session += " export %s" % self.export_protocol + if self.export_processid is not None: + xml_str += "%s" % self.export_processid + cmd_session += " %s" % self.export_processid + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + + if self.import_ipprefix or self.import_aclnumorname or self.import_routepolicyname: + cmd_session = "filter-policy" + if self.import_aclnumorname: + xml_str = "aclNumOrName" + xml_str += "%s" % self.import_aclnumorname + if isinstance(self.import_aclnumorname, int): + cmd_session += " %s" % self.import_aclnumorname + elif isinstance(self.import_aclnumorname, str): + cmd_session += " acl-name %s" % self.import_aclnumorname + if self.import_ipprefix: + xml_str = "ipPrefix" + xml_str += "%s" % self.import_ipprefix + cmd_session += " ip-prefix %s" % self.import_ipprefix + if self.import_routepolicyname: + xml_str = "routePolicy" + xml_str += "%s" % self.import_routepolicyname + cmd_session += " route-policy %s" % self.import_routepolicyname + cmd_session += "import" + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + else: + # absent + if self.description and self.description == instance.get("description"): + xml_str += "%s" % self.description + self.updates_cmd.append("undo description") + + if self.islevel and self.islevel == instance.get("isLevel"): + xml_str += "level_1_2" + self.updates_cmd.append("undo is-level") + + if self.coststyle and self.coststyle == instance.get("costStyle"): + xml_str += "%s" % ("narrow") + xml_str += "false" + self.updates_cmd.append("undo cost-style") + + if self.stdlevel1cost and str(self.stdlevel1cost) == instance.get("stdLevel1Cost"): + xml_str += "%s" % self.stdlevel1cost + self.updates_cmd.append("undo circuit-cost %s level-1" % self.stdlevel1cost) + + if self.stdlevel2cost and str(self.stdlevel2cost) == instance.get("stdLevel2Cost"): + xml_str += "%s" % self.stdlevel2cost + self.updates_cmd.append("undo circuit-cost %s level-2" % self.stdlevel2cost) + + if self.stdbandwidth and str(self.stdbandwidth) == instance.get("stdbandwidth"): + xml_str += "100" + self.updates_cmd.append("undo bandwidth-reference") + + if self.netentity and self.netentity == instance.get("netEntity"): + xml_str = CE_NC_DELATE_ISIS_ENTITY % (self.instance_id, self.netentity) + self.updates_cmd.append("undo network-entity %s" % self.netentity) + + if self.preference_value or self.route_policy_name: + xml_str = "" + if self.preference_value and str(self.preference_value) == instance.get("preferenceValue"): + xml_str = "%s" % self.preference_value + if self.route_policy_name and self.route_policy_name == instance.get("routePolicyName"): + xml_str += "%s" % self.route_policy_name + self.updates_cmd.append("undo preference") + elif not self.preference_value and self.route_policy_name and self.route_policy_name == instance.get("routePolicyName"): + xml_str = "%s" % self.route_policy_name + self.updates_cmd.append("undo preference") + xml_str = CE_NC_DELETE_ISIS_PREFERENCE % (self.instance_id, xml_str) + + if self.max_load and str(self.max_load) == instance.get("maxLoadBalancing"): + xml_str = CE_NC_DELETE_ISIS_MAXLOAD % self.instance_id + self.updates_cmd.append("undo maximum load-balancing") + + if self.ip_address: + xml_str = CE_NC_DELETE_ISIS_NEXTHOP % (self.instance_id, self.ip_address) + self.updates_cmd.append("undo nexthop %s" % self.ip_address) + + if self.penetration_direct: + if self.penetration_direct == "level2-level1": + self.updates_cmd.append("undo import-route isis level-2 into level-1") + elif self.penetration_direct == "level1-level2": + self.updates_cmd.append("undo import-route isis level-1 into level-2") + self.updates_cmd.append("import-route isis level-1 into level-2 disable") + + if self.bfd_min_rx or self.bfd_min_tx or self.bfd_multiplier_num is not None: + xml_str = CE_NC_DELETE_ISIS_BFDLINK % self.instance_id + self.updates_cmd.append("undo bfd all-interfaces enable") + cmd_session = "undo bfd all-interfaces" + if self.bfd_min_rx: + cmd_session += " min-rx-interval %s" % self.bfd_min_rx + if self.bfd_min_tx: + cmd_session += " min-tx-interval %s" % self.bfd_min_tx + if self.bfd_multiplier_num: + cmd_session += " detect-multiplier %s" % self.bfd_multiplier_num + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + + if self.defaultmode: + xml_str = CE_NC_DELETE_ISIS_DEFAULTROUTE % self.instance_id + self.updates_cmd.append("undo default-route-advertise") + + if self.protocol: + if self.protocol == "rip" or self.protocol == "isis" or self.protocol == "ospf": + self.updates_cmd.append("undo import-route %s %s" % (self.protocol, self.processid)) + else: + self.updates_cmd.append("undo import-route %s" % self.protocol) + + if self.export_protocol: + cmd_session = "undo filter-policy" + if self.export_aclnumorname: + if isinstance(self.export_aclnumorname, int): + cmd_session += " %s" % self.export_aclnumorname + elif isinstance(self.export_aclnumorname, str): + cmd_session += " acl-name %s" % self.export_aclnumorname + if self.export_ipprefix: + cmd_session += " ip-prefix %s" % self.export_ipprefix + if self.export_routepolicyname: + cmd_session += " route-policy %s" % self.export_routepolicyname + cmd_session += " export %s" % self.export_protocol + if self.export_processid is not None: + cmd_session += " %s" % self.export_processid + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + if self.import_ipprefix or self.import_aclnumorname or self.import_routepolicyname: + cmd_session = "undo filter-policy" + if self.import_aclnumorname: + if isinstance(self.import_aclnumorname, int): + cmd_session += " %s" % self.import_aclnumorname + elif isinstance(self.import_aclnumorname, str): + cmd_session += " acl-name %s" % self.import_aclnumorname + if self.import_ipprefix: + cmd_session += " ip-prefix %s" % self.import_ipprefix + if self.import_routepolicyname: + cmd_session += " route-policy %s" % self.import_routepolicyname + cmd_session += " import" + cmd_list.insert(0, cmd_session) + self.updates_cmd.extend(cmd_list) + + if self.autocostenable and instance.get("stdAutoCostEnable", "false") == "false": + xml_str += "true" + self.updates_cmd.append("auto-cost enable") + elif not self.autocostenable and instance.get("stdAutoCostEnable", "false") == "true": + xml_str += "false" + xml_str += "false" + self.updates_cmd.append("undo auto-cost enable") + + if self.autocostenable: + if self.autocostenablecompatible and instance.get("stdAutoCostEnableCompatible", "false") == "false": + xml_str += "true" + self.updates_cmd.append("auto-cost enable compatible") + elif not self.autocostenablecompatible and instance.get("stdAutoCostEnableCompatible", "false") == "true": + xml_str += "false" + self.updates_cmd.append("auto-cost enable") + + if self.state == "present": + if self.netentity or self.preference_value or self.route_policy_name or self.max_load or self.ip_address: + return xml_str + elif self.penetration_direct: + if self.penetration_direct == "level2-level1": + return CE_NC_MERGE_ISIS_LEAKROUTELEVEL2 % (self.instance_id, xml_str) + elif self.penetration_direct == "level1-level2": + return CE_NC_MERGE_ISIS_LEAKROUTELEVEL1 % (self.instance_id, xml_str) + elif self.defaultmode: + return CE_NC_MERGE_ISIS_DEFAULTROUTE % (self.instance_id, xml_str) + elif self.protocol: + return CE_NC_MERGE_ISIS_IMPORTROUTE % (self.instance_id, xml_str) + elif self.export_protocol: + return CE_NC_MERGE_ISIS_EXPORTROUTE % (self.instance_id, xml_str) + elif self.import_routepolicyname or self.import_aclnumorname or self.import_ipprefix: + return CE_NC_MERGE_ISIS_IMPORTIPROUTE % (self.instance_id, xml_str) + elif self.bfd_min_rx or self.bfd_min_tx or self.bfd_multiplier_num: + return CE_NC_MERGE_ISIS_BFDLINK % (self.instance_id, xml_str) + else: + return '' + xml_str + '' + else: + if self.netentity or self.preference_value or self.route_policy_name or self.max_load \ + or self.ip_address or self.defaultmode or self.bfd_min_rx or self.bfd_min_tx or self.bfd_multiplier_num is not None: + return xml_str + else: + return '' + xml_str + '' + + def netconf_load_config(self, xml_str): + """load bfd config by netconf""" + + if not xml_str: + return + if xml_str == "%s" % self.instance_id: + pass + else: + xml_cfg = """ + + + %s + + """ % xml_str + set_nc_config(self.module, xml_cfg) + self.changed = True + + def check_params(self): + """Check all input params""" + levelcost = 16777215 + if not self.instance_id: + self.module.fail_json(msg="Error: Missing required arguments: instance_id.") + + if self.instance_id: + if self.instance_id < 1 or self.instance_id > 4294967295: + self.module.fail_json(msg="Error: Instance id is not ranges from 1 to 4294967295.") + + # check description + if self.description: + if len(self.description) < 1 or len(self.description) > 80: + self.module.fail_json(msg="Error: description is invalid.") + + # + if self.stdbandwidth: + if self.stdbandwidth < 1 or self.stdbandwidth > 2147483648: + self.module.fail_json(msg="Error: stdbandwidth is not ranges from 1 to 2147483648.") + + if self.relaxspfLimit is not None and not self.coststyle: + self.module.fail_json(msg="Error: relaxspfLimit must set after coststyle.") + + if self.coststyle: + if self.coststyle != "wide" and self.coststyle != "wtransition": + levelcost = 63 + else: + levelcost = 16777215 + if self.stdlevel1cost: + if self.stdlevel1cost < 1 or self.stdlevel1cost > levelcost: + self.module.fail_json(msg="Error: stdlevel1cost is not ranges from 1 to %s." % levelcost) + + if self.stdlevel2cost: + if self.stdlevel2cost < 1 or self.stdlevel2cost > levelcost: + self.module.fail_json(msg="Error: stdlevel2cost is not ranges from 1 to %s." % levelcost) + + if self.coststyle: + if self.coststyle != "ntransition" and self.coststyle != "transition": + if self.relaxspfLimit: + self.module.fail_json(msg="Error: relaxspfLimit can not be set while the coststyle is not ntransition or transition") + + if self.autocostenablecompatible: + if not self.autocostenable: + self.module.fail_json(msg="Error: you shoule enable the autocostenable first.") + + if self.preference_value: + if self.preference_value < 1 or self.preference_value > 255: + self.module.fail_json(msg="Error: preference_value is not ranges from 1 to 255.") + + if self.route_policy_name: + if len(self.route_policy_name) < 1 or len(self.route_policy_name) > 200: + self.module.fail_json(msg="Error: route_policy_name is invalid.") + + if self.max_load: + if self.max_load < 1 or self.max_load > 32: + self.module.fail_json(msg="Error: max_load is not ranges from 1 to 32.") + + if self.weight: + if self.weight < 1 or self.weight > 254: + self.module.fail_json(msg="Error: weight is not ranges from 1 to 254.") + + if self.aclnum_or_name: + if isinstance(self.aclnum_or_name, int): + if self.aclnum_or_name < 2000 or self.aclnum_or_name > 2999: + self.module.fail_json(msg="Error: acl_num is not ranges from 2000 to 2999.") + elif isinstance(self.aclnum_or_name, str): + if len(self.aclnum_or_name) < 1 or len(self.aclnum_or_name) > 32: + self.module.fail_json(msg="Error: acl_name is invalid.") + if self.ip_prefix_name: + if len(self.ip_prefix_name) < 1 or len(self.ip_prefix_name) > 169: + self.module.fail_json(msg="Error: ip_prefix_name is invalid.") + if self.import_routepolicy_name: + if len(self.import_routepolicy_name) < 1 or len(self.import_routepolicy_name) > 200: + self.module.fail_json(msg="Error: import_routepolicy_name is invalid.") + if self.tag: + if self.tag < 1 or self.tag > 4294967295: + self.module.fail_json(msg="Error: tag is not ranges from 1 to 4294967295.") + + if self.mode_routepolicyname: + if len(self.mode_routepolicyname) < 1 or len(self.mode_routepolicyname) > 200: + self.module.fail_json(msg="Error: mode_routepolicyname is invalid.") + if self.cost is not None: + if self.cost < 0 or self.cost > 4261412864: + self.module.fail_json(msg="Error: cost is not ranges from 0 to 4261412864.") + if self.mode_tag: + if self.mode_tag < 1 or self.mode_tag > 4294967295: + self.module.fail_json(msg="Error: mode_tag is not ranges from 1 to 4294967295.") + + if self.processid is not None: + if self.processid < 0 or self.processid > 4294967295: + self.module.fail_json(msg="Error: processid is not ranges from 0 to 4294967295.") + + if self.import_cost is not None: + if self.import_cost < 0 or self.import_cost > 4261412864: + self.module.fail_json(msg="Error: import_cost is not ranges from 0 to 4261412864.") + + if self.import_tag: + if self.import_tag < 1 or self.import_tag > 4294967295: + self.module.fail_json(msg="Error: import_tag is not ranges from 1 to 4294967295.") + + if self.export_aclnumorname: + if isinstance(self.export_aclnumorname, int): + if self.export_aclnumorname < 2000 or self.export_aclnumorname > 2999: + self.module.fail_json(msg="Error: acl_num is not ranges from 2000 to 2999.") + elif isinstance(self.export_aclnumorname, str): + if len(self.export_aclnumorname) < 1 or len(self.export_aclnumorname) > 32: + self.module.fail_json(msg="Error: acl_name is invalid.") + + if self.export_processid: + if self.export_processid < 1 or self.export_processid > 4294967295: + self.module.fail_json(msg="Error: export_processid is not ranges from 1 to 4294967295.") + + if self.export_ipprefix: + if len(self.export_ipprefix) < 1 or len(self.export_ipprefix) > 169: + self.module.fail_json(msg="Error: export_ipprefix is invalid.") + + if self.export_routepolicyname: + if len(self.export_routepolicyname) < 1 or len(self.export_routepolicyname) > 200: + self.module.fail_json(msg="Error: export_routepolicyname is invalid.") + + if self.bfd_min_rx: + if self.bfd_min_rx < 50 or self.bfd_min_rx > 1000: + self.module.fail_json(msg="Error: bfd_min_rx is not ranges from 50 to 1000.") + + if self.bfd_min_tx: + if self.bfd_min_tx < 50 or self.bfd_min_tx > 1000: + self.module.fail_json(msg="Error: bfd_min_tx is not ranges from 50 to 1000.") + + if self.bfd_multiplier_num: + if self.bfd_multiplier_num < 3 or self.bfd_multiplier_num > 50: + self.module.fail_json(msg="Error: bfd_multiplier_num is not ranges from 3 to 50.") + + if self.import_routepolicyname: + if len(self.import_routepolicyname) < 1 or len(self.import_routepolicyname) > 200: + self.module.fail_json(msg="Error: import_routepolicyname is invalid.") + + if self.import_aclnumorname: + if isinstance(self.import_aclnumorname, int): + if self.import_aclnumorname < 2000 or self.import_aclnumorname > 2999: + self.module.fail_json(msg="Error: acl_num is not ranges from 2000 to 2999.") + elif isinstance(self.import_aclnumorname, str): + if len(self.import_aclnumorname) < 1 or len(self.import_aclnumorname) > 32: + self.module.fail_json(msg="Error: acl_name is invalid.") + + def get_proposed(self): + """get proposed info""" + # base config + self.proposed["instance_id"] = self.instance_id + self.proposed["description"] = self.description + self.proposed["islevel"] = self.islevel + self.proposed["coststyle"] = self.coststyle + self.proposed["relaxspfLimit"] = self.relaxspfLimit + self.proposed["stdlevel1cost"] = self.stdlevel1cost + self.proposed["stdlevel2cost"] = self.stdlevel2cost + self.proposed["stdbandwidth"] = self.stdbandwidth + self.proposed["autocostenable"] = self.autocostenable + self.proposed["autocostenablecompatible"] = self.autocostenablecompatible + self.proposed["netentity"] = self.netentity + self.proposed["preference_value"] = self.preference_value + self.proposed["route_policy_name"] = self.route_policy_name + self.proposed["max_load"] = self.max_load + self.proposed["ip_address"] = self.ip_address + self.proposed["weight"] = self.weight + self.proposed["penetration_direct"] = self.penetration_direct + self.proposed["aclnum_or_name"] = self.aclnum_or_name + self.proposed["ip_prefix_name"] = self.ip_prefix_name + self.proposed["import_routepolicy_name"] = self.import_routepolicy_name + self.proposed["tag"] = self.tag + self.proposed["allow_filter"] = self.allow_filter + self.proposed["allow_up_down"] = self.allow_up_down + self.proposed["enablelevel1tolevel2"] = self.enablelevel1tolevel2 + self.proposed["protocol"] = self.protocol + self.proposed["processid"] = self.processid + self.proposed["cost_type"] = self.cost_type + self.proposed["import_cost"] = self.import_cost + self.proposed["import_tag"] = self.import_tag + self.proposed["import_route_policy"] = self.import_route_policy + self.proposed["impotr_leveltype"] = self.impotr_leveltype + self.proposed["inheritcost"] = self.inheritcost + self.proposed["permitibgp"] = self.permitibgp + self.proposed["export_protocol"] = self.export_protocol + self.proposed["export_policytype"] = self.export_policytype + self.proposed["export_processid"] = self.export_processid + self.proposed["export_aclnumorname"] = self.export_aclnumorname + self.proposed["export_ipprefix"] = self.export_ipprefix + self.proposed["export_routepolicyname"] = self.export_routepolicyname + self.proposed["import_aclnumorname"] = self.import_aclnumorname + self.proposed["import_ipprefix"] = self.import_ipprefix + self.proposed["import_routepolicyname"] = self.import_routepolicyname + self.proposed["bfd_min_rx"] = self.bfd_min_rx + self.proposed["bfd_min_tx"] = self.bfd_min_tx + self.proposed["bfd_multiplier_num"] = self.bfd_multiplier_num + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.isis_dict: + self.existing["instance"] = None + else: + self.existing["instance"] = self.isis_dict.get("instance") + + def get_end_state(self): + """get end state info""" + + isis_dict = self.get_isis_dict() + if not isis_dict: + self.end_state["instance"] = None + else: + self.end_state["instance"] = isis_dict.get("instance") + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.isis_dict = self.get_isis_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.instance_id: + xml_str += self.config_session() + # update to device + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + instance_id=dict(required=True, type='int'), + description=dict(required=False, type='str'), + islevel=dict(required=False, type='str', choices=['level_1', 'level_2', 'level_1_2']), + coststyle=dict(required=False, type='str', choices=['narrow', 'wide', 'transition', 'ntransition', 'wtransition']), + relaxspfLimit=dict(required=False, type='bool'), + stdlevel1cost=dict(required=False, type='int'), + stdlevel2cost=dict(required=False, type='int'), + stdbandwidth=dict(required=False, type='int'), + autocostenable=dict(required=False, type='bool'), + autocostenablecompatible=dict(required=False, type='bool'), + netentity=dict(required=False, type='str'), + preference_value=dict(required=False, type='int'), + route_policy_name=dict(required=False, type='str'), + max_load=dict(required=False, type='int'), + ip_address=dict(required=False, type='str'), + weight=dict(required=False, type='int'), + penetration_direct=dict(required=False, type='str', choices=['level2-level1', 'level1-level2']), + aclnum_or_name=dict(required=False, type='str'), + ip_prefix_name=dict(required=False, type='str'), + import_routepolicy_name=dict(required=False, type='str'), + tag=dict(required=False, type='int'), + allow_filter=dict(required=False, type='bool'), + allow_up_down=dict(required=False, type='bool'), + enablelevel1tolevel2=dict(required=False, type='bool'), + defaultmode=dict(required=False, type='str', choices=['always', 'matchDefault', 'matchAny']), + mode_routepolicyname=dict(required=False, type='str'), + cost=dict(required=False, type='int'), + mode_tag=dict(required=False, type='int'), + level_type=dict(required=False, type='str', choices=['level_1', 'level_2', 'level_1_2']), + avoid_learning=dict(required=False, type='bool'), + protocol=dict(required=False, type='str', choices=['direct', 'ospf', 'isis', 'static', 'rip', 'bgp', 'ospfv3', 'all']), + processid=dict(required=False, type='int'), + cost_type=dict(required=False, type='str', choices=['external', 'internal']), + import_cost=dict(required=False, type='int'), + import_tag=dict(required=False, type='int'), + import_route_policy=dict(required=False, type='str'), + impotr_leveltype=dict(required=False, type='str', choices=['level_1', 'level_2', 'level_1_2']), + inheritcost=dict(required=False, type='bool'), + permitibgp=dict(required=False, type='bool'), + export_protocol=dict(required=False, type='str', choices=['direct', 'ospf', 'isis', 'static', 'rip', 'bgp', 'ospfv3', 'all']), + export_policytype=dict(required=False, type='str', choices=['aclNumOrName', 'ipPrefix', 'routePolicy']), + export_processid=dict(required=False, type='int'), + export_aclnumorname=dict(required=False, type='str'), + export_ipprefix=dict(required=False, type='str'), + export_routepolicyname=dict(required=False, type='str'), + import_aclnumorname=dict(required=False, type='str'), + import_ipprefix=dict(required=False, type='str'), + import_routepolicyname=dict(required=False, type='str'), + bfd_min_rx=dict(required=False, type='int'), + bfd_min_tx=dict(required=False, type='int'), + bfd_multiplier_num=dict(required=False, type='int'), + state=dict(required=False, default='present', choices=['present', 'absent']) + ) + + module = ISIS_View(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_lacp.py b/plugins/modules/ce_lacp.py new file mode 100644 index 0000000..ee74ff9 --- /dev/null +++ b/plugins/modules/ce_lacp.py @@ -0,0 +1,487 @@ +#!/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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: ce_lacp +short_description: Manages Eth-Trunk interfaces on HUAWEI CloudEngine switches +description: + - Manages Eth-Trunk specific configuration parameters on HUAWEI CloudEngine switches. +author: xuxiaowei0512 (@CloudEngine-Ansible) +notes: + - C(state=absent) removes the Eth-Trunk config and interface if it already exists. If members to be removed are not explicitly + passed, all existing members (if any), are removed, and Eth-Trunk removed. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + trunk_id: + description: + - Eth-Trunk interface number. + The value is an integer. + The value range depends on the assign forward eth-trunk mode command. + When 256 is specified, the value ranges from 0 to 255. + When 512 is specified, the value ranges from 0 to 511. + When 1024 is specified, the value ranges from 0 to 1023. + type: int + mode: + description: + - Specifies the working mode of an Eth-Trunk interface. + default: null + choices: ['Manual','Dynamic','Static'] + type: str + preempt_enable: + description: + - Specifies lacp preempt enable of Eth-Trunk lacp. + The value is an boolean 'true' or 'false'. + type: bool + state_flapping: + description: + - Lacp dampening state-flapping. + type: bool + port_id_extension_enable: + description: + - Enable the function of extending the LACP negotiation port number. + type: bool + unexpected_mac_disable: + description: + - Lacp dampening unexpected-mac disable. + type: bool + system_id: + description: + - Link Aggregation Control Protocol System ID,interface Eth-Trunk View. + - Formate 'X-X-X',X is hex(a,aa,aaa, or aaaa) + type: str + timeout_type: + description: + - Lacp timeout type,that may be 'Fast' or 'Slow'. + choices: ['Slow', 'Fast'] + type: str + fast_timeout: + description: + - When lacp timeout type is 'Fast', user-defined time can be a number(3~90). + type: int + mixed_rate_link_enable: + description: + - Value of max active linknumber. + type: bool + preempt_delay: + description: + - Value of preemption delay time. + type: int + collector_delay: + description: + - Value of delay time in units of 10 microseconds. + type: int + max_active_linknumber: + description: + - Max active linknumber in link aggregation group. + type: int + select: + description: + - Select priority or speed to preempt. + choices: ['Speed', 'Prority'] + type: str + priority: + description: + - The priority of eth-trunk member interface. + type: int + global_priority: + description: + - Configure lacp priority on system-view. + type: int + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] + type: str +''' +EXAMPLES = r''' + - name: Ensure Eth-Trunk100 is created, and set to mode lacp-static + ce_lacp: + trunk_id: 100 + mode: 'lacp-static' + state: present + - name: Ensure Eth-Trunk100 is created, add two members, and set global priority to 1231 + ce_lacp: + trunk_id: 100 + global_priority: 1231 + state: present + - name: Ensure Eth-Trunk100 is created, and set mode to Dynamic and configure other options + ce_lacp: + trunk_id: 100 + mode: Dynamic + preempt_enable: True, + state_flapping: True, + port_id_extension_enable: True, + unexpected_mac_disable: True, + timeout_type: Fast, + fast_timeout: 123, + mixed_rate_link_enable: True, + preempt_delay: 23, + collector_delay: 33, + state: present +''' + +RETURN = r''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"trunk_id": "100", "members": ['10GE1/0/24','10GE1/0/25'], "mode": "lacp-static"} +existing: + description: k/v pairs of existing Eth-Trunk + returned: always + type: dict + sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [ + {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}], + "min_links": "1", "mode": "manual"} +end_state: + description: k/v pairs of Eth-Trunk info after module execution + returned: always + type: dict + sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [ + {"memberIfName": "10GE1/0/24", "memberIfState": "Down"}, + {"memberIfName": "10GE1/0/25", "memberIfState": "Down"}], + "min_links": "1", "mode": "lacp-static"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface Eth-Trunk 100", + "mode lacp-static", + "interface 10GE1/0/25", + "eth-trunk 100"] +''' + +import xml.etree.ElementTree as ET +import re +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +LACP = {'trunk_id': 'ifName', + 'mode': 'workMode', + 'preempt_enable': 'isSupportPrmpt', + 'state_flapping': 'dampStaFlapEn', + 'port_id_extension_enable': 'trunkPortIdExt', + 'unexpected_mac_disable': 'dampUnexpMacEn', + 'system_id': 'trunkSysMac', + 'timeout_type': 'rcvTimeoutType', + 'fast_timeout': 'fastTimeoutUserDefinedValue', + 'mixed_rate_link_enable': 'mixRateEnable', + 'preempt_delay': 'promptDelay', + 'collector_delay': 'collectMaxDelay', + 'max_active_linknumber': 'maxActiveNum', + 'select': 'selectPortStd', + 'weight': 'weight', + 'priority': 'portPriority', + 'global_priority': 'priority' + } + + +def has_element(parent, xpath): + """get or create a element by xpath""" + ele = parent.find('./' + xpath) + if ele is not None: + return ele + ele = parent + lpath = xpath.split('/') + for p in lpath: + e = parent.find('.//' + p) + if e is None: + e = ET.SubElement(ele, p) + ele = e + return ele + + +def bulid_xml(kwargs, operation='get'): + """create a xml tree by dictionary with operation,get,merge and delete""" + attrib = {'xmlns': "http://www.huawei.com/netconf/vrp", + 'content-version': "1.0", 'format-version': "1.0"} + + root = ET.Element('ifmtrunk') + for key in kwargs.keys(): + if key in ('global_priority',): + xpath = 'lacpSysInfo' + elif key in ('priority',): + xpath = 'TrunkIfs/TrunkIf/TrunkMemberIfs/TrunkMemberIf/lacpPortInfo/lacpPort' + elif key in ['preempt_enable', 'timeout_type', 'fast_timeout', 'select', 'preempt_delay', + 'max_active_linknumber', 'collector_delay', 'mixed_rate_link_enable', + 'state_flapping', 'unexpected_mac_disable', 'system_id', + 'port_id_extension_enable']: + xpath = 'TrunkIfs/TrunkIf/lacpTrunk' + elif key in ('trunk_id', 'mode'): + xpath = 'TrunkIfs/TrunkIf' + if xpath != '': + parent = has_element(root, xpath) + element = ET.SubElement(parent, LACP[key]) + if operation == 'merge': + parent.attrib = dict(operation=operation) + element.text = str(kwargs[key]) + if key == 'mode': + element.text = str(kwargs[key]) + if key == 'trunk_id': + element.text = 'Eth-Trunk' + str(kwargs[key]) + root.attrib = attrib + config = ET.tostring(root) + if operation == 'merge' or operation == 'delete': + return '%s' % to_native(config) + return '%s' % to_native(config) + + +def check_param(kwargs): + """check args list,the boolean or list values cloud not be checked,because they are limit by args list in main""" + + for key in kwargs: + if kwargs[key] is None: + continue + if key == 'trunk_id': + value = int(kwargs[key]) + # maximal value is 1024,although the value is limit by command 'assign forward eth-trunk mode ' + if value < 0 or value > 1024: + return 'Error: Wrong Value of Eth-Trunk interface number' + elif key == 'system_id': + # X-X-X ,X is hex(4 bit) + if not re.match(r'[0-9a-f]{1,4}\-[0-9a-f]{1,4}\-[0-9a-f]{1,4}', kwargs[key], re.IGNORECASE): + return 'Error: The system-id is invalid.' + values = kwargs[key].split('-') + flag = 0 + # all 'X' is 0,that is invalid value + for v in values: + if len(v.strip('0')) < 1: + flag += 1 + if flag == 3: + return 'Error: The system-id is invalid.' + elif key == 'timeout_type': + # select a value from choices, choices=['Slow','Fast'],it's checked by AnsibleModule + pass + elif key == 'fast_timeout': + value = int(kwargs[key]) + if value < 3 or value > 90: + return 'Error: Wrong Value of timeout,fast user-defined value<3-90>' + rtype = str(kwargs.get('timeout_type')) + if rtype == 'Slow': + return 'Error: Short timeout period for receiving packets is need,when user define the time.' + elif key == 'preempt_delay': + value = int(kwargs[key]) + if value < 0 or value > 180: + return 'Error: Value of preemption delay time is from 0 to 180' + elif key == 'collector_delay': + value = int(kwargs[key]) + if value < 0 or value > 65535: + return 'Error: Value of collector delay time is from 0 to 65535' + elif key == 'max_active_linknumber': + value = int(kwargs[key]) + if value < 0 or value > 64: + return 'Error: Value of collector delay time is from 0 to 64' + elif key == 'priority' or key == 'global_priority': + value = int(kwargs[key]) + if value < 0 or value > 65535: + return 'Error: Value of priority is from 0 to 65535' + return 'ok' + + +def xml_to_dict(args): + """transfer xml string into dict """ + rdict = dict() + args = re.sub(r'xmlns=\".+?\"', '', args) + root = ET.fromstring(args) + ifmtrunk = root.find('.//ifmtrunk') + if ifmtrunk is not None: + for ele in ifmtrunk.getiterator(): + if ele.text is not None and len(ele.text.strip()) > 0: + rdict[ele.tag] = ele.text + return rdict + + +def compare_config(module, kwarg_exist, kwarg_end): + """compare config between exist and end""" + dic_command = {'isSupportPrmpt': 'lacp preempt enable', + 'rcvTimeoutType': 'lacp timeout', # lacp timeout fast user-defined 23 + 'fastTimeoutUserDefinedValue': 'lacp timeout user-defined', + 'selectPortStd': 'lacp select', + 'promptDelay': 'lacp preempt delay', + 'maxActiveNum': 'lacp max active-linknumber', + 'collectMaxDelay': 'lacp collector delay', + 'mixRateEnable': 'lacp mixed-rate link enable', + 'dampStaFlapEn': 'lacp dampening state-flapping', + 'dampUnexpMacEn': 'lacp dampening unexpected-mac disable', + 'trunkSysMac': 'lacp system-id', + 'trunkPortIdExt': 'lacp port-id-extension enable', + 'portPriority': 'lacp priority', # interface 10GE1/0/1 + 'lacpMlagPriority': 'lacp m-lag priority', + 'lacpMlagSysId': 'lacp m-lag system-id', + 'priority': 'lacp priority' + } + rlist = list() + exist = set(kwarg_exist.keys()) + end = set(kwarg_end.keys()) + undo = exist - end + add = end - exist + update = end & exist + + for key in undo: + if key in dic_command: + rlist.append('undo ' + dic_command[key]) + for key in add: + if key in dic_command: + rlist.append(dic_command[key] + ' ' + kwarg_end[key]) + for key in update: + if kwarg_exist[key] != kwarg_end[key] and key in dic_command: + if kwarg_exist[key] == 'true' and kwarg_end[key] == 'false': + rlist.append('undo ' + dic_command[key]) + elif kwarg_exist[key] == 'false' and kwarg_end[key] == 'true': + rlist.append(dic_command[key]) + else: + rlist.append(dic_command[key] + ' ' + kwarg_end[key].lower()) + return rlist + + +class Lacp(object): + """ + Manages Eth-Trunk interfaces LACP. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.trunk_id = self.module.params['trunk_id'] + self.mode = self.module.params['mode'] + self.param = dict() + + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """ init AnsibleModule """ + + self.module = AnsibleModule( + argument_spec=self.spec, + mutually_exclusive=[['trunk_id', 'global_priority']], + required_one_of=[['trunk_id', 'global_priority']], + supports_check_mode=True) + + def check_params(self): + """check module params """ + for key in self.module.params.keys(): + if key in LACP.keys() and self.module.params[key] is not None: + self.param[key] = self.module.params[key] + if isinstance(self.module.params[key], bool): + self.param[key] = str(self.module.params[key]).lower() + msg = check_param(self.param) + if msg != 'ok': + self.module.fail_json(msg=msg) + + def get_existing(self): + """get existing""" + xml_str = bulid_xml(self.param) + xml = get_nc_config(self.module, xml_str) + return xml_to_dict(xml) + + def get_proposed(self): + """get proposed""" + proposed = dict(state=self.state) + proposed.update(self.param) + return proposed + + def get_end_state(self): + """ get end_state""" + xml_str = bulid_xml(self.param) + xml = get_nc_config(self.module, xml_str) + return xml_to_dict(xml) + + def work(self): + """worker""" + + self.check_params() + existing = self.get_existing() + proposed = self.get_proposed() + + # deal present or absent + if self.state == "present": + operation = 'merge' + else: + operation = 'delete' + + xml_str = bulid_xml(self.param, operation=operation) + set_nc_config(self.module, xml_str) + end_state = self.get_end_state() + + self.results['proposed'] = proposed + self.results['existing'] = existing + self.results['end_state'] = end_state + updates_cmd = compare_config(self.module, existing, end_state) + self.results['updates'] = updates_cmd + if updates_cmd: + self.results['changed'] = True + else: + self.results['changed'] = False + + self.module.exit_json(**self.results) + + +def main(): + + argument_spec = dict( + mode=dict(required=False, + choices=['Manual', 'Dynamic', 'Static'], + type='str'), + trunk_id=dict(required=False, type='int'), + preempt_enable=dict(required=False, type='bool'), + state_flapping=dict(required=False, type='bool'), + port_id_extension_enable=dict(required=False, type='bool'), + unexpected_mac_disable=dict(required=False, type='bool'), + system_id=dict(required=False, type='str'), + timeout_type=dict(required=False, type='str', choices=['Slow', 'Fast']), + fast_timeout=dict(required=False, type='int'), + mixed_rate_link_enable=dict(required=False, type='bool'), + preempt_delay=dict(required=False, type='int'), + collector_delay=dict(required=False, type='int'), + max_active_linknumber=dict(required=False, type='int'), + select=dict(required=False, type='str', choices=['Speed', 'Prority']), + priority=dict(required=False, type='int'), + global_priority=dict(required=False, type='int'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + + module = Lacp(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_link_status.py b/plugins/modules/ce_link_status.py new file mode 100644 index 0000000..43c24c3 --- /dev/null +++ b/plugins/modules/ce_link_status.py @@ -0,0 +1,565 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- + +module: ce_link_status +short_description: Get interface link status on HUAWEI CloudEngine switches. +description: + - Get interface link status on HUAWEI CloudEngine switches. +author: + - Zhijin Zhou (@QijunPan) +notes: + - Current physical state shows an interface's physical status. + - Current link state shows an interface's link layer protocol status. + - Current IPv4 state shows an interface's IPv4 protocol status. + - Current IPv6 state shows an interface's IPv6 protocol status. + - Inbound octets(bytes) shows the number of bytes that an interface received. + - Inbound unicast(pkts) shows the number of unicast packets that an interface received. + - Inbound multicast(pkts) shows the number of multicast packets that an interface received. + - Inbound broadcast(pkts) shows the number of broadcast packets that an interface received. + - Inbound error(pkts) shows the number of error packets that an interface received. + - Inbound drop(pkts) shows the total number of packets that were sent to the interface but dropped by an interface. + - Inbound rate(byte/sec) shows the rate at which an interface receives bytes within an interval. + - Inbound rate(pkts/sec) shows the rate at which an interface receives packets within an interval. + - Outbound octets(bytes) shows the number of the bytes that an interface sent. + - Outbound unicast(pkts) shows the number of unicast packets that an interface sent. + - Outbound multicast(pkts) shows the number of multicast packets that an interface sent. + - Outbound broadcast(pkts) shows the number of broadcast packets that an interface sent. + - Outbound error(pkts) shows the total number of packets that an interface sent but dropped by the remote interface. + - Outbound drop(pkts) shows the number of dropped packets that an interface sent. + - Outbound rate(byte/sec) shows the rate at which an interface sends bytes within an interval. + - Outbound rate(pkts/sec) shows the rate at which an interface sends packets within an interval. + - Speed shows the rate for an Ethernet interface. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - For the interface parameter, you can enter C(all) to display information about all interfaces, + an interface type such as C(40GE) to display information about interfaces of the specified type, + or full name of an interface such as C(40GE1/0/22) or C(vlanif10) + to display information about the specific interface. + required: true +''' + +EXAMPLES = ''' + +- name: Link status test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Get specified interface link status information + ce_link_status: + interface: 40GE1/0/1 + provider: "{{ cli }}" + + - name: Get specified interface type link status information + ce_link_status: + interface: 40GE + provider: "{{ cli }}" + + - name: Get all interfaces link status information + ce_link_status: + interface: all + provider: "{{ cli }}" +''' + +RETURN = ''' +result: + description: Interface link status information + returned: always + type: dict + sample: { + "40ge2/0/8": { + "Current IPv4 state": "down", + "Current IPv6 state": "down", + "Current link state": "up", + "Current physical state": "up", + "Inbound broadcast(pkts)": "0", + "Inbound drop(pkts)": "0", + "Inbound error(pkts)": "0", + "Inbound multicast(pkts)": "20151", + "Inbound octets(bytes)": "7314813", + "Inbound rate(byte/sec)": "11", + "Inbound rate(pkts/sec)": "0", + "Inbound unicast(pkts)": "0", + "Outbound broadcast(pkts)": "1", + "Outbound drop(pkts)": "0", + "Outbound error(pkts)": "0", + "Outbound multicast(pkts)": "20152", + "Outbound octets(bytes)": "7235021", + "Outbound rate(byte/sec)": "11", + "Outbound rate(pkts/sec)": "0", + "Outbound unicast(pkts)": "0", + "Speed": "40GE" + } + } +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, get_nc_config, get_nc_next + +CE_NC_GET_PORT_SPEED = """ + + + + + %s + + + + + + + +""" + +CE_NC_GET_INT_STATISTICS = """ + + + + + %s + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +INTERFACE_ALL = 1 +INTERFACE_TYPE = 2 +INTERFACE_FULL_NAME = 3 + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + if interface.upper().startswith('GE'): + return 'ge' + elif interface.upper().startswith('10GE'): + return '10ge' + elif interface.upper().startswith('25GE'): + return '25ge' + elif interface.upper().startswith('4X10GE'): + return '4x10ge' + elif interface.upper().startswith('40GE'): + return '40ge' + elif interface.upper().startswith('100GE'): + return '100ge' + elif interface.upper().startswith('VLANIF'): + return 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + return 'loopback' + elif interface.upper().startswith('METH'): + return 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + return 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + return 'vbdif' + elif interface.upper().startswith('NVE'): + return 'nve' + elif interface.upper().startswith('TUNNEL'): + return 'tunnel' + elif interface.upper().startswith('ETHERNET'): + return 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + return 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + return 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + return 'stack-Port' + elif interface.upper().startswith('NULL'): + return 'null' + else: + return None + + +def is_ethernet_port(interface): + """Judge whether it is ethernet port""" + + ethernet_port = ['ge', '10ge', '25ge', '4x10ge', '40ge', '100ge', 'meth'] + if_type = get_interface_type(interface) + if if_type in ethernet_port: + return True + return False + + +class LinkStatus(object): + """Get interface link status information""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # interface name + self.interface = self.module.params['interface'] + self.interface = self.interface.replace(' ', '').lower() + self.param_type = None + self.if_type = None + + # state + self.results = dict() + self.result = dict() + + def check_params(self): + """Check all input params""" + + if not self.interface: + self.module.fail_json(msg='Error: Interface name cannot be empty.') + + if self.interface and self.interface != 'all': + if not self.if_type: + self.module.fail_json( + msg='Error: Interface name of %s is error.' % self.interface) + + def init_module(self): + """Init module object""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def show_result(self): + """Show result""" + + self.results['result'] = self.result + + self.module.exit_json(**self.results) + + def get_intf_dynamic_info(self, dyn_info, intf_name): + """Get interface dynamic information""" + + if not intf_name: + return + + if dyn_info: + for eles in dyn_info: + if eles.tag in ["ifPhyStatus", "ifV4State", "ifV6State", "ifLinkStatus"]: + if eles.tag == "ifPhyStatus": + self.result[intf_name][ + 'Current physical state'] = eles.text + elif eles.tag == "ifLinkStatus": + self.result[intf_name][ + 'Current link state'] = eles.text + elif eles.tag == "ifV4State": + self.result[intf_name][ + 'Current IPv4 state'] = eles.text + elif eles.tag == "ifV6State": + self.result[intf_name][ + 'Current IPv6 state'] = eles.text + + def get_intf_statistics_info(self, stat_info, intf_name): + """Get interface statistics information""" + + if not intf_name: + return + + if_type = get_interface_type(intf_name) + if if_type == 'fcoe-port' or if_type == 'nve' or if_type == 'tunnel' or \ + if_type == 'vbdif' or if_type == 'vlanif': + return + + if stat_info: + for eles in stat_info: + if eles.tag in ["receiveByte", "sendByte", "rcvUniPacket", "rcvMutiPacket", "rcvBroadPacket", + "sendUniPacket", "sendMutiPacket", "sendBroadPacket", "rcvErrorPacket", + "rcvDropPacket", "sendErrorPacket", "sendDropPacket"]: + if eles.tag == "receiveByte": + self.result[intf_name][ + 'Inbound octets(bytes)'] = eles.text + elif eles.tag == "rcvUniPacket": + self.result[intf_name][ + 'Inbound unicast(pkts)'] = eles.text + elif eles.tag == "rcvMutiPacket": + self.result[intf_name][ + 'Inbound multicast(pkts)'] = eles.text + elif eles.tag == "rcvBroadPacket": + self.result[intf_name][ + 'Inbound broadcast(pkts)'] = eles.text + elif eles.tag == "rcvErrorPacket": + self.result[intf_name][ + 'Inbound error(pkts)'] = eles.text + elif eles.tag == "rcvDropPacket": + self.result[intf_name][ + 'Inbound drop(pkts)'] = eles.text + elif eles.tag == "sendByte": + self.result[intf_name][ + 'Outbound octets(bytes)'] = eles.text + elif eles.tag == "sendUniPacket": + self.result[intf_name][ + 'Outbound unicast(pkts)'] = eles.text + elif eles.tag == "sendMutiPacket": + self.result[intf_name][ + 'Outbound multicast(pkts)'] = eles.text + elif eles.tag == "sendBroadPacket": + self.result[intf_name][ + 'Outbound broadcast(pkts)'] = eles.text + elif eles.tag == "sendErrorPacket": + self.result[intf_name][ + 'Outbound error(pkts)'] = eles.text + elif eles.tag == "sendDropPacket": + self.result[intf_name][ + 'Outbound drop(pkts)'] = eles.text + + def get_intf_cleared_stat(self, clr_stat, intf_name): + """Get interface cleared state information""" + + if not intf_name: + return + + if_type = get_interface_type(intf_name) + if if_type == 'fcoe-port' or if_type == 'nve' or if_type == 'tunnel' or \ + if_type == 'vbdif' or if_type == 'vlanif': + return + + if clr_stat: + for eles in clr_stat: + if eles.tag in ["inByteRate", "inPacketRate", "outByteRate", "outPacketRate"]: + if eles.tag == "inByteRate": + self.result[intf_name][ + 'Inbound rate(byte/sec)'] = eles.text + elif eles.tag == "inPacketRate": + self.result[intf_name][ + 'Inbound rate(pkts/sec)'] = eles.text + elif eles.tag == "outByteRate": + self.result[intf_name][ + 'Outbound rate(byte/sec)'] = eles.text + elif eles.tag == "outPacketRate": + self.result[intf_name][ + 'Outbound rate(pkts/sec)'] = eles.text + + def get_all_interface_info(self, intf_type=None): + """Get interface information by all or by interface type""" + + xml_str = CE_NC_GET_INT_STATISTICS % '' + con_obj = get_nc_next(self.module, xml_str) + if "" in con_obj: + return + + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get link status information + root = ElementTree.fromstring(xml_str) + intfs_info = root.findall("ifm/interfaces/interface") + if not intfs_info: + return + + intf_name = '' + flag = False + for eles in intfs_info: + if eles.tag == "interface": + for ele in eles: + if ele.tag in ["ifName", "ifDynamicInfo", "ifStatistics", "ifClearedStat"]: + if ele.tag == "ifName": + intf_name = ele.text.lower() + if intf_type: + if get_interface_type(intf_name) != intf_type.lower(): + break + else: + flag = True + self.init_interface_data(intf_name) + if is_ethernet_port(intf_name): + self.get_port_info(intf_name) + if ele.tag == "ifDynamicInfo": + self.get_intf_dynamic_info(ele, intf_name) + elif ele.tag == "ifStatistics": + self.get_intf_statistics_info(ele, intf_name) + elif ele.tag == "ifClearedStat": + self.get_intf_cleared_stat(ele, intf_name) + if intf_type and not flag: + self.module.fail_json( + msg='Error: %s interface type does not exist.' % intf_type.upper()) + + def get_interface_info(self): + """Get interface information""" + + xml_str = CE_NC_GET_INT_STATISTICS % self.interface.upper() + con_obj = get_nc_config(self.module, xml_str) + if "" in con_obj: + self.module.fail_json( + msg='Error: %s interface does not exist.' % self.interface.upper()) + return + + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get link status information + root = ElementTree.fromstring(xml_str) + intf_info = root.find("ifm/interfaces/interface") + if intf_info: + for eles in intf_info: + if eles.tag in ["ifDynamicInfo", "ifStatistics", "ifClearedStat"]: + if eles.tag == "ifDynamicInfo": + self.get_intf_dynamic_info(eles, self.interface) + elif eles.tag == "ifStatistics": + self.get_intf_statistics_info(eles, self.interface) + elif eles.tag == "ifClearedStat": + self.get_intf_cleared_stat(eles, self.interface) + + def init_interface_data(self, intf_name): + """Init interface data""" + + # init link status data + self.result[intf_name] = dict() + self.result[intf_name]['Current physical state'] = 'down' + self.result[intf_name]['Current link state'] = 'down' + self.result[intf_name]['Current IPv4 state'] = 'down' + self.result[intf_name]['Current IPv6 state'] = 'down' + self.result[intf_name]['Inbound octets(bytes)'] = '--' + self.result[intf_name]['Inbound unicast(pkts)'] = '--' + self.result[intf_name]['Inbound multicast(pkts)'] = '--' + self.result[intf_name]['Inbound broadcast(pkts)'] = '--' + self.result[intf_name]['Inbound error(pkts)'] = '--' + self.result[intf_name]['Inbound drop(pkts)'] = '--' + self.result[intf_name]['Inbound rate(byte/sec)'] = '--' + self.result[intf_name]['Inbound rate(pkts/sec)'] = '--' + self.result[intf_name]['Outbound octets(bytes)'] = '--' + self.result[intf_name]['Outbound unicast(pkts)'] = '--' + self.result[intf_name]['Outbound multicast(pkts)'] = '--' + self.result[intf_name]['Outbound broadcast(pkts)'] = '--' + self.result[intf_name]['Outbound error(pkts)'] = '--' + self.result[intf_name]['Outbound drop(pkts)'] = '--' + self.result[intf_name]['Outbound rate(byte/sec)'] = '--' + self.result[intf_name]['Outbound rate(pkts/sec)'] = '--' + self.result[intf_name]['Speed'] = '--' + + def get_port_info(self, interface): + """Get port information""" + + if_type = get_interface_type(interface) + if if_type == 'meth': + xml_str = CE_NC_GET_PORT_SPEED % interface.lower().replace('meth', 'MEth') + else: + xml_str = CE_NC_GET_PORT_SPEED % interface.upper() + con_obj = get_nc_config(self.module, xml_str) + if "" in con_obj: + return + + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get link status information + root = ElementTree.fromstring(xml_str) + port_info = root.find("devm/ports/port") + if port_info: + for eles in port_info: + if eles.tag == "ethernetPort": + for ele in eles: + if ele.tag == 'speed': + self.result[interface]['Speed'] = ele.text + + def get_link_status(self): + """Get link status information""" + + if self.param_type == INTERFACE_FULL_NAME: + self.init_interface_data(self.interface) + self.get_interface_info() + if is_ethernet_port(self.interface): + self.get_port_info(self.interface) + elif self.param_type == INTERFACE_TYPE: + self.get_all_interface_info(self.interface) + else: + self.get_all_interface_info() + + def get_intf_param_type(self): + """Get the type of input interface parameter""" + + if self.interface == 'all': + self.param_type = INTERFACE_ALL + return + + if self.if_type == self.interface: + self.param_type = INTERFACE_TYPE + return + + self.param_type = INTERFACE_FULL_NAME + + def work(self): + """Worker""" + + self.if_type = get_interface_type(self.interface) + self.check_params() + self.get_intf_param_type() + self.get_link_status() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + interface=dict(required=True, type='str'), + ) + argument_spec.update(ce_argument_spec) + linkstatus_obj = LinkStatus(argument_spec) + linkstatus_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_lldp.py b/plugins/modules/ce_lldp.py new file mode 100644 index 0000000..3bc1391 --- /dev/null +++ b/plugins/modules/ce_lldp.py @@ -0,0 +1,791 @@ +#!/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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- + +module: ce_lldp +short_description: Manages LLDP configuration on HUAWEI CloudEngine switches. +description: + - Manages LLDP configuration on HUAWEI CloudEngine switches. +author: + - xuxiaowei0512 (@CloudEngine-Ansible) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + lldpenable: + description: + - Set global LLDP enable state. + required: false + choices: ['enabled', 'disabled'] + type: str + mdnstatus: + description: + - Set global MDN enable state. + required: false + choices: ['rxOnly', 'disabled'] + type: str + interval: + description: + - Frequency at which LLDP advertisements are sent (in seconds). + required: false + type: int + hold_multiplier: + description: + - Time multiplier for device information in neighbor devices. + required: false + type: int + restart_delay: + description: + - Specifies the delay time of the interface LLDP module from disabled state to re enable. + required: false + type: int + transmit_delay: + description: + - Delay time for sending LLDP messages. + required: false + type: int + notification_interval: + description: + - Suppression time for sending LLDP alarm. + required: false + type: int + fast_count: + description: + - The number of LLDP messages sent to the neighbor nodes by the specified device. + required: false + type: int + mdn_notification_interval: + description: + - Delay time for sending MDN neighbor information change alarm. + required: false + type: int + management_address: + description: + - The management IP address of LLDP. + required: false + default: null + type: str + bind_name: + description: + - Binding interface name. + required: false + default: null + type: str + state: + description: + - Manage the state of the resource. + required: false + default: present + type: str + choices: ['present','absent'] +''' + +EXAMPLES = ''' + - name: "Configure global LLDP enable state" + ce_lldp: + lldpenable: enabled + + - name: "Configure global MDN enable state" + ce_lldp: + mdnstatus: rxOnly + + - name: "Configure LLDP transmit interval and ensure global LLDP state is already enabled" + ce_lldp: + enable: enable + interval: 32 + + - name: "Configure LLDP transmit multiplier hold and ensure global LLDP state is already enabled" + ce_lldp: + enable: enable + hold_multiplier: 5 + + - name: "Configure the delay time of the interface LLDP module from disabled state to re enable" + ce_lldp: + enable: enable + restart_delay: 3 + + - name: "Reset the delay time for sending LLDP messages" + ce_lldp: + enable: enable + transmit_delay: 4 + + - name: "Configure device to send neighbor device information change alarm delay time" + ce_lldp: + lldpenable: enabled + notification_interval: 6 + + - name: "Configure the number of LLDP messages sent to the neighbor nodes by the specified device" + ce_lldp: + enable: enable + fast_count: 5 + + - name: "Configure the delay time for sending MDN neighbor information change alarm" + ce_lldp: + enable: enable + mdn_notification_interval: 6 + - name: "Configuring the management IP address of LLDP" + ce_lldp: + enable: enable + management_address: 10.1.0.1 + + - name: "Configuring LLDP to manage the binding relationship between IP addresses and interfaces" + ce_lldp: + enable: enable + bind_name: LoopBack2 +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "mdnstatus": "rxOnly", + "interval": "32", + "hold_multiplier": "5", + "restart_delay": "3", + "transmit_delay": "4", + "notification_interval": "6", + "fast_count": "5", + "mdn_notification_interval": "6", + "management_address": "10.1.0.1", + "bind_name": "LoopBack2", + "state": "present" + } +existing: + description: k/v pairs of existing global LLDP configuration. + returned: always + type: dict + sample: { + "lldpenable": "disabled", + "mdnstatus": "disabled" + } +end_state: + description: k/v pairs of global LLDP configuration after module execution. + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "mdnstatus": "rxOnly", + "interval": "32", + "hold_multiplier": "5", + "restart_delay": "3", + "transmit_delay": "4", + "notification_interval": "6", + "fast_count": "5", + "mdn_notification_interval": "6", + "management_address": "10.1.0.1", + "bind_name": "LoopBack2" + } +updates: + description: command sent to the device + returned: always + type: list + sample: [ + "lldp enable", + "lldp mdn enable", + "lldp transmit interval 32", + "lldp transmit multiplier 5", + "lldp restart 3", + "lldp transmit delay 4", + "lldp trap-interval 6", + "lldp fast-count 5", + "lldp mdn trap-interval 6", + "lldp management-address 10.1.0.1", + "lldp management-address bind interface LoopBack 2" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import copy +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import set_nc_config, get_nc_config + +CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG = """ + + + + + + + + +""" + +CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG = """ + + + + %s + + + +""" + +CE_NC_MERGE_GLOBA_MDNENABLE_CONFIG = """ + + + + %s + + + +""" + +CE_NC_GET_GLOBAL_LLDP_CONFIG = """ + + + + + + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER = """ + + + + +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_INTERVAL = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HOLD_MULTIPLIER = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_RESTART_DELAY = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TRANSMIT_DELAY = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_NOTIFICATION_INTERVAL = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_FAST_COUNT = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_MDN_NOTIFICATION_INTERVAL = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_MANAGEMENT_ADDRESS = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_BIND_NAME = """ + %s +""" + +CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL = """ + + + + +""" + + +class Lldp(object): + """Manage global lldp enable configuration""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + self.lldpenable = self.module.params['lldpenable'] or None + self.interval = self.module.params['interval'] or None + self.mdnstatus = self.module.params['mdnstatus'] or None + self.hold_multiplier = self.module.params['hold_multiplier'] or None + self.restart_delay = self.module.params['restart_delay'] or None + self.transmit_delay = self.module.params['transmit_delay'] or None + self.notification_interval = self.module.params['notification_interval'] or None + self.fast_count = self.module.params['fast_count'] or None + self.mdn_notification_interval = self.module.params['mdn_notification_interval'] or None + self.management_address = self.module.params['management_address'] + self.bind_name = self.module.params['bind_name'] + self.state = self.module.params['state'] + self.lldp_conf = dict() + self.conf_exsit = False + self.conf_exsit_lldp = False + self.enable_flag = 0 + self.check_params() + self.existing_state_value = dict() + self.existing_end_state_value = dict() + self.changed = False + self.proposed_changed = dict() + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def is_valid_v4addr(self): + """check if ipv4 addr is valid""" + if self.management_address.find('.') != -1: + addr_list = self.management_address.split('.') + if self.management_address == "0.0.0.0": + self.module.fail_json(msg='Error: The management address is 0.0.0.0 .') + if len(addr_list) != 4: + self.module.fail_json(msg='Error: Invalid IPV4 address.') + for each_num in addr_list: + each_num_tmp = str(each_num) + if not each_num_tmp.isdigit(): + self.module.fail_json(msg='Error: The ip address is not digit.') + if (int(each_num) > 255) or (int(each_num) < 0): + self.module.fail_json( + msg='Error: The value of ip address is out of [0 - 255].') + else: + self.module.fail_json(msg='Error: Invalid IP address.') + + def check_params(self): + """Check all input params""" + + if self.interval: + if int(self.interval) < 5 or int(self.interval) > 32768: + self.module.fail_json( + msg='Error: The value of interval is out of [5 - 32768].') + + if self.hold_multiplier: + if int(self.hold_multiplier) < 2 or int(self.hold_multiplier) > 10: + self.module.fail_json( + msg='Error: The value of hold_multiplier is out of [2 - 10].') + + if self.restart_delay: + if int(self.restart_delay) < 1 or int(self.restart_delay) > 10: + self.module.fail_json( + msg='Error: The value of restart_delay is out of [1 - 10].') + + if self.transmit_delay: + if int(self.transmit_delay) < 1 or int(self.transmit_delay) > 8192: + self.module.fail_json( + msg='Error: The value of transmit_delay is out of [1 - 8192].') + + if self.notification_interval: + if int(self.notification_interval) < 5 or int(self.notification_interval) > 3600: + self.module.fail_json( + msg='Error: The value of notification_interval is out of [5 - 3600].') + + if self.fast_count: + if int(self.fast_count) < 1 or int(self.fast_count) > 8: + self.module.fail_json( + msg='Error: The value of fast_count is out of [1 - 8].') + + if self.mdn_notification_interval: + if int(self.mdn_notification_interval) < 5 or int(self.mdn_notification_interval) > 3600: + self.module.fail_json( + msg='Error: The value of mdn_notification_interval is out of [5 - 3600].') + + if self.management_address: + self.is_valid_v4addr() + + if self.bind_name: + if (len(self.bind_name) < 1) or (len(self.bind_name) > 63): + self.module.fail_json( + msg='Error: Bind_name length is between 1 and 63.') + + def init_module(self): + """Init module object""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def config_lldp(self): + """Configure lldp enabled and mdn enabled parameters""" + + if self.state == 'present': + if (self.enable_flag == 1 and self.lldpenable == 'enabled') and not self.conf_exsit: + if self.mdnstatus: + xml_str = CE_NC_MERGE_GLOBA_MDNENABLE_CONFIG % self.mdnstatus + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "MDN_ENABLE_CONFIG") + + if self.lldpenable == 'enabled' and not self.conf_exsit: + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_ENABLE_CONFIG") + + if self.mdnstatus: + xml_str = CE_NC_MERGE_GLOBA_MDNENABLE_CONFIG % self.mdnstatus + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "MDN_ENABLE_CONFIG") + + if (self.enable_flag == 1) and not self.conf_exsit: + if self.mdnstatus: + xml_str = CE_NC_MERGE_GLOBA_MDNENABLE_CONFIG % self.mdnstatus + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "MDN_ENABLE_CONFIG") + + if (self.lldpenable == 'enabled' or self.enable_flag == 1) and not self.conf_exsit_lldp: + if self.hold_multiplier: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HOLD_MULTIPLIER % self.hold_multiplier) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.interval: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_INTERVAL % self.interval) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.restart_delay: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_RESTART_DELAY % self.restart_delay) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.transmit_delay: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TRANSMIT_DELAY % self.transmit_delay) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.notification_interval: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_NOTIFICATION_INTERVAL % self.notification_interval) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.fast_count: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_FAST_COUNT % self.fast_count) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.mdn_notification_interval: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_MDN_NOTIFICATION_INTERVAL % self.mdn_notification_interval) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.management_address: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_MANAGEMENT_ADDRESS % self.management_address) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.bind_name: + xml_str = CE_NC_MERGE_GLOBAL_LLDP_CONFIG_HEADER + \ + (CE_NC_MERGE_GLOBAL_LLDP_CONFIG_BIND_NAME % self.bind_name) + \ + CE_NC_MERGE_GLOBAL_LLDP_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_CONFIG_INTERVAL") + + if self.lldpenable == 'disabled' and not self.conf_exsit: + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_DISABLE_CONFIG") + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def get_lldp_exist_config(self): + """Get lldp existed configure""" + + lldp_config = list() + lldp_dict = dict() + + conf_enable_str = CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG + conf_enable_obj = get_nc_config(self.module, conf_enable_str) + + xml_enable_str = conf_enable_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get lldp enable config info + root_enable = ElementTree.fromstring(xml_enable_str) + ntpsite_enable = root_enable.findall("lldp/lldpSys") + for nexthop_enable in ntpsite_enable: + for ele_enable in nexthop_enable: + if ele_enable.tag in ["lldpEnable", "mdnStatus"]: + lldp_dict[ele_enable.tag] = ele_enable.text + + if self.state == "present": + cur_lldp_cfg = dict(lldpenable=lldp_dict['lldpEnable'], mdnstatus=lldp_dict['mdnStatus']) + exp_lldp_cfg = dict(lldpenable=self.lldpenable, mdnstatus=self.mdnstatus) + if lldp_dict['lldpEnable'] == 'enabled': + self.enable_flag = 1 + if cur_lldp_cfg == exp_lldp_cfg: + self.conf_exsit = True + lldp_config.append(dict(lldpenable=lldp_dict['lldpEnable'], mdnstatus=lldp_dict['mdnStatus'])) + + conf_str = CE_NC_GET_GLOBAL_LLDP_CONFIG + conf_obj = get_nc_config(self.module, conf_str) + if "" in conf_obj: + pass + + else: + xml_str = conf_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get all ntp config info + root = ElementTree.fromstring(xml_str) + ntpsite = root.findall("lldp/lldpSys/lldpSysParameter") + for nexthop in ntpsite: + for ele in nexthop: + if ele.tag in ["messageTxInterval", "messageTxHoldMultiplier", "reinitDelay", "txDelay", + "notificationInterval", "fastMessageCount", "mdnNotificationInterval", + "configManAddr", "bindifName"]: + lldp_dict[ele.tag] = ele.text + + if self.state == "present": + cur_ntp_cfg = dict(interval=lldp_dict['messageTxInterval'], + hold_multiplier=lldp_dict['messageTxHoldMultiplier'], + restart_delay=lldp_dict['reinitDelay'], + transmit_delay=lldp_dict['txDelay'], + notification_interval=lldp_dict['notificationInterval'], + fast_count=lldp_dict['fastMessageCount'], + mdn_notification_interval=lldp_dict['mdnNotificationInterval'], + management_address=lldp_dict['configManAddr'], + bind_name=lldp_dict['bindifName']) + + exp_ntp_cfg = dict(interval=self.interval, hold_multiplier=self.hold_multiplier, + restart_delay=self.restart_delay, transmit_delay=self.transmit_delay, + notification_interval=self.notification_interval, + fast_count=self.fast_count, mdn_notification_interval=self.mdn_notification_interval, + management_address=self.management_address, bind_name=self.bind_name) + + if cur_ntp_cfg == exp_ntp_cfg: + self.conf_exsit_lldp = True + + lldp_config.append(dict(interval=lldp_dict['messageTxInterval'], + hold_multiplier=lldp_dict['messageTxHoldMultiplier'], + restart_delay=lldp_dict['reinitDelay'], transmit_delay=lldp_dict['txDelay'], + notification_interval=lldp_dict['notificationInterval'], + fast_count=lldp_dict['fastMessageCount'], + mdn_notification_interval=lldp_dict['mdnNotificationInterval'], + management_address=lldp_dict['configManAddr'], + bind_name=lldp_dict['bindifName'])) + + tmp_dict = dict() + str_1 = str(lldp_config) + temp_1 = str_1.replace('[', '').replace(']', '').replace('{', '').replace('}', '').replace('\'', '') + if temp_1: + tmp_2 = temp_1.split(',') + for i in tmp_2: + tmp_value = re.match(r'(.*):(.*)', i) + key_tmp = tmp_value.group(1) + key_value = tmp_value.group(2) + tmp_dict[key_tmp] = key_value + return tmp_dict + + def get_existing(self): + """Get existing info""" + + self.existing = self.get_lldp_exist_config() + + def get_proposed(self): + """Get proposed info""" + + if self.enable_flag == 1: + if self.lldpenable == 'enabled': + self.proposed = dict(lldpenable=self.lldpenable) + if self.mdnstatus: + self.proposed = dict(mdnstatus=self.mdnstatus) + elif self.lldpenable == 'disabled': + self.proposed = dict(lldpenable=self.lldpenable) + self.changed = True + else: + if self.mdnstatus: + self.proposed = dict(mdnstatus=self.mdnstatus) + else: + if self.lldpenable == 'enabled': + self.proposed = dict(lldpenable=self.lldpenable) + self.changed = True + if self.mdnstatus: + self.proposed = dict(mdnstatus=self.mdnstatus) + if self.enable_flag == 1 or self.lldpenable == 'enabled': + if self.interval: + self.proposed = dict(interval=self.interval) + if self.hold_multiplier: + self.proposed = dict(hold_multiplier=self.hold_multiplier) + if self.restart_delay: + self.proposed = dict(restart_delay=self.restart_delay) + if self.transmit_delay: + self.proposed = dict(transmit_delay=self.transmit_delay) + if self.notification_interval: + self.proposed = dict(notification_interval=self.notification_interval) + if self.fast_count: + self.proposed = dict(fast_count=self.fast_count) + if self.mdn_notification_interval: + self.proposed = dict(mdn_notification_interval=self.mdn_notification_interval) + if self.management_address: + self.proposed = dict(management_address=self.management_address) + if self.bind_name: + self.proposed = dict(bind_name=self.bind_name) + + def get_end_state(self): + """Get end state info""" + + self.end_state = self.get_lldp_exist_config() + existing_key_list = self.existing.keys() + end_state_key_list = self.end_state.keys() + for i in end_state_key_list: + for j in existing_key_list: + if i == j and self.existing[i] != self.end_state[j]: + self.changed = True + + def get_update_cmd(self): + """Get updated commands""" + + if self.conf_exsit and self.conf_exsit_lldp: + return + + if self.state == "present": + if self.lldpenable == "enabled": + self.updates_cmd.append("lldp enable") + + if self.mdnstatus: + self.updates_cmd.append("lldp mdn enable") + if self.mdnstatus == "rxOnly": + self.updates_cmd.append("lldp mdn enable") + else: + self.updates_cmd.append("undo lldp mdn enable") + if self.interval: + self.updates_cmd.append("lldp transmit interval %s" % self.interval) + if self.hold_multiplier: + self.updates_cmd.append("lldp transmit multiplier %s" % self.hold_multiplier) + if self.restart_delay: + self.updates_cmd.append("lldp restart %s" % self.restart_delay) + if self.transmit_delay: + self.updates_cmd.append("lldp transmit delay %s" % self.transmit_delay) + if self.notification_interval: + self.updates_cmd.append("lldp trap-interval %s" % self.notification_interval) + if self.fast_count: + self.updates_cmd.append("lldp fast-count %s" % self.fast_count) + if self.mdn_notification_interval: + self.updates_cmd.append("lldp mdn trap-interval %s" % self.mdn_notification_interval) + if self.management_address: + self.updates_cmd.append("lldp management-address %s" % self.management_address) + if self.bind_name: + self.updates_cmd.append("lldp management-address bind interface %s" % self.bind_name) + elif self.lldpenable == "disabled": + self.updates_cmd.append("undo lldp enable") + else: + if self.enable_flag == 1: + if self.mdnstatus: + if self.mdnstatus == "rxOnly": + self.updates_cmd.append("lldp mdn enable") + else: + self.updates_cmd.append("undo lldp mdn enable") + if self.interval: + self.updates_cmd.append("lldp transmit interval %s" % self.interval) + if self.hold_multiplier: + self.updates_cmd.append("lldp transmit multiplier %s" % self.hold_multiplier) + if self.restart_delay: + self.updates_cmd.append("lldp restart %s" % self.restart_delay) + if self.transmit_delay: + self.updates_cmd.append("lldp transmit delay %s" % self.transmit_delay) + if self.notification_interval: + self.updates_cmd.append("lldp trap-interval %s" % self.notification_interval) + if self.fast_count: + self.updates_cmd.append("lldp fast-count %s" % self.fast_count) + if self.mdn_notification_interval: + self.updates_cmd.append("lldp mdn trap-interval %s" % self.mdn_notification_interval) + if self.management_address: + self.updates_cmd.append("lldp management-address %s" % self.management_address) + if self.bind_name: + self.updates_cmd.append("lldp management-address bind interface %s" % self.bind_name) + + def work(self): + """Execute task""" + self.check_params() + self.get_existing() + self.get_proposed() + self.config_lldp() + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + lldpenable=dict(required=False, choices=['enabled', 'disabled']), + mdnstatus=dict(required=False, choices=['rxOnly', 'disabled']), + interval=dict(required=False, type='int'), + hold_multiplier=dict(required=False, type='int'), + restart_delay=dict(required=False, type='int'), + transmit_delay=dict(required=False, type='int'), + notification_interval=dict(required=False, type='int'), + fast_count=dict(required=False, type='int'), + mdn_notification_interval=dict(required=False, type='int'), + management_address=dict(required=False, type='str'), + bind_name=dict(required=False, type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + lldp_obj = Lldp(argument_spec) + lldp_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_lldp_interface.py b/plugins/modules/ce_lldp_interface.py new file mode 100644 index 0000000..9ce0096 --- /dev/null +++ b/plugins/modules/ce_lldp_interface.py @@ -0,0 +1,1384 @@ +#!/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 . +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: ce_lldp_interface +short_description: Manages INTERFACE LLDP configuration on HUAWEI CloudEngine switches. +description: + - Manages INTERFACE LLDP configuration on HUAWEI CloudEngine switches. +author: xuxiaowei0512 (@CloudEngine-Ansible) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + lldpenable: + description: + - Set global LLDP enable state. + type: str + choices: ['enabled', 'disabled'] + function_lldp_interface_flag: + description: + - Used to distinguish between command line functions. + type: str + choices: ['disableINTERFACE','tlvdisableINTERFACE','tlvenableINTERFACE','intervalINTERFACE'] + type_tlv_disable: + description: + - Used to distinguish between command line functions. + type: str + choices: ['basic_tlv', 'dot3_tlv'] + type_tlv_enable: + description: + - Used to distinguish between command line functions. + type: str + choices: ['dot1_tlv','dcbx'] + lldpadminstatus: + description: + - Set interface lldp enable state. + type: str + choices: ['txOnly', 'rxOnly', 'txAndRx', 'disabled'] + ifname: + description: + - Interface name. + type: str + txinterval: + description: + - LLDP send message interval. + type: int + txprotocolvlanid: + description: + - Set tx protocol vlan id. + type: int + txvlannameid: + description: + - Set tx vlan name id. + type: int + vlannametxenable: + description: + - Set vlan name tx enable or not. + type: bool + manaddrtxenable: + description: + - Make it able to send management address TLV. + type: bool + portdesctxenable: + description: + - Enabling the ability to send a description of TLV. + type: bool + syscaptxenable: + description: + - Enable the ability to send system capabilities TLV. + type: bool + sysdesctxenable: + description: + - Enable the ability to send system description TLV. + type: bool + sysnametxenable: + description: + - Enable the ability to send system name TLV. + type: bool + portvlantxenable: + description: + - Enable port vlan tx. + type: bool + protovlantxenable: + description: + - Enable protocol vlan tx. + type: bool + protoidtxenable: + description: + - Enable the ability to send protocol identity TLV. + type: bool + macphytxenable: + description: + - Enable MAC/PHY configuration and state TLV to be sent. + type: bool + linkaggretxenable: + description: + - Enable the ability to send link aggregation TLV. + type: bool + maxframetxenable: + description: + - Enable the ability to send maximum frame length TLV. + type: bool + eee: + description: + - Enable the ability to send EEE TLV. + type: bool + dcbx: + description: + - Enable the ability to send DCBX TLV. + type: bool + state: + description: + - Manage the state of the resource. + type: str + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' + - name: "Configure global LLDP enable state" + ce_lldp_interface_interface: + lldpenable: enabled + + - name: "Configure interface lldp enable state" + ce_lldp_interface: + function_lldp_interface_flag: disableINTERFACE + ifname: 10GE1/0/1 + lldpadminstatus: rxOnly + - name: "Configure LLDP transmit interval and ensure global LLDP state is already enabled" + ce_lldp_interface: + function_lldp_interface_flag: intervalINTERFACE + ifname: 10GE1/0/1 + txinterval: 4 + + - name: "Configure basic-tlv: management-address TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: basic_tlv + ifname: 10GE1/0/1 + manaddrtxenable: true + + - name: "Configure basic-tlv: prot description TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: basic_tlv + ifname: 10GE1/0/1 + portdesctxenable: true + + - name: "Configure basic-tlv: system capabilities TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: basic_tlv + ifname: 10GE1/0/1 + syscaptxenable: true + + - name: "Configure basic-tlv: system description TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: basic_tlv + ifname: 10GE1/0/1 + sysdesctxenable: true + + - name: "Configure basic-tlv: system name TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: basic_tlv + ifname: 10GE1/0/1 + sysnametxenable: true + + - name: "TLV types that are forbidden to be published on the configuration interface, link aggregation TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: dot3_tlv + ifname: 10GE1/0/1 + linkAggreTxEnable: true + + - name: "TLV types that are forbidden to be published on the configuration interface, MAC/PHY configuration/status TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: dot3_tlv + ifname: 10GE1/0/1 + macPhyTxEnable: true + + - name: "TLV types that are forbidden to be published on the configuration interface, maximum frame size TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: dot3_tlv + ifname: 10GE1/0/1 + maxFrameTxEnable: true + + - name: "TLV types that are forbidden to be published on the configuration interface, EEE TLV" + ce_lldp_interface: + function_lldp_interface_flag: tlvdisableINTERFACE + type_tlv_disable: dot3_tlv + ifname: 10GE1/0/1 + eee: true + + - name: "Configure the interface to publish an optional DCBX TLV type " + ce_lldp_interface: + function_lldp_interface_flag: tlvenableINTERFACE + ifname: 10GE1/0/1 + type_tlv_enable: dcbx +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "lldpadminstatus": "rxOnly", + "function_lldp_interface_flag": "tlvenableINTERFACE", + "type_tlv_enable": "dot1_tlv", + "ifname": "10GE1/0/1", + "state": "present" + } +existing: + description: k/v pairs of existing global LLDP configration + returned: always + type: dict + sample: { + "lldpenable": "disabled", + "ifname": "10GE1/0/1", + "lldpadminstatus": "txAndRx" + } +end_state: + description: k/v pairs of global DLDP configration after module execution + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "lldpadminstatus": "rxOnly", + "function_lldp_interface_flag": "tlvenableINTERFACE", + "type_tlv_enable": "dot1_tlv", + "ifname": "10GE1/0/1" + } +updates: + description: command sent to the device + returned: always + type: list + sample: [ + "lldp enable", + "interface 10ge 1/0/1", + "undo lldp disable", + "lldp tlv-enable dot1-tlv vlan-name 4", + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import copy +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import set_nc_config, get_nc_config + +CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG = """ + + + + + + + +""" + +CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG = """ + + + + %s + + + +""" + +CE_NC_GET_INTERFACE_LLDP_CONFIG = """ + + + + + + + + + + +""" + +CE_NC_MERGE_INTERFACE_LLDP_CONFIG = """ + + + + + %s + %s + + + + +""" + +CE_NC_GET_INTERFACE_INTERVAl_CONFIG = """ + + + + + + + + + + + + +""" + +CE_NC_MERGE_INTERFACE_INTERVAl_CONFIG = """ + + + + + %s + + %s + + + + + +""" + +CE_NC_GET_INTERFACE_TLV_ENABLE_CONFIG = """ + + + + + + + + + + + + + +""" + +CE_NC_GET_INTERFACE_TLV_DISABLE_CONFIG = """ + + + + + + + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER = """ + + + + + %s + +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_ENABLE_PROTOIDTXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_ENABLE_DCBX = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_MANADDRTXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_PORTDESCTXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_SYSCAPTXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_SYSDESCTXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_SYSNAMETXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_LINKAGGRETXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_MACPHYTXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_MAXFRAMETXENABLE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_EEE = """ + %s +""" + +CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL = """ + + + + + +""" + +CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG = """ + + + + %s + + + +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('PORT-GROUP'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + return iftype.lower() + + +class Lldp_interface(object): + """Manage global lldp enable configuration""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + self.lldpenable = self.module.params['lldpenable'] or None + self.function_lldp_interface_flag = self.module.params['function_lldp_interface_flag'] + self.type_tlv_disable = self.module.params['type_tlv_disable'] + self.type_tlv_enable = self.module.params['type_tlv_enable'] + self.ifname = self.module.params['ifname'] + if self.function_lldp_interface_flag == 'disableINTERFACE': + self.ifname = self.module.params['ifname'] + self.lldpadminstatus = self.module.params['lldpadminstatus'] + elif self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + if self.type_tlv_disable == 'basic_tlv': + self.ifname = self.module.params['ifname'] + self.manaddrtxenable = self.module.params['manaddrtxenable'] + self.portdesctxenable = self.module.params['portdesctxenable'] + self.syscaptxenable = self.module.params['syscaptxenable'] + self.sysdesctxenable = self.module.params['sysdesctxenable'] + self.sysnametxenable = self.module.params['sysnametxenable'] + if self.type_tlv_disable == 'dot3_tlv': + self.ifname = self.module.params['ifname'] + self.macphytxenable = self.module.params['macphytxenable'] + self.linkaggretxenable = self.module.params['linkaggretxenable'] + self.maxframetxenable = self.module.params['maxframetxenable'] + self.eee = self.module.params['eee'] + elif self.function_lldp_interface_flag == 'tlvenableINTERFACE': + if self.type_tlv_enable == 'dot1_tlv': + self.ifname = self.module.params['ifname'] + self.protoidtxenable = self.module.params['protoidtxenable'] + if self.type_tlv_enable == 'dcbx': + self.ifname = self.module.params['ifname'] + self.dcbx = self.module.params['dcbx'] + elif self.function_lldp_interface_flag == 'intervalINTERFACE': + self.ifname = self.module.params['ifname'] + self.txinterval = self.module.params['txinterval'] + self.state = self.module.params['state'] + + self.lldp_conf = dict() + self.conf_disable_exsit = False + self.conf_interface_lldp_disable_exsit = False + self.conf_interval_exsit = False + self.conf_tlv_disable_exsit = False + self.conf_tlv_enable_exsit = False + self.enable_flag = 0 + self.check_params() + self.existing_state_value = dict() + self.existing_end_state_value = dict() + self.interface_lldp_info = list() + + # state + self.changed = False + self.proposed_changed = dict() + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def check_params(self): + """Check all input params""" + + if self.ifname: + intf_type = get_interface_type(self.ifname) + if not intf_type: + self.module.fail_json(msg='Error: ifname name of %s is error.' % self.ifname) + if (len(self.ifname) < 1) or (len(self.ifname) > 63): + self.module.fail_json(msg='Error: Ifname length is beetween 1 and 63.') + + if self.function_lldp_interface_flag == 'intervalINTERFACE': + if self.txinterval: + if int(self.txinterval) < 1 or int(self.txinterval) > 32768: + self.module.fail_json( + msg='Error: The value of txinterval is out of [1 - 32768].') + if self.ifname: + intf_type = get_interface_type(self.ifname) + if not intf_type: + self.module.fail_json( + msg='Error: ifname name of %s ' + 'is error.' % self.ifname) + if (len(self.ifname) < 1) or (len(self.ifname) > 63): + self.module.fail_json( + msg='Error: Ifname length is beetween 1 and 63.') + + if self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + if self.type_tlv_disable == 'dot1_tlv': + if self.ifname: + intf_type = get_interface_type(self.ifname) + if not intf_type: + self.module.fail_json( + msg='Error: ifname name of %s ' + 'is error.' % self.ifname) + if (len(self.ifname) < 1) or (len(self.ifname) > 63): + self.module.fail_json( + msg='Error: Ifname length is beetween 1 and 63.') + + if self.function_lldp_interface_flag == 'tlvenableINTERFACE': + if self.type_tlv_enable == 'dot1_tlv': + if self.ifname: + intf_type = get_interface_type(self.ifname) + if not intf_type: + self.module.fail_json( + msg='Error: ifname name of %s ' + 'is error.' % self.ifname) + if (len(self.ifname) < 1) or (len(self.ifname) > 63): + self.module.fail_json( + msg='Error: Ifname length is beetween 1 and 63.') + + def check_response(self, xml_str, xml_name): + """Check if response message is already OK""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def get_lldp_enable_pre_config(self): + """Get lldp enable configure""" + + lldp_dict = dict() + lldp_config = list() + conf_enable_str = CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG + conf_enable_obj = get_nc_config(self.module, conf_enable_str) + xml_enable_str = conf_enable_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get lldp enable config info + root_enable = ElementTree.fromstring(xml_enable_str) + ntpsite_enable = root_enable.findall("lldp/lldpSys") + for nexthop_enable in ntpsite_enable: + for ele_enable in nexthop_enable: + if ele_enable.tag in ["lldpEnable"]: + lldp_dict[ele_enable.tag] = ele_enable.text + if lldp_dict['lldpEnable'] == 'enabled': + self.enable_flag = 1 + lldp_config.append(dict(lldpenable=lldp_dict['lldpEnable'])) + return lldp_config + + def get_interface_lldp_disable_pre_config(self): + """Get interface undo lldp disable configure""" + lldp_dict = dict() + interface_lldp_disable_dict = dict() + if self.enable_flag == 1: + conf_enable_str = CE_NC_GET_INTERFACE_LLDP_CONFIG + conf_enable_obj = get_nc_config(self.module, conf_enable_str) + if "" in conf_enable_obj: + return + xml_enable_str = conf_enable_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_enable_str) + lldp_disable_enable = root.findall("lldp/lldpInterfaces/lldpInterface") + for nexthop_enable in lldp_disable_enable: + name = nexthop_enable.find("ifName") + status = nexthop_enable.find("lldpAdminStatus") + if name is not None and status is not None: + interface_lldp_disable_dict[name.text] = status.text + return interface_lldp_disable_dict + + def get_interface_lldp_disable_config(self): + lldp_config = list() + interface_lldp_disable_dict_tmp = dict() + if self.state == "present": + if self.ifname: + interface_lldp_disable_dict_tmp = self.get_interface_lldp_disable_pre_config() + key_list = interface_lldp_disable_dict_tmp.keys() + if len(key_list) != 0: + for key in key_list: + if key == self.ifname: + if interface_lldp_disable_dict_tmp[key] != self.lldpadminstatus: + self.conf_interface_lldp_disable_exsit = True + else: + self.conf_interface_lldp_disable_exsit = False + elif self.ifname not in key_list: + self.conf_interface_lldp_disable_exsit = True + elif (len(key_list) == 0) and self.ifname and self.lldpadminstatus: + self.conf_interface_lldp_disable_exsit = True + lldp_config.append(interface_lldp_disable_dict_tmp) + return lldp_config + + def get_interface_tlv_disable_config(self): + lldp_config = list() + lldp_dict = dict() + cur_interface_mdn_cfg = dict() + exp_interface_mdn_cfg = dict() + + if self.enable_flag == 1: + conf_str = CE_NC_GET_INTERFACE_TLV_DISABLE_CONFIG + conf_obj = get_nc_config(self.module, conf_str) + if "" in conf_obj: + return lldp_config + xml_str = conf_obj.replace('\r', '').replace('\n', '') + xml_str = xml_str.replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "") + xml_str = xml_str.replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + lldp_tlvdisable_ifname = root.findall("lldp/lldpInterfaces/lldpInterface") + for ele in lldp_tlvdisable_ifname: + ifname_tmp = ele.find("ifName") + manaddrtxenable_tmp = ele.find("tlvTxEnable/manAddrTxEnable") + portdesctxenable_tmp = ele.find("tlvTxEnable/portDescTxEnable") + syscaptxenable_tmp = ele.find("tlvTxEnable/sysCapTxEnable") + sysdesctxenable_tmp = ele.find("tlvTxEnable/sysDescTxEnable") + sysnametxenable_tmp = ele.find("tlvTxEnable/sysNameTxEnable") + linkaggretxenable_tmp = ele.find("tlvTxEnable/linkAggreTxEnable") + macphytxenable_tmp = ele.find("tlvTxEnable/macPhyTxEnable") + maxframetxenable_tmp = ele.find("tlvTxEnable/maxFrameTxEnable") + eee_tmp = ele.find("tlvTxEnable/eee") + if ifname_tmp is not None: + if ifname_tmp.text is not None: + cur_interface_mdn_cfg["ifname"] = ifname_tmp.text + if ifname_tmp is not None and manaddrtxenable_tmp is not None: + if manaddrtxenable_tmp.text is not None: + cur_interface_mdn_cfg["manaddrtxenable"] = manaddrtxenable_tmp.text + if ifname_tmp is not None and portdesctxenable_tmp is not None: + if portdesctxenable_tmp.text is not None: + cur_interface_mdn_cfg['portdesctxenable'] = portdesctxenable_tmp.text + if ifname_tmp is not None and syscaptxenable_tmp is not None: + if syscaptxenable_tmp.text is not None: + cur_interface_mdn_cfg['syscaptxenable'] = syscaptxenable_tmp.text + if ifname_tmp is not None and sysdesctxenable_tmp is not None: + if sysdesctxenable_tmp.text is not None: + cur_interface_mdn_cfg['sysdesctxenable'] = sysdesctxenable_tmp.text + if ifname_tmp is not None and sysnametxenable_tmp is not None: + if sysnametxenable_tmp.text is not None: + cur_interface_mdn_cfg['sysnametxenable'] = sysnametxenable_tmp.text + if ifname_tmp is not None and linkaggretxenable_tmp is not None: + if linkaggretxenable_tmp.text is not None: + cur_interface_mdn_cfg['linkaggretxenable'] = linkaggretxenable_tmp.text + if ifname_tmp is not None and macphytxenable_tmp is not None: + if macphytxenable_tmp.text is not None: + cur_interface_mdn_cfg['macphytxenable'] = macphytxenable_tmp.text + if ifname_tmp is not None and maxframetxenable_tmp is not None: + if maxframetxenable_tmp.text is not None: + cur_interface_mdn_cfg['maxframetxenable'] = maxframetxenable_tmp.text + if ifname_tmp is not None and eee_tmp is not None: + if eee_tmp.text is not None: + cur_interface_mdn_cfg['eee'] = eee_tmp.text + if self.state == "present": + if self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + if self.type_tlv_disable == 'basic_tlv': + if self.ifname: + exp_interface_mdn_cfg['ifname'] = self.ifname + if self.manaddrtxenable: + exp_interface_mdn_cfg['manaddrtxenable'] = self.manaddrtxenable + if self.portdesctxenable: + exp_interface_mdn_cfg['portdesctxenable'] = self.portdesctxenable + if self.syscaptxenable: + exp_interface_mdn_cfg['syscaptxenable'] = self.syscaptxenable + if self.sysdesctxenable: + exp_interface_mdn_cfg['sysdesctxenable'] = self.sysdesctxenable + if self.sysnametxenable: + exp_interface_mdn_cfg['sysnametxenable'] = self.sysnametxenable + if self.ifname == ifname_tmp.text: + key_list = exp_interface_mdn_cfg.keys() + key_list_cur = cur_interface_mdn_cfg.keys() + if len(key_list) != 0: + for key in key_list: + if key == "ifname" and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(ifname=cur_interface_mdn_cfg['ifname'])) + if "manaddrtxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(manaddrtxenable=cur_interface_mdn_cfg['manaddrtxenable'])) + if "portdesctxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(portdesctxenable=cur_interface_mdn_cfg['portdesctxenable'])) + if "syscaptxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(syscaptxenable=cur_interface_mdn_cfg['syscaptxenable'])) + if "sysdesctxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(sysdesctxenable=cur_interface_mdn_cfg['sysdesctxenable'])) + if "sysnametxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(sysnametxenable=cur_interface_mdn_cfg['sysnametxenable'])) + if key in key_list_cur: + if str(exp_interface_mdn_cfg[key]) != str(cur_interface_mdn_cfg[key]): + self.conf_tlv_disable_exsit = True + self.changed = True + return lldp_config + else: + self.conf_tlv_disable_exsit = True + return lldp_config + + if self.type_tlv_disable == 'dot3_tlv': + if self.ifname: + exp_interface_mdn_cfg['ifname'] = self.ifname + if self.linkaggretxenable: + exp_interface_mdn_cfg['linkaggretxenable'] = self.linkaggretxenable + if self.macphytxenable: + exp_interface_mdn_cfg['macphytxenable'] = self.macphytxenable + if self.maxframetxenable: + exp_interface_mdn_cfg['maxframetxenable'] = self.maxframetxenable + if self.eee: + exp_interface_mdn_cfg['eee'] = self.eee + if self.ifname == ifname_tmp.text: + key_list = exp_interface_mdn_cfg.keys() + key_list_cur = cur_interface_mdn_cfg.keys() + if len(key_list) != 0: + for key in key_list: + if key == "ifname" and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(ifname=cur_interface_mdn_cfg['ifname'])) + if "linkaggretxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(linkaggretxenable=cur_interface_mdn_cfg['linkaggretxenable'])) + if "macphytxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(macphytxenable=cur_interface_mdn_cfg['macphytxenable'])) + if "maxframetxenable" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(maxframetxenable=cur_interface_mdn_cfg['maxframetxenable'])) + if "eee" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(eee=cur_interface_mdn_cfg['eee'])) + if key in key_list_cur: + if str(exp_interface_mdn_cfg[key]) != str(cur_interface_mdn_cfg[key]): + self.conf_tlv_disable_exsit = True + self.changed = True + return lldp_config + else: + self.conf_tlv_disable_exsit = True + return lldp_config + return lldp_config + + def get_interface_tlv_enable_config(self): + lldp_config = list() + lldp_dict = dict() + cur_interface_mdn_cfg = dict() + exp_interface_mdn_cfg = dict() + if self.enable_flag == 1: + conf_str = CE_NC_GET_INTERFACE_TLV_ENABLE_CONFIG + conf_obj = get_nc_config(self.module, conf_str) + if "" in conf_obj: + return lldp_config + xml_str = conf_obj.replace('\r', '').replace('\n', '') + xml_str = xml_str.replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "") + xml_str = xml_str.replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + lldpenablesite = root.findall("lldp/lldpInterfaces/lldpInterface") + for ele in lldpenablesite: + ifname_tmp = ele.find("ifName") + protoidtxenable_tmp = ele.find("tlvTxEnable/protoIdTxEnable") + dcbx_tmp = ele.find("tlvTxEnable/dcbx") + if ifname_tmp is not None: + if ifname_tmp.text is not None: + cur_interface_mdn_cfg["ifname"] = ifname_tmp.text + if ifname_tmp is not None and protoidtxenable_tmp is not None: + if protoidtxenable_tmp.text is not None: + cur_interface_mdn_cfg["protoidtxenable"] = protoidtxenable_tmp.text + if ifname_tmp is not None and dcbx_tmp is not None: + if dcbx_tmp.text is not None: + cur_interface_mdn_cfg['dcbx'] = dcbx_tmp.text + if self.state == "present": + if self.function_lldp_interface_flag == 'tlvenableINTERFACE': + if self.type_tlv_enable == 'dot1_tlv': + if self.ifname: + exp_interface_mdn_cfg['ifname'] = self.ifname + if self.protoidtxenable: + exp_interface_mdn_cfg['protoidtxenable'] = self.protoidtxenable + if self.ifname == ifname_tmp.text: + key_list = exp_interface_mdn_cfg.keys() + key_list_cur = cur_interface_mdn_cfg.keys() + if len(key_list) != 0: + for key in key_list: + if "protoidtxenable" == str(key) and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(protoidtxenable=cur_interface_mdn_cfg['protoidtxenable'])) + if key in key_list_cur: + if str(exp_interface_mdn_cfg[key]) != str(cur_interface_mdn_cfg[key]): + self.conf_tlv_enable_exsit = True + self.changed = True + return lldp_config + else: + self.conf_tlv_enable_exsit = True + return lldp_config + if self.type_tlv_enable == 'dcbx': + if self.ifname: + exp_interface_mdn_cfg['ifname'] = self.ifname + if self.dcbx: + exp_interface_mdn_cfg['dcbx'] = self.dcbx + if self.ifname == ifname_tmp.text: + key_list = exp_interface_mdn_cfg.keys() + key_list_cur = cur_interface_mdn_cfg.keys() + if len(key_list) != 0: + for key in key_list: + if "dcbx" == key and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(dcbx=cur_interface_mdn_cfg['dcbx'])) + if key in key_list_cur: + if str(exp_interface_mdn_cfg[key]) != str(cur_interface_mdn_cfg[key]): + self.conf_tlv_enable_exsit = True + self.changed = True + return lldp_config + else: + self.conf_tlv_enable_exsit = True + return lldp_config + return lldp_config + + def get_interface_interval_config(self): + lldp_config = list() + lldp_dict = dict() + cur_interface_mdn_cfg = dict() + exp_interface_mdn_cfg = dict() + interface_lldp_disable_dict_tmp2 = self.get_interface_lldp_disable_pre_config() + if self.enable_flag == 1: + if interface_lldp_disable_dict_tmp2[self.ifname] != 'disabled': + conf_str = CE_NC_GET_INTERFACE_INTERVAl_CONFIG + conf_obj = get_nc_config(self.module, conf_str) + if "" in conf_obj: + return lldp_config + xml_str = conf_obj.replace('\r', '').replace('\n', '') + xml_str = xml_str.replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "") + xml_str = xml_str.replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + txintervalsite = root.findall("lldp/lldpInterfaces/lldpInterface") + for ele in txintervalsite: + ifname_tmp = ele.find("ifName") + txinterval_tmp = ele.find("msgInterval/txInterval") + if ifname_tmp is not None: + if ifname_tmp.text is not None: + cur_interface_mdn_cfg["ifname"] = ifname_tmp.text + if txinterval_tmp is not None: + if txinterval_tmp.text is not None: + cur_interface_mdn_cfg["txinterval"] = txinterval_tmp.text + if self.state == "present": + if self.ifname: + exp_interface_mdn_cfg["ifname"] = self.ifname + if self.txinterval: + exp_interface_mdn_cfg["txinterval"] = self.txinterval + if self.ifname == ifname_tmp.text: + key_list = exp_interface_mdn_cfg.keys() + key_list_cur = cur_interface_mdn_cfg.keys() + if len(key_list) != 0: + for key in key_list: + if "txinterval" == str(key) and self.ifname == cur_interface_mdn_cfg['ifname']: + lldp_config.append(dict(ifname=cur_interface_mdn_cfg['ifname'], txinterval=exp_interface_mdn_cfg['txinterval'])) + if key in key_list_cur: + if str(exp_interface_mdn_cfg[key]) != str(cur_interface_mdn_cfg[key]): + self.conf_interval_exsit = True + lldp_config.append(cur_interface_mdn_cfg) + return lldp_config + else: + self.conf_interval_exsit = True + return lldp_config + return lldp_config + + def config_global_lldp_enable(self): + if self.state == 'present': + if self.enable_flag == 0 and self.lldpenable == 'enabled': + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_ENABLE_CONFIG") + self.changed = True + elif self.enable_flag == 1 and self.lldpenable == 'disabled': + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_ENABLE_CONFIG") + self.changed = True + + def config_interface_lldp_disable_config(self): + if self.function_lldp_interface_flag == 'disableINTERFACE': + if self.enable_flag == 1 and self.conf_interface_lldp_disable_exsit: + if self.ifname: + xml_str = CE_NC_MERGE_INTERFACE_LLDP_CONFIG % (self.ifname, self.lldpadminstatus) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "INTERFACE_LLDP_DISABLE_CONFIG") + self.changed = True + + def config_interface_tlv_disable_config(self): + if self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + if self.enable_flag == 1 and self.conf_tlv_disable_exsit: + if self.type_tlv_disable == 'basic_tlv': + if self.ifname: + if self.portdesctxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_PORTDESCTXENABLE % self.portdesctxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_PORTDESCTXENABLE") + self.changed = True + if self.manaddrtxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_MANADDRTXENABLE % self.manaddrtxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_MANADDRTXENABLE") + self.changed = True + if self.syscaptxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_SYSCAPTXENABLE % self.syscaptxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_SYSCAPTXENABLE") + self.changed = True + if self.sysdesctxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_SYSDESCTXENABLE % self.sysdesctxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_SYSDESCTXENABLE") + self.changed = True + if self.sysnametxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_SYSNAMETXENABLE % self.sysnametxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_SYSNAMETXENABLE") + self.changed = True + if self.type_tlv_disable == 'dot3_tlv': + if self.ifname: + if self.linkaggretxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_LINKAGGRETXENABLE % self.linkaggretxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_LINKAGGRETXENABLE") + self.changed = True + if self.macphytxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_MACPHYTXENABLE % self.macphytxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_MACPHYTXENABLE") + self.changed = True + if self.maxframetxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_MAXFRAMETXENABLE % self.maxframetxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_MAXFRAMETXENABLE") + self.changed = True + if self.eee: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_DISABLE_EEE % self.eee) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_DISABLE_EEE") + self.changed = True + + def config_interface_tlv_enable_config(self): + if self.function_lldp_interface_flag == 'tlvenableINTERFACE': + if self.enable_flag == 1 and self.conf_tlv_enable_exsit: + if self.type_tlv_enable == 'dot1_tlv': + if self.ifname: + if self.protoidtxenable: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_ENABLE_PROTOIDTXENABLE % self.protoidtxenable) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_ENABLE_DOT1_PORT_VLAN") + self.changed = True + if self.type_tlv_enable == 'dcbx': + if self.ifname: + if self.dcbx: + xml_str = (CE_NC_MERGE_INTERFACE_TLV_CONFIG_HEADER % self.ifname) + \ + (CE_NC_MERGE_INTERFACE_TLV_CONFIG_ENABLE_DCBX % self.dcbx) + \ + CE_NC_MERGE_INTERFACE_TLV_CONFIG_TAIL + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "TLV_ENABLE_DCBX_VLAN") + self.changed = True + + def config_interface_interval_config(self): + if self.function_lldp_interface_flag == 'intervalINTERFACE': + tmp = self.get_interface_lldp_disable_pre_config() + if self.enable_flag == 1 and self.conf_interval_exsit and tmp[self.ifname] != 'disabled': + if self.ifname: + if self.txinterval: + xml_str = CE_NC_MERGE_INTERFACE_INTERVAl_CONFIG % (self.ifname, self.txinterval) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "INTERFACE_INTERVAL_CONFIG") + self.changed = True + + def get_existing(self): + """get existing information""" + self.get_lldp_enable_pre_config() + if self.lldpenable: + self.existing['globalLLDPENABLE'] = self.get_lldp_enable_pre_config() + if self.function_lldp_interface_flag == 'disableINTERFACE': + self.existing['disableINTERFACE'] = self.get_interface_lldp_disable_config() + if self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + self.existing['tlvdisableINTERFACE'] = self.get_interface_tlv_disable_config() + if self.function_lldp_interface_flag == 'tlvenableINTERFACE': + self.existing['tlvenableINTERFACE'] = self.get_interface_tlv_enable_config() + if self.function_lldp_interface_flag == 'intervalINTERFACE': + self.existing['intervalINTERFACE'] = self.get_interface_interval_config() + + def get_proposed(self): + """get proposed""" + if self.lldpenable: + self.proposed = dict(lldpenable=self.lldpenable) + if self.function_lldp_interface_flag == 'disableINTERFACE': + if self.enable_flag == 1: + self.proposed = dict(ifname=self.ifname, lldpadminstatus=self.lldpadminstatus) + if self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + if self.enable_flag == 1: + if self.type_tlv_disable == 'basic_tlv': + if self.ifname: + if self.manaddrtxenable: + self.proposed = dict(ifname=self.ifname, manaddrtxenable=self.manaddrtxenable) + if self.portdesctxenable: + self.proposed = dict(ifname=self.ifname, portdesctxenable=self.portdesctxenable) + if self.syscaptxenable: + self.proposed = dict(ifname=self.ifname, syscaptxenable=self.syscaptxenable) + if self.sysdesctxenable: + self.proposed = dict(ifname=self.ifname, sysdesctxenable=self.sysdesctxenable) + if self.sysnametxenable: + self.proposed = dict(ifname=self.ifname, sysnametxenable=self.sysnametxenable) + if self.type_tlv_disable == 'dot3_tlv': + if self.ifname: + if self.linkaggretxenable: + self.proposed = dict(ifname=self.ifname, linkaggretxenable=self.linkaggretxenable) + if self.macphytxenable: + self.proposed = dict(ifname=self.ifname, macphytxenable=self.macphytxenable) + if self.maxframetxenable: + self.proposed = dict(ifname=self.ifname, maxframetxenable=self.maxframetxenable) + if self.eee: + self.proposed = dict(ifname=self.ifname, eee=self.eee) + if self.function_lldp_interface_flag == 'tlvenableINTERFACE': + if self.enable_flag == 1: + if self.type_tlv_enable == 'dot1_tlv': + if self.ifname: + if self.protoidtxenable: + self.proposed = dict(ifname=self.ifname, protoidtxenable=self.protoidtxenable) + if self.type_tlv_enable == 'dcbx': + if self.ifname: + if self.dcbx: + self.proposed = dict(ifname=self.ifname, dcbx=self.dcbx) + if self.function_lldp_interface_flag == 'intervalINTERFACE': + tmp1 = self.get_interface_lldp_disable_pre_config() + if self.enable_flag == 1 and tmp1[self.ifname] != 'disabled': + self.proposed = dict(ifname=self.ifname, txinterval=self.txinterval) + + def config_lldp_interface(self): + """config lldp interface""" + if self.lldpenable: + self.config_global_lldp_enable() + if self.function_lldp_interface_flag == 'disableINTERFACE': + self.config_interface_lldp_disable_config() + elif self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + self.config_interface_tlv_disable_config() + elif self.function_lldp_interface_flag == 'tlvenableINTERFACE': + self.config_interface_tlv_enable_config() + elif self.function_lldp_interface_flag == 'intervalINTERFACE': + self.config_interface_interval_config() + + def get_end_state(self): + """get end_state information""" + self.get_lldp_enable_pre_config() + if self.lldpenable: + self.end_state['globalLLDPENABLE'] = self.get_lldp_enable_pre_config() + if self.function_lldp_interface_flag == 'disableINTERFACE': + self.end_state['disableINTERFACE'] = self.get_interface_lldp_disable_config() + if self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + self.end_state['tlvdisableINTERFACE'] = self.get_interface_tlv_disable_config() + if self.function_lldp_interface_flag == 'tlvenableINTERFACE': + self.end_state['tlvenableINTERFACE'] = self.get_interface_tlv_enable_config() + if self.function_lldp_interface_flag == 'intervalINTERFACE': + self.end_state['intervalINTERFACE'] = self.get_interface_interval_config() + + def get_update_cmd(self): + """Get updated commands""" + + cmds = [] + if self.state == "present": + if self.lldpenable == "enabled": + cmds.append("lldp enable") + if self.function_lldp_interface_flag == 'disableINTERFACE': + if self.ifname: + cmds.append("%s %s" % ("interface", self.ifname)) + if self.lldpadminstatus == 'disabled': + cmds.append("lldp disable") + else: + cmds.append("undo lldp disable") + elif self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + if self.type_tlv_disable == 'basic_tlv': + if self.ifname: + cmds.append("%s %s" % ("interface", self.ifname)) + if self.manaddrtxenable: + if self.manaddrtxenable == "false": + cmds.append("lldp tlv-disable basic-tlv management-address") + if self.manaddrtxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv management-address") + if self.portdesctxenable: + if self.portdesctxenable == "false": + cmds.append("lldp tlv-disable basic-tlv port-description") + if self.portdesctxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv port-description") + if self.syscaptxenable: + if self.syscaptxenable == "false": + cmds.append("lldp tlv-disable basic-tlv system-capability") + if self.syscaptxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv system-capability") + if self.sysdesctxenable: + if self.sysdesctxenable == "false": + cmds.append("lldp tlv-disable basic-tlv system-description") + if self.sysdesctxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv system-description") + if self.sysnametxenable: + if self.sysnametxenable == "false": + cmds.append("lldp tlv-disable basic-tlv system-name") + if self.sysnametxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv system-name") + if self.type_tlv_disable == 'dot3_tlv': + if self.ifname: + cmds.append("%s %s" % ("interface", self.ifname)) + if self.linkaggretxenable: + if self.linkaggretxenable == "false": + cmds.append("lldp tlv-disable dot3-tlv link-aggregation") + if self.linkaggretxenable == "true": + cmds.append("undo lldp tlv-disable dot3-tlv link-aggregation") + if self.macphytxenable: + if self.macphytxenable == "false": + cmds.append("lldp tlv-disable dot3-tlv mac-physic") + if self.macphytxenable == "true": + cmds.append("undo lldp tlv-disable dot3-tlv mac-physic") + if self.maxframetxenable: + if self.maxframetxenable == "false": + cmds.append("lldp tlv-disable dot3-tlv max-frame-size") + if self.maxframetxenable == "true": + cmds.append("undo lldp tlv-disable dot3-tlv max-frame-size") + if self.eee: + if self.eee == "false": + cmds.append("lldp tlv-disable dot3-tlv eee") + if self.eee == "true": + cmds.append("undo lldp tlv-disable dot3-tlv eee") + elif self.function_lldp_interface_flag == 'tlvenableINTERFACE': + if self.type_tlv_enable == 'dot1_tlv': + if self.ifname: + cmds.append("%s %s" % ("interface", self.ifname)) + if self.protoidtxenable: + if self.protoidtxenable == "false": + cmds.append("undo lldp tlv-enable dot1-tlv protocol-identity") + if self.protoidtxenable == "true": + cmds.append("lldp tlv-enable dot1-tlv protocol-identity") + if self.type_tlv_enable == 'dcbx': + if self.ifname: + cmds.append("%s %s" % ("interface", self.ifname)) + if self.dcbx: + if self.dcbx == "false": + cmds.append("undo lldp tlv-enable dcbx") + if self.dcbx == "true": + cmds.append("lldp tlv-enable dcbx") + elif self.function_lldp_interface_flag == 'intervalINTERFACE': + if self.ifname: + cmds.append("%s %s" % ("interface", self.ifname)) + if self.txinterval: + cmds.append("lldp transmit fast-mode interval %s" % self.txinterval) + elif self.lldpenable == "disabled": + cmds.append("undo lldp enable") + else: + if self.enable_flag == 1: + if self.function_lldp_interface_flag == 'disableINTERFACE': + if self.ifname: + cmds.append("interface %s" % self.ifname) + if self.lldpadminstatus == 'disabled': + cmds.append("lldp disable") + else: + cmds.append("undo lldp disable") + elif self.function_lldp_interface_flag == 'tlvdisableINTERFACE': + if self.type_tlv_disable == 'basic_tlv': + if self.ifname: + cmds.append("interface %s" % self.ifname) + if self.manaddrtxenable: + if self.manaddrtxenable == "false": + cmds.append("lldp tlv-disable basic-tlv management-address") + if self.manaddrtxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv management-address") + if self.portdesctxenable: + if self.portdesctxenable == "false": + cmds.append("lldp tlv-disable basic-tlv port-description") + if self.portdesctxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv port-description") + if self.syscaptxenable: + if self.syscaptxenable == "false": + cmds.append("lldp tlv-disable basic-tlv system-capability") + if self.syscaptxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv system-capability") + if self.sysdesctxenable: + if self.sysdesctxenable == "false": + cmds.append("lldp tlv-disable basic-tlv system-description") + if self.sysdesctxenable == "true": + cli_str = "%s %s\n" % (cli_str, "undo lldp tlv-disable basic-tlv system-description") + if self.sysnametxenable: + if self.sysnametxenable == "false": + cmds.append("lldp tlv-disable basic-tlv system-name") + if self.sysnametxenable == "true": + cmds.append("undo lldp tlv-disable basic-tlv system-name") + if self.type_tlv_disable == 'dot3_tlv': + if self.ifname: + cmds.append("interface %s" % self.ifname) + if self.linkaggretxenable: + if self.linkaggretxenable == "false": + cmds.append("lldp tlv-disable dot3-tlv link-aggregation") + if self.linkaggretxenable == "true": + cmds.append("undo lldp tlv-disable dot3-tlv link-aggregation") + if self.macphytxenable: + if self.macphytxenable == "false": + cmds.append("lldp tlv-disable dot3-tlv mac-physic") + if self.macphytxenable == "true": + cli_str = "%s %s\n" % (cli_str, "undo lldp tlv-disable dot3-tlv mac-physic") + if self.maxframetxenable: + if self.maxframetxenable == "false": + cmds.append("lldp tlv-disable dot3-tlv max-frame-size") + if self.maxframetxenable == "true": + cmds.append("undo lldp tlv-disable dot3-tlv max-frame-size") + if self.eee: + if self.eee == "false": + cmds.append("lldp tlv-disable dot3-tlv eee") + if self.eee == "true": + cmds.append("undo lldp tlv-disable dot3-tlv eee") + elif self.function_lldp_interface_flag == 'tlvenableINTERFACE': + if self.type_tlv_enable == 'dot1_tlv': + if self.ifname: + cmds.append("interface %s" % self.ifname) + if self.protoidtxenable: + if self.protoidtxenable == "false": + cmds.append("undo lldp tlv-enable dot1-tlv protocol-identity") + if self.protoidtxenable == "true": + cmds.append("lldp tlv-enable dot1-tlv protocol-identity") + if self.type_tlv_enable == 'dcbx': + if self.ifname: + cmds.append("interface %s" % self.ifname) + if self.dcbx: + if self.dcbx == "false": + cmds.append("undo lldp tlv-enable dcbx") + if self.dcbx == "true": + cmds.append("lldp tlv-enable dcbx") + elif self.function_lldp_interface_flag == 'intervalINTERFACE': + if self.ifname: + cmds.append("interface %s" % self.ifname) + if self.txinterval: + cmds.append("lldp transmit fast-mode interval %s" % self.txinterval) + self.updates_cmd = cmds + + def work(self): + """Execute task""" + self.check_params() + self.get_existing() + self.get_proposed() + self.config_lldp_interface() + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """Main function""" + + argument_spec = dict( + lldpenable=dict(choices=['enabled', 'disabled']), + function_lldp_interface_flag=dict(choices=['disableINTERFACE', 'tlvdisableINTERFACE', 'tlvenableINTERFACE', 'intervalINTERFACE'], type='str'), + type_tlv_disable=dict(choices=['basic_tlv', 'dot3_tlv'], type='str'), + type_tlv_enable=dict(choices=['dot1_tlv', 'dcbx'], type='str'), + ifname=dict(type='str'), + lldpadminstatus=dict(choices=['txOnly', 'rxOnly', 'txAndRx', 'disabled'], type='str'), + manaddrtxenable=dict(type='bool'), + portdesctxenable=dict(type='bool'), + syscaptxenable=dict(type='bool'), + sysdesctxenable=dict(type='bool'), + sysnametxenable=dict(type='bool'), + portvlantxenable=dict(type='bool'), + protovlantxenable=dict(type='bool'), + txprotocolvlanid=dict(type='int'), + vlannametxenable=dict(type='bool'), + txvlannameid=dict(type='int'), + txinterval=dict(type='int'), + protoidtxenable=dict(type='bool'), + macphytxenable=dict(type='bool'), + linkaggretxenable=dict(type='bool'), + maxframetxenable=dict(type='bool'), + eee=dict(type='bool'), + dcbx=dict(type='bool'), + state=dict(type='str', choices=['absent', 'present'], default='present'), + ) + + lldp_interface_obj = Lldp_interface(argument_spec) + lldp_interface_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_mdn_interface.py b/plugins/modules/ce_mdn_interface.py new file mode 100644 index 0000000..92edf77 --- /dev/null +++ b/plugins/modules/ce_mdn_interface.py @@ -0,0 +1,402 @@ +#!/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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- + +module: ce_mdn_interface +short_description: Manages MDN configuration on HUAWEI CloudEngine switches. +description: + - Manages MDN configuration on HUAWEI CloudEngine switches. +author: xuxiaowei0512 (@CloudEngine-Ansible) +options: + lldpenable: + description: + - Set global LLDP enable state. + type: str + choices: ['enabled', 'disabled'] + mdnstatus: + description: + - Set interface MDN enable state. + type: str + choices: ['rxOnly', 'disabled'] + ifname: + description: + - Interface name. + type: str + state: + description: + - Manage the state of the resource. + default: present + type: str + choices: ['present','absent'] +notes: + - This module requires the netconf system service be enabled on + the remote device being managed. + - This module works with connection C(netconf). +''' + +EXAMPLES = ''' + - name: "Configure global LLDP enable state" + ce_mdn_interface: + lldpenable: enabled + + - name: "Configure interface MDN enable state" + ce_mdn_interface: + ifname: 10GE1/0/1 + mdnstatus: rxOnly +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "ifname": "10GE1/0/1", + "mdnstatus": "rxOnly", + "state":"present" + } +existing: + description: k/v pairs of existing global LLDP configration + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "ifname": "10GE1/0/1", + "mdnstatus": "disabled" + } +end_state: + description: k/v pairs of global LLDP configration after module execution + returned: always + type: dict + sample: { + "lldpenable": "enabled", + "ifname": "10GE1/0/1", + "mdnstatus": "rxOnly" + } +updates: + description: command sent to the device + returned: always + type: list + sample: [ + "interface 10ge 1/0/1", + "lldp mdn enable", + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import copy +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import set_nc_config, get_nc_config, execute_nc_action + +CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG = """ + + + + + + + +""" + +CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG = """ + + + + %s + + + +""" + +CE_NC_GET_INTERFACE_MDNENABLE_CONFIG = """ + + + + + + + + + + +""" + +CE_NC_MERGE_INTERFACE_MDNENABLE_CONFIG = """ + + + + + %s + %s + + + + +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('PORT-GROUP'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + return iftype.lower() + + +class Interface_mdn(object): + """Manage global lldp enable configration""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # LLDP global configration info + self.lldpenable = self.module.params['lldpenable'] or None + self.ifname = self.module.params['ifname'] + self.mdnstatus = self.module.params['mdnstatus'] or None + self.state = self.module.params['state'] + self.lldp_conf = dict() + self.conf_exsit = False + self.enable_flag = 0 + self.check_params() + + # state + self.changed = False + self.proposed_changed = dict() + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def check_params(self): + """Check all input params""" + + if self.ifname: + intf_type = get_interface_type(self.ifname) + if not intf_type: + self.module.fail_json( + msg='Error: ifname name of %s ' + 'is error.' % self.ifname) + if (len(self.ifname) < 1) or (len(self.ifname) > 63): + self.module.fail_json( + msg='Error: Ifname length is beetween 1 and 63.') + + def init_module(self): + """Init module object""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def config_interface_mdn(self): + """Configure lldp enabled and interface mdn enabled parameters""" + + if self.state == 'present': + if self.enable_flag == 0 and self.lldpenable == 'enabled': + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_ENABLE_CONFIG") + self.changed = True + elif self.enable_flag == 1 and self.lldpenable == 'disabled': + xml_str = CE_NC_MERGE_GLOBA_LLDPENABLE_CONFIG % self.lldpenable + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "LLDP_ENABLE_CONFIG") + self.changed = True + elif self.enable_flag == 1 and self.conf_exsit: + xml_str = CE_NC_MERGE_INTERFACE_MDNENABLE_CONFIG % (self.ifname, self.mdnstatus) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "INTERFACE_MDN_ENABLE_CONFIG") + self.changed = True + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def get_interface_mdn_exist_config(self): + """Get lldp existed configure""" + + lldp_config = list() + lldp_dict = dict() + conf_enable_str = CE_NC_GET_GLOBAL_LLDPENABLE_CONFIG + conf_enable_obj = get_nc_config(self.module, conf_enable_str) + xml_enable_str = conf_enable_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get lldp enable config info + root_enable = ElementTree.fromstring(xml_enable_str) + ntpsite_enable = root_enable.findall("lldp/lldpSys") + for nexthop_enable in ntpsite_enable: + for ele_enable in nexthop_enable: + if ele_enable.tag in ["lldpEnable"]: + lldp_dict[ele_enable.tag] = ele_enable.text + + if self.state == "present": + if lldp_dict['lldpEnable'] == 'enabled': + self.enable_flag = 1 + lldp_config.append(dict(lldpenable=lldp_dict['lldpEnable'])) + + if self.enable_flag == 1: + conf_str = CE_NC_GET_INTERFACE_MDNENABLE_CONFIG + conf_obj = get_nc_config(self.module, conf_str) + if "" in conf_obj: + return lldp_config + xml_str = conf_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + # get all ntp config info + root = ElementTree.fromstring(xml_str) + ntpsite = root.findall("lldp/mdnInterfaces/mdnInterface") + for nexthop in ntpsite: + for ele in nexthop: + if ele.tag in ["ifName", "mdnStatus"]: + lldp_dict[ele.tag] = ele.text + if self.state == "present": + cur_interface_mdn_cfg = dict(ifname=lldp_dict['ifName'], mdnstatus=lldp_dict['mdnStatus']) + exp_interface_mdn_cfg = dict(ifname=self.ifname, mdnstatus=self.mdnstatus) + if self.ifname == lldp_dict['ifName']: + if cur_interface_mdn_cfg != exp_interface_mdn_cfg: + self.conf_exsit = True + lldp_config.append(dict(ifname=lldp_dict['ifName'], mdnstatus=lldp_dict['mdnStatus'])) + return lldp_config + lldp_config.append(dict(ifname=lldp_dict['ifName'], mdnstatus=lldp_dict['mdnStatus'])) + return lldp_config + + def get_existing(self): + """Get existing info""" + + self.existing = self.get_interface_mdn_exist_config() + + def get_proposed(self): + """Get proposed info""" + + if self.lldpenable: + self.proposed = dict(lldpenable=self.lldpenable) + if self.enable_flag == 1: + if self.ifname: + self.proposed = dict(ifname=self.ifname, mdnstatus=self.mdnstatus) + + def get_end_state(self): + """Get end state info""" + + self.end_state = self.get_interface_mdn_exist_config() + + def get_update_cmd(self): + """Get updated commands""" + + update_list = list() + if self.state == "present": + if self.lldpenable == "enabled": + cli_str = "lldp enable" + update_list.append(cli_str) + if self.ifname: + cli_str = "%s %s" % ("interface", self.ifname) + update_list.append(cli_str) + if self.mdnstatus: + if self.mdnstatus == "rxOnly": + cli_str = "lldp mdn enable" + update_list.append(cli_str) + else: + cli_str = "undo lldp mdn enable" + update_list.append(cli_str) + + elif self.lldpenable == "disabled": + cli_str = "undo lldp enable" + update_list.append(cli_str) + else: + if self.enable_flag == 1: + if self.ifname: + cli_str = "%s %s" % ("interface", self.ifname) + update_list.append(cli_str) + if self.mdnstatus: + if self.mdnstatus == "rxOnly": + cli_str = "lldp mdn enable" + update_list.append(cli_str) + else: + cli_str = "undo lldp mdn enable" + update_list.append(cli_str) + + self.updates_cmd.append(update_list) + + def work(self): + """Excute task""" + self.check_params() + self.get_existing() + self.get_proposed() + self.config_interface_mdn() + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + lldpenable=dict(type='str', choices=['enabled', 'disabled']), + mdnstatus=dict(type='str', choices=['rxOnly', 'disabled']), + ifname=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + lldp_obj = Interface_mdn(argument_spec) + lldp_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_mlag_config.py b/plugins/modules/ce_mlag_config.py new file mode 100644 index 0000000..0a6f596 --- /dev/null +++ b/plugins/modules/ce_mlag_config.py @@ -0,0 +1,913 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_mlag_config +short_description: Manages MLAG configuration on HUAWEI CloudEngine switches. +description: + - Manages MLAG configuration on HUAWEI CloudEngine switches. +author: + - Li Yanfeng (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + dfs_group_id: + description: + - ID of a DFS group. The value is 1. + default: present + nickname: + description: + - The nickname bound to a DFS group. The value is an integer that ranges from 1 to 65471. + pseudo_nickname: + description: + - A pseudo nickname of a DFS group. The value is an integer that ranges from 1 to 65471. + pseudo_priority: + description: + - The priority of a pseudo nickname. The value is an integer that ranges from 128 to 255. + The default value is 192. A larger value indicates a higher priority. + ip_address: + description: + - IP address bound to the DFS group. The value is in dotted decimal notation. + vpn_instance_name: + description: + - Name of the VPN instance bound to the DFS group. The value is a string of 1 to 31 case-sensitive + characters without spaces. If the character string is quoted by double quotation marks, the character + string can contain spaces. The value _public_ is reserved and cannot be used as the VPN instance name. + priority_id: + description: + - Priority of a DFS group. The value is an integer that ranges from 1 to 254. The default value is 100. + eth_trunk_id: + description: + - Name of the peer-link interface. The value is in the range from 0 to 511. + peer_link_id: + description: + - Number of the peer-link interface. The value is 1. + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: mlag config module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Create DFS Group id + ce_mlag_config: + dfs_group_id: 1 + provider: "{{ cli }}" + - name: Set dfs-group priority + ce_mlag_config: + dfs_group_id: 1 + priority_id: 3 + state: present + provider: "{{ cli }}" + - name: Set pseudo nickname + ce_mlag_config: + dfs_group_id: 1 + pseudo_nickname: 3 + pseudo_priority: 130 + state: present + provider: "{{ cli }}" + - name: Set ip + ce_mlag_config: + dfs_group_id: 1 + ip_address: 11.1.1.2 + vpn_instance_name: 6 + provider: "{{ cli }}" + - name: Set peer link + ce_mlag_config: + eth_trunk_id: 3 + peer_link_id: 2 + state: present + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { "eth_trunk_id": "3", + "peer_link_id": "1", + "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: { } +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: { "eth_trunk_id": "Eth-Trunk3", + "peer_link_id": "1"} +updates: + description: command sent to the device + returned: always + type: list + sample: {"peer-link 1"} +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_DFS_GROUP_INFO = """ + + + + + + + + + + + + + + + + + +""" +CE_NC_GET_PEER_LINK_INFO = """ + + + + + + + + + + + +""" + +CE_NC_CREATE_DFS_GROUP_INFO_HEADER = """ + + + + + %s +""" + +CE_NC_CREATE_DFS_GROUP_INFO_TAIL = """ + + + + +""" + +CE_NC_MERGE_DFS_GROUP_INFO_HEADER = """ + + + + + %s +""" + +CE_NC_MERGE_DFS_GROUP_INFO_TAIL = """ + + + + +""" + +CE_NC_DELETE_DFS_GROUP_ATTRIBUTE_HEADER = """ + + + + + %s +""" + +CE_NC_DELETE_DFS_GROUP_ATTRIBUTE_TAIL = """ + + + + +""" + +CE_NC_DELETE_DFS_GROUP_INFO_HEADER = """ + + + + + %s +""" + +CE_NC_DELETE_DFS_GROUP_INFO_TAIL = """ + + + + +""" + +CE_NC_CREATE_PEER_LINK_INFO = """ + + + + + 1 + %s + %s + + + + +""" + +CE_NC_MERGE_PEER_LINK_INFO = """ + + + + + 1 + %s + %s + + + + +""" +CE_NC_DELETE_PEER_LINK_INFO = """ + + + + + 1 + %s + %s + + + + +""" + + +def is_valid_address(address): + """check ip-address is valid""" + + if address.find('.') != -1: + addr_list = address.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +class MlagConfig(object): + """ + Manages Manages MLAG config information. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.dfs_group_id = self.module.params['dfs_group_id'] + self.nickname = self.module.params['nickname'] + self.pseudo_nickname = self.module.params['pseudo_nickname'] + self.pseudo_priority = self.module.params['pseudo_priority'] + self.ip_address = self.module.params['ip_address'] + self.vpn_instance_name = self.module.params['vpn_instance_name'] + self.priority_id = self.module.params['priority_id'] + self.eth_trunk_id = self.module.params['eth_trunk_id'] + self.peer_link_id = self.module.params['peer_link_id'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + self.commands = list() + # DFS group info + self.dfs_group_info = None + # peer link info + self.peer_link_info = None + + def init_module(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, con_obj, xml_name): + """Check if response message is already succeed.""" + + xml_str = con_obj.xml + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_dfs_group_info(self): + """ get dfs group attributes info.""" + + dfs_group_info = dict() + conf_str = CE_NC_GET_DFS_GROUP_INFO + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return dfs_group_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + dfs_info = root.findall( + "dfs/groupInstances/groupInstance") + if dfs_info: + for tmp in dfs_info: + for site in tmp: + if site.tag in ["groupId", "priority", "ipAddress", "srcVpnName"]: + dfs_group_info[site.tag] = site.text + + dfs_nick_info = root.findall( + "dfs/groupInstances/groupInstance/trillType") + + if dfs_nick_info: + for tmp in dfs_nick_info: + for site in tmp: + if site.tag in ["localNickname", "pseudoNickname", "pseudoPriority"]: + dfs_group_info[site.tag] = site.text + return dfs_group_info + + def get_peer_link_info(self): + """ get peer link info.""" + + peer_link_info = dict() + conf_str = CE_NC_GET_PEER_LINK_INFO + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return peer_link_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + link_info = root.findall( + "mlag/peerlinks/peerlink") + if link_info: + for tmp in link_info: + for site in tmp: + if site.tag in ["linkId", "portName"]: + peer_link_info[site.tag] = site.text + return peer_link_info + + def is_dfs_group_info_change(self): + """whether dfs group info""" + if not self.dfs_group_info: + return False + + if self.priority_id and self.dfs_group_info["priority"] != self.priority_id: + return True + if self.ip_address and self.dfs_group_info["ipAddress"] != self.ip_address: + return True + if self.vpn_instance_name and self.dfs_group_info["srcVpnName"] != self.vpn_instance_name: + return True + if self.nickname and self.dfs_group_info["localNickname"] != self.nickname: + return True + if self.pseudo_nickname and self.dfs_group_info["pseudoNickname"] != self.pseudo_nickname: + return True + if self.pseudo_priority and self.dfs_group_info["pseudoPriority"] != self.pseudo_priority: + return True + return False + + def check_dfs_group_info_change(self): + """check dfs group info""" + if not self.dfs_group_info: + return True + + if self.priority_id and self.dfs_group_info["priority"] == self.priority_id: + return True + if self.ip_address and self.dfs_group_info["ipAddress"] == self.ip_address: + return True + if self.vpn_instance_name and self.dfs_group_info["srcVpnName"] == self.vpn_instance_name: + return True + if self.nickname and self.dfs_group_info["localNickname"] == self.nickname: + return True + if self.pseudo_nickname and self.dfs_group_info["pseudoNickname"] == self.pseudo_nickname: + return True + if self.pseudo_priority and self.dfs_group_info["pseudoPriority"] == self.pseudo_priority: + return True + return False + + def modify_dfs_group(self): + """modify dfs group info""" + + if self.is_dfs_group_info_change(): + + conf_str = CE_NC_MERGE_DFS_GROUP_INFO_HEADER % self.dfs_group_id + if self.priority_id and self.dfs_group_info["priority"] != self.priority_id: + conf_str += "%s" % self.priority_id + if self.ip_address and self.dfs_group_info["ipAddress"] != self.ip_address: + conf_str += "%s" % self.ip_address + if self.vpn_instance_name and self.dfs_group_info["srcVpnName"] != self.vpn_instance_name: + if not self.ip_address: + self.module.fail_json( + msg='Error: ip_address can not be null if vpn_instance_name is exist.') + conf_str += "%s" % self.vpn_instance_name + + if self.nickname or self.pseudo_nickname or self.pseudo_priority: + conf_str += "" + if self.nickname and self.dfs_group_info["localNickname"] != self.nickname: + conf_str += "%s" % self.nickname + if self.pseudo_nickname and self.dfs_group_info["pseudoNickname"] != self.pseudo_nickname: + conf_str += "%s" % self.pseudo_nickname + + if self.pseudo_priority and self.dfs_group_info["pseudoPriority"] != self.pseudo_priority: + if not self.pseudo_nickname: + self.module.fail_json( + msg='Error: pseudo_nickname can not be null if pseudo_priority is exist.') + conf_str += "%s" % self.pseudo_priority + conf_str += "" + + conf_str += CE_NC_MERGE_DFS_GROUP_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge DFS group info failed.') + + self.updates_cmd.append("dfs-group 1") + if self.priority_id: + self.updates_cmd.append("priority %s" % self.priority_id) + if self.ip_address: + if self.vpn_instance_name: + self.updates_cmd.append( + "source ip %s vpn-instance %s" % (self.ip_address, self.vpn_instance_name)) + else: + self.updates_cmd.append("source ip %s" % self.ip_address) + if self.nickname: + self.updates_cmd.append("source nickname %s" % self.nickname) + if self.pseudo_nickname: + if self.pseudo_priority: + self.updates_cmd.append( + "pseudo-nickname %s priority %s" % (self.pseudo_nickname, self.pseudo_priority)) + else: + self.updates_cmd.append( + "pseudo-nickname %s" % self.pseudo_nickname) + + self.changed = True + + def create_dfs_group(self): + """create dfs group info""" + + conf_str = CE_NC_CREATE_DFS_GROUP_INFO_HEADER % self.dfs_group_id + if self.priority_id and self.priority_id != 100: + conf_str += "%s" % self.priority_id + if self.ip_address: + conf_str += "%s" % self.ip_address + if self.vpn_instance_name: + if not self.ip_address: + self.module.fail_json( + msg='Error: ip_address can not be null if vpn_instance_name is exist.') + conf_str += "%s" % self.vpn_instance_name + + if self.nickname or self.pseudo_nickname or self.pseudo_priority: + conf_str += "" + if self.nickname: + conf_str += "%s" % self.nickname + if self.pseudo_nickname: + conf_str += "%s" % self.pseudo_nickname + if self.pseudo_priority: + if not self.pseudo_nickname: + self.module.fail_json( + msg='Error: pseudo_nickname can not be null if pseudo_priority is exist.') + conf_str += "%s" % self.pseudo_priority + conf_str += "" + + conf_str += CE_NC_CREATE_DFS_GROUP_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge DFS group info failed.') + + self.updates_cmd.append("dfs-group 1") + if self.priority_id: + self.updates_cmd.append("priority %s" % self.priority_id) + if self.ip_address: + if self.vpn_instance_name: + self.updates_cmd.append( + "source ip %s vpn-instance %s" % (self.ip_address, self.vpn_instance_name)) + else: + self.updates_cmd.append("source ip %s" % self.ip_address) + if self.nickname: + self.updates_cmd.append("source nickname %s" % self.nickname) + if self.pseudo_nickname: + if self.pseudo_priority: + self.updates_cmd.append( + "pseudo-nickname %s priority %s" % (self.pseudo_nickname, self.pseudo_priority)) + else: + self.updates_cmd.append( + "pseudo-nickname %s" % self.pseudo_nickname) + + self.changed = True + + def delete_dfs_group(self): + """delete dfg group""" + + conf_str = CE_NC_DELETE_DFS_GROUP_INFO_HEADER % self.dfs_group_id + conf_str += CE_NC_DELETE_DFS_GROUP_INFO_TAIL + + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Delete DFS group id failed.') + self.updates_cmd.append("undo dfs-group 1") + self.changed = True + + def delete_dfs_group_attribute(self): + """delete dfg group attribute info""" + + conf_str = CE_NC_DELETE_DFS_GROUP_ATTRIBUTE_HEADER % self.dfs_group_id + change = False + if self.priority_id and self.dfs_group_info["priority"] == self.priority_id: + conf_str += "%s" % self.priority_id + change = True + self.updates_cmd.append("undo priority %s" % self.priority_id) + if self.ip_address and self.dfs_group_info["ipAddress"] == self.ip_address: + if self.vpn_instance_name and self.dfs_group_info["srcVpnName"] == self.vpn_instance_name: + conf_str += "%s" % self.ip_address + conf_str += "%s" % self.vpn_instance_name + self.updates_cmd.append( + "undo source ip %s vpn-instance %s" % (self.ip_address, self.vpn_instance_name)) + else: + conf_str += "%s" % self.ip_address + self.updates_cmd.append("undo source ip %s" % self.ip_address) + change = True + + conf_str += CE_NC_DELETE_DFS_GROUP_ATTRIBUTE_TAIL + + if change: + self.updates_cmd.append("undo dfs-group 1") + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Delete DFS group attribute failed.') + self.changed = True + + def delete_dfs_group_nick(self): + + conf_str = CE_NC_DELETE_DFS_GROUP_ATTRIBUTE_HEADER % self.dfs_group_id + conf_str = conf_str.replace('', '') + change = False + + if self.nickname or self.pseudo_nickname: + conf_str += "" + if self.nickname and self.dfs_group_info["localNickname"] == self.nickname: + conf_str += "%s" % self.nickname + change = True + self.updates_cmd.append("undo source nickname %s" % self.nickname) + if self.pseudo_nickname and self.dfs_group_info["pseudoNickname"] == self.pseudo_nickname: + conf_str += "%s" % self.pseudo_nickname + if self.pseudo_priority and self.dfs_group_info["pseudoPriority"] == self.pseudo_priority: + self.updates_cmd.append( + "undo pseudo-nickname %s priority %s" % (self.pseudo_nickname, self.pseudo_priority)) + if not self.pseudo_priority: + self.updates_cmd.append( + "undo pseudo-nickname %s" % self.pseudo_nickname) + change = True + conf_str += "" + + conf_str += CE_NC_DELETE_DFS_GROUP_ATTRIBUTE_TAIL + + if change: + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Delete DFS group attribute failed.') + self.changed = True + + def modify_peer_link(self): + """modify peer link info""" + + eth_trunk_id = "Eth-Trunk" + eth_trunk_id += self.eth_trunk_id + if self.eth_trunk_id and eth_trunk_id != self.peer_link_info.get("portName"): + conf_str = CE_NC_MERGE_PEER_LINK_INFO % ( + self.peer_link_id, eth_trunk_id) + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Merge peer link failed.') + self.updates_cmd.append("peer-link %s" % self.peer_link_id) + self.changed = True + + def delete_peer_link(self): + """delete peer link info""" + + eth_trunk_id = "Eth-Trunk" + eth_trunk_id += self.eth_trunk_id + if self.eth_trunk_id and eth_trunk_id == self.peer_link_info.get("portName"): + conf_str = CE_NC_DELETE_PEER_LINK_INFO % ( + self.peer_link_id, eth_trunk_id) + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: Delete peer link failed.') + self.updates_cmd.append("undo peer-link %s" % self.peer_link_id) + self.changed = True + + def check_params(self): + """Check all input params""" + + # dfs_group_id check + if self.dfs_group_id: + if self.dfs_group_id != "1": + self.module.fail_json( + msg='Error: The value of dfs_group_id must be 1.') + + # nickname check + if self.nickname: + if not self.nickname.isdigit(): + self.module.fail_json( + msg='Error: The value of nickname is an integer.') + if int(self.nickname) < 1 or int(self.nickname) > 65471: + self.module.fail_json( + msg='Error: The nickname is not in the range from 1 to 65471.') + + # pseudo_nickname check + if self.pseudo_nickname: + if not self.pseudo_nickname.isdigit(): + self.module.fail_json( + msg='Error: The value of pseudo_nickname is an integer.') + if int(self.pseudo_nickname) < 1 or int(self.pseudo_nickname) > 65471: + self.module.fail_json( + msg='Error: The pseudo_nickname is not in the range from 1 to 65471.') + + # pseudo_priority check + if self.pseudo_priority: + if not self.pseudo_priority.isdigit(): + self.module.fail_json( + msg='Error: The value of pseudo_priority is an integer.') + if int(self.pseudo_priority) < 128 or int(self.pseudo_priority) > 255: + self.module.fail_json( + msg='Error: The pseudo_priority is not in the range from 128 to 255.') + + # ip_address check + if self.ip_address: + if not is_valid_address(self.ip_address): + self.module.fail_json( + msg='Error: The %s is not a valid ip address.' % self.ip_address) + + # vpn_instance_name check + if self.vpn_instance_name: + if len(self.vpn_instance_name) > 31 \ + or len(self.vpn_instance_name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: The length of vpn_instance_name is not in the range from 1 to 31.') + + # priority_id check + if self.priority_id: + if not self.priority_id.isdigit(): + self.module.fail_json( + msg='Error: The value of priority_id is an integer.') + if int(self.priority_id) < 1 or int(self.priority_id) > 254: + self.module.fail_json( + msg='Error: The priority_id is not in the range from 1 to 254.') + + # peer_link_id check + if self.peer_link_id: + if self.peer_link_id != "1": + self.module.fail_json( + msg='Error: The value of peer_link_id must be 1.') + + # eth_trunk_id check + if self.eth_trunk_id: + if not self.eth_trunk_id.isdigit(): + self.module.fail_json( + msg='Error: The value of eth_trunk_id is an integer.') + if int(self.eth_trunk_id) < 0 or int(self.eth_trunk_id) > 511: + self.module.fail_json( + msg='Error: The value of eth_trunk_id is not in the range from 0 to 511.') + + def get_proposed(self): + """get proposed info""" + + if self.dfs_group_id: + self.proposed["dfs_group_id"] = self.dfs_group_id + if self.nickname: + self.proposed["nickname"] = self.nickname + if self.pseudo_nickname: + self.proposed["pseudo_nickname"] = self.pseudo_nickname + if self.pseudo_priority: + self.proposed["pseudo_priority"] = self.pseudo_priority + if self.ip_address: + self.proposed["ip_address"] = self.ip_address + if self.vpn_instance_name: + self.proposed["vpn_instance_name"] = self.vpn_instance_name + if self.priority_id: + self.proposed["priority_id"] = self.priority_id + if self.eth_trunk_id: + self.proposed["eth_trunk_id"] = self.eth_trunk_id + if self.peer_link_id: + self.proposed["peer_link_id"] = self.peer_link_id + if self.state: + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + if self.dfs_group_id: + self.dfs_group_info = self.get_dfs_group_info() + if self.peer_link_id and self.eth_trunk_id: + self.peer_link_info = self.get_peer_link_info() + if self.dfs_group_info: + if self.dfs_group_id: + self.existing["dfs_group_id"] = self.dfs_group_info["groupId"] + if self.nickname: + self.existing["nickname"] = self.dfs_group_info[ + "localNickname"] + if self.pseudo_nickname: + self.existing["pseudo_nickname"] = self.dfs_group_info[ + "pseudoNickname"] + if self.pseudo_priority: + self.existing["pseudo_priority"] = self.dfs_group_info[ + "pseudoPriority"] + if self.ip_address: + self.existing["ip_address"] = self.dfs_group_info["ipAddress"] + if self.vpn_instance_name: + self.existing["vpn_instance_name"] = self.dfs_group_info[ + "srcVpnName"] + if self.priority_id: + self.existing["priority_id"] = self.dfs_group_info["priority"] + if self.peer_link_info: + if self.eth_trunk_id: + self.existing["eth_trunk_id"] = self.peer_link_info["portName"] + if self.peer_link_id: + self.existing["peer_link_id"] = self.peer_link_info["linkId"] + + def get_end_state(self): + """get end state info""" + if self.dfs_group_id: + self.dfs_group_info = self.get_dfs_group_info() + if self.peer_link_id and self.eth_trunk_id: + self.peer_link_info = self.get_peer_link_info() + + if self.dfs_group_info: + if self.dfs_group_id: + self.end_state["dfs_group_id"] = self.dfs_group_info["groupId"] + if self.nickname: + self.end_state["nickname"] = self.dfs_group_info[ + "localNickname"] + if self.pseudo_nickname: + self.end_state["pseudo_nickname"] = self.dfs_group_info[ + "pseudoNickname"] + if self.pseudo_priority: + self.end_state["pseudo_priority"] = self.dfs_group_info[ + "pseudoPriority"] + if self.ip_address: + self.end_state["ip_address"] = self.dfs_group_info["ipAddress"] + if self.vpn_instance_name: + self.end_state["vpn_instance_name"] = self.dfs_group_info[ + "srcVpnName"] + if self.priority_id: + self.end_state["priority_id"] = self.dfs_group_info["priority"] + if self.peer_link_info: + if self.eth_trunk_id: + self.end_state[ + "eth_trunk_id"] = self.peer_link_info["portName"] + if self.peer_link_id: + self.end_state["peer_link_id"] = self.peer_link_info["linkId"] + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + if self.dfs_group_id: + if self.state == "present": + if self.dfs_group_info: + if self.nickname or self.pseudo_nickname or self.pseudo_priority or self.priority_id \ + or self.ip_address or self.vpn_instance_name: + if self.nickname: + if self.dfs_group_info["ipAddress"] not in ["0.0.0.0", None]: + self.module.fail_json(msg='Error: nickname and ip_address can not be exist at the ' + 'same time.') + if self.ip_address: + if self.dfs_group_info["localNickname"] not in ["0", None]: + self.module.fail_json(msg='Error: nickname and ip_address can not be exist at the ' + 'same time.') + self.modify_dfs_group() + else: + self.create_dfs_group() + else: + if not self.dfs_group_info: + self.module.fail_json( + msg='Error: DFS Group does not exist.') + if not self.nickname and not self.pseudo_nickname and not self.pseudo_priority and not self.priority_id\ + and not self.ip_address and not self.vpn_instance_name: + self.delete_dfs_group() + else: + self.updates_cmd.append("dfs-group 1") + self.delete_dfs_group_attribute() + self.delete_dfs_group_nick() + if "undo dfs-group 1" in self.updates_cmd: + self.updates_cmd = ["undo dfs-group 1"] + + if self.eth_trunk_id and not self.peer_link_id: + self.module.fail_json( + msg='Error: eth_trunk_id and peer_link_id must be config at the same time.') + if self.peer_link_id and not self.eth_trunk_id: + self.module.fail_json( + msg='Error: eth_trunk_id and peer_link_id must be config at the same time.') + + if self.eth_trunk_id and self.peer_link_id: + if self.state == "present": + self.modify_peer_link() + else: + if self.peer_link_info: + self.delete_peer_link() + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + dfs_group_id=dict(type='str'), + nickname=dict(type='str'), + pseudo_nickname=dict(type='str'), + pseudo_priority=dict(type='str'), + ip_address=dict(type='str'), + vpn_instance_name=dict(type='str'), + priority_id=dict(type='str'), + eth_trunk_id=dict(type='str'), + peer_link_id=dict(type='str'), + state=dict(type='str', default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = MlagConfig(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_mlag_interface.py b/plugins/modules/ce_mlag_interface.py new file mode 100644 index 0000000..8537a1b --- /dev/null +++ b/plugins/modules/ce_mlag_interface.py @@ -0,0 +1,1039 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_mlag_interface +short_description: Manages MLAG interfaces on HUAWEI CloudEngine switches. +description: + - Manages MLAG interface attributes on HUAWEI CloudEngine switches. +author: + - Li Yanfeng (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + eth_trunk_id: + description: + - Name of the local M-LAG interface. The value is ranging from 0 to 511. + dfs_group_id: + description: + - ID of a DFS group.The value is 1. + default: present + mlag_id: + description: + - ID of the M-LAG. The value is an integer that ranges from 1 to 2048. + mlag_system_id: + description: + - M-LAG global LACP system MAC address. The value is a string of 0 to 255 characters. The default value + is the MAC address of the Ethernet port of MPU. + mlag_priority_id: + description: + - M-LAG global LACP system priority. The value is an integer ranging from 0 to 65535. + The default value is 32768. + interface: + description: + - Name of the interface that enters the Error-Down state when the peer-link fails. + The value is a string of 1 to 63 characters. + mlag_error_down: + description: + - Configure the interface on the slave device to enter the Error-Down state. + choices: ['enable','disable'] + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] + +''' + +EXAMPLES = ''' +- name: mlag interface module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Set interface mlag error down + ce_mlag_interface: + interface: 10GE2/0/1 + mlag_error_down: enable + provider: "{{ cli }}" + - name: Create mlag + ce_mlag_interface: + eth_trunk_id: 1 + dfs_group_id: 1 + mlag_id: 4 + provider: "{{ cli }}" + - name: Set mlag global attribute + ce_mlag_interface: + mlag_system_id: 0020-1409-0407 + mlag_priority_id: 5 + provider: "{{ cli }}" + - name: Set mlag interface attribute + ce_mlag_interface: + eth_trunk_id: 1 + mlag_system_id: 0020-1409-0400 + mlag_priority_id: 3 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { "interface": "eth-trunk1", + "mlag_error_down": "disable", + "state": "present" + } +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: { "mlagErrorDownInfos": [ + { + "dfsgroupId": "1", + "portName": "Eth-Trunk1" + } + ] + } +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {} +updates: + description: command sent to the device + returned: always + type: list + sample: { "interface eth-trunk1", + "undo m-lag unpaired-port suspend"} +''' + +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_MLAG_INFO = """ + + + + + %s + + + + +""" + +CE_NC_CREATE_MLAG_INFO = """ + + + + + %s + %s + %s + + + + +""" + +CE_NC_DELETE_MLAG_INFO = """ + + + + + %s + %s + + + + +""" + +CE_NC_GET_LACP_MLAG_INFO = """ + + + + + %s + + + + + + + + +""" + +CE_NC_SET_LACP_MLAG_INFO_HEAD = """ + + + + + %s + +""" + +CE_NC_SET_LACP_MLAG_INFO_TAIL = """ + + + + + +""" + +CE_NC_GET_GLOBAL_LACP_MLAG_INFO = """ + + + + + + + + + + +""" + +CE_NC_SET_GLOBAL_LACP_MLAG_INFO_HEAD = """ + + + + +""" + +CE_NC_SET_GLOBAL_LACP_MLAG_INFO_TAIL = """ + + + + +""" + +CE_NC_GET_MLAG_ERROR_DOWN_INFO = """ + + + + + + + + + + + + +""" + +CE_NC_CREATE_MLAG_ERROR_DOWN_INFO = """ + + + + + 1 + %s + + + + +""" + +CE_NC_DELETE_MLAG_ERROR_DOWN_INFO = """ + + + + + 1 + %s + + + + + +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class MlagInterface(object): + """ + Manages Manages MLAG interface information. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.eth_trunk_id = self.module.params['eth_trunk_id'] + self.dfs_group_id = self.module.params['dfs_group_id'] + self.mlag_id = self.module.params['mlag_id'] + self.mlag_system_id = self.module.params['mlag_system_id'] + self.mlag_priority_id = self.module.params['mlag_priority_id'] + self.interface = self.module.params['interface'] + self.mlag_error_down = self.module.params['mlag_error_down'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + # mlag info + self.commands = list() + self.mlag_info = None + self.mlag_global_info = None + self.mlag_error_down_info = None + self.mlag_trunk_attribute_info = None + + def init_module(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) # show updates result + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def get_mlag_info(self): + """ get mlag info.""" + + mlag_info = dict() + conf_str = CE_NC_GET_MLAG_INFO % ("Eth-Trunk%s" % self.eth_trunk_id) + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return mlag_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + mlag_info["mlagInfos"] = list() + root = ElementTree.fromstring(xml_str) + dfs_mlag_infos = root.findall( + "./mlag/mlagInstances/mlagInstance") + + if dfs_mlag_infos: + for dfs_mlag_info in dfs_mlag_infos: + mlag_dict = dict() + for ele in dfs_mlag_info: + if ele.tag in ["dfsgroupId", "mlagId", "localMlagPort"]: + mlag_dict[ele.tag] = ele.text + mlag_info["mlagInfos"].append(mlag_dict) + return mlag_info + + def get_mlag_global_info(self): + """ get mlag global info.""" + + mlag_global_info = dict() + conf_str = CE_NC_GET_GLOBAL_LACP_MLAG_INFO + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return mlag_global_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + global_info = root.findall( + "./ifmtrunk/lacpSysInfo/lacpMlagGlobal") + + if global_info: + for tmp in global_info: + for site in tmp: + if site.tag in ["lacpMlagSysId", "lacpMlagPriority"]: + mlag_global_info[site.tag] = site.text + return mlag_global_info + + def get_mlag_trunk_attribute_info(self): + """ get mlag global info.""" + + mlag_trunk_attribute_info = dict() + eth_trunk = "Eth-Trunk" + eth_trunk += self.eth_trunk_id + conf_str = CE_NC_GET_LACP_MLAG_INFO % eth_trunk + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return mlag_trunk_attribute_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + global_info = root.findall( + "./ifmtrunk/TrunkIfs/TrunkIf/lacpMlagIf") + + if global_info: + for tmp in global_info: + for site in tmp: + if site.tag in ["lacpMlagSysId", "lacpMlagPriority"]: + mlag_trunk_attribute_info[site.tag] = site.text + return mlag_trunk_attribute_info + + def get_mlag_error_down_info(self): + """ get error down info.""" + + mlag_error_down_info = dict() + conf_str = CE_NC_GET_MLAG_ERROR_DOWN_INFO + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return mlag_error_down_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + mlag_error_down_info["mlagErrorDownInfos"] = list() + root = ElementTree.fromstring(xml_str) + mlag_error_infos = root.findall( + "./mlag/errordowns/errordown") + + if mlag_error_infos: + for mlag_error_info in mlag_error_infos: + mlag_error_dict = dict() + for ele in mlag_error_info: + if ele.tag in ["dfsgroupId", "portName"]: + mlag_error_dict[ele.tag] = ele.text + mlag_error_down_info[ + "mlagErrorDownInfos"].append(mlag_error_dict) + return mlag_error_down_info + + def check_macaddr(self): + """check mac-address whether valid""" + + valid_char = '0123456789abcdef-' + mac = self.mlag_system_id + + if len(mac) > 16: + return False + + mac_list = re.findall(r'([0-9a-fA-F]+)', mac) + if len(mac_list) != 3: + return False + + if mac.count('-') != 2: + return False + + for _, value in enumerate(mac, start=0): + if value.lower() not in valid_char: + return False + if all((int(mac_list[0], base=16) == 0, int(mac_list[1], base=16) == 0, int(mac_list[2], base=16) == 0)): + return False + a = "000" + mac_list[0] + b = "000" + mac_list[1] + c = "000" + mac_list[2] + self.mlag_system_id = "-".join([a[-4:], b[-4:], c[-4:]]) + return True + + def check_params(self): + """Check all input params""" + + # eth_trunk_id check + if self.eth_trunk_id: + if not self.eth_trunk_id.isdigit(): + self.module.fail_json( + msg='Error: The value of eth_trunk_id is an integer.') + if int(self.eth_trunk_id) < 0 or int(self.eth_trunk_id) > 511: + self.module.fail_json( + msg='Error: The value of eth_trunk_id is not in the range from 0 to 511.') + + # dfs_group_id check + if self.dfs_group_id: + if self.dfs_group_id != "1": + self.module.fail_json( + msg='Error: The value of dfs_group_id must be 1.') + + # mlag_id check + if self.mlag_id: + if not self.mlag_id.isdigit(): + self.module.fail_json( + msg='Error: The value of mlag_id is an integer.') + if int(self.mlag_id) < 1 or int(self.mlag_id) > 2048: + self.module.fail_json( + msg='Error: The value of mlag_id is not in the range from 1 to 2048.') + + # mlag_system_id check + if self.mlag_system_id: + if not self.check_macaddr(): + self.module.fail_json( + msg="Error: mlag_system_id has invalid value %s." % self.mlag_system_id) + + # mlag_priority_id check + if self.mlag_priority_id: + if not self.mlag_priority_id.isdigit(): + self.module.fail_json( + msg='Error: The value of mlag_priority_id is an integer.') + if int(self.mlag_priority_id) < 0 or int(self.mlag_priority_id) > 254: + self.module.fail_json( + msg='Error: The value of mlag_priority_id is not in the range from 0 to 254.') + + # interface check + if self.interface: + intf_type = get_interface_type(self.interface) + if not intf_type: + self.module.fail_json( + msg='Error: Interface name of %s ' + 'is error.' % self.interface) + + def is_mlag_info_change(self): + """whether mlag info change""" + + if not self.mlag_info: + return True + + eth_trunk = "Eth-Trunk" + eth_trunk += self.eth_trunk_id + for info in self.mlag_info["mlagInfos"]: + if info["mlagId"] == self.mlag_id and info["localMlagPort"] == eth_trunk: + return False + return True + + def is_mlag_info_exist(self): + """whether mlag info exist""" + + if not self.mlag_info: + return False + + eth_trunk = "Eth-Trunk" + eth_trunk += self.eth_trunk_id + + for info in self.mlag_info["mlagInfos"]: + if info["localMlagPort"] == eth_trunk: + return True + return False + + def is_mlag_error_down_info_change(self): + """whether mlag error down info change""" + + if not self.mlag_error_down_info: + return True + + for info in self.mlag_error_down_info["mlagErrorDownInfos"]: + if info["portName"].upper() == self.interface.upper(): + return False + return True + + def is_mlag_error_down_info_exist(self): + """whether mlag error down info exist""" + + if not self.mlag_error_down_info: + return False + + for info in self.mlag_error_down_info["mlagErrorDownInfos"]: + if info["portName"].upper() == self.interface.upper(): + return True + return False + + def is_mlag_interface_info_change(self): + """whether mlag interface attribute info change""" + + if not self.mlag_trunk_attribute_info: + return True + + if self.mlag_system_id: + if self.mlag_trunk_attribute_info["lacpMlagSysId"] != self.mlag_system_id: + return True + if self.mlag_priority_id: + if self.mlag_trunk_attribute_info["lacpMlagPriority"] != self.mlag_priority_id: + return True + return False + + def is_mlag_interface_info_exist(self): + """whether mlag interface attribute info exist""" + + if not self.mlag_trunk_attribute_info: + return False + + if self.mlag_system_id: + if self.mlag_priority_id: + if self.mlag_trunk_attribute_info["lacpMlagSysId"] == self.mlag_system_id \ + and self.mlag_trunk_attribute_info["lacpMlagPriority"] == self.mlag_priority_id: + return True + else: + if self.mlag_trunk_attribute_info["lacpMlagSysId"] == self.mlag_system_id: + return True + + if self.mlag_priority_id: + if self.mlag_system_id: + if self.mlag_trunk_attribute_info["lacpMlagSysId"] == self.mlag_system_id \ + and self.mlag_trunk_attribute_info["lacpMlagPriority"] == self.mlag_priority_id: + return True + else: + if self.mlag_trunk_attribute_info["lacpMlagPriority"] == self.mlag_priority_id: + return True + + return False + + def is_mlag_global_info_change(self): + """whether mlag global attribute info change""" + + if not self.mlag_global_info: + return True + + if self.mlag_system_id: + if self.mlag_global_info["lacpMlagSysId"] != self.mlag_system_id: + return True + if self.mlag_priority_id: + if self.mlag_global_info["lacpMlagPriority"] != self.mlag_priority_id: + return True + return False + + def is_mlag_global_info_exist(self): + """whether mlag global attribute info exist""" + + if not self.mlag_global_info: + return False + + if self.mlag_system_id: + if self.mlag_priority_id: + if self.mlag_global_info["lacpMlagSysId"] == self.mlag_system_id \ + and self.mlag_global_info["lacpMlagPriority"] == self.mlag_priority_id: + return True + else: + if self.mlag_global_info["lacpMlagSysId"] == self.mlag_system_id: + return True + + if self.mlag_priority_id: + if self.mlag_system_id: + if self.mlag_global_info["lacpMlagSysId"] == self.mlag_system_id \ + and self.mlag_global_info["lacpMlagPriority"] == self.mlag_priority_id: + return True + else: + if self.mlag_global_info["lacpMlagPriority"] == self.mlag_priority_id: + return True + + return False + + def create_mlag(self): + """create mlag info""" + + if self.is_mlag_info_change(): + mlag_port = "Eth-Trunk" + mlag_port += self.eth_trunk_id + conf_str = CE_NC_CREATE_MLAG_INFO % ( + self.dfs_group_id, self.mlag_id, mlag_port) + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: create mlag info failed.') + + self.updates_cmd.append("interface %s" % mlag_port) + self.updates_cmd.append("dfs-group %s m-lag %s" % + (self.dfs_group_id, self.mlag_id)) + self.changed = True + + def delete_mlag(self): + """delete mlag info""" + + if self.is_mlag_info_exist(): + mlag_port = "Eth-Trunk" + mlag_port += self.eth_trunk_id + conf_str = CE_NC_DELETE_MLAG_INFO % ( + self.dfs_group_id, mlag_port) + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: delete mlag info failed.') + + self.updates_cmd.append("interface %s" % mlag_port) + self.updates_cmd.append( + "undo dfs-group %s m-lag %s" % (self.dfs_group_id, self.mlag_id)) + self.changed = True + + def create_mlag_error_down(self): + """create mlag error down info""" + + if self.is_mlag_error_down_info_change(): + conf_str = CE_NC_CREATE_MLAG_ERROR_DOWN_INFO % self.interface + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: create mlag error down info failed.') + + self.updates_cmd.append("interface %s" % self.interface) + self.updates_cmd.append("m-lag unpaired-port suspend") + self.changed = True + + def delete_mlag_error_down(self): + """delete mlag error down info""" + + if self.is_mlag_error_down_info_exist(): + + conf_str = CE_NC_DELETE_MLAG_ERROR_DOWN_INFO % self.interface + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: delete mlag error down info failed.') + + self.updates_cmd.append("interface %s" % self.interface) + self.updates_cmd.append("undo m-lag unpaired-port suspend") + self.changed = True + + def set_mlag_interface(self): + """set mlag interface attribute info""" + + if self.is_mlag_interface_info_change(): + mlag_port = "Eth-Trunk" + mlag_port += self.eth_trunk_id + conf_str = CE_NC_SET_LACP_MLAG_INFO_HEAD % mlag_port + if self.mlag_priority_id: + conf_str += "%s" % self.mlag_priority_id + if self.mlag_system_id: + conf_str += "%s" % self.mlag_system_id + conf_str += CE_NC_SET_LACP_MLAG_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set mlag interface attribute info failed.') + + self.updates_cmd.append("interface %s" % mlag_port) + if self.mlag_priority_id: + self.updates_cmd.append( + "lacp m-lag priority %s" % self.mlag_priority_id) + + if self.mlag_system_id: + self.updates_cmd.append( + "lacp m-lag system-id %s" % self.mlag_system_id) + self.changed = True + + def delete_mlag_interface(self): + """delete mlag interface attribute info""" + + if self.is_mlag_interface_info_exist(): + mlag_port = "Eth-Trunk" + mlag_port += self.eth_trunk_id + conf_str = CE_NC_SET_LACP_MLAG_INFO_HEAD % mlag_port + cmd = "interface %s" % mlag_port + self.cli_add_command(cmd) + + if self.mlag_priority_id: + cmd = "lacp m-lag priority %s" % self.mlag_priority_id + conf_str += "" + self.cli_add_command(cmd, True) + + if self.mlag_system_id: + cmd = "lacp m-lag system-id %s" % self.mlag_system_id + conf_str += "" + self.cli_add_command(cmd, True) + + if self.commands: + conf_str += CE_NC_SET_LACP_MLAG_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set mlag interface atrribute info failed.') + + self.changed = True + + def set_mlag_global(self): + """set mlag global attribute info""" + + if self.is_mlag_global_info_change(): + conf_str = CE_NC_SET_GLOBAL_LACP_MLAG_INFO_HEAD + if self.mlag_priority_id: + conf_str += "%s" % self.mlag_priority_id + if self.mlag_system_id: + conf_str += "%s" % self.mlag_system_id + conf_str += CE_NC_SET_GLOBAL_LACP_MLAG_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set mlag interface attribute info failed.') + + if self.mlag_priority_id: + self.updates_cmd.append( + "lacp m-lag priority %s" % self.mlag_priority_id) + + if self.mlag_system_id: + self.updates_cmd.append( + "lacp m-lag system-id %s" % self.mlag_system_id) + self.changed = True + + def delete_mlag_global(self): + """delete mlag global attribute info""" + + xml_str = '' + if self.is_mlag_global_info_exist(): + if self.mlag_priority_id: + cmd = "lacp m-lag priority %s" % self.mlag_priority_id + xml_str += '' + self.cli_add_command(cmd, True) + + if self.mlag_system_id: + cmd = "lacp m-lag system-id %s" % self.mlag_system_id + xml_str += '' + self.cli_add_command(cmd, True) + + if xml_str != '': + conf_str = CE_NC_SET_GLOBAL_LACP_MLAG_INFO_HEAD + xml_str + CE_NC_SET_GLOBAL_LACP_MLAG_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set mlag interface atrribute info failed.') + self.changed = True + + def get_proposed(self): + """get proposed info""" + + if self.eth_trunk_id: + self.proposed["eth_trunk_id"] = self.eth_trunk_id + if self.dfs_group_id: + self.proposed["dfs_group_id"] = self.dfs_group_id + if self.mlag_id: + self.proposed["mlag_id"] = self.mlag_id + if self.mlag_system_id: + self.proposed["mlag_system_id"] = self.mlag_system_id + if self.mlag_priority_id: + self.proposed["mlag_priority_id"] = self.mlag_priority_id + if self.interface: + self.proposed["interface"] = self.interface + if self.mlag_error_down: + self.proposed["mlag_error_down"] = self.mlag_error_down + if self.state: + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + self.mlag_info = self.get_mlag_info() + self.mlag_global_info = self.get_mlag_global_info() + self.mlag_error_down_info = self.get_mlag_error_down_info() + + if self.eth_trunk_id or self.dfs_group_id or self.mlag_id: + if not self.mlag_system_id and not self.mlag_priority_id: + if self.mlag_info: + self.existing["mlagInfos"] = self.mlag_info["mlagInfos"] + + if self.mlag_system_id or self.mlag_priority_id: + if self.eth_trunk_id: + if self.mlag_trunk_attribute_info: + if self.mlag_system_id: + self.existing["lacpMlagSysId"] = self.mlag_trunk_attribute_info[ + "lacpMlagSysId"] + if self.mlag_priority_id: + self.existing["lacpMlagPriority"] = self.mlag_trunk_attribute_info[ + "lacpMlagPriority"] + else: + if self.mlag_global_info: + if self.mlag_system_id: + self.existing["lacpMlagSysId"] = self.mlag_global_info[ + "lacpMlagSysId"] + if self.mlag_priority_id: + self.existing["lacpMlagPriority"] = self.mlag_global_info[ + "lacpMlagPriority"] + + if self.interface or self.mlag_error_down: + if self.mlag_error_down_info: + self.existing["mlagErrorDownInfos"] = self.mlag_error_down_info[ + "mlagErrorDownInfos"] + + def get_end_state(self): + """get end state info""" + + if self.eth_trunk_id or self.dfs_group_id or self.mlag_id: + self.mlag_info = self.get_mlag_info() + if not self.mlag_system_id and not self.mlag_priority_id: + if self.mlag_info: + self.end_state["mlagInfos"] = self.mlag_info["mlagInfos"] + + if self.mlag_system_id or self.mlag_priority_id: + if self.eth_trunk_id: + self.mlag_trunk_attribute_info = self.get_mlag_trunk_attribute_info() + if self.mlag_trunk_attribute_info: + if self.mlag_system_id: + self.end_state["lacpMlagSysId"] = self.mlag_trunk_attribute_info[ + "lacpMlagSysId"] + if self.mlag_priority_id: + self.end_state["lacpMlagPriority"] = self.mlag_trunk_attribute_info[ + "lacpMlagPriority"] + else: + self.mlag_global_info = self.get_mlag_global_info() + if self.mlag_global_info: + if self.mlag_system_id: + self.end_state["lacpMlagSysId"] = self.mlag_global_info[ + "lacpMlagSysId"] + if self.mlag_priority_id: + self.end_state["lacpMlagPriority"] = self.mlag_global_info[ + "lacpMlagPriority"] + + if self.interface or self.mlag_error_down: + self.mlag_error_down_info = self.get_mlag_error_down_info() + if self.mlag_error_down_info: + self.end_state["mlagErrorDownInfos"] = self.mlag_error_down_info[ + "mlagErrorDownInfos"] + + def work(self): + """worker""" + + self.check_params() + self.get_proposed() + self.get_existing() + + if self.eth_trunk_id or self.dfs_group_id or self.mlag_id: + self.mlag_info = self.get_mlag_info() + if self.eth_trunk_id and self.dfs_group_id and self.mlag_id: + if self.state == "present": + self.create_mlag() + else: + self.delete_mlag() + else: + if not self.mlag_system_id and not self.mlag_priority_id: + self.module.fail_json( + msg='Error: eth_trunk_id, dfs_group_id, mlag_id must be config at the same time.') + + if self.mlag_system_id or self.mlag_priority_id: + + if self.eth_trunk_id: + self.mlag_trunk_attribute_info = self.get_mlag_trunk_attribute_info() + if self.mlag_system_id or self.mlag_priority_id: + if self.state == "present": + self.set_mlag_interface() + else: + self.delete_mlag_interface() + else: + self.mlag_global_info = self.get_mlag_global_info() + if self.mlag_system_id or self.mlag_priority_id: + if self.state == "present": + self.set_mlag_global() + else: + self.delete_mlag_global() + + if self.interface or self.mlag_error_down: + self.mlag_error_down_info = self.get_mlag_error_down_info() + if self.interface and self.mlag_error_down: + if self.mlag_error_down == "enable": + self.create_mlag_error_down() + else: + self.delete_mlag_error_down() + else: + self.module.fail_json( + msg='Error: interface, mlag_error_down must be config at the same time.') + + self.get_end_state() + if self.existing == self.end_state: + self.changed = False + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + eth_trunk_id=dict(type='str'), + dfs_group_id=dict(type='str'), + mlag_id=dict(type='str'), + mlag_system_id=dict(type='str'), + mlag_priority_id=dict(type='str'), + interface=dict(type='str'), + mlag_error_down=dict(type='str', choices=['enable', 'disable']), + state=dict(type='str', default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = MlagInterface(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_mtu.py b/plugins/modules/ce_mtu.py new file mode 100644 index 0000000..0698797 --- /dev/null +++ b/plugins/modules/ce_mtu.py @@ -0,0 +1,582 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_mtu +short_description: Manages MTU settings on HUAWEI CloudEngine switches. +description: + - Manages MTU settings on HUAWEI CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - Either C(sysmtu) param is required or C(interface) AND C(mtu) params are req'd. + - C(state=absent) unconfigures a given MTU if that value is currently present. + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - Full name of interface, i.e. 40GE1/0/22. + mtu: + description: + - MTU for a specific interface. + The value is an integer ranging from 46 to 9600, in bytes. + jumbo_max: + description: + - Maximum frame size. The default value is 9216. + The value is an integer and expressed in bytes. The value range is 1536 to 12224 for the CE12800 + and 1536 to 12288 for ToR switches. + jumbo_min: + description: + - Non-jumbo frame size threshold. The default value is 1518. + The value is an integer that ranges from 1518 to jumbo_max, in bytes. + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: Mtu test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config jumboframe on 40GE1/0/22" + ce_mtu: + interface: 40GE1/0/22 + jumbo_max: 9000 + jumbo_min: 8000 + provider: "{{ cli }}" + + - name: "Config mtu on 40GE1/0/22 (routed interface)" + ce_mtu: + interface: 40GE1/0/22 + mtu: 1600 + provider: "{{ cli }}" + + - name: "Config mtu on 40GE1/0/23 (switched interface)" + ce_mtu: + interface: 40GE1/0/22 + mtu: 9216 + provider: "{{ cli }}" + + - name: "Config mtu and jumboframe on 40GE1/0/22 (routed interface)" + ce_mtu: + interface: 40GE1/0/22 + mtu: 1601 + jumbo_max: 9001 + jumbo_min: 8001 + provider: "{{ cli }}" + + - name: "Unconfigure mtu and jumboframe on a given interface" + ce_mtu: + state: absent + interface: 40GE1/0/22 + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"mtu": "1700", "jumbo_max": "9000", jumbo_min: "8000"} +existing: + description: k/v pairs of existing mtu/sysmtu on the interface/system + returned: always + type: dict + sample: {"mtu": "1600", "jumbo_max": "9216", "jumbo_min": "1518"} +end_state: + description: k/v pairs of mtu/sysmtu values after module execution + returned: always + type: dict + sample: {"mtu": "1700", "jumbo_max": "9000", jumbo_min: "8000"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface 40GE1/0/23", "mtu 1700", "jumboframe enable 9000 8000"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +import copy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, load_config +from ansible.module_utils.connection import exec_command + + +def is_interface_support_setjumboframe(interface): + """is interface support set jumboframe""" + + if interface is None: + return False + support_flag = False + if interface.upper().startswith('GE'): + support_flag = True + elif interface.upper().startswith('10GE'): + support_flag = True + elif interface.upper().startswith('25GE'): + support_flag = True + elif interface.upper().startswith('4X10GE'): + support_flag = True + elif interface.upper().startswith('40GE'): + support_flag = True + elif interface.upper().startswith('100GE'): + support_flag = True + else: + support_flag = False + return support_flag + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class Mtu(object): + """set mtu""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # interface info + self.interface = self.module.params['interface'] + self.mtu = self.module.params['mtu'] + self.state = self.module.params['state'] + self.jbf_max = self.module.params['jumbo_max'] or None + self.jbf_min = self.module.params['jumbo_min'] or None + self.jbf_config = list() + self.jbf_cli = "" + self.commands = list() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + self.intf_info = dict() # one interface info + self.intf_type = None # loopback tunnel ... + + def init_module(self): + """ init_module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_interface_dict(self, ifname): + """ get one interface attributes dict.""" + intf_info = dict() + + flags = list() + exp = r"| ignore-case section include ^#\s+interface %s\s+" % ifname.replace(" ", "") + flags.append(exp) + output = self.get_config(flags) + output_list = output.split('\n') + if output_list is None: + return intf_info + + mtu = None + for config in output_list: + config = config.strip() + if config.startswith('mtu'): + mtu = re.findall(r'.*mtu\s*([0-9]*)', output)[0] + + intf_info = dict(ifName=ifname, + ifMtu=mtu) + + return intf_info + + def prase_jumboframe_para(self, config_str): + """prase_jumboframe_para""" + + interface_cli = "interface %s" % (self.interface.replace(" ", "").lower()) + if config_str.find(interface_cli) == -1: + self.module.fail_json(msg='Error: Interface does not exist.') + + try: + npos1 = config_str.index('jumboframe enable') + except ValueError: + # return default vale + return [9216, 1518] + try: + npos2 = config_str.index('\n', npos1) + config_str_tmp = config_str[npos1:npos2] + except ValueError: + config_str_tmp = config_str[npos1:] + + return re.findall(r'([0-9]+)', config_str_tmp) + + def cli_load_config(self): + """load config by cli""" + + if not self.module.check_mode: + if len(self.commands) > 1: + load_config(self.module, self.commands) + self.changed = True + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + + def get_jumboframe_config(self): + """ get_jumboframe_config""" + + flags = list() + exp = r"| ignore-case section include ^#\s+interface %s\s+" % self.interface.replace(" ", "") + flags.append(exp) + output = self.get_config(flags) + output = output.replace('*', '').lower() + + return self.prase_jumboframe_para(output) + + def set_jumboframe(self): + """ set_jumboframe""" + + if self.state == "present": + if not self.jbf_max and not self.jbf_min: + return + + jbf_value = self.get_jumboframe_config() + self.jbf_config = copy.deepcopy(jbf_value) + if len(jbf_value) == 1: + jbf_value.append("1518") + self.jbf_config.append("1518") + if not self.jbf_max: + return + + if (len(jbf_value) > 2) or (len(jbf_value) == 0): + self.module.fail_json( + msg='Error: Get jubmoframe config value num error.') + if self.jbf_min is None: + if jbf_value[0] == self.jbf_max: + return + else: + if (jbf_value[0] == self.jbf_max) \ + and (jbf_value[1] == self.jbf_min): + return + if jbf_value[0] != self.jbf_max: + jbf_value[0] = self.jbf_max + if (jbf_value[1] != self.jbf_min) and (self.jbf_min is not None): + jbf_value[1] = self.jbf_min + else: + jbf_value.pop(1) + else: + jbf_value = self.get_jumboframe_config() + self.jbf_config = copy.deepcopy(jbf_value) + if (jbf_value == [9216, 1518]): + return + jbf_value = [9216, 1518] + + if len(jbf_value) == 2: + self.jbf_cli = "jumboframe enable %s %s" % ( + jbf_value[0], jbf_value[1]) + else: + self.jbf_cli = "jumboframe enable %s" % (jbf_value[0]) + self.cli_add_command(self.jbf_cli) + + if self.state == "present": + if self.jbf_min: + self.updates_cmd.append( + "jumboframe enable %s %s" % (self.jbf_max, self.jbf_min)) + else: + self.updates_cmd.append("jumboframe enable %s" % (self.jbf_max)) + else: + self.updates_cmd.append("undo jumboframe enable") + + return + + def merge_interface(self, ifname, mtu): + """ Merge interface mtu.""" + + xmlstr = '' + change = False + + command = "interface %s" % ifname + self.cli_add_command(command) + + if self.state == "present": + if mtu and self.intf_info["ifMtu"] != mtu: + command = "mtu %s" % mtu + self.cli_add_command(command) + self.updates_cmd.append("mtu %s" % mtu) + change = True + else: + if self.intf_info["ifMtu"] != '1500' and self.intf_info["ifMtu"]: + command = "mtu 1500" + self.cli_add_command(command) + self.updates_cmd.append("undo mtu") + change = True + + return + + def check_params(self): + """Check all input params""" + + # interface type check + if self.interface: + self.intf_type = get_interface_type(self.interface) + if not self.intf_type: + self.module.fail_json( + msg='Error: Interface name of %s ' + 'is error.' % self.interface) + + if not self.intf_type: + self.module.fail_json( + msg='Error: Interface %s is error.') + + # mtu check mtu + if self.mtu: + if not self.mtu.isdigit(): + self.module.fail_json(msg='Error: Mtu is invalid.') + # check mtu range + if int(self.mtu) < 46 or int(self.mtu) > 9600: + self.module.fail_json( + msg='Error: Mtu is not in the range from 46 to 9600.') + # get interface info + self.intf_info = self.get_interface_dict(self.interface) + if not self.intf_info: + self.module.fail_json(msg='Error: interface does not exist.') + + # check interface can set jumbo frame + if self.state == 'present': + if self.jbf_max: + if not is_interface_support_setjumboframe(self.interface): + self.module.fail_json( + msg='Error: Interface %s does not support jumboframe set.' % self.interface) + if not self.jbf_max.isdigit(): + self.module.fail_json( + msg='Error: Max jumboframe is not digit.') + if (int(self.jbf_max) > 12288) or (int(self.jbf_max) < 1536): + self.module.fail_json( + msg='Error: Max jumboframe is between 1536 to 12288.') + + if self.jbf_min: + if not self.jbf_min.isdigit(): + self.module.fail_json( + msg='Error: Min jumboframe is not digit.') + if not self.jbf_max: + self.module.fail_json( + msg='Error: please specify max jumboframe value.') + if (int(self.jbf_min) > int(self.jbf_max)) or (int(self.jbf_min) < 1518): + self.module.fail_json( + msg='Error: Min jumboframe is between ' + '1518 to jumboframe max value.') + + if self.jbf_min is not None: + if self.jbf_max is None: + self.module.fail_json( + msg='Error: please input MAX jumboframe ' + 'value.') + + def get_proposed(self): + """ get_proposed""" + + self.proposed['state'] = self.state + if self.interface: + self.proposed["interface"] = self.interface + + if self.state == 'present': + if self.mtu: + self.proposed["mtu"] = self.mtu + if self.jbf_max: + if self.jbf_min: + self.proposed["jumboframe"] = "jumboframe enable %s %s" % ( + self.jbf_max, self.jbf_min) + else: + self.proposed[ + "jumboframe"] = "jumboframe enable %s %s" % (self.jbf_max, 1518) + + def get_existing(self): + """ get_existing""" + + if self.intf_info: + self.existing["interface"] = self.intf_info["ifName"] + self.existing["mtu"] = self.intf_info["ifMtu"] + + if self.intf_info: + if not self.existing["interface"]: + self.existing["interface"] = self.interface + + if len(self.jbf_config) != 2: + return + + self.existing["jumboframe"] = "jumboframe enable %s %s" % ( + self.jbf_config[0], self.jbf_config[1]) + + def get_end_state(self): + """ get_end_state""" + + if self.intf_info: + end_info = self.get_interface_dict(self.interface) + if end_info: + self.end_state["interface"] = end_info["ifName"] + self.end_state["mtu"] = end_info["ifMtu"] + if self.intf_info: + if not self.end_state["interface"]: + self.end_state["interface"] = self.interface + + if self.state == 'absent': + self.end_state["jumboframe"] = "jumboframe enable %s %s" % ( + 9216, 1518) + elif not self.jbf_max and not self.jbf_min: + if len(self.jbf_config) != 2: + return + self.end_state["jumboframe"] = "jumboframe enable %s %s" % ( + self.jbf_config[0], self.jbf_config[1]) + elif self.jbf_min: + self.end_state["jumboframe"] = "jumboframe enable %s %s" % ( + self.jbf_max, self.jbf_min) + else: + self.end_state[ + "jumboframe"] = "jumboframe enable %s %s" % (self.jbf_max, 1518) + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + self.check_params() + + self.get_proposed() + + self.merge_interface(self.interface, self.mtu) + self.set_jumboframe() + self.cli_load_config() + + self.get_existing() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """ main""" + + argument_spec = dict( + interface=dict(required=True, type='str'), + mtu=dict(type='str'), + state=dict(choices=['absent', 'present'], + default='present', required=False), + jumbo_max=dict(type='str'), + jumbo_min=dict(type='str'), + ) + argument_spec.update(ce_argument_spec) + interface = Mtu(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_multicast_global.py b/plugins/modules/ce_multicast_global.py new file mode 100644 index 0000000..fd8bf6e --- /dev/null +++ b/plugins/modules/ce_multicast_global.py @@ -0,0 +1,289 @@ +#!/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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_multicast_global +author: xuxiaowei0512 (@xuxiaowei0512) +short_description: Manages multicast global configuration on HUAWEI CloudEngine switches. +description: + - Manages multicast global on HUAWEI CloudEngine switches. +notes: + - If no vrf is supplied, vrf is set to default. + - If I(state=absent), the route will be removed, regardless of the non-required parameters. + - This module requires the netconf system service be enabled on the remote device being managed. + - This module works with connection C(netconf). +options: + aftype: + description: + - Destination ip address family type of static route. + required: true + type: str + choices: ['v4','v6'] + vrf: + description: + - VPN instance of destination ip address. + type: str + state: + description: + - Specify desired state of the resource. + type: str + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +--- + - name: multicast routing-enable + ce_multicast_global: + aftype: v4 + state: absent + provider: "{{ cli }}" + - name: multicast routing-enable + ce_multicast_global: + aftype: v4 + state: present + provider: "{{ cli }}" + - name: multicast routing-enable + ce_multicast_global: + aftype: v4 + vrf: vrf1 + provider: "{{ cli }}" + +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"addressFamily": "ipv4unicast", "state": "present", "vrfName": "_public_"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"addressFamily": "ipv4unicast", "state": "present", "vrfName": "_public_"} +updates: + description: command list sent to the device + returned: always + type: list + sample: ["multicast routing-enable"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +CE_NC_GET_MULTICAST_GLOBAL = """ + + + + + %s + %s + + + + +""" +CE_NC_MERGE_MULTICAST_GLOBAL = """ + + + + %s + %s + + + +""" +CE_NC_DELETE_MULTICAST_GLOBAL = """ + + + + %s + %s + + + +""" + + +def build_config_xml(xmlstr): + """build config xml""" + + return ' ' + xmlstr + ' ' + + +class MulticastGlobal(object): + """multicast global module""" + + def __init__(self, argument_spec): + """multicast global info""" + self.spec = argument_spec + self.module = None + self._initmodule_() + + self.aftype = self.module.params['aftype'] + self.state = self.module.params['state'] + if self.aftype == "v4": + self.version = "ipv4unicast" + else: + self.version = "ipv6unicast" + # vpn instance info + self.vrf = self.module.params['vrf'] + if self.vrf is None: + self.vrf = "_public_" + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + self.multicast_global_info = dict() + + def _initmodule_(self): + """init module""" + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=False) + + def _checkresponse_(self, xml_str, xml_name): + """check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def set_change_state(self): + """set change state""" + state = self.state + change = False + self.get_multicast_global() + # new or edit + if state == 'present': + if not self.multicast_global_info.get('multicast_global'): + # i.e. self.multicast_global_info['multicast_global'] has not value + change = True + else: + # delete + if self.multicast_global_info.get('multicast_global'): + # i.e. self.multicast_global_info['multicast_global'] has value + change = True + self.changed = change + + def get_multicast_global(self): + """get one data""" + self.multicast_global_info["multicast_global"] = list() + getxmlstr = CE_NC_GET_MULTICAST_GLOBAL % ( + self.version, self.vrf) + xml_str = get_nc_config(self.module, getxmlstr) + if 'data/' in xml_str: + return + xml_str = xml_str.replace('\r', '').replace('\n', ''). \ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', ""). \ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + mcast_enable = root.findall( + "mcastbase/mcastAfsEnables/mcastAfsEnable") + if mcast_enable: + # i.e. mcast_enable = [{vrfName:11,addressFamily:'xx'},{vrfName:22,addressFamily:'xx'}...] + for mcast_enable_key in mcast_enable: + # i.e. mcast_enable_key = {vrfName:11,addressFamily:'xx'} + mcast_info = dict() + for ele in mcast_enable_key: + if ele.tag in ["vrfName", "addressFamily"]: + mcast_info[ele.tag] = ele.text + self.multicast_global_info['multicast_global'].append(mcast_info) + + def get_existing(self): + """get existing information""" + self.set_change_state() + self.existing["multicast_global"] = self.multicast_global_info["multicast_global"] + + def get_proposed(self): + """get proposed information""" + self.proposed['addressFamily'] = self.version + self.proposed['state'] = self.state + self.proposed['vrfName'] = self.vrf + + def set_multicast_global(self): + """set multicast global""" + if not self.changed: + return + version = self.version + state = self.state + if state == "present": + configxmlstr = CE_NC_MERGE_MULTICAST_GLOBAL % (self.vrf, version) + else: + configxmlstr = CE_NC_DELETE_MULTICAST_GLOBAL % (self.vrf, version) + + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "SET_MULTICAST_GLOBAL") + + def set_update_cmd(self): + """set update command""" + if not self.changed: + return + if self.state == "present": + self.updates_cmd.append('multicast routing-enable') + else: + self.updates_cmd.append('undo multicast routing-enable') + + def get_end_state(self): + """get end state information""" + self.get_multicast_global() + self.end_state["multicast_global"] = self.multicast_global_info["multicast_global"] + + def work(self): + """worker""" + self.get_existing() + self.get_proposed() + self.set_multicast_global() + self.set_update_cmd() + self.get_end_state() + self.results['changed'] = self.changed + self.results['existing'] = self.existing + self.results['proposed'] = self.proposed + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + self.module.exit_json(**self.results) + + +def main(): + """main""" + + argument_spec = dict( + aftype=dict(choices=['v4', 'v6'], required=True), + vrf=dict(required=False, type='str'), + state=dict(choices=['absent', 'present'], default='present', required=False), + ) + interface = MulticastGlobal(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_multicast_igmp_enable.py b/plugins/modules/ce_multicast_igmp_enable.py new file mode 100644 index 0000000..649da0c --- /dev/null +++ b/plugins/modules/ce_multicast_igmp_enable.py @@ -0,0 +1,544 @@ +#!/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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_multicast_igmp_enable +author: xuxiaowei0512 (@CloudEngine-Ansible) +short_description: Manages multicast igmp enable configuration on HUAWEI CloudEngine switches. +description: + - Manages multicast igmp on HUAWEI CloudEngine switches. +notes: + - If no vrf is supplied, vrf is set to default. + If I(state=absent), the route will be removed, regardless of the + non-required parameters. + - This module requires the netconf system service be enabled on + the remote device being managed. + - This module works with connection C(netconf). +options: + aftype: + description: + - Destination ip address family type of static route. + required: true + type: str + choices: ['v4','v6'] + features: + description: + - Distinguish between Globally Enabled IGMP or + - Enabled IGMP under vlanID. + required: true + type: str + choices: ['global','vlan'] + vlan_id: + description: + - Virtual LAN identity. + type: int + igmp: + description: + - Enable Layer 2 multicast Snooping in a VLAN. + type: bool + version: + description: + - Specifies the IGMP version that can be processed. + default: 2 + type: int + proxy: + description: + - Layer 2 multicast snooping proxy is enabled. + type: bool + state: + description: + - Specify desired state of the resource. + choices: ['present','absent'] + default: present + type: str +''' + +EXAMPLES = ''' + + - name: configure global igmp enable + ce_multicast_igmp_enable: + aftype: v4 + features: 'global' + state: present + + - name: configure global igmp disable + ce_multicast_igmp_enable: + features: 'global' + aftype: v4 + state: absent + + - name: configure vlan igmp enable + ce_multicast_igmp_enable: + features: 'vlan' + aftype: v4 + vlan_id: 1 + igmp: true + + - name: new proxy,igmp,version + ce_multicast_igmp_enable: + features: 'vlan' + aftype: v4 + vlan_id: 1 + proxy: true + igmp: true + version: 1 + + - name: modify proxy,igmp,version + ce_multicast_igmp_enable: + features: 'vlan' + aftype: v4 + vlan_id: 1 + version: 2 + + - name: delete proxy,igmp,version + ce_multicast_igmp_enable: + features: 'vlan' + aftype: v4 + vlan_id: 1 + state: absent +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"addrFamily": "ipv4unicast", "features": "vlan", "proxyEnable": "false", + "snoopingEnable": "false", "state": "absent", "version": 2, "vlanId": 1} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {} +updates: + description: command list sent to the device + returned: always + type: list + sample: ["undo igmp snooping enable", + "undo igmp snooping version", + "undo igmp snooping proxy"] +changed: + description: check if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +CE_NC_GET_IGMP_GLOBAL = """ + + + + + %s + + + + +""" +CE_NC_MERGE_IGMP_SYSVIEW = """ + + + + %s + + + +""" +CE_NC_DELETE_IGMP_SYSVIEW = """ + + + + %s + + + +""" +CE_NC_GET_IGMP_VLAN_INFO = """ + + + + + + %s + %s + + + + + + + + +""" +CE_NC_MERGE_IGMP_VLANVIEW = """ + + + + + %s + %s%s%s%s + + + + +""" +CE_NC_MERGE_IGMP_VLANVIEW_SNOENABLE = """ +%s +""" +CE_NC_MERGE_IGMP_VLANVIEW_VERSION = """ +%s +""" +CE_NC_MERGE_IGMP_VLANVIEW_PROXYENABLE = """ +%s +""" +CE_NC_DELETE_IGMP_VLANVIEW = """ + + + + + %s + %s + + + + +""" + + +def get_xml(xml, value): + """operate xml""" + tempxml = xml % value + return tempxml + + +def build_config_xml(xmlstr): + """build config xml""" + + return ' ' + xmlstr + ' ' + + +class IgmpSnoop(object): + """igmp snooping module""" + + def __init__(self, argument_spec): + """igmp snooping info""" + self.spec = argument_spec + self.module = None + self._initmodule_() + + self.aftype = self.module.params['aftype'] + self.state = self.module.params['state'] + if self.aftype == "v4": + self.addr_family = "ipv4unicast" + else: + self.addr_family = "ipv6unicast" + self.features = self.module.params['features'] + self.vlan_id = self.module.params['vlan_id'] + self.igmp = str(self.module.params['igmp']).lower() + self.version = self.module.params['version'] + if self.version is None: + self.version = 2 + self.proxy = str(self.module.params['proxy']).lower() + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + self.igmp_info_data = dict() + + def _initmodule_(self): + """init module""" + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=False) + + def _checkresponse_(self, xml_str, xml_name): + """check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def _checkparams_(self): + """check all input params""" + # check vlan id + if self.features == 'vlan': + if not self.vlan_id: + self.module.fail_json(msg='Error: missing required arguments: vlan_id.') + + if self.vlan_id: + if self.vlan_id <= 0 or self.vlan_id > 4094: + self.module.fail_json( + msg='Error: Vlan id is not in the range from 1 to 4094.') + # check version + if self.version: + if self.version <= 0 or self.version > 3: + self.module.fail_json( + msg='Error: Version id is not in the range from 1 to 3.') + + def set_change_state(self): + """set change state""" + state = self.state + change = False + # vlan view igmp + if self.features == 'vlan': + self.get_igmp_vlan() + change = self.compare_data() + else: + # sys view igmp(global) + self.get_igmp_global() + # new or edit + if state == 'present': + if not self.igmp_info_data["igmp_info"]: + # igmp_info_data has not igmp_info value. + change = True + else: + # delete + if self.igmp_info_data["igmp_info"]: + # igmp_info_data has not igmp_info value. + change = True + self.changed = change + + def compare_data(self): + """compare new data and old data""" + state = self.state + change = False + # new or edit + if state == 'present': + # edit + if self.igmp_info_data["igmp_info"]: + for data in self.igmp_info_data["igmp_info"]: + if self.addr_family == data["addrFamily"] and str(self.vlan_id) == data["vlanId"]: + if self.igmp: + if self.igmp != data["snoopingEnable"]: + change = True + if self.version: + if str(self.version) != data["version"]: + change = True + if self.proxy: + if self.proxy != data["proxyEnable"]: + change = True + # new + else: + change = True + else: + # delete + if self.igmp_info_data["igmp_info"]: + change = True + return change + + def get_igmp_vlan(self): + """get igmp vlan info data""" + self.igmp_info_data["igmp_info"] = list() + getxmlstr = CE_NC_GET_IGMP_VLAN_INFO % (self.addr_family, self.vlan_id) + xml_str = get_nc_config(self.module, getxmlstr) + if 'data/' in xml_str: + return + xml_str = xml_str.replace('\r', '').replace('\n', ''). \ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', ""). \ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + igmp_enable = root.findall( + "l2mc/vlan/l2McVlanCfgs/l2McVlanCfg") + if igmp_enable: + # igmp_enable = [{addressFamily:'xx'}] + for igmp_enable_key in igmp_enable: + # igmp_enable_key = {addressFamily:'xx'} + igmp_global_info = dict() + for ele in igmp_enable_key: + if ele.tag in ["addrFamily", "vlanId", "snoopingEnable", "version", "proxyEnable"]: + igmp_global_info[ele.tag] = ele.text + self.igmp_info_data["igmp_info"].append(igmp_global_info) + + def get_igmp_global(self): + """get igmp global data""" + self.igmp_info_data["igmp_info"] = list() + getxmlstr = CE_NC_GET_IGMP_GLOBAL % ( + self.addr_family) + xml_str = get_nc_config(self.module, getxmlstr) + if 'data/' in xml_str: + return + xml_str = xml_str.replace('\r', '').replace('\n', ''). \ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', ""). \ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + igmp_enable = root.findall( + 'l2mc/l2McSnpgEnables/l2McSnpgEnable') + if igmp_enable: + # igmp_enable = [{addressFamily:'xx'}] + for igmp_enable_key in igmp_enable: + # igmp_enable_key = {addressFamily:'xx'} + igmp_global_info = dict() + for ele in igmp_enable_key: + if ele.tag in ["addrFamily"]: + igmp_global_info[ele.tag] = ele.text + self.igmp_info_data["igmp_info"].append(igmp_global_info) + + def set_vlanview_igmp(self): + """set igmp of vlanview""" + if not self.changed: + return + addr_family = self.addr_family + state = self.state + igmp_xml = """\n""" + version_xml = """\n""" + proxy_xml = """\n""" + if state == "present": + if self.igmp: + igmp_xml = get_xml(CE_NC_MERGE_IGMP_VLANVIEW_SNOENABLE, self.igmp.lower()) + if str(self.version): + version_xml = get_xml(CE_NC_MERGE_IGMP_VLANVIEW_VERSION, self.version) + if self.proxy: + proxy_xml = get_xml(CE_NC_MERGE_IGMP_VLANVIEW_PROXYENABLE, self.proxy.lower()) + configxmlstr = CE_NC_MERGE_IGMP_VLANVIEW % ( + addr_family, self.vlan_id, igmp_xml, version_xml, proxy_xml) + else: + configxmlstr = CE_NC_DELETE_IGMP_VLANVIEW % (addr_family, self.vlan_id) + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "SET_VLANVIEW_IGMP") + + def set_sysview_igmp(self): + """set igmp of sysview""" + if not self.changed: + return + version = self.addr_family + state = self.state + if state == "present": + configxmlstr = CE_NC_MERGE_IGMP_SYSVIEW % (version) + else: + configxmlstr = CE_NC_DELETE_IGMP_SYSVIEW % (version) + + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "SET_SYSVIEW_IGMP") + + def set_sysview_cmd(self): + """set sysview update command""" + if not self.changed: + return + if self.state == "present": + self.updates_cmd.append('igmp snooping enable') + else: + self.updates_cmd.append('undo igmp snooping enable') + + def set_vlanview_cmd(self): + """set vlanview update command""" + if not self.changed: + return + if self.state == "present": + if self.igmp: + if self.igmp.lower() == 'true': + self.updates_cmd.append('igmp snooping enable') + else: + self.updates_cmd.append('undo igmp snooping enable') + if str(self.version): + self.updates_cmd.append('igmp snooping version %s' % (self.version)) + else: + self.updates_cmd.append('undo igmp snooping version') + if self.proxy: + if self.proxy.lower() == 'true': + self.updates_cmd.append('igmp snooping proxy') + else: + self.updates_cmd.append('undo igmp snooping proxy') + + else: + self.updates_cmd.append('undo igmp snooping enable') + self.updates_cmd.append('undo igmp snooping version') + self.updates_cmd.append('undo igmp snooping proxy') + + def get_existing(self): + """get existing information""" + self.set_change_state() + self.existing["igmp_info"] = self.igmp_info_data["igmp_info"] + + def get_proposed(self): + """get proposed information""" + self.proposed['addrFamily'] = self.addr_family + self.proposed['features'] = self.features + if self.features == 'vlan': + self.proposed['snoopingEnable'] = self.igmp + self.proposed['version'] = self.version + self.proposed['vlanId'] = self.vlan_id + self.proposed['proxyEnable'] = self.proxy + self.proposed['state'] = self.state + + def set_igmp_netconf(self): + """config netconf""" + if self.features == 'vlan': + self.set_vlanview_igmp() + else: + self.set_sysview_igmp() + + def set_update_cmd(self): + """set update command""" + if self.features == 'vlan': + self.set_vlanview_cmd() + else: + self.set_sysview_cmd() + + def get_end_state(self): + """get end state information""" + if self.features == 'vlan': + self.get_igmp_vlan() + else: + # sys view igmp(global) + self.get_igmp_global() + self.end_state["igmp_info"] = self.igmp_info_data["igmp_info"] + + def work(self): + """worker""" + self._checkparams_() + self.get_existing() + self.get_proposed() + self.set_igmp_netconf() + self.set_update_cmd() + self.get_end_state() + self.results['changed'] = self.changed + self.results['existing'] = self.existing + self.results['proposed'] = self.proposed + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + self.module.exit_json(**self.results) + + +def main(): + """main""" + argument_spec = dict( + aftype=dict(choices=['v4', 'v6'], required=True), + features=dict(required=True, choices=['global', 'vlan'], type='str'), + vlan_id=dict(type='int'), + igmp=dict(type='bool', default=False), + version=dict(type='int', default=2), + proxy=dict(type='bool', default=False), + state=dict(choices=['absent', 'present'], default='present'), + ) + interface = IgmpSnoop(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_netconf.py b/plugins/modules/ce_netconf.py new file mode 100644 index 0000000..d03b605 --- /dev/null +++ b/plugins/modules/ce_netconf.py @@ -0,0 +1,203 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_netconf +short_description: Run an arbitrary netconf command on HUAWEI CloudEngine switches. +description: + - Sends an arbitrary netconf command on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + rpc: + description: + - The type of rpc. + required: true + choices: ['get', 'edit-config', 'execute-action', 'execute-cli'] + cfg_xml: + description: + - The config xml string. + required: true +''' + +EXAMPLES = ''' + +- name: CloudEngine netconf test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Netconf get operation" + ce_netconf: + rpc: get + cfg_xml: ' + + + + 10 + + + + + + + + + ' + provider: "{{ cli }}" + + - name: "Netconf edit-config operation" + ce_netconf: + rpc: edit-config + cfg_xml: ' + + + + default_wdz + local + invalid + + + + ' + provider: "{{ cli }}" + + - name: "Netconf execute-action operation" + ce_netconf: + rpc: execute-action + cfg_xml: ' + + + ipv4unicast + + + ' + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"result": ["ok"]} +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import execute_nc_action, ce_argument_spec, execute_nc_cli + + +def main(): + """ main """ + + argument_spec = dict( + rpc=dict(choices=['get', 'edit-config', + 'execute-action', 'execute-cli'], required=True), + cfg_xml=dict(required=True) + ) + + argument_spec.update(ce_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + rpc = module.params['rpc'] + cfg_xml = module.params['cfg_xml'] + changed = False + end_state = dict() + + if rpc == "get": + + response = get_nc_config(module, cfg_xml) + + if "" in response: + end_state["result"] = "" + else: + tmp1 = response.split(r"") + tmp2 = tmp1[1].split(r"") + result = tmp2[0].split("\n") + + end_state["result"] = result + + elif rpc == "edit-config": + + response = set_nc_config(module, cfg_xml) + + if "" not in response: + module.fail_json(msg='rpc edit-config failed.') + + changed = True + end_state["result"] = "ok" + + elif rpc == "execute-action": + + response = execute_nc_action(module, cfg_xml) + + if "" not in response: + module.fail_json(msg='rpc execute-action failed.') + + changed = True + end_state["result"] = "ok" + + elif rpc == "execute-cli": + + response = execute_nc_cli(module, cfg_xml) + + if "" in response: + end_state["result"] = "" + else: + tmp1 = response.split(r"") + tmp2 = tmp1[1].split(r"") + result = tmp2[0].split("\n") + + end_state["result"] = result + + else: + module.fail_json(msg='please input correct rpc.') + + results = dict() + results['changed'] = changed + results['end_state'] = end_state + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_netstream_aging.py b/plugins/modules/ce_netstream_aging.py new file mode 100644 index 0000000..787f65c --- /dev/null +++ b/plugins/modules/ce_netstream_aging.py @@ -0,0 +1,517 @@ +#!/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 . +# +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_netstream_aging +short_description: Manages timeout mode of NetStream on HUAWEI CloudEngine switches. +description: + - Manages timeout mode of NetStream on HUAWEI CloudEngine switches. +author: YangYang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + timeout_interval: + description: + - Netstream timeout interval. + If is active type the interval is 1-60. + If is inactive ,the interval is 5-600. + default: 30 + type: + description: + - Specifies the packet type of netstream timeout active interval. + choices: ['ip', 'vxlan'] + state: + description: + - Specify desired state of the resource. + choices: ['present', 'absent'] + default: present + timeout_type: + description: + - Netstream timeout type. + choices: ['active', 'inactive', 'tcp-session', 'manual'] + manual_slot: + description: + - Specifies the slot number of netstream manual timeout. +''' + +EXAMPLES = ''' +- name: netstream aging module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configure netstream ip timeout active interval , the interval is 40 minutes. + ce_netstream_aging: + timeout_interval: 40 + type: ip + timeout_type: active + state: present + provider: "{{ cli }}" + + - name: Configure netstream vxlan timeout active interval , the interval is 40 minutes. + ce_netstream_aging: + timeout_interval: 40 + type: vxlan + timeout_type: active + active_state: present + provider: "{{ cli }}" + + - name: Delete netstream ip timeout active interval , set the ip timeout interval to 30 minutes. + ce_netstream_aging: + type: ip + timeout_type: active + state: absent + provider: "{{ cli }}" + + - name: Delete netstream vxlan timeout active interval , set the vxlan timeout interval to 30 minutes. + ce_netstream_aging: + type: vxlan + timeout_type: active + state: absent + provider: "{{ cli }}" + + - name: Enable netstream ip tcp session timeout. + ce_netstream_aging: + type: ip + timeout_type: tcp-session + state: present + provider: "{{ cli }}" + + - name: Enable netstream vxlan tcp session timeout. + ce_netstream_aging: + type: vxlan + timeout_type: tcp-session + state: present + provider: "{{ cli }}" + + - name: Disable netstream ip tcp session timeout. + ce_netstream_aging: + type: ip + timeout_type: tcp-session + state: absent + provider: "{{ cli }}" + + - name: Disable netstream vxlan tcp session timeout. + ce_netstream_aging: + type: vxlan + timeout_type: tcp-session + state: absent + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"timeout_interval": "40", + "type": "ip", + "state": "absent", + "timeout_type": active} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"active_timeout": [ + { + "ip": "40", + "vxlan": 30 + } + ], + "inactive_timeout": [ + { + "ip": 30, + "vxlan": 30 + } + ], + "tcp_timeout": [ + { + "ip": "disable", + "vxlan": "disable" + } + ]} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"active_timeout": [ + { + "ip": 30, + "vxlan": 30 + } + ], + "inactive_timeout": [ + { + "ip": 30, + "vxlan": 30 + } + ], + "tcp_timeout": [ + { + "ip": "disable", + "vxlan": "disable" + } + ]} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["undo netstream timeout ip active 40"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + + +class NetStreamAging(object): + """ + Manages netstream aging. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.timeout_interval = self.module.params['timeout_interval'] + self.type = self.module.params['type'] + self.state = self.module.params['state'] + self.timeout_type = self.module.params['timeout_type'] + self.manual_slot = self.module.params['manual_slot'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + # local parameters + self.existing["active_timeout"] = list() + self.existing["inactive_timeout"] = list() + self.existing["tcp_timeout"] = list() + self.end_state["active_timeout"] = list() + self.end_state["inactive_timeout"] = list() + self.end_state["tcp_timeout"] = list() + self.active_changed = False + self.inactive_changed = False + self.tcp_changed = False + + def init_module(self): + """init module""" + + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) + + def get_exist_timer_out_para(self): + """Get exist netstream timeout parameters""" + + active_tmp = dict() + inactive_tmp = dict() + tcp_tmp = dict() + active_tmp["ip"] = "30" + active_tmp["vxlan"] = "30" + inactive_tmp["ip"] = "30" + inactive_tmp["vxlan"] = "30" + tcp_tmp["ip"] = "absent" + tcp_tmp["vxlan"] = "absent" + + cmd = "display current-configuration | include ^netstream timeout" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + config = str(out).strip() + if config: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem = config_mem.lstrip() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 4 and config_mem_list[2] == "ip": + if config_mem_list[3] == "active": + active_tmp["ip"] = config_mem_list[4] + if config_mem_list[3] == "inactive": + inactive_tmp["ip"] = config_mem_list[4] + if config_mem_list[3] == "tcp-session": + tcp_tmp["ip"] = "present" + if len(config_mem_list) > 4 and config_mem_list[2] == "vxlan": + if config_mem_list[4] == "active": + active_tmp["vxlan"] = config_mem_list[5] + if config_mem_list[4] == "inactive": + inactive_tmp["vxlan"] = config_mem_list[5] + if config_mem_list[4] == "tcp-session": + tcp_tmp["vxlan"] = "present" + self.existing["active_timeout"].append(active_tmp) + self.existing["inactive_timeout"].append(inactive_tmp) + self.existing["tcp_timeout"].append(tcp_tmp) + + def get_end_timer_out_para(self): + """Get end netstream timeout parameters""" + + active_tmp = dict() + inactive_tmp = dict() + tcp_tmp = dict() + active_tmp["ip"] = "30" + active_tmp["vxlan"] = "30" + inactive_tmp["ip"] = "30" + inactive_tmp["vxlan"] = "30" + tcp_tmp["ip"] = "absent" + tcp_tmp["vxlan"] = "absent" + cmd = "display current-configuration | include ^netstream timeout" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + config = str(out).strip() + if config: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem = config_mem.lstrip() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 4 and config_mem_list[2] == "ip": + if config_mem_list[3] == "active": + active_tmp["ip"] = config_mem_list[4] + if config_mem_list[3] == "inactive": + inactive_tmp["ip"] = config_mem_list[4] + if config_mem_list[3] == "tcp-session": + tcp_tmp["ip"] = "present" + if len(config_mem_list) > 4 and config_mem_list[2] == "vxlan": + if config_mem_list[4] == "active": + active_tmp["vxlan"] = config_mem_list[5] + if config_mem_list[4] == "inactive": + inactive_tmp["vxlan"] = config_mem_list[5] + if config_mem_list[4] == "tcp-session": + tcp_tmp["vxlan"] = "present" + self.end_state["active_timeout"].append(active_tmp) + self.end_state["inactive_timeout"].append(inactive_tmp) + self.end_state["tcp_timeout"].append(tcp_tmp) + + def check_params(self): + """Check all input params""" + + # interval check + if not str(self.timeout_interval).isdigit(): + self.module.fail_json( + msg='Error: Timeout interval should be numerical.') + if self.timeout_type == "active": + if int(self.timeout_interval) < 1 or int(self.timeout_interval) > 60: + self.module.fail_json( + msg="Error: Active interval should between 1 - 60 minutes.") + if self.timeout_type == "inactive": + if int(self.timeout_interval) < 5 or int(self.timeout_interval) > 600: + self.module.fail_json( + msg="Error: Inactive interval should between 5 - 600 seconds.") + if self.timeout_type == "manual": + if not self.manual_slot: + self.module.fail_json( + msg="Error: If use manual timeout mode,slot number is needed.") + if re.match(r'^\d+(\/\d*)?$', self.manual_slot) is None: + self.module.fail_json( + msg='Error: Slot number should be numerical.') + + def get_proposed(self): + """get proposed info""" + + if self.timeout_interval: + self.proposed["timeout_interval"] = self.timeout_interval + if self.timeout_type: + self.proposed["timeout_type"] = self.timeout_type + if self.type: + self.proposed["type"] = self.type + if self.state: + self.proposed["state"] = self.state + if self.manual_slot: + self.proposed["manual_slot"] = self.manual_slot + + def get_existing(self): + """get existing info""" + active_tmp = dict() + inactive_tmp = dict() + tcp_tmp = dict() + + self.get_exist_timer_out_para() + + if self.timeout_type == "active": + for active_tmp in self.existing["active_timeout"]: + if self.state == "present": + if str(active_tmp[self.type]) != self.timeout_interval: + self.active_changed = True + else: + if self.timeout_interval != "30": + if str(active_tmp[self.type]) != "30": + if str(active_tmp[self.type]) != self.timeout_interval: + self.module.fail_json( + msg='Error: The specified active interval do not exist.') + if str(active_tmp[self.type]) != "30": + self.timeout_interval = active_tmp[self.type] + self.active_changed = True + if self.timeout_type == "inactive": + for inactive_tmp in self.existing["inactive_timeout"]: + if self.state == "present": + if str(inactive_tmp[self.type]) != self.timeout_interval: + self.inactive_changed = True + else: + if self.timeout_interval != "30": + if str(inactive_tmp[self.type]) != "30": + if str(inactive_tmp[self.type]) != self.timeout_interval: + self.module.fail_json( + msg='Error: The specified inactive interval do not exist.') + if str(inactive_tmp[self.type]) != "30": + self.timeout_interval = inactive_tmp[self.type] + self.inactive_changed = True + if self.timeout_type == "tcp-session": + for tcp_tmp in self.existing["tcp_timeout"]: + if str(tcp_tmp[self.type]) != self.state: + self.tcp_changed = True + + def operate_time_out(self): + """configure timeout parameters""" + + cmd = "" + if self.timeout_type == "manual": + if self.type == "ip": + self.cli_add_command("quit") + cmd = "reset netstream cache ip slot %s" % self.manual_slot + self.cli_add_command(cmd) + elif self.type == "vxlan": + self.cli_add_command("quit") + cmd = "reset netstream cache vxlan inner-ip slot %s" % self.manual_slot + self.cli_add_command(cmd) + + if not self.active_changed and not self.inactive_changed and not self.tcp_changed: + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + return + + if self.active_changed or self.inactive_changed: + if self.type == "ip": + cmd = "netstream timeout ip %s %s" % (self.timeout_type, self.timeout_interval) + elif self.type == "vxlan": + cmd = "netstream timeout vxlan inner-ip %s %s" % (self.timeout_type, self.timeout_interval) + if self.state == "absent": + self.cli_add_command(cmd, undo=True) + else: + self.cli_add_command(cmd) + if self.timeout_type == "tcp-session" and self.tcp_changed: + if self.type == "ip": + if self.state == "present": + cmd = "netstream timeout ip tcp-session" + else: + cmd = "undo netstream timeout ip tcp-session" + + elif self.type == "vxlan": + if self.state == "present": + cmd = "netstream timeout vxlan inner-ip tcp-session" + else: + cmd = "undo netstream timeout vxlan inner-ip tcp-session" + self.cli_add_command(cmd) + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + def get_end_state(self): + """get end state info""" + + self.get_end_timer_out_para() + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + self.operate_time_out() + self.get_end_state() + if self.existing == self.end_state: + self.changed = False + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + timeout_interval=dict(required=False, type='str', default='30'), + type=dict(required=False, choices=['ip', 'vxlan']), + state=dict(required=False, choices=['present', 'absent'], default='present'), + timeout_type=dict(required=False, choices=['active', 'inactive', 'tcp-session', 'manual']), + manual_slot=dict(required=False, type='str'), + ) + argument_spec.update(ce_argument_spec) + module = NetStreamAging(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_netstream_export.py b/plugins/modules/ce_netstream_export.py new file mode 100644 index 0000000..da67e74 --- /dev/null +++ b/plugins/modules/ce_netstream_export.py @@ -0,0 +1,558 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_netstream_export +short_description: Manages netstream export on HUAWEI CloudEngine switches. +description: + - Configure NetStream flow statistics exporting and versions for exported packets on HUAWEI CloudEngine switches. +author: Zhijin Zhou (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + type: + description: + - Specifies NetStream feature. + required: true + choices: ['ip', 'vxlan'] + source_ip: + description: + - Specifies source address which can be IPv6 or IPv4 of the exported NetStream packet. + host_ip: + description: + - Specifies destination address which can be IPv6 or IPv4 of the exported NetStream packet. + host_port: + description: + - Specifies the destination UDP port number of the exported packets. + The value is an integer that ranges from 1 to 65535. + host_vpn: + description: + - Specifies the VPN instance of the exported packets carrying flow statistics. + Ensure the VPN instance has been created on the device. + version: + description: + - Sets the version of exported packets. + choices: ['5', '9'] + as_option: + description: + - Specifies the AS number recorded in the statistics as the original or the peer AS number. + choices: ['origin', 'peer'] + bgp_nexthop: + description: + - Configures the statistics to carry BGP next hop information. Currently, only V9 supports the exported + packets carrying BGP next hop information. + choices: ['enable','disable'] + default: 'disable' + state: + description: + - Manage the state of the resource. + choices: ['present','absent'] + default: present +''' + +EXAMPLES = ''' +- name: netstream export module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configures the source address for the exported packets carrying IPv4 flow statistics. + ce_netstream_export: + type: ip + source_ip: 192.8.2.2 + provider: "{{ cli }}" + + - name: Configures the source IP address for the exported packets carrying VXLAN flexible flow statistics. + ce_netstream_export: + type: vxlan + source_ip: 192.8.2.3 + provider: "{{ cli }}" + + - name: Configures the destination IP address and destination UDP port number for the exported packets carrying IPv4 flow statistics. + ce_netstream_export: + type: ip + host_ip: 192.8.2.4 + host_port: 25 + host_vpn: test + provider: "{{ cli }}" + + - name: Configures the destination IP address and destination UDP port number for the exported packets carrying VXLAN flexible flow statistics. + ce_netstream_export: + type: vxlan + host_ip: 192.8.2.5 + host_port: 26 + host_vpn: test + provider: "{{ cli }}" + + - name: Configures the version number of the exported packets carrying IPv4 flow statistics. + ce_netstream_export: + type: ip + version: 9 + as_option: origin + bgp_nexthop: enable + provider: "{{ cli }}" + + - name: Configures the version for the exported packets carrying VXLAN flexible flow statistics. + ce_netstream_export: + type: vxlan + version: 9 + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "as_option": "origin", + "bgp_nexthop": "enable", + "host_ip": "192.8.5.6", + "host_port": "26", + "host_vpn": "test", + "source_ip": "192.8.2.5", + "state": "present", + "type": "ip", + "version": "9" + } +existing: + description: k/v pairs of existing attributes on the device + returned: always + type: dict + sample: { + "as_option": null, + "bgp_nexthop": "disable", + "host_ip": null, + "host_port": null, + "host_vpn": null, + "source_ip": null, + "type": "ip", + "version": null + } +end_state: + description: k/v pairs of end attributes on the device + returned: always + type: dict + sample: { + "as_option": "origin", + "bgp_nexthop": "enable", + "host_ip": "192.8.5.6", + "host_port": "26", + "host_vpn": "test", + "source_ip": "192.8.2.5", + "type": "ip", + "version": "9" + } +updates: + description: command list sent to the device + returned: always + type: list + sample: [ + "netstream export ip source 192.8.2.5", + "netstream export ip host 192.8.5.6 26 vpn-instance test", + "netstream export ip version 9 origin-as bgp-nexthop" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + + +def is_ipv4_addr(ip_addr): + """check ipaddress validate""" + + rule1 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.' + rule2 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])' + ipv4_regex = '%s%s%s%s%s%s' % ('^', rule1, rule1, rule1, rule2, '$') + + return bool(re.match(ipv4_regex, ip_addr)) + + +def is_config_exist(cmp_cfg, test_cfg): + """is configuration exist""" + + test_cfg_tmp = test_cfg + ' *$' + '|' + test_cfg + ' *\n' + obj = re.compile(test_cfg_tmp) + result = re.findall(obj, cmp_cfg) + if not result: + return False + return True + + +class NetstreamExport(object): + """Manage NetStream export""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # NetStream export configuration parameters + self.type = self.module.params['type'] + self.source_ip = self.module.params['source_ip'] + self.host_ip = self.module.params['host_ip'] + self.host_port = self.module.params['host_port'] + self.host_vpn = self.module.params['host_vpn'] + self.version = self.module.params['version'] + self.as_option = self.module.params['as_option'] + self.bgp_netxhop = self.module.params['bgp_nexthop'] + self.state = self.module.params['state'] + + self.commands = list() + self.config = None + self.exist_conf = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def get_netstream_config(self): + """get current netstream configuration""" + + cmd = "display current-configuration | include ^netstream export" + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + config = str(out).strip() + return config + + def get_existing(self): + """get existing config""" + + self.existing = dict(type=self.type, + source_ip=self.exist_conf['source_ip'], + host_ip=self.exist_conf['host_ip'], + host_port=self.exist_conf['host_port'], + host_vpn=self.exist_conf['host_vpn'], + version=self.exist_conf['version'], + as_option=self.exist_conf['as_option'], + bgp_nexthop=self.exist_conf['bgp_netxhop']) + + def get_proposed(self): + """get proposed config""" + + self.proposed = dict(type=self.type, + source_ip=self.source_ip, + host_ip=self.host_ip, + host_port=self.host_port, + host_vpn=self.host_vpn, + version=self.version, + as_option=self.as_option, + bgp_nexthop=self.bgp_netxhop, + state=self.state) + + def get_end_state(self): + """get end config""" + self.get_config_data() + self.end_state = dict(type=self.type, + source_ip=self.exist_conf['source_ip'], + host_ip=self.exist_conf['host_ip'], + host_port=self.exist_conf['host_port'], + host_vpn=self.exist_conf['host_vpn'], + version=self.exist_conf['version'], + as_option=self.exist_conf['as_option'], + bgp_nexthop=self.exist_conf['bgp_netxhop']) + + def show_result(self): + """show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + if cmd not in self.updates_cmd: + self.updates_cmd.append(cmd) # show updates result + + def config_nets_export_src_addr(self): + """Configures the source address for the exported packets""" + + if is_ipv4_addr(self.source_ip): + if self.type == 'ip': + cmd = "netstream export ip source %s" % self.source_ip + else: + cmd = "netstream export vxlan inner-ip source %s" % self.source_ip + else: + if self.type == 'ip': + cmd = "netstream export ip source ipv6 %s" % self.source_ip + else: + cmd = "netstream export vxlan inner-ip source ipv6 %s" % self.source_ip + + if is_config_exist(self.config, cmd): + self.exist_conf['source_ip'] = self.source_ip + if self.state == 'present': + return + else: + undo = True + else: + if self.state == 'absent': + return + else: + undo = False + + self.cli_add_command(cmd, undo) + + def config_nets_export_host_addr(self): + """Configures the destination IP address and destination UDP port number""" + + if is_ipv4_addr(self.host_ip): + if self.type == 'ip': + cmd = 'netstream export ip host %s %s' % (self.host_ip, self.host_port) + else: + cmd = 'netstream export vxlan inner-ip host %s %s' % (self.host_ip, self.host_port) + else: + if self.type == 'ip': + cmd = 'netstream export ip host ipv6 %s %s' % (self.host_ip, self.host_port) + else: + cmd = 'netstream export vxlan inner-ip host ipv6 %s %s' % (self.host_ip, self.host_port) + + if self.host_vpn: + cmd += " vpn-instance %s" % self.host_vpn + + if is_config_exist(self.config, cmd): + self.exist_conf['host_ip'] = self.host_ip + self.exist_conf['host_port'] = self.host_port + if self.host_vpn: + self.exist_conf['host_vpn'] = self.host_vpn + + if self.state == 'present': + return + else: + undo = True + else: + if self.state == 'absent': + return + else: + undo = False + + self.cli_add_command(cmd, undo) + + def config_nets_export_vxlan_ver(self): + """Configures the version for the exported packets carrying VXLAN flexible flow statistics""" + + cmd = 'netstream export vxlan inner-ip version 9' + + if is_config_exist(self.config, cmd): + self.exist_conf['version'] = self.version + + if self.state == 'present': + return + else: + undo = True + else: + if self.state == 'absent': + return + else: + undo = False + + self.cli_add_command(cmd, undo) + + def config_nets_export_ip_ver(self): + """Configures the version number of the exported packets carrying IPv4 flow statistics""" + + cmd = 'netstream export ip version %s' % self.version + if self.version == '5': + if self.as_option == 'origin': + cmd += ' origin-as' + elif self.as_option == 'peer': + cmd += ' peer-as' + else: + if self.as_option == 'origin': + cmd += ' origin-as' + elif self.as_option == 'peer': + cmd += ' peer-as' + + if self.bgp_netxhop == 'enable': + cmd += ' bgp-nexthop' + + if cmd == 'netstream export ip version 5': + cmd_tmp = "netstream export ip version" + if cmd_tmp in self.config: + if self.state == 'present': + self.cli_add_command(cmd, False) + else: + self.exist_conf['version'] = self.version + return + + if is_config_exist(self.config, cmd): + self.exist_conf['version'] = self.version + self.exist_conf['as_option'] = self.as_option + self.exist_conf['bgp_netxhop'] = self.bgp_netxhop + + if self.state == 'present': + return + else: + undo = True + else: + if self.state == 'absent': + return + else: + undo = False + + self.cli_add_command(cmd, undo) + + def config_netstream_export(self): + """configure netstream export""" + + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + def check_params(self): + """Check all input params""" + + if not self.type: + self.module.fail_json(msg='Error: The value of type cannot be empty.') + + if self.host_port: + if not self.host_port.isdigit(): + self.module.fail_json(msg='Error: Host port is invalid.') + if int(self.host_port) < 1 or int(self.host_port) > 65535: + self.module.fail_json(msg='Error: Host port is not in the range from 1 to 65535.') + + if self.host_vpn: + if self.host_vpn == '_public_': + self.module.fail_json( + msg='Error: The host vpn name _public_ is reserved.') + if len(self.host_vpn) < 1 or len(self.host_vpn) > 31: + self.module.fail_json(msg='Error: The host vpn name length is not in the range from 1 to 31.') + + if self.type == 'vxlan' and self.version == '5': + self.module.fail_json(msg="Error: When type is vxlan, version must be 9.") + + if self.type == 'ip' and self.version == '5' and self.bgp_netxhop == 'enable': + self.module.fail_json(msg="Error: When type=ip and version=5, bgp_netxhop is not supported.") + + if (self.host_ip and not self.host_port) or (self.host_port and not self.host_ip): + self.module.fail_json(msg="Error: host_ip and host_port must both exist or not exist.") + + def get_config_data(self): + """get configuration commands and current configuration""" + + self.exist_conf['type'] = self.type + self.exist_conf['source_ip'] = None + self.exist_conf['host_ip'] = None + self.exist_conf['host_port'] = None + self.exist_conf['host_vpn'] = None + self.exist_conf['version'] = None + self.exist_conf['as_option'] = None + self.exist_conf['bgp_netxhop'] = 'disable' + + self.config = self.get_netstream_config() + + if self.type and self.source_ip: + self.config_nets_export_src_addr() + + if self.type and self.host_ip and self.host_port: + self.config_nets_export_host_addr() + + if self.type == 'vxlan' and self.version == '9': + self.config_nets_export_vxlan_ver() + + if self.type == 'ip' and self.version: + self.config_nets_export_ip_ver() + + def work(self): + """execute task""" + + self.check_params() + self.get_proposed() + self.get_config_data() + self.get_existing() + + self.config_netstream_export() + + self.get_end_state() + self.show_result() + + +def main(): + """main function entry""" + + argument_spec = dict( + type=dict(required=True, type='str', choices=['ip', 'vxlan']), + source_ip=dict(required=False, type='str'), + host_ip=dict(required=False, type='str'), + host_port=dict(required=False, type='str'), + host_vpn=dict(required=False, type='str'), + version=dict(required=False, type='str', choices=['5', '9']), + as_option=dict(required=False, type='str', choices=['origin', 'peer']), + bgp_nexthop=dict(required=False, type='str', choices=['enable', 'disable'], default='disable'), + state=dict(choices=['absent', 'present'], default='present', required=False) + ) + argument_spec.update(ce_argument_spec) + netstream_export = NetstreamExport(argument_spec) + netstream_export.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_netstream_global.py b/plugins/modules/ce_netstream_global.py new file mode 100644 index 0000000..6b4323d --- /dev/null +++ b/plugins/modules/ce_netstream_global.py @@ -0,0 +1,943 @@ +#!/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 . +# +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_netstream_global +short_description: Manages global parameters of NetStream on HUAWEI CloudEngine switches. +description: + - Manages global parameters of NetStream on HUAWEI CloudEngine switches. +author: YangYang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + type: + description: + - Specifies the type of netstream global. + choices: ['ip', 'vxlan'] + default: 'ip' + state: + description: + - Specify desired state of the resource. + choices: ['present', 'absent'] + default: present + interface: + description: + - Netstream global interface. + required: true + sampler_interval: + description: + - Specifies the netstream sampler interval, length is 1 - 65535. + sampler_direction: + description: + - Specifies the netstream sampler direction. + choices: ['inbound', 'outbound'] + statistics_direction: + description: + - Specifies the netstream statistic direction. + choices: ['inbound', 'outbound'] + statistics_record: + description: + - Specifies the flexible netstream statistic record, length is 1 - 32. + index_switch: + description: + - Specifies the netstream index-switch. + choices: ['16', '32'] + default: '16' +''' + +EXAMPLES = ''' +- name: netstream global module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configure a netstream sampler at interface 10ge1/0/2, direction is outbound,interval is 30. + ce_netstream_global: + interface: 10ge1/0/2 + type: ip + sampler_interval: 30 + sampler_direction: outbound + state: present + provider: "{{ cli }}" + - name: Configure a netstream flexible statistic at interface 10ge1/0/2, record is test1, type is ip. + ce_netstream_global: + type: ip + interface: 10ge1/0/2 + statistics_record: test1 + provider: "{{ cli }}" + - name: Set the vxlan index-switch to 32. + ce_netstream_global: + type: vxlan + interface: all + index_switch: 32 + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"index_switch": "16", + "interface": "10ge1/0/2", + "state": "present", + "statistics_record": "test", + "type": "vxlan"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"flexible_statistic": [ + { + "interface": "10ge1/0/2", + "statistics_record": [], + "type": "ip" + }, + { + "interface": "10ge1/0/2", + "statistics_record": [], + "type": "vxlan" + } + ], + "index-switch": [ + { + "index-switch": "16", + "type": "ip" + }, + { + "index-switch": "16", + "type": "vxlan" + } + ], + "ip_record": [ + "test", + "test1" + ], + "sampler": [ + { + "interface": "all", + "sampler_direction": "null", + "sampler_interval": "null" + } + ], + "statistic": [ + { + "interface": "10ge1/0/2", + "statistics_direction": [], + "type": "null" + } + ], + "vxlan_record": [ + "test" + ]} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"flexible_statistic": [ + { + "interface": "10ge1/0/2", + "statistics_record": [], + "type": "ip" + }, + { + "interface": "10ge1/0/2", + "statistics_record": [ + "test" + ], + "type": "vxlan" + } + ], + "index-switch": [ + { + "index-switch": "16", + "type": "ip" + }, + { + "index-switch": "16", + "type": "vxlan" + } + ], + "sampler": [ + { + "interface": "all", + "sampler_direction": "null", + "sampler_interval": "null" + } + ], + "statistic": [ + { + "interface": "10ge1/0/2", + "statistics_direction": [], + "type": "null" + } + ]} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface 10ge1/0/2", + "netstream record test vxlan inner-ip"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_connection, rm_config_prefix +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('ALL'): + iftype = 'all' + else: + return None + + return iftype.lower() + + +def get_config(module, flags): + + """Retrieves the current config from the device or cache + """ + time_stamp_regex = re.compile(r'\s*\d{4}-\d{1,2}-\d{1,2}\s+\d{2}\:\d{2}\:\d{2}\.\d+\s*') + flags = [] if flags is None else flags + if isinstance(flags, str): + flags = [flags] + elif not isinstance(flags, list): + flags = [] + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + conn = get_connection(module) + rc, out, err = conn.exec_command(cmd) + if rc != 0: + module.fail_json(msg=err) + cfg = str(out).strip() + # remove default configuration prefix '~' + for flag in flags: + if "include-default" in flag: + cfg = rm_config_prefix(cfg) + break + lines = cfg.split('\n') + lines = [l for l in lines if time_stamp_regex.match(l) is None] + if cfg.startswith('display'): + if len(lines) > 1: + lines.pop(0) + else: + return '' + return '\n'.join(lines) + + +class NetStreamGlobal(object): + """ + Manages netstream global parameters. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.type = self.module.params['type'] + self.interface = self.module.params['interface'] + self.sampler_interval = self.module.params['sampler_interval'] + self.sampler_direction = self.module.params['sampler_direction'] + self.statistics_direction = self.module.params['statistics_direction'] + self.statistics_record = self.module.params['statistics_record'] + self.index_switch = self.module.params['index_switch'] + self.state = self.module.params['state'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + # local parameters + self.existing["sampler"] = list() + self.existing["statistic"] = list() + self.existing["flexible_statistic"] = list() + self.existing["index-switch"] = list() + self.existing["ip_record"] = list() + self.existing["vxlan_record"] = list() + self.end_state["sampler"] = list() + self.end_state["statistic"] = list() + self.end_state["flexible_statistic"] = list() + self.end_state["index-switch"] = list() + self.sampler_changed = False + self.statistic_changed = False + self.flexible_changed = False + self.index_switch_changed = False + + def init_module(self): + """init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) + + def get_exist_sampler_interval(self): + """get exist netstream sampler interval""" + + sampler_tmp = dict() + sampler_tmp1 = dict() + flags = list() + exp = " | ignore-case include ^netstream sampler random-packets" + flags.append(exp) + config = get_config(self.module, flags) + if not config: + sampler_tmp["sampler_interval"] = "null" + sampler_tmp["sampler_direction"] = "null" + sampler_tmp["interface"] = "null" + else: + config_list = config.split(' ') + config_num = len(config_list) + sampler_tmp["sampler_direction"] = config_list[config_num - 1] + sampler_tmp["sampler_interval"] = config_list[config_num - 2] + sampler_tmp["interface"] = "all" + self.existing["sampler"].append(sampler_tmp) + if self.interface != "all": + flags = list() + exp = r" | ignore-case section include ^#\s+interface %s" \ + r" | include netstream sampler random-packets" % self.interface + flags.append(exp) + config = get_config(self.module, flags) + if not config: + sampler_tmp1["sampler_interval"] = "null" + sampler_tmp1["sampler_direction"] = "null" + else: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + sampler_tmp1 = dict() + config_mem_list = config_mem.split(' ') + config_num = len(config_mem_list) + if config_num > 1: + sampler_tmp1["sampler_direction"] = config_mem_list[ + config_num - 1] + sampler_tmp1["sampler_interval"] = config_mem_list[ + config_num - 2] + sampler_tmp1["interface"] = self.interface + self.existing["sampler"].append(sampler_tmp1) + + def get_exist_statistic_record(self): + """get exist netstream statistic record parameter""" + + if self.statistics_record and self.statistics_direction: + self.module.fail_json( + msg='Error: The statistic direction and record can not exist at the same time.') + statistic_tmp = dict() + statistic_tmp1 = dict() + statistic_tmp["statistics_record"] = list() + statistic_tmp["interface"] = self.interface + statistic_tmp1["statistics_record"] = list() + statistic_tmp1["interface"] = self.interface + flags = list() + exp = r" | ignore-case section include ^#\s+interface %s" \ + r" | include netstream record"\ + % (self.interface) + flags.append(exp) + config = get_config(self.module, flags) + if not config: + statistic_tmp["type"] = "ip" + self.existing["flexible_statistic"].append(statistic_tmp) + statistic_tmp1["type"] = "vxlan" + self.existing["flexible_statistic"].append(statistic_tmp1) + else: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem = config_mem.lstrip() + statistic_tmp["statistics_record"] = list() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 3 and str(config_mem_list[3]) == "ip": + statistic_tmp["statistics_record"].append( + str(config_mem_list[2])) + statistic_tmp["type"] = "ip" + self.existing["flexible_statistic"].append(statistic_tmp) + for config_mem in config_list: + statistic_tmp1["statistics_record"] = list() + config_mem = config_mem.lstrip() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 3 and str(config_mem_list[3]) == "vxlan": + statistic_tmp1["statistics_record"].append( + str(config_mem_list[2])) + statistic_tmp1["type"] = "vxlan" + self.existing["flexible_statistic"].append(statistic_tmp1) + + def get_exist_interface_statistic(self): + """get exist netstream interface statistic parameter""" + + statistic_tmp1 = dict() + statistic_tmp1["statistics_direction"] = list() + flags = list() + exp = r" | ignore-case section include ^#\s+interface %s" \ + r" | include netstream inbound|outbound"\ + % self.interface + flags.append(exp) + config = get_config(self.module, flags) + if not config: + statistic_tmp1["type"] = "null" + else: + statistic_tmp1["type"] = "ip" + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem = config_mem.lstrip() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 1: + statistic_tmp1["statistics_direction"].append( + str(config_mem_list[1])) + statistic_tmp1["interface"] = self.interface + self.existing["statistic"].append(statistic_tmp1) + + def get_exist_index_switch(self): + """get exist netstream index-switch""" + + index_switch_tmp = dict() + index_switch_tmp1 = dict() + index_switch_tmp["index-switch"] = "16" + index_switch_tmp["type"] = "ip" + index_switch_tmp1["index-switch"] = "16" + index_switch_tmp1["type"] = "vxlan" + flags = list() + exp = " | ignore-case include index-switch" + flags.append(exp) + config = get_config(self.module, flags) + if not config: + self.existing["index-switch"].append(index_switch_tmp) + self.existing["index-switch"].append(index_switch_tmp1) + else: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 2 and str(config_mem_list[2]) == "ip": + index_switch_tmp["index-switch"] = "32" + index_switch_tmp["type"] = "ip" + if len(config_mem_list) > 2 and str(config_mem_list[2]) == "vxlan": + index_switch_tmp1["index-switch"] = "32" + index_switch_tmp1["type"] = "vxlan" + self.existing["index-switch"].append(index_switch_tmp) + self.existing["index-switch"].append(index_switch_tmp1) + + def get_exist_record(self): + """get exist netstream record""" + + flags = list() + exp = " | ignore-case include netstream record" + flags.append(exp) + config = get_config(self.module, flags) + if config: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 3 and config_mem_list[3] == "ip": + self.existing["ip_record"].append(config_mem_list[2]) + if len(config_mem_list) > 3 and config_mem_list[3] == "vxlan": + self.existing["vxlan_record"].append(config_mem_list[2]) + + def get_end_sampler_interval(self): + """get end netstream sampler interval""" + + sampler_tmp = dict() + sampler_tmp1 = dict() + flags = list() + exp = " | ignore-case include ^netstream sampler random-packets" + flags.append(exp) + config = get_config(self.module, flags) + if not config: + sampler_tmp["sampler_interval"] = "null" + sampler_tmp["sampler_direction"] = "null" + else: + config_list = config.split(' ') + config_num = len(config_list) + if config_num > 1: + sampler_tmp["sampler_direction"] = config_list[config_num - 1] + sampler_tmp["sampler_interval"] = config_list[config_num - 2] + sampler_tmp["interface"] = "all" + self.end_state["sampler"].append(sampler_tmp) + if self.interface != "all": + flags = list() + exp = r" | ignore-case section include ^#\s+interface %s" \ + r" | include netstream sampler random-packets" % self.interface + flags.append(exp) + config = get_config(self.module, flags) + if not config: + sampler_tmp1["sampler_interval"] = "null" + sampler_tmp1["sampler_direction"] = "null" + else: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + sampler_tmp1 = dict() + config_mem_list = config_mem.split(' ') + config_num = len(config_mem_list) + if config_num > 1: + sampler_tmp1["sampler_direction"] = config_mem_list[ + config_num - 1] + sampler_tmp1["sampler_interval"] = config_mem_list[ + config_num - 2] + sampler_tmp1["interface"] = self.interface + self.end_state["sampler"].append(sampler_tmp1) + + def get_end_statistic_record(self): + """get end netstream statistic record parameter""" + + if self.statistics_record and self.statistics_direction: + self.module.fail_json( + msg='Error: The statistic direction and record can not exist at the same time.') + statistic_tmp = dict() + statistic_tmp1 = dict() + statistic_tmp["statistics_record"] = list() + statistic_tmp["interface"] = self.interface + statistic_tmp1["statistics_record"] = list() + statistic_tmp1["interface"] = self.interface + flags = list() + exp = r" | ignore-case section include ^#\s+interface %s" \ + r" | include netstream record"\ + % (self.interface) + flags.append(exp) + config = get_config(self.module, flags) + if not config: + statistic_tmp["type"] = "ip" + self.end_state["flexible_statistic"].append(statistic_tmp) + statistic_tmp1["type"] = "vxlan" + self.end_state["flexible_statistic"].append(statistic_tmp1) + else: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem = config_mem.lstrip() + statistic_tmp["statistics_record"] = list() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 3 and str(config_mem_list[3]) == "ip": + statistic_tmp["statistics_record"].append( + str(config_mem_list[2])) + statistic_tmp["type"] = "ip" + self.end_state["flexible_statistic"].append(statistic_tmp) + for config_mem in config_list: + statistic_tmp1["statistics_record"] = list() + config_mem = config_mem.lstrip() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 3 and str(config_mem_list[3]) == "vxlan": + statistic_tmp1["statistics_record"].append( + str(config_mem_list[2])) + statistic_tmp1["type"] = "vxlan" + self.end_state["flexible_statistic"].append(statistic_tmp1) + + def get_end_interface_statistic(self): + """get end netstream interface statistic parameters""" + + statistic_tmp1 = dict() + statistic_tmp1["statistics_direction"] = list() + flags = list() + exp = r" | ignore-case section include ^#\s+interface %s" \ + r" | include netstream inbound|outbound"\ + % self.interface + flags.append(exp) + config = get_config(self.module, flags) + if not config: + statistic_tmp1["type"] = "null" + else: + statistic_tmp1["type"] = "ip" + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem = config_mem.lstrip() + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 1: + statistic_tmp1["statistics_direction"].append( + str(config_mem_list[1])) + statistic_tmp1["interface"] = self.interface + self.end_state["statistic"].append(statistic_tmp1) + + def get_end_index_switch(self): + """get end netstream index switch""" + + index_switch_tmp = dict() + index_switch_tmp1 = dict() + index_switch_tmp["index-switch"] = "16" + index_switch_tmp["type"] = "ip" + index_switch_tmp1["index-switch"] = "16" + index_switch_tmp1["type"] = "vxlan" + flags = list() + exp = " | ignore-case include index-switch" + flags.append(exp) + config = get_config(self.module, flags) + if not config: + self.end_state["index-switch"].append(index_switch_tmp) + self.end_state["index-switch"].append(index_switch_tmp1) + else: + config = config.lstrip() + config_list = config.split('\n') + for config_mem in config_list: + config_mem_list = config_mem.split(' ') + if len(config_mem_list) > 2 and str(config_mem_list[2]) == "ip": + index_switch_tmp["index-switch"] = "32" + index_switch_tmp["type"] = "ip" + if len(config_mem_list) > 2 and str(config_mem_list[2]) == "vxlan": + index_switch_tmp1["index-switch"] = "32" + index_switch_tmp1["type"] = "vxlan" + self.end_state["index-switch"].append(index_switch_tmp) + self.end_state["index-switch"].append(index_switch_tmp1) + + def check_params(self): + """check all input params""" + + # netstream parameters check + if not get_interface_type(self.interface): + self.module.fail_json( + msg='Error: Interface name of %s is error.' % self.interface) + if self.sampler_interval: + if not str(self.sampler_interval).isdigit(): + self.module.fail_json( + msg='Error: Active interval should be numerical.') + if int(self.sampler_interval) < 1 or int(self.sampler_interval) > 65535: + self.module.fail_json( + msg="Error: Sampler interval should between 1 - 65535.") + if self.statistics_record: + if len(self.statistics_record) < 1 or len(self.statistics_record) > 32: + self.module.fail_json( + msg="Error: Statistic record length should between 1 - 32.") + if self.interface == "all": + if self.statistics_record or self.statistics_direction: + self.module.fail_json( + msg="Error: Statistic function should be used at interface.") + if self.statistics_direction: + if self.type == "vxlan": + self.module.fail_json( + msg="Error: Vxlan do not support inbound or outbound statistic.") + if (self.sampler_interval and not self.sampler_direction) \ + or (self.sampler_direction and not self.sampler_interval): + self.module.fail_json( + msg="Error: Sampler interval and direction must be set at the same time.") + + if self.statistics_record and not self.type: + self.module.fail_json( + msg="Error: Statistic type and record must be set at the same time.") + + self.get_exist_record() + if self.statistics_record: + if self.type == "ip": + if self.statistics_record not in self.existing["ip_record"]: + self.module.fail_json( + msg="Error: The statistic record is not exist.") + if self.type == "vxlan": + if self.statistics_record not in self.existing["vxlan_record"]: + self.module.fail_json( + msg="Error: The statistic record is not exist.") + + def get_proposed(self): + """get proposed info""" + + if self.type: + self.proposed["type"] = self.type + if self.interface: + self.proposed["interface"] = self.interface + if self.sampler_interval: + self.proposed["sampler_interval"] = self.sampler_interval + if self.sampler_direction: + self.proposed["sampler_direction"] = self.sampler_direction + if self.statistics_direction: + self.proposed["statistics_direction"] = self.statistics_direction + if self.statistics_record: + self.proposed["statistics_record"] = self.statistics_record + if self.index_switch: + self.proposed["index_switch"] = self.index_switch + if self.state: + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + sampler_tmp = dict() + statistic_tmp = dict() + statistic_tmp1 = dict() + index_tmp = dict() + temp = False + + self.get_exist_sampler_interval() + self.get_exist_interface_statistic() + self.get_exist_statistic_record() + self.get_exist_index_switch() + + if self.state == "present": + for sampler_tmp in self.existing["sampler"]: + if self.interface == str(sampler_tmp["interface"]): + temp = True + if (self.sampler_interval and str(sampler_tmp["sampler_interval"]) != self.sampler_interval) \ + or (self.sampler_direction and + str(sampler_tmp["sampler_direction"]) != self.sampler_direction): + self.sampler_changed = True + if not temp: + if self.sampler_direction or self.sampler_interval: + self.sampler_changed = True + for statistic_tmp in self.existing["statistic"]: + if str(statistic_tmp["interface"]) == self.interface and self.interface != "all": + if self.type == "vxlan": + if statistic_tmp["statistics_direction"] \ + and 'outbound' in statistic_tmp["statistics_direction"]: + self.module.fail_json( + msg='Error: The NetStream record vxlan ' + 'cannot be configured because the port has been configured NetStream outbound ip.') + if statistic_tmp["statistics_direction"] and self.statistics_direction: + if self.statistics_direction not in statistic_tmp["statistics_direction"]: + self.statistic_changed = True + else: + if self.statistics_direction: + self.statistic_changed = True + for statistic_tmp1 in self.existing["flexible_statistic"]: + if self.interface != "all" \ + and self.type == str(statistic_tmp1["type"]) \ + and self.interface == str(statistic_tmp1["interface"]): + if statistic_tmp1["statistics_record"] and self.statistics_record: + if self.statistics_record not in statistic_tmp1["statistics_record"]: + self.flexible_changed = True + else: + if self.statistics_record: + self.flexible_changed = True + for index_tmp in self.existing["index-switch"]: + if self.type == str(index_tmp["type"]): + if self.index_switch != str(index_tmp["index-switch"]): + self.index_switch_changed = True + else: + for sampler_tmp in self.existing["sampler"]: + if self.interface == str(sampler_tmp["interface"]): + if (self.sampler_interval and str(sampler_tmp["sampler_interval"]) == self.sampler_interval) \ + and (self.sampler_direction and str(sampler_tmp["sampler_direction"]) == self.sampler_direction): + self.sampler_changed = True + for statistic_tmp in self.existing["statistic"]: + if str(statistic_tmp["interface"]) == self.interface and self.interface != "all": + if len(statistic_tmp["statistics_direction"]) and self.statistics_direction: + if self.statistics_direction in statistic_tmp["statistics_direction"]: + self.statistic_changed = True + for statistic_tmp1 in self.existing["flexible_statistic"]: + if self.interface != "all" \ + and self.type == str(statistic_tmp1["type"]) \ + and self.interface == str(statistic_tmp1["interface"]): + if len(statistic_tmp1["statistics_record"]) and self.statistics_record: + if self.statistics_record in statistic_tmp1["statistics_record"]: + self.flexible_changed = True + for index_tmp in self.existing["index-switch"]: + if self.type == str(index_tmp["type"]): + if self.index_switch == str(index_tmp["index-switch"]): + if self.index_switch != "16": + self.index_switch_changed = True + + def operate_ns_gloabl(self): + """configure netstream global parameters""" + + cmd = "" + if not self.sampler_changed and not self.statistic_changed \ + and not self.flexible_changed and not self.index_switch_changed: + self.changed = False + return + + if self.sampler_changed is True: + if self.type == "vxlan": + self.module.fail_json( + msg="Error: Netstream do not support vxlan sampler.") + if self.interface != "all": + cmd = "interface %s" % self.interface + self.cli_add_command(cmd) + cmd = "netstream sampler random-packets %s %s" % ( + self.sampler_interval, self.sampler_direction) + if self.state == "present": + self.cli_add_command(cmd) + else: + self.cli_add_command(cmd, undo=True) + if self.interface != "all": + cmd = "quit" + self.cli_add_command(cmd) + if self.statistic_changed is True: + if self.interface != "all": + cmd = "interface %s" % self.interface + self.cli_add_command(cmd) + cmd = "netstream %s ip" % self.statistics_direction + if self.state == "present": + self.cli_add_command(cmd) + else: + self.cli_add_command(cmd, undo=True) + if self.interface != "all": + cmd = "quit" + self.cli_add_command(cmd) + if self.flexible_changed is True: + if self.interface != "all": + cmd = "interface %s" % self.interface + self.cli_add_command(cmd) + if self.state == "present": + for statistic_tmp in self.existing["flexible_statistic"]: + tmp_list = statistic_tmp["statistics_record"] + if self.type == statistic_tmp["type"]: + if self.type == "ip": + if len(tmp_list) > 0: + cmd = "netstream record %s ip" % tmp_list[0] + self.cli_add_command(cmd, undo=True) + cmd = "netstream record %s ip" % self.statistics_record + self.cli_add_command(cmd) + if self.type == "vxlan": + if len(tmp_list) > 0: + cmd = "netstream record %s vxlan inner-ip" % tmp_list[ + 0] + self.cli_add_command(cmd, undo=True) + cmd = "netstream record %s vxlan inner-ip" % self.statistics_record + self.cli_add_command(cmd) + else: + if self.type == "ip": + cmd = "netstream record %s ip" % self.statistics_record + self.cli_add_command(cmd, undo=True) + if self.type == "vxlan": + cmd = "netstream record %s vxlan inner-ip" % self.statistics_record + self.cli_add_command(cmd, undo=True) + if self.interface != "all": + cmd = "quit" + self.cli_add_command(cmd) + if self.index_switch_changed is True: + if self.interface != "all": + self.module.fail_json( + msg="Error: Index-switch function should be used globally.") + if self.type == "ip": + cmd = "netstream export ip index-switch %s" % self.index_switch + else: + cmd = "netstream export vxlan inner-ip index-switch %s" % self.index_switch + if self.state == "present": + self.cli_add_command(cmd) + else: + self.cli_add_command(cmd, undo=True) + + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + def get_end_state(self): + """get end state info""" + + self.get_end_sampler_interval() + self.get_end_interface_statistic() + self.get_end_statistic_record() + self.get_end_index_switch() + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + self.operate_ns_gloabl() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + type=dict(required=False, choices=['ip', 'vxlan'], default='ip'), + interface=dict(required=True, type='str'), + sampler_interval=dict(required=False, type='str'), + sampler_direction=dict(required=False, choices=['inbound', 'outbound']), + statistics_direction=dict(required=False, choices=['inbound', 'outbound']), + statistics_record=dict(required=False, type='str'), + index_switch=dict(required=False, choices=['16', '32'], default='16'), + state=dict(required=False, choices=['present', 'absent'], default='present'), + ) + argument_spec.update(ce_argument_spec) + module = NetStreamGlobal(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_netstream_template.py b/plugins/modules/ce_netstream_template.py new file mode 100644 index 0000000..41db8a2 --- /dev/null +++ b/plugins/modules/ce_netstream_template.py @@ -0,0 +1,495 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_netstream_template +short_description: Manages NetStream template configuration on HUAWEI CloudEngine switches. +description: + - Manages NetStream template configuration on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present', 'absent'] + type: + description: + - Configure the type of netstream record. + required: true + choices: ['ip', 'vxlan'] + record_name: + description: + - Configure the name of netstream record. + The value is a string of 1 to 32 case-insensitive characters. + match: + description: + - Configure flexible flow statistics template keywords. + choices: ['destination-address', 'destination-port', 'tos', 'protocol', 'source-address', 'source-port'] + collect_counter: + description: + - Configure the number of packets and bytes that are included in the flexible flow statistics sent to NSC. + choices: ['bytes', 'packets'] + collect_interface: + description: + - Configure the input or output interface that are included in the flexible flow statistics sent to NSC. + choices: ['input', 'output'] + description: + description: + - Configure the description of netstream record. + The value is a string of 1 to 80 case-insensitive characters. +''' + +EXAMPLES = ''' +- name: netstream template module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Config ipv4 netstream record + ce_netstream_template: + state: present + type: ip + record_name: test + provider: "{{ cli }}" + - name: Undo ipv4 netstream record + ce_netstream_template: + state: absent + type: ip + record_name: test + provider: "{{ cli }}" + - name: Config ipv4 netstream record collect_counter + ce_netstream_template: + state: present + type: ip + record_name: test + collect_counter: bytes + provider: "{{ cli }}" + - name: Undo ipv4 netstream record collect_counter + ce_netstream_template: + state: absent + type: ip + record_name: test + collect_counter: bytes + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"record_name": "test", + "type": "ip", + "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"record_name": "test", + "type": "ip"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["netstream record test ip"] +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_connection, rm_config_prefix +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + + +def get_config(module, flags): + + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + if isinstance(flags, str): + flags = [flags] + elif not isinstance(flags, list): + flags = [] + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + conn = get_connection(module) + rc, out, err = conn.exec_command(cmd) + if rc != 0: + module.fail_json(msg=err) + cfg = str(out).strip() + # remove default configuration prefix '~' + for flag in flags: + if "include-default" in flag: + cfg = rm_config_prefix(cfg) + break + if cfg.startswith('display'): + lines = cfg.split('\n') + if len(lines) > 1: + return '\n'.join(lines[1:]) + else: + return '' + return cfg + + +class NetstreamTemplate(object): + """ Manages netstream template configuration """ + + def __init__(self, **kwargs): + """ Netstream template module init """ + + # module + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # netstream config + self.netstream_cfg = None + + # module args + self.state = self.module.params['state'] or None + self.type = self.module.params['type'] or None + self.record_name = self.module.params['record_name'] or None + self.match = self.module.params['match'] or None + self.collect_counter = self.module.params['collect_counter'] or None + self.collect_interface = self.module.params['collect_interface'] or None + self.description = self.module.params['description'] or None + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def cli_load_config(self, commands): + """ Cli load configuration """ + + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_get_netstream_config(self): + """ Cli get netstream configuration """ + + if self.type == "ip": + cmd = "netstream record %s ip" % self.record_name + else: + cmd = "netstream record %s vxlan inner-ip" % self.record_name + flags = list() + regular = "| section include %s" % cmd + flags.append(regular) + self.netstream_cfg = get_config(self.module, flags) + + def check_args(self): + """ Check module args """ + + if not self.type or not self.record_name: + self.module.fail_json( + msg='Error: Please input type and record_name.') + + if self.record_name: + if len(self.record_name) < 1 or len(self.record_name) > 32: + self.module.fail_json( + msg='Error: The len of record_name is out of [1 - 32].') + + if self.description: + if len(self.description) < 1 or len(self.description) > 80: + self.module.fail_json( + msg='Error: The len of description is out of [1 - 80].') + + def get_proposed(self): + """ Get module proposed """ + + self.proposed["state"] = self.state + + if self.type: + self.proposed["type"] = self.type + if self.record_name: + self.proposed["record_name"] = self.record_name + if self.match: + self.proposed["match"] = self.match + if self.collect_counter: + self.proposed["collect_counter"] = self.collect_counter + if self.collect_interface: + self.proposed["collect_interface"] = self.collect_interface + if self.description: + self.proposed["description"] = self.description + + def get_existing(self): + """ Get existing configuration """ + + self.cli_get_netstream_config() + + if self.netstream_cfg is not None and "netstream record" in self.netstream_cfg: + self.existing["type"] = self.type + self.existing["record_name"] = self.record_name + + if self.description: + tmp_value = re.findall(r'description (.*)', self.netstream_cfg) + if tmp_value is not None and len(tmp_value) > 0: + self.existing["description"] = tmp_value[0] + + if self.match: + if self.type == "ip": + tmp_value = re.findall(r'match ip (.*)', self.netstream_cfg) + else: + tmp_value = re.findall(r'match inner-ip (.*)', self.netstream_cfg) + + if tmp_value: + self.existing["match"] = tmp_value + + if self.collect_counter: + tmp_value = re.findall(r'collect counter (.*)', self.netstream_cfg) + if tmp_value: + self.existing["collect_counter"] = tmp_value + + if self.collect_interface: + tmp_value = re.findall(r'collect interface (.*)', self.netstream_cfg) + if tmp_value: + self.existing["collect_interface"] = tmp_value + + def get_end_state(self): + """ Get end state """ + + self.cli_get_netstream_config() + + if self.netstream_cfg is not None and "netstream record" in self.netstream_cfg: + self.end_state["type"] = self.type + self.end_state["record_name"] = self.record_name + + if self.description: + tmp_value = re.findall(r'description (.*)', self.netstream_cfg) + if tmp_value is not None and len(tmp_value) > 0: + self.end_state["description"] = tmp_value[0] + + if self.match: + if self.type == "ip": + tmp_value = re.findall(r'match ip (.*)', self.netstream_cfg) + else: + tmp_value = re.findall(r'match inner-ip (.*)', self.netstream_cfg) + + if tmp_value: + self.end_state["match"] = tmp_value + + if self.collect_counter: + tmp_value = re.findall(r'collect counter (.*)', self.netstream_cfg) + if tmp_value: + self.end_state["collect_counter"] = tmp_value + + if self.collect_interface: + tmp_value = re.findall(r'collect interface (.*)', self.netstream_cfg) + if tmp_value: + self.end_state["collect_interface"] = tmp_value + if self.end_state == self.existing: + self.changed = False + self.updates_cmd = list() + + def present_netstream(self): + """ Present netstream configuration """ + + cmds = list() + need_create_record = False + + if self.type == "ip": + cmd = "netstream record %s ip" % self.record_name + else: + cmd = "netstream record %s vxlan inner-ip" % self.record_name + cmds.append(cmd) + + if self.existing.get('record_name') != self.record_name: + self.updates_cmd.append(cmd) + need_create_record = True + + if self.description: + cmd = "description %s" % self.description.strip() + if need_create_record or not self.netstream_cfg or cmd not in self.netstream_cfg: + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.match: + if self.type == "ip": + cmd = "match ip %s" % self.match + cfg = "match ip" + else: + cmd = "match inner-ip %s" % self.match + cfg = "match inner-ip" + + if need_create_record or cfg not in self.netstream_cfg or self.match != self.existing["match"][0]: + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.collect_counter: + cmd = "collect counter %s" % self.collect_counter + if need_create_record or cmd not in self.netstream_cfg: + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.collect_interface: + cmd = "collect interface %s" % self.collect_interface + if need_create_record or cmd not in self.netstream_cfg: + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if cmds: + self.cli_load_config(cmds) + self.changed = True + + def absent_netstream(self): + """ Absent netstream configuration """ + + cmds = list() + absent_netstream_attr = False + + if not self.netstream_cfg: + return + + if self.description or self.match or self.collect_counter or self.collect_interface: + absent_netstream_attr = True + + if absent_netstream_attr: + if self.type == "ip": + cmd = "netstream record %s ip" % self.record_name + else: + cmd = "netstream record %s vxlan inner-ip" % self.record_name + + cmds.append(cmd) + + if self.description: + cfg = "description %s" % self.description + if self.netstream_cfg and cfg in self.netstream_cfg: + cmd = "undo description %s" % self.description + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.match: + if self.type == "ip": + cfg = "match ip %s" % self.match + else: + cfg = "match inner-ip %s" % self.match + if self.netstream_cfg and cfg in self.netstream_cfg: + if self.type == "ip": + cmd = "undo match ip %s" % self.match + else: + cmd = "undo match inner-ip %s" % self.match + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.collect_counter: + cfg = "collect counter %s" % self.collect_counter + if self.netstream_cfg and cfg in self.netstream_cfg: + cmd = "undo collect counter %s" % self.collect_counter + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.collect_interface: + cfg = "collect interface %s" % self.collect_interface + if self.netstream_cfg and cfg in self.netstream_cfg: + cmd = "undo collect interface %s" % self.collect_interface + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if len(cmds) > 1: + self.cli_load_config(cmds) + self.changed = True + + else: + if self.type == "ip": + cmd = "undo netstream record %s ip" % self.record_name + else: + cmd = "undo netstream record %s vxlan inner-ip" % self.record_name + + cmds.append(cmd) + self.updates_cmd.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def work(self): + """ Work function """ + + self.check_args() + self.get_proposed() + self.get_existing() + + if self.state == "present": + self.present_netstream() + else: + self.absent_netstream() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + type=dict(choices=['ip', 'vxlan'], required=True), + record_name=dict(type='str'), + match=dict(choices=['destination-address', 'destination-port', + 'tos', 'protocol', 'source-address', 'source-port']), + collect_counter=dict(choices=['bytes', 'packets']), + collect_interface=dict(choices=['input', 'output']), + description=dict(type='str') + ) + argument_spec.update(ce_argument_spec) + module = NetstreamTemplate(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_ntp.py b/plugins/modules/ce_ntp.py new file mode 100644 index 0000000..aac49aa --- /dev/null +++ b/plugins/modules/ce_ntp.py @@ -0,0 +1,616 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_ntp +short_description: Manages core NTP configuration on HUAWEI CloudEngine switches. +description: + - Manages core NTP configuration on HUAWEI CloudEngine switches. +author: + - Zhijin Zhou (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + server: + description: + - Network address of NTP server. + peer: + description: + - Network address of NTP peer. + key_id: + description: + - Authentication key identifier to use with given NTP server or peer. + is_preferred: + description: + - Makes given NTP server or peer the preferred NTP server or peer for the device. + choices: ['enable', 'disable'] + vpn_name: + description: + - Makes the device communicate with the given + NTP server or peer over a specific vpn. + default: '_public_' + source_int: + description: + - Local source interface from which NTP messages are sent. + Must be fully qualified interface name, i.e. C(40GE1/0/22), C(vlanif10). + Interface types, such as C(10GE), C(40GE), C(100GE), C(Eth-Trunk), C(LoopBack), + C(MEth), C(NULL), C(Tunnel), C(Vlanif). + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: NTP test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Set NTP Server with parameters" + ce_ntp: + server: 192.8.2.6 + vpn_name: js + source_int: vlanif4001 + is_preferred: enable + key_id: 32 + provider: "{{ cli }}" + + - name: "Set NTP Peer with parameters" + ce_ntp: + peer: 192.8.2.6 + vpn_name: js + source_int: vlanif4001 + is_preferred: enable + key_id: 32 + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"server": "2.2.2.2", "key_id": "48", + "is_preferred": "enable", "vpn_name":"js", + "source_int": "vlanif4002", "state":"present"} +existing: + description: k/v pairs of existing ntp server/peer + returned: always + type: dict + sample: {"server": "2.2.2.2", "key_id": "32", + "is_preferred": "disable", "vpn_name":"js", + "source_int": "vlanif4002"} +end_state: + description: k/v pairs of ntp info after module execution + returned: always + type: dict + sample: {"server": "2.2.2.2", "key_id": "48", + "is_preferred": "enable", "vpn_name":"js", + "source_int": "vlanif4002"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["ntp server 2.2.2.2 authentication-keyid 48 source-interface vlanif4002 vpn-instance js preferred"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, get_nc_config, set_nc_config + +CE_NC_GET_NTP_CONFIG = """ + + + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_NTP_CONFIG = """ + + + + + %s + %s + %s + %s + %s + %s + %s + %s + 0-0 + + + + +""" + +CE_NC_DELETE_NTP_CONFIG = """ + + + + + %s + %s + %s + %s + %s + 0-0 + + + + +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class Ntp(object): + """Ntp class""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.mutually_exclusive = [('server', 'peer')] + self.init_module() + + # ntp configuration info + self.server = self.module.params['server'] or None + self.peer = self.module.params['peer'] or None + self.key_id = self.module.params['key_id'] + self.is_preferred = self.module.params['is_preferred'] + self.vpn_name = self.module.params['vpn_name'] + self.interface = self.module.params['source_int'] or "" + self.state = self.module.params['state'] + self.ntp_conf = dict() + self.conf_exsit = False + self.ip_ver = 'IPv4' + + if self.server: + self.peer_type = 'Server' + self.address = self.server + elif self.peer: + self.peer_type = 'Peer' + self.address = self.peer + else: + self.peer_type = None + self.address = None + + self.check_params() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = list() + self.end_state = list() + + self.init_data() + + def init_data(self): + """Init data""" + + if self.interface is not None: + self.interface = self.interface.lower() + + if not self.key_id: + self.key_id = "" + + if not self.is_preferred: + self.is_preferred = 'disable' + + def init_module(self): + """Init module""" + + required_one_of = [("server", "peer")] + self.module = AnsibleModule( + argument_spec=self.spec, + supports_check_mode=True, + required_one_of=required_one_of, + mutually_exclusive=self.mutually_exclusive + ) + + def check_ipaddr_validate(self): + """Check ipaddress validate""" + + rule1 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.' + rule2 = r'(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])' + ipv4_regex = '%s%s%s%s%s%s' % ('^', rule1, rule1, rule1, rule2, '$') + ipv6_regex = '^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$' + + flag = False + if bool(re.match(ipv4_regex, self.address)): + flag = True + self.ip_ver = "IPv4" + if not self.ntp_ucast_ipv4_validate(): + flag = False + elif bool(re.match(ipv6_regex, self.address)): + flag = True + self.ip_ver = "IPv6" + else: + flag = True + self.ip_ver = "IPv6" + + if not flag: + if self.peer_type == "Server": + self.module.fail_json(msg='Error: Illegal server ip-address.') + else: + self.module.fail_json(msg='Error: Illegal peer ip-address.') + + def ntp_ucast_ipv4_validate(self): + """Check ntp ucast ipv4 address""" + + addr_list = re.findall(r'(.*)\.(.*)\.(.*)\.(.*)', self.address) + if not addr_list: + self.module.fail_json(msg='Error: Match ip-address fail.') + + value = ((int(addr_list[0][0])) * 0x1000000) + (int(addr_list[0][1]) * 0x10000) + \ + (int(addr_list[0][2]) * 0x100) + (int(addr_list[0][3])) + if (value & (0xff000000) == 0x7f000000) or (value & (0xF0000000) == 0xF0000000) \ + or (value & (0xF0000000) == 0xE0000000) or (value == 0): + return False + return True + + def check_params(self): + """Check all input params""" + + # check interface type + if self.interface: + intf_type = get_interface_type(self.interface) + if not intf_type: + self.module.fail_json( + msg='Error: Interface name of %s ' + 'is error.' % self.interface) + + if self.vpn_name: + if (len(self.vpn_name) < 1) or (len(self.vpn_name) > 31): + self.module.fail_json( + msg='Error: VPN name length is between 1 and 31.') + + if self.address: + self.check_ipaddr_validate() + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def set_ntp(self, *args): + """Configure ntp parameters""" + + if self.state == 'present': + if self.ip_ver == 'IPv4': + xml_str = CE_NC_MERGE_NTP_CONFIG % ( + args[0], args[1], '::', args[2], args[3], args[4], args[5], args[6]) + elif self.ip_ver == 'IPv6': + xml_str = CE_NC_MERGE_NTP_CONFIG % ( + args[0], '0.0.0.0', args[1], args[2], args[3], args[4], args[5], args[6]) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "NTP_CORE_CONFIG") + else: + if self.ip_ver == 'IPv4': + xml_str = CE_NC_DELETE_NTP_CONFIG % ( + args[0], args[1], '::', args[2], args[3]) + elif self.ip_ver == 'IPv6': + xml_str = CE_NC_DELETE_NTP_CONFIG % ( + args[0], '0.0.0.0', args[1], args[2], args[3]) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "UNDO_NTP_CORE_CONFIG") + + def config_ntp(self): + """Config ntp""" + + if self.state == "present": + if self.address and not self.conf_exsit: + if self.is_preferred == 'enable': + is_preferred = 'true' + else: + is_preferred = 'false' + self.set_ntp(self.ip_ver, self.address, self.peer_type, + self.vpn_name, self.key_id, is_preferred, self.interface) + self.changed = True + else: + if self.address: + self.set_ntp(self.ip_ver, self.address, + self.peer_type, self.vpn_name, '', '', '') + self.changed = True + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def get_ntp_exist_config(self): + """Get ntp existed configure""" + + ntp_config = list() + conf_str = CE_NC_GET_NTP_CONFIG + con_obj = get_nc_config(self.module, conf_str) + if "" in con_obj: + return ntp_config + + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get all ntp config info + root = ElementTree.fromstring(xml_str) + ntpsite = root.findall("ntp/ntpUCastCfgs/ntpUCastCfg") + for nexthop in ntpsite: + ntp_dict = dict() + for ele in nexthop: + if ele.tag in ["addrFamily", "vpnName", "ifName", "ipv4Addr", + "ipv6Addr", "type", "isPreferred", "keyId"]: + ntp_dict[ele.tag] = ele.text + + ip_addr = ntp_dict['ipv6Addr'] + if ntp_dict['addrFamily'] == "IPv4": + ip_addr = ntp_dict['ipv4Addr'] + if ntp_dict['ifName'] is None: + ntp_dict['ifName'] = "" + if ntp_dict['isPreferred'] == 'true': + is_preferred = 'enable' + else: + is_preferred = 'disable' + + if self.state == "present": + key_id = ntp_dict['keyId'] or "" + cur_ntp_cfg = dict(vpn_name=ntp_dict['vpnName'], source_int=ntp_dict['ifName'].lower(), address=ip_addr, + peer_type=ntp_dict['type'], prefer=is_preferred, key_id=key_id) + exp_ntp_cfg = dict(vpn_name=self.vpn_name, source_int=self.interface.lower(), address=self.address, + peer_type=self.peer_type, prefer=self.is_preferred, key_id=self.key_id) + if cur_ntp_cfg == exp_ntp_cfg: + self.conf_exsit = True + + vpn_name = ntp_dict['vpnName'] + if ntp_dict['vpnName'] == "_public_": + vpn_name = None + + if_name = ntp_dict['ifName'] + if if_name == "": + if_name = None + if self.peer_type == 'Server': + ntp_config.append(dict(vpn_name=vpn_name, + source_int=if_name, server=ip_addr, + is_preferred=is_preferred, key_id=ntp_dict['keyId'])) + else: + ntp_config.append(dict(vpn_name=vpn_name, + source_int=if_name, peer=ip_addr, + is_preferred=is_preferred, key_id=ntp_dict['keyId'])) + + return ntp_config + + def get_existing(self): + """Get existing info""" + + if self.address: + self.existing = self.get_ntp_exist_config() + + def get_proposed(self): + """Get proposed info""" + + if self.address: + vpn_name = self.vpn_name + if vpn_name == "_public_": + vpn_name = None + + if_name = self.interface + if if_name == "": + if_name = None + + key_id = self.key_id + if key_id == "": + key_id = None + if self.peer_type == 'Server': + self.proposed = dict(state=self.state, vpn_name=vpn_name, + source_int=if_name, server=self.address, + is_preferred=self.is_preferred, key_id=key_id) + else: + self.proposed = dict(state=self.state, vpn_name=vpn_name, + source_int=if_name, peer=self.address, + is_preferred=self.is_preferred, key_id=key_id) + + def get_end_state(self): + """Get end state info""" + + if self.address: + self.end_state = self.get_ntp_exist_config() + + def get_update_cmd(self): + """Get updated commands""" + + if self.conf_exsit: + return + + cli_str = "" + if self.state == "present": + if self.address: + if self.peer_type == 'Server': + if self.ip_ver == "IPv4": + cli_str = "%s %s" % ( + "ntp unicast-server", self.address) + else: + cli_str = "%s %s" % ( + "ntp unicast-server ipv6", self.address) + elif self.peer_type == 'Peer': + if self.ip_ver == "IPv4": + cli_str = "%s %s" % ("ntp unicast-peer", self.address) + else: + cli_str = "%s %s" % ( + "ntp unicast-peer ipv6", self.address) + + if self.key_id: + cli_str = "%s %s %s" % ( + cli_str, "authentication-keyid", self.key_id) + if self.interface: + cli_str = "%s %s %s" % ( + cli_str, "source-interface", self.interface) + if (self.vpn_name) and (self.vpn_name != '_public_'): + cli_str = "%s %s %s" % ( + cli_str, "vpn-instance", self.vpn_name) + if self.is_preferred == "enable": + cli_str = "%s %s" % (cli_str, "preferred") + else: + if self.address: + if self.peer_type == 'Server': + if self.ip_ver == "IPv4": + cli_str = "%s %s" % ( + "undo ntp unicast-server", self.address) + else: + cli_str = "%s %s" % ( + "undo ntp unicast-server ipv6", self.address) + elif self.peer_type == 'Peer': + if self.ip_ver == "IPv4": + cli_str = "%s %s" % ( + "undo ntp unicast-peer", self.address) + else: + cli_str = "%s %s" % ( + "undo ntp unicast-peer ipv6", self.address) + if (self.vpn_name) and (self.vpn_name != '_public_'): + cli_str = "%s %s %s" % ( + cli_str, "vpn-instance", self.vpn_name) + + self.updates_cmd.append(cli_str) + + def work(self): + """Execute task""" + + self.get_existing() + self.get_proposed() + + self.config_ntp() + + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + server=dict(type='str'), + peer=dict(type='str'), + key_id=dict(type='str'), + is_preferred=dict(type='str', choices=['enable', 'disable']), + vpn_name=dict(type='str', default='_public_'), + source_int=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + argument_spec.update(ce_argument_spec) + ntp_obj = Ntp(argument_spec) + ntp_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_ntp_auth.py b/plugins/modules/ce_ntp_auth.py new file mode 100644 index 0000000..1044c6e --- /dev/null +++ b/plugins/modules/ce_ntp_auth.py @@ -0,0 +1,517 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- + +module: ce_ntp_auth +short_description: Manages NTP authentication configuration on HUAWEI CloudEngine switches. +description: + - Manages NTP authentication configuration on HUAWEI CloudEngine switches. +author: + - Zhijin Zhou (@QijunPan) +notes: + - If C(state=absent), the module will attempt to remove the given key configuration. + If a matching key configuration isn't found on the device, the module will fail. + - If C(state=absent) and C(authentication=on), authentication will be turned on. + - If C(state=absent) and C(authentication=off), authentication will be turned off. + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + key_id: + description: + - Authentication key identifier (numeric). + required: true + auth_pwd: + description: + - Plain text with length of 1 to 255, encrypted text with length of 20 to 392. + auth_mode: + description: + - Specify authentication algorithm. + choices: ['hmac-sha256', 'md5'] + auth_type: + description: + - Whether the given password is in cleartext or + has been encrypted. If in cleartext, the device + will encrypt it before storing it. + default: encrypt + choices: ['text', 'encrypt'] + trusted_key: + description: + - Whether the given key is required to be supplied by a time source + for the device to synchronize to the time source. + default: 'disable' + choices: ['enable', 'disable'] + authentication: + description: + - Configure ntp authentication enable or unconfigure ntp authentication enable. + choices: ['enable', 'disable'] + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: NTP AUTH test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Configure ntp authentication key-id" + ce_ntp_auth: + key_id: 32 + auth_mode: md5 + auth_pwd: 11111111111111111111111 + provider: "{{ cli }}" + + - name: "Configure ntp authentication key-id and trusted authentication keyid" + ce_ntp_auth: + key_id: 32 + auth_mode: md5 + auth_pwd: 11111111111111111111111 + trusted_key: enable + provider: "{{ cli }}" + + - name: "Configure ntp authentication key-id and authentication enable" + ce_ntp_auth: + key_id: 32 + auth_mode: md5 + auth_pwd: 11111111111111111111111 + authentication: enable + provider: "{{ cli }}" + + - name: "Unconfigure ntp authentication key-id and trusted authentication keyid" + ce_ntp_auth: + key_id: 32 + state: absent + provider: "{{ cli }}" + + - name: "Unconfigure ntp authentication key-id and authentication enable" + ce_ntp_auth: + key_id: 32 + authentication: enable + state: absent + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "auth_type": "text", + "authentication": "enable", + "key_id": "32", + "auth_pwd": "1111", + "auth_mode": "md5", + "trusted_key": "enable", + "state": "present" + } +existing: + description: k/v pairs of existing ntp authentication + returned: always + type: dict + sample: { + "authentication": "off", + "authentication-keyid": [ + { + "auth_mode": "md5", + "key_id": "1", + "trusted_key": "disable" + } + ] + } +end_state: + description: k/v pairs of ntp authentication after module execution + returned: always + type: dict + sample: { + "authentication": "off", + "authentication-keyid": [ + { + "auth_mode": "md5", + "key_id": "1", + "trusted_key": "disable" + }, + { + "auth_mode": "md5", + "key_id": "32", + "trusted_key": "enable" + } + ] + } +state: + description: state as sent in from the playbook + returned: always + type: str + sample: "present" +updates: + description: command sent to the device + returned: always + type: list + sample: [ + "ntp authentication-key 32 md5 1111", + "ntp trusted-key 32", + "ntp authentication enable" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import copy +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, load_config +from ansible.module_utils.connection import exec_command + + +class NtpAuth(object): + """Manage ntp authentication""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # ntp_auth configuration info + self.key_id = self.module.params['key_id'] + self.password = self.module.params['auth_pwd'] or None + self.auth_mode = self.module.params['auth_mode'] or None + self.auth_type = self.module.params['auth_type'] + self.trusted_key = self.module.params['trusted_key'] + self.authentication = self.module.params['authentication'] or None + self.state = self.module.params['state'] + self.check_params() + + self.ntp_auth_conf = dict() + self.key_id_exist = False + self.cur_trusted_key = 'disable' + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = list() + self.end_state = list() + + self.get_ntp_auth_exist_config() + + def check_params(self): + """Check all input params""" + + if not self.key_id.isdigit(): + self.module.fail_json( + msg='Error: key_id is not digit.') + + if (int(self.key_id) < 1) or (int(self.key_id) > 4294967295): + self.module.fail_json( + msg='Error: The length of key_id is between 1 and 4294967295.') + if self.state == "present" and not self.password: + self.module.fail_json( + msg='Error: The password cannot be empty.') + if self.state == "present" and self.password: + if (self.auth_type == 'encrypt') and\ + ((len(self.password) < 20) or (len(self.password) > 392)): + self.module.fail_json( + msg='Error: The length of encrypted password is between 20 and 392.') + elif (self.auth_type == 'text') and\ + ((len(self.password) < 1) or (len(self.password) > 255)): + self.module.fail_json( + msg='Error: The length of text password is between 1 and 255.') + + def init_module(self): + """Init module object""" + + required_if = [("state", "present", ("auth_pwd", "auth_mode"))] + self.module = AnsibleModule( + argument_spec=self.spec, + required_if=required_if, + supports_check_mode=True + ) + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_ntp_auth_enable(self): + """Get ntp authentication enable state""" + + flags = list() + exp = "| exclude undo | include ntp authentication" + flags.append(exp) + config = self.get_config(flags) + auth_en = re.findall( + r'.*ntp\s*authentication\s*enable.*', config) + if auth_en: + self.ntp_auth_conf['authentication'] = 'enable' + else: + self.ntp_auth_conf['authentication'] = 'disable' + + def get_ntp_all_auth_keyid(self): + """Get all authentication keyid info""" + + ntp_auth_conf = list() + + flags = list() + exp = "| include authentication-keyid %s" % self.key_id + flags.append(exp) + config = self.get_config(flags) + ntp_config_list = config.split('\n') + if not ntp_config_list: + self.ntp_auth_conf["authentication-keyid"] = "None" + return ntp_auth_conf + + self.key_id_exist = True + cur_auth_mode = "" + cur_auth_pwd = "" + for ntp_config in ntp_config_list: + ntp_auth_mode = re.findall(r'.*authentication-mode(\s\S*)\s\S*\s(\S*)', ntp_config) + ntp_auth_trust = re.findall(r'.*trusted.*', ntp_config) + if ntp_auth_trust: + self.cur_trusted_key = 'enable' + if ntp_auth_mode: + cur_auth_mode = ntp_auth_mode[0][0].strip() + cur_auth_pwd = ntp_auth_mode[0][1].strip() + ntp_auth_conf.append(dict(key_id=self.key_id, + auth_mode=cur_auth_mode, + auth_pwd=cur_auth_pwd, + trusted_key=self.cur_trusted_key)) + self.ntp_auth_conf["authentication-keyid"] = ntp_auth_conf + + return ntp_auth_conf + + def get_ntp_auth_exist_config(self): + """Get ntp authentication existed configure""" + + self.get_ntp_auth_enable() + self.get_ntp_all_auth_keyid() + + def config_ntp_auth_keyid(self): + """Config ntp authentication keyid""" + + commands = list() + if self.auth_type == 'encrypt': + config_cli = "ntp authentication-keyid %s authentication-mode %s cipher %s" % ( + self.key_id, self.auth_mode, self.password) + else: + config_cli = "ntp authentication-keyid %s authentication-mode %s %s" % ( + self.key_id, self.auth_mode, self.password) + + commands.append(config_cli) + + if self.trusted_key != self.cur_trusted_key: + if self.trusted_key == 'enable': + config_cli_trust = "ntp trusted authentication-keyid %s" % (self.key_id) + commands.append(config_cli_trust) + else: + config_cli_trust = "undo ntp trusted authentication-keyid %s" % (self.key_id) + commands.append(config_cli_trust) + + self.cli_load_config(commands) + + def config_ntp_auth_enable(self): + """Config ntp authentication enable""" + + commands = list() + if self.ntp_auth_conf['authentication'] != self.authentication: + if self.authentication == 'enable': + config_cli = "ntp authentication enable" + else: + config_cli = "undo ntp authentication enable" + commands.append(config_cli) + + self.cli_load_config(commands) + + def undo_config_ntp_auth_keyid(self): + """Undo ntp authentication key-id""" + + commands = list() + config_cli = "undo ntp authentication-keyid %s" % self.key_id + commands.append(config_cli) + + self.cli_load_config(commands) + + def cli_load_config(self, commands): + """Load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def config_ntp_auth(self): + """Config ntp authentication""" + + if self.state == "present": + self.config_ntp_auth_keyid() + else: + if not self.key_id_exist: + self.module.fail_json( + msg='Error: The Authentication-keyid does not exist.') + self.undo_config_ntp_auth_keyid() + + if self.authentication: + self.config_ntp_auth_enable() + + self.changed = True + + def get_existing(self): + """Get existing info""" + + self.existing = copy.deepcopy(self.ntp_auth_conf) + + def get_proposed(self): + """Get proposed result""" + + auth_type = self.auth_type + trusted_key = self.trusted_key + if self.state == 'absent': + auth_type = None + trusted_key = None + self.proposed = dict(key_id=self.key_id, auth_pwd=self.password, + auth_mode=self.auth_mode, auth_type=auth_type, + trusted_key=trusted_key, authentication=self.authentication, + state=self.state) + + def get_update_cmd(self): + """Get updated commands""" + + cli_str = "" + if self.state == "present": + cli_str = "ntp authentication-keyid %s authentication-mode %s " % ( + self.key_id, self.auth_mode) + if self.auth_type == 'encrypt': + cli_str = "%s cipher %s" % (cli_str, self.password) + else: + cli_str = "%s %s" % (cli_str, self.password) + else: + cli_str = "undo ntp authentication-keyid %s" % self.key_id + + self.updates_cmd.append(cli_str) + + if self.authentication: + cli_str = "" + + if self.ntp_auth_conf['authentication'] != self.authentication: + if self.authentication == 'enable': + cli_str = "ntp authentication enable" + else: + cli_str = "undo ntp authentication enable" + + if cli_str != "": + self.updates_cmd.append(cli_str) + + cli_str = "" + if self.state == "present": + if self.trusted_key != self.cur_trusted_key: + if self.trusted_key == 'enable': + cli_str = "ntp trusted authentication-keyid %s" % self.key_id + else: + cli_str = "undo ntp trusted authentication-keyid %s" % self.key_id + else: + cli_str = "undo ntp trusted authentication-keyid %s" % self.key_id + + if cli_str != "": + self.updates_cmd.append(cli_str) + + def get_end_state(self): + """Get end state info""" + + self.ntp_auth_conf = dict() + self.get_ntp_auth_exist_config() + self.end_state = copy.deepcopy(self.ntp_auth_conf) + if self.end_state == self.existing: + self.changed = False + + def show_result(self): + """Show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def work(self): + """Execute task""" + + self.get_existing() + self.get_proposed() + self.get_update_cmd() + + self.config_ntp_auth() + + self.get_end_state() + self.show_result() + + +def main(): + """Main function entry""" + + argument_spec = dict( + key_id=dict(required=True, type='str'), + auth_pwd=dict(type='str', no_log=True), + auth_mode=dict(choices=['md5', 'hmac-sha256'], type='str'), + auth_type=dict(choices=['text', 'encrypt'], default='encrypt'), + trusted_key=dict(choices=['enable', 'disable'], default='disable'), + authentication=dict(choices=['enable', 'disable']), + state=dict(choices=['absent', 'present'], default='present'), + ) + argument_spec.update(ce_argument_spec) + ntp_auth_obj = NtpAuth(argument_spec) + ntp_auth_obj.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_ospf.py b/plugins/modules/ce_ospf.py new file mode 100644 index 0000000..b46b67a --- /dev/null +++ b/plugins/modules/ce_ospf.py @@ -0,0 +1,969 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_ospf +short_description: Manages configuration of an OSPF instance on HUAWEI CloudEngine switches. +description: + - Manages configuration of an OSPF instance on HUAWEI CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + process_id: + description: + - Specifies a process ID. + The value is an integer ranging from 1 to 4294967295. + required: true + area: + description: + - Specifies the area ID. The area with the area-id being 0 is a backbone area. + Valid values are a string, formatted as an IP address + (i.e. "0.0.0.0") or as an integer between 1 and 4294967295. + addr: + description: + - Specifies the address of the network segment where the interface resides. + The value is in dotted decimal notation. + mask: + description: + - IP network wildcard bits in decimal format between 0 and 32. + auth_mode: + description: + - Specifies the authentication type. + choices: ['none', 'hmac-sha256', 'md5', 'hmac-md5', 'simple'] + auth_text_simple: + description: + - Specifies a password for simple authentication. + The value is a string of 1 to 8 characters. + auth_key_id: + description: + - Authentication key id when C(auth_mode) is 'hmac-sha256', 'md5' or 'hmac-md5. + Valid value is an integer is in the range from 1 to 255. + auth_text_md5: + description: + - Specifies a password for MD5, HMAC-MD5, or HMAC-SHA256 authentication. + The value is a string of 1 to 255 case-sensitive characters, spaces not supported. + nexthop_addr: + description: + - IPv4 address for configure next-hop address's weight. + Valid values are a string, formatted as an IP address. + nexthop_weight: + description: + - Indicates the weight of the next hop. + The smaller the value is, the higher the preference of the route is. + It is an integer that ranges from 1 to 254. + max_load_balance: + description: + - The maximum number of paths for forward packets over multiple paths. + Valid value is an integer in the range from 1 to 64. + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: ospf module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configure ospf + ce_ospf: + process_id: 1 + area: 100 + state: present + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"process_id": "1", "area": "100"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"process_id": "1", "areas": [], "nexthops":[], "max_load_balance": "32"} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"process_id": "1", + "areas": [{"areaId": "0.0.0.100", "areaType": "Normal"}], + "nexthops":[], "max_load_balance": "32"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["ospf 1", "area 0.0.0.100"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_OSPF = """ + + + + + + %s + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +CE_NC_CREATE_PROCESS = """ + + + + + + %s + + + + + +""" + +CE_NC_DELETE_PROCESS = """ + + + + + + %s + + + + + +""" + +CE_NC_XML_BUILD_MERGE_PROCESS = """ + + + + + + %s + %s + + + + + +""" + +CE_NC_XML_BUILD_PROCESS = """ + + + + + + %s + %s + + + + + +""" + +CE_NC_XML_BUILD_MERGE_AREA = """ + + + %s + %s + + +""" + +CE_NC_XML_BUILD_DELETE_AREA = """ + + + %s + %s + + +""" + +CE_NC_XML_BUILD_AREA = """ + + + %s + %s + + +""" + +CE_NC_XML_SET_AUTH_MODE = """ + %s +""" +CE_NC_XML_SET_AUTH_TEXT_SIMPLE = """ + %s +""" + +CE_NC_XML_SET_AUTH_MD5 = """ + %s + %s +""" + + +CE_NC_XML_MERGE_NETWORKS = """ + + + %s + %s + + +""" + +CE_NC_XML_DELETE_NETWORKS = """ + + + %s + %s + + +""" + +CE_NC_XML_SET_LB = """ + %s +""" + + +CE_NC_XML_BUILD_MERGE_TOPO = """ + + + base + %s + + + +""" + +CE_NC_XML_BUILD_TOPO = """ + + + base + %s + + + +""" + +CE_NC_XML_MERGE_NEXTHOP = """ + + + %s + %s + + +""" + +CE_NC_XML_DELETE_NEXTHOP = """ + + + %s + + +""" + + +class OSPF(object): + """ + Manages configuration of an ospf instance. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.process_id = self.module.params['process_id'] + self.area = self.module.params['area'] + self.addr = self.module.params['addr'] + self.mask = self.module.params['mask'] + self.auth_mode = self.module.params['auth_mode'] + self.auth_text_simple = self.module.params['auth_text_simple'] + self.auth_key_id = self.module.params['auth_key_id'] + self.auth_text_md5 = self.module.params['auth_text_md5'] + self.nexthop_addr = self.module.params['nexthop_addr'] + self.nexthop_weight = self.module.params['nexthop_weight'] + self.max_load_balance = self.module.params['max_load_balance'] + self.state = self.module.params['state'] + + # ospf info + self.ospf_info = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """ init module """ + + required_together = [ + ("addr", "mask"), + ("auth_key_id", "auth_text_md5"), + ("nexthop_addr", "nexthop_weight") + ] + self.module = AnsibleModule( + argument_spec=self.spec, required_together=required_together, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_wildcard_mask(self): + """convert mask length to ip address wildcard mask, i.e. 24 to 0.0.0.255""" + + mask_int = ["255"] * 4 + length = int(self.mask) + + if length > 32: + self.module.fail_json(msg='IPv4 ipaddress mask length is invalid') + if length < 8: + mask_int[0] = str(int(~(0xFF << (8 - length % 8)) & 0xFF)) + if length >= 8: + mask_int[0] = '0' + mask_int[1] = str(int(~(0xFF << (16 - (length % 16))) & 0xFF)) + if length >= 16: + mask_int[1] = '0' + mask_int[2] = str(int(~(0xFF << (24 - (length % 24))) & 0xFF)) + if length >= 24: + mask_int[2] = '0' + mask_int[3] = str(int(~(0xFF << (32 - (length % 32))) & 0xFF)) + if length == 32: + mask_int[3] = '0' + + return '.'.join(mask_int) + + def get_area_ip(self): + """convert integer to ip address""" + + if not self.area.isdigit(): + return self.area + + addr_int = ['0'] * 4 + addr_int[0] = str(((int(self.area) & 0xFF000000) >> 24) & 0xFF) + addr_int[1] = str(((int(self.area) & 0x00FF0000) >> 16) & 0xFF) + addr_int[2] = str(((int(self.area) & 0x0000FF00) >> 8) & 0XFF) + addr_int[3] = str(int(self.area) & 0xFF) + + return '.'.join(addr_int) + + def get_ospf_dict(self, process_id): + """ get one ospf attributes dict.""" + + ospf_info = dict() + conf_str = CE_NC_GET_OSPF % process_id + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return ospf_info + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get process base info + root = ElementTree.fromstring(xml_str) + ospfsite = root.find("ospfv2/ospfv2comm/ospfSites/ospfSite") + if ospfsite: + for site in ospfsite: + if site.tag in ["processId", "routerId", "vrfName"]: + ospf_info[site.tag] = site.text + + # get Topology info + topo = root.find( + "ospfv2/ospfv2comm/ospfSites/ospfSite/ProcessTopologys/ProcessTopology") + if topo: + for eles in topo: + if eles.tag in ["maxLoadBalancing"]: + ospf_info[eles.tag] = eles.text + + # get nexthop info + ospf_info["nexthops"] = list() + nexthops = root.findall( + "ospfv2/ospfv2comm/ospfSites/ospfSite/ProcessTopologys/ProcessTopology/nexthopMTs/nexthopMT") + if nexthops: + for nexthop in nexthops: + nh_dict = dict() + for ele in nexthop: + if ele.tag in ["ipAddress", "weight"]: + nh_dict[ele.tag] = ele.text + ospf_info["nexthops"].append(nh_dict) + + # get areas info + ospf_info["areas"] = list() + areas = root.findall( + "ospfv2/ospfv2comm/ospfSites/ospfSite/areas/area") + if areas: + for area in areas: + area_dict = dict() + for ele in area: + if ele.tag in ["areaId", "authTextSimple", "areaType", + "authenticationMode", "keyId", "authTextMd5"]: + area_dict[ele.tag] = ele.text + if ele.tag == "networks": + # get networks info + area_dict["networks"] = list() + for net in ele: + net_dict = dict() + for net_ele in net: + if net_ele.tag in ["ipAddress", "wildcardMask"]: + net_dict[net_ele.tag] = net_ele.text + area_dict["networks"].append(net_dict) + + ospf_info["areas"].append(area_dict) + return ospf_info + + def is_area_exist(self): + """is ospf area exist""" + if not self.ospf_info: + return False + for area in self.ospf_info["areas"]: + if area["areaId"] == self.get_area_ip(): + return True + + return False + + def is_network_exist(self): + """is ospf area network exist""" + if not self.ospf_info: + return False + + for area in self.ospf_info["areas"]: + if area["areaId"] == self.get_area_ip(): + if not area.get("networks"): + return False + for network in area.get("networks"): + if network["ipAddress"] == self.addr and network["wildcardMask"] == self.get_wildcard_mask(): + return True + return False + + def is_nexthop_exist(self): + """is ospf nexthop exist""" + + if not self.ospf_info: + return False + for nexthop in self.ospf_info["nexthops"]: + if nexthop["ipAddress"] == self.nexthop_addr: + return True + + return False + + def is_nexthop_change(self): + """is ospf nexthop change""" + if not self.ospf_info: + return True + + for nexthop in self.ospf_info["nexthops"]: + if nexthop["ipAddress"] == self.nexthop_addr: + if nexthop["weight"] == self.nexthop_weight: + return False + else: + return True + + return True + + def create_process(self): + """Create ospf process""" + + xml_area = "" + self.updates_cmd.append("ospf %s" % self.process_id) + xml_create = CE_NC_CREATE_PROCESS % self.process_id + set_nc_config(self.module, xml_create) + + # nexthop weight + xml_nh = "" + if self.nexthop_addr: + xml_nh = CE_NC_XML_MERGE_NEXTHOP % ( + self.nexthop_addr, self.nexthop_weight) + self.updates_cmd.append("nexthop %s weight %s" % ( + self.nexthop_addr, self.nexthop_weight)) + + # max load balance + xml_lb = "" + if self.max_load_balance: + xml_lb = CE_NC_XML_SET_LB % self.max_load_balance + self.updates_cmd.append( + "maximum load-balancing %s" % self.max_load_balance) + + xml_topo = "" + if xml_lb or xml_nh: + xml_topo = CE_NC_XML_BUILD_TOPO % (xml_nh + xml_lb) + + if self.area: + self.updates_cmd.append("area %s" % self.get_area_ip()) + xml_auth = "" + xml_network = "" + + # networks + if self.addr and self.mask: + xml_network = CE_NC_XML_MERGE_NETWORKS % ( + self.addr, self.get_wildcard_mask()) + self.updates_cmd.append("network %s %s" % ( + self.addr, self.get_wildcard_mask())) + + # authentication mode + if self.auth_mode: + xml_auth += CE_NC_XML_SET_AUTH_MODE % self.auth_mode + if self.auth_mode == "none": + self.updates_cmd.append("undo authentication-mode") + else: + self.updates_cmd.append( + "authentication-mode %s" % self.auth_mode) + if self.auth_mode == "simple" and self.auth_text_simple: + xml_auth += CE_NC_XML_SET_AUTH_TEXT_SIMPLE % self.auth_text_simple + self.updates_cmd.pop() + self.updates_cmd.append( + "authentication-mode %s %s" % (self.auth_mode, self.auth_text_simple)) + if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: + if self.auth_key_id and self.auth_text_md5: + xml_auth += CE_NC_XML_SET_AUTH_MD5 % ( + self.auth_key_id, self.auth_text_md5) + self.updates_cmd.pop() + self.updates_cmd.append( + "authentication-mode %s %s %s" % (self.auth_mode, self.auth_key_id, self.auth_text_md5)) + if xml_network or xml_auth or not self.is_area_exist(): + xml_area += CE_NC_XML_BUILD_MERGE_AREA % ( + self.get_area_ip(), xml_network + xml_auth) + + xml_str = CE_NC_XML_BUILD_MERGE_PROCESS % ( + self.process_id, xml_topo + xml_area) + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "CREATE_PROCESS") + self.changed = True + + def delete_process(self): + """Delete ospf process""" + + xml_str = CE_NC_DELETE_PROCESS % self.process_id + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "DELETE_PROCESS") + self.updates_cmd.append("undo ospf %s" % self.process_id) + self.changed = True + + def merge_process(self): + """merge ospf process""" + + xml_area = "" + xml_str = "" + self.updates_cmd.append("ospf %s" % self.process_id) + + # nexthop weight + xml_nh = "" + if self.nexthop_addr and self.is_nexthop_change(): + xml_nh = CE_NC_XML_MERGE_NEXTHOP % ( + self.nexthop_addr, self.nexthop_weight) + self.updates_cmd.append("nexthop %s weight %s" % ( + self.nexthop_addr, self.nexthop_weight)) + + # max load balance + xml_lb = "" + if self.max_load_balance and self.ospf_info.get("maxLoadBalancing") != self.max_load_balance: + xml_lb = CE_NC_XML_SET_LB % self.max_load_balance + self.updates_cmd.append( + "maximum load-balancing %s" % self.max_load_balance) + + xml_topo = "" + if xml_lb or xml_nh: + xml_topo = CE_NC_XML_BUILD_MERGE_TOPO % (xml_nh + xml_lb) + + if self.area: + self.updates_cmd.append("area %s" % self.get_area_ip()) + xml_network = "" + xml_auth = "" + if self.addr and self.mask: + if not self.is_network_exist(): + xml_network += CE_NC_XML_MERGE_NETWORKS % ( + self.addr, self.get_wildcard_mask()) + self.updates_cmd.append("network %s %s" % ( + self.addr, self.get_wildcard_mask())) + + # NOTE: for security, authentication config will always be update + if self.auth_mode: + xml_auth += CE_NC_XML_SET_AUTH_MODE % self.auth_mode + if self.auth_mode == "none": + self.updates_cmd.append("undo authentication-mode") + else: + self.updates_cmd.append( + "authentication-mode %s" % self.auth_mode) + if self.auth_mode == "simple" and self.auth_text_simple: + xml_auth += CE_NC_XML_SET_AUTH_TEXT_SIMPLE % self.auth_text_simple + self.updates_cmd.pop() + self.updates_cmd.append( + "authentication-mode %s %s" % (self.auth_mode, self.auth_text_simple)) + if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: + if self.auth_key_id and self.auth_text_md5: + xml_auth += CE_NC_XML_SET_AUTH_MD5 % ( + self.auth_key_id, self.auth_text_md5) + self.updates_cmd.pop() + self.updates_cmd.append( + "authentication-mode %s %s %s" % (self.auth_mode, self.auth_key_id, self.auth_text_md5)) + if xml_network or xml_auth or not self.is_area_exist(): + xml_area += CE_NC_XML_BUILD_MERGE_AREA % ( + self.get_area_ip(), xml_network + xml_auth) + elif self.is_area_exist(): + self.updates_cmd.pop() # remove command: area + else: + pass + + if xml_area or xml_topo: + xml_str = CE_NC_XML_BUILD_MERGE_PROCESS % ( + self.process_id, xml_topo + xml_area) + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "MERGE_PROCESS") + self.changed = True + + def remove_area_network(self): + """remvoe ospf area network""" + + if not self.is_network_exist(): + return + + xml_network = CE_NC_XML_DELETE_NETWORKS % ( + self.addr, self.get_wildcard_mask()) + xml_area = CE_NC_XML_BUILD_AREA % (self.get_area_ip(), xml_network) + xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, xml_area) + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "DELETE_AREA_NETWORK") + self.updates_cmd.append("ospf %s" % self.process_id) + self.updates_cmd.append("area %s" % self.get_area_ip()) + self.updates_cmd.append("undo network %s %s" % + (self.addr, self.get_wildcard_mask())) + self.changed = True + + def remove_area(self): + """remove ospf area""" + + if not self.is_area_exist(): + return + + xml_area = CE_NC_XML_BUILD_DELETE_AREA % (self.get_area_ip(), "") + xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, xml_area) + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "DELETE_AREA") + self.updates_cmd.append("ospf %s" % self.process_id) + self.updates_cmd.append("undo area %s" % self.get_area_ip()) + self.changed = True + + def remove_nexthop(self): + """remove ospf nexthop weight""" + + if not self.is_nexthop_exist(): + return + + xml_nh = CE_NC_XML_DELETE_NEXTHOP % self.nexthop_addr + xml_topo = CE_NC_XML_BUILD_TOPO % xml_nh + xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, xml_topo) + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "DELETE_NEXTHOP_WEIGHT") + self.updates_cmd.append("ospf %s" % self.process_id) + self.updates_cmd.append("undo nexthop %s" % self.nexthop_addr) + self.changed = True + + def is_valid_v4addr(self, addr): + """check is ipv4 addr is valid""" + + if addr.find('.') != -1: + addr_list = addr.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + def convert_ip_to_network(self): + """convert ip to subnet address""" + + ip_list = self.addr.split('.') + mask_list = self.get_wildcard_mask().split('.') + + for i in range(len(ip_list)): + ip_list[i] = str((int(ip_list[i]) & (~int(mask_list[i]))) & 0xff) + + self.addr = '.'.join(ip_list) + + def check_params(self): + """Check all input params""" + + # process_id check + if not self.process_id.isdigit(): + self.module.fail_json(msg="Error: process_id is not digit.") + if int(self.process_id) < 1 or int(self.process_id) > 4294967295: + self.module.fail_json( + msg="Error: process_id must be an integer between 1 and 4294967295.") + + if self.area: + # area check + if self.area.isdigit(): + if int(self.area) < 0 or int(self.area) > 4294967295: + self.module.fail_json( + msg="Error: area id (Integer) must be between 0 and 4294967295.") + + else: + if not self.is_valid_v4addr(self.area): + self.module.fail_json(msg="Error: area id is invalid.") + + # area network check + if self.addr: + if not self.is_valid_v4addr(self.addr): + self.module.fail_json( + msg="Error: network addr is invalid.") + if not self.mask.isdigit(): + self.module.fail_json( + msg="Error: network mask is not digit.") + if int(self.mask) < 0 or int(self.mask) > 32: + self.module.fail_json( + msg="Error: network mask is invalid.") + + # area authentication check + if self.state == "present" and self.auth_mode: + if self.auth_mode == "simple": + if self.auth_text_simple and len(self.auth_text_simple) > 8: + self.module.fail_json( + msg="Error: auth_text_simple is not in the range from 1 to 8.") + if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: + if self.auth_key_id: + if not self.auth_key_id.isdigit(): + self.module.fail_json( + msg="Error: auth_key_id is not digit.") + if int(self.auth_key_id) < 1 or int(self.auth_key_id) > 255: + self.module.fail_json( + msg="Error: auth_key_id is not in the range from 1 to 255.") + if self.auth_text_md5 and len(self.auth_text_md5) > 255: + self.module.fail_json( + msg="Error: auth_text_md5 is not in the range from 1 to 255.") + + # process max load balance check + if self.state == "present" and self.max_load_balance: + if not self.max_load_balance.isdigit(): + self.module.fail_json( + msg="Error: max_load_balance is not digit.") + if int(self.max_load_balance) < 1 or int(self.max_load_balance) > 64: + self.module.fail_json( + msg="Error: max_load_balance is not in the range from 1 to 64.") + + # process nexthop weight check + if self.nexthop_addr: + if not self.is_valid_v4addr(self.nexthop_addr): + self.module.fail_json(msg="Error: nexthop_addr is invalid.") + if not self.nexthop_weight.isdigit(): + self.module.fail_json( + msg="Error: nexthop_weight is not digit.") + if int(self.nexthop_weight) < 1 or int(self.nexthop_weight) > 254: + self.module.fail_json( + msg="Error: nexthop_weight is not in the range from 1 to 254.") + + if self.addr: + self.convert_ip_to_network() + + def get_proposed(self): + """get proposed info""" + + self.proposed["process_id"] = self.process_id + self.proposed["area"] = self.area + if self.area: + self.proposed["addr"] = self.addr + self.proposed["mask"] = self.mask + if self.auth_mode: + self.proposed["auth_mode"] = self.auth_mode + if self.auth_mode == "simple": + self.proposed["auth_text_simple"] = self.auth_text_simple + if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: + self.proposed["auth_key_id"] = self.auth_key_id + self.proposed["auth_text_md5"] = self.auth_text_md5 + + if self.nexthop_addr: + self.proposed["nexthop_addr"] = self.nexthop_addr + self.proposed["nexthop_weight"] = self.nexthop_weight + self.proposed["max_load_balance"] = self.max_load_balance + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.ospf_info: + return + + self.existing["process_id"] = self.process_id + self.existing["areas"] = self.ospf_info["areas"] + self.existing["nexthops"] = self.ospf_info["nexthops"] + self.existing["max_load_balance"] = self.ospf_info.get( + "maxLoadBalancing") + + def get_end_state(self): + """get end state info""" + + ospf_info = self.get_ospf_dict(self.process_id) + + if not ospf_info: + return + + self.end_state["process_id"] = self.process_id + self.end_state["areas"] = ospf_info["areas"] + self.end_state["nexthops"] = ospf_info["nexthops"] + self.end_state["max_load_balance"] = ospf_info.get("maxLoadBalancing") + + if self.end_state == self.existing: + if not self.auth_text_simple and not self.auth_text_md5: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.ospf_info = self.get_ospf_dict(self.process_id) + self.get_existing() + self.get_proposed() + + # deal present or absent + if self.state == "present": + if not self.ospf_info: + # create ospf process + self.create_process() + else: + # merge ospf + self.merge_process() + else: + if self.ospf_info: + if self.area: + if self.addr: + # remove ospf area network + self.remove_area_network() + else: + # remove ospf area + self.remove_area() + if self.nexthop_addr: + # remove ospf nexthop weight + self.remove_nexthop() + + if not self.area and not self.nexthop_addr: + # remove ospf process + self.delete_process() + else: + self.module.fail_json(msg='Error: ospf process does not exist') + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + process_id=dict(required=True, type='str'), + area=dict(required=False, type='str'), + addr=dict(required=False, type='str'), + mask=dict(required=False, type='str'), + auth_mode=dict(required=False, + choices=['none', 'hmac-sha256', 'md5', 'hmac-md5', 'simple'], type='str'), + auth_text_simple=dict(required=False, type='str', no_log=True), + auth_key_id=dict(required=False, type='str'), + auth_text_md5=dict(required=False, type='str', no_log=True), + nexthop_addr=dict(required=False, type='str'), + nexthop_weight=dict(required=False, type='str'), + max_load_balance=dict(required=False, type='str'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = OSPF(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_ospf_vrf.py b/plugins/modules/ce_ospf_vrf.py new file mode 100644 index 0000000..8b65886 --- /dev/null +++ b/plugins/modules/ce_ospf_vrf.py @@ -0,0 +1,1620 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_ospf_vrf +short_description: Manages configuration of an OSPF VPN instance on HUAWEI CloudEngine switches. +description: + - Manages configuration of an OSPF VPN instance on HUAWEI CloudEngine switches. +author: Yang yang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + ospf: + description: + - The ID of the ospf process. + Valid values are an integer, 1 - 4294967295, the default value is 1. + required: true + route_id: + description: + - Specifies the ospf private route id,. + Valid values are a string, formatted as an IP address + (i.e. "10.1.1.1") the length is 0 - 20. + vrf: + description: + - Specifies the vpn instance which use ospf,length is 1 - 31. + Valid values are a string. + default: _public_ + description: + description: + - Specifies the description information of ospf process. + bandwidth: + description: + - Specifies the reference bandwidth used to assign ospf cost. + Valid values are an integer, in Mbps, 1 - 2147483648, the default value is 100. + lsaalflag: + description: + - Specifies the mode of timer to calculate interval of arrive LSA. + If set the parameter but not specifies value, the default will be used. + If true use general timer. + If false use intelligent timer. + type: bool + default: 'no' + lsaainterval: + description: + - Specifies the interval of arrive LSA when use the general timer. + Valid value is an integer, in millisecond, from 0 to 10000. + lsaamaxinterval: + description: + - Specifies the max interval of arrive LSA when use the intelligent timer. + Valid value is an integer, in millisecond, from 0 to 10000, the default value is 1000. + lsaastartinterval: + description: + - Specifies the start interval of arrive LSA when use the intelligent timer. + Valid value is an integer, in millisecond, from 0 to 10000, the default value is 500. + lsaaholdinterval: + description: + - Specifies the hold interval of arrive LSA when use the intelligent timer. + Valid value is an integer, in millisecond, from 0 to 10000, the default value is 500. + lsaointervalflag: + description: + - Specifies whether cancel the interval of LSA originate or not. + If set the parameter but noe specifies value, the default will be used. + true:cancel the interval of LSA originate, the interval is 0. + false:do not cancel the interval of LSA originate. + type: bool + default: 'no' + lsaointerval: + description: + - Specifies the interval of originate LSA . + Valid value is an integer, in second, from 0 to 10, the default value is 5. + lsaomaxinterval: + description: + - Specifies the max interval of originate LSA . + Valid value is an integer, in millisecond, from 1 to 10000, the default value is 5000. + lsaostartinterval: + description: + - Specifies the start interval of originate LSA . + Valid value is an integer, in millisecond, from 0 to 1000, the default value is 500. + lsaoholdinterval: + description: + - Specifies the hold interval of originate LSA . + Valid value is an integer, in millisecond, from 0 to 5000, the default value is 1000. + spfintervaltype: + description: + - Specifies the mode of timer which used to calculate SPF. + If set the parameter but noe specifies value, the default will be used. + If is intelligent-timer, then use intelligent timer. + If is timer, then use second level timer. + If is millisecond, then use millisecond level timer. + choices: ['intelligent-timer','timer','millisecond'] + default: intelligent-timer + spfinterval: + description: + - Specifies the interval to calculate SPF when use second level timer. + Valid value is an integer, in second, from 1 to 10. + spfintervalmi: + description: + - Specifies the interval to calculate SPF when use millisecond level timer. + Valid value is an integer, in millisecond, from 1 to 10000. + spfmaxinterval: + description: + - Specifies the max interval to calculate SPF when use intelligent timer. + Valid value is an integer, in millisecond, from 1 to 20000, the default value is 5000. + spfstartinterval: + description: + - Specifies the start interval to calculate SPF when use intelligent timer. + Valid value is an integer, in millisecond, from 1 to 1000, the default value is 50. + spfholdinterval: + description: + - Specifies the hold interval to calculate SPF when use intelligent timer. + Valid value is an integer, in millisecond, from 1 to 5000, the default value is 200. + state: + description: + - Specify desired state of the resource. + choices: ['present', 'absent'] + default: present +''' + +EXAMPLES = ''' +- name: ospf vrf module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configure ospf route id + ce_ospf_vrf: + ospf: 2 + route_id: 2.2.2.2 + lsaointervalflag: False + lsaointerval: 2 + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: { + "bandwidth": "100", + "description": null, + "lsaaholdinterval": "500", + "lsaainterval": null, + "lsaamaxinterval": "1000", + "lsaastartinterval": "500", + "lsaalflag": "False", + "lsaoholdinterval": "1000", + "lsaointerval": "2", + "lsaointervalflag": "False", + "lsaomaxinterval": "5000", + "lsaostartinterval": "500", + "process_id": "2", + "route_id": "2.2.2.2", + "spfholdinterval": "1000", + "spfinterval": null, + "spfintervalmi": null, + "spfintervaltype": "intelligent-timer", + "spfmaxinterval": "10000", + "spfstartinterval": "500", + "vrf": "_public_" + } +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: { + "bandwidthReference": "100", + "description": null, + "lsaArrivalFlag": "false", + "lsaArrivalHoldInterval": "500", + "lsaArrivalInterval": null, + "lsaArrivalMaxInterval": "1000", + "lsaArrivalStartInterval": "500", + "lsaOriginateHoldInterval": "1000", + "lsaOriginateInterval": "2", + "lsaOriginateIntervalFlag": "false", + "lsaOriginateMaxInterval": "5000", + "lsaOriginateStartInterval": "500", + "processId": "2", + "routerId": "2.2.2.2", + "spfScheduleHoldInterval": "1000", + "spfScheduleInterval": null, + "spfScheduleIntervalMillisecond": null, + "spfScheduleIntervalType": "intelligent-timer", + "spfScheduleMaxInterval": "10000", + "spfScheduleStartInterval": "500", + "vrfName": "_public_" + } +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: { + "bandwidthReference": "100", + "description": null, + "lsaArrivalFlag": "false", + "lsaArrivalHoldInterval": "500", + "lsaArrivalInterval": null, + "lsaArrivalMaxInterval": "1000", + "lsaArrivalStartInterval": "500", + "lsaOriginateHoldInterval": "1000", + "lsaOriginateInterval": "2", + "lsaOriginateIntervalFlag": "false", + "lsaOriginateMaxInterval": "5000", + "lsaOriginateStartInterval": "500", + "processId": "2", + "routerId": "2.2.2.2", + "spfScheduleHoldInterval": "1000", + "spfScheduleInterval": null, + "spfScheduleIntervalMillisecond": null, + "spfScheduleIntervalType": "intelligent-timer", + "spfScheduleMaxInterval": "10000", + "spfScheduleStartInterval": "500", + "vrfName": "_public_" + } +updates: + description: commands sent to the device + returned: always + type: list + sample: ["ospf 2"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: False +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_OSPF_VRF = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +CE_NC_CREATE_OSPF_VRF = """ + + + + + %s +%s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + %s + + + + +""" +CE_NC_CREATE_ROUTE_ID = """ + %s +""" + +CE_NC_DELETE_OSPF = """ + + + + + %s + %s + %s + + + + +""" + + +def build_config_xml(xmlstr): + """build_config_xml""" + + return ' ' + xmlstr + ' ' + + +class OspfVrf(object): + """ + Manages configuration of an ospf instance. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.ospf = self.module.params['ospf'] + self.route_id = self.module.params['route_id'] + self.vrf = self.module.params['vrf'] + self.description = self.module.params['description'] + self.bandwidth = self.module.params['bandwidth'] + self.lsaalflag = self.module.params['lsaalflag'] + self.lsaainterval = self.module.params['lsaainterval'] + self.lsaamaxinterval = self.module.params['lsaamaxinterval'] + self.lsaastartinterval = self.module.params['lsaastartinterval'] + self.lsaaholdinterval = self.module.params['lsaaholdinterval'] + self.lsaointervalflag = self.module.params['lsaointervalflag'] + self.lsaointerval = self.module.params['lsaointerval'] + self.lsaomaxinterval = self.module.params['lsaomaxinterval'] + self.lsaostartinterval = self.module.params['lsaostartinterval'] + self.lsaoholdinterval = self.module.params['lsaoholdinterval'] + self.spfintervaltype = self.module.params['spfintervaltype'] + self.spfinterval = self.module.params['spfinterval'] + self.spfintervalmi = self.module.params['spfintervalmi'] + self.spfmaxinterval = self.module.params['spfmaxinterval'] + self.spfstartinterval = self.module.params['spfstartinterval'] + self.spfholdinterval = self.module.params['spfholdinterval'] + self.state = self.module.params['state'] + + # ospf info + self.ospf_info = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + self.lsa_arrival_changed = False + self.lsa_originate_changed = False + self.spf_changed = False + self.route_id_changed = False + self.bandwidth_changed = False + self.description_changed = False + self.vrf_changed = False + + def init_module(self): + """" init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def is_valid_ospf_process_id(self): + """check whether the input ospf process id is valid""" + + if not self.ospf.isdigit(): + return False + if int(self.ospf) > 4294967295 or int(self.ospf) < 1: + return False + return True + + def is_valid_ospf_route_id(self): + """check is ipv4 addr is valid""" + + if self.route_id.find('.') != -1: + addr_list = self.route_id.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + return False + + def is_valid_vrf_name(self): + """check whether the input ospf vrf name is valid""" + + if len(self.vrf) > 31 or len(self.vrf) < 1: + return False + if self.vrf.find('?') != -1: + return False + if self.vrf.find(' ') != -1: + return False + return True + + def is_valid_description(self): + """check whether the input ospf description is valid""" + + if len(self.description) > 80 or len(self.description) < 1: + return False + if self.description.find('?') != -1: + return False + return True + + def is_valid_bandwidth(self): + """check whether the input ospf bandwidth reference is valid""" + + if not self.bandwidth.isdigit(): + return False + if int(self.bandwidth) > 2147483648 or int(self.bandwidth) < 1: + return False + return True + + def is_valid_lsa_arrival_interval(self): + """check whether the input ospf lsa arrival interval is valid""" + + if self.lsaainterval is None: + return False + if not self.lsaainterval.isdigit(): + return False + if int(self.lsaainterval) > 10000 or int(self.lsaainterval) < 0: + return False + return True + + def isvalidlsamaxarrivalinterval(self): + """check whether the input ospf lsa max arrival interval is valid""" + + if not self.lsaamaxinterval.isdigit(): + return False + if int(self.lsaamaxinterval) > 10000 or int(self.lsaamaxinterval) < 1: + return False + return True + + def isvalidlsastartarrivalinterval(self): + """check whether the input ospf lsa start arrival interval is valid""" + + if not self.lsaastartinterval.isdigit(): + return False + if int(self.lsaastartinterval) > 1000 or int(self.lsaastartinterval) < 0: + return False + return True + + def isvalidlsaholdarrivalinterval(self): + """check whether the input ospf lsa hold arrival interval is valid""" + + if not self.lsaaholdinterval.isdigit(): + return False + if int(self.lsaaholdinterval) > 5000 or int(self.lsaaholdinterval) < 0: + return False + return True + + def is_valid_lsa_originate_interval(self): + """check whether the input ospf lsa originate interval is valid""" + + if not self.lsaointerval.isdigit(): + return False + if int(self.lsaointerval) > 10 or int(self.lsaointerval) < 0: + return False + return True + + def isvalidlsaoriginatemaxinterval(self): + """check whether the input ospf lsa originate max interval is valid""" + + if not self.lsaomaxinterval.isdigit(): + return False + if int(self.lsaomaxinterval) > 10000 or int(self.lsaomaxinterval) < 1: + return False + return True + + def isvalidlsaostartinterval(self): + """check whether the input ospf lsa originate start interval is valid""" + + if not self.lsaostartinterval.isdigit(): + return False + if int(self.lsaostartinterval) > 1000 or int(self.lsaostartinterval) < 0: + return False + return True + + def isvalidlsaoholdinterval(self): + """check whether the input ospf lsa originate hold interval is valid""" + + if not self.lsaoholdinterval.isdigit(): + return False + if int(self.lsaoholdinterval) > 5000 or int(self.lsaoholdinterval) < 1: + return False + return True + + def is_valid_spf_interval(self): + """check whether the input ospf spf interval is valid""" + + if not self.spfinterval.isdigit(): + return False + if int(self.spfinterval) > 10 or int(self.spfinterval) < 1: + return False + return True + + def is_valid_spf_milli_interval(self): + """check whether the input ospf spf millisecond level interval is valid""" + + if not self.spfintervalmi.isdigit(): + return False + if int(self.spfintervalmi) > 10000 or int(self.spfintervalmi) < 1: + return False + return True + + def is_valid_spf_max_interval(self): + """check whether the input ospf spf intelligent timer max interval is valid""" + + if not self.spfmaxinterval.isdigit(): + return False + if int(self.spfmaxinterval) > 20000 or int(self.spfmaxinterval) < 1: + return False + return True + + def is_valid_spf_start_interval(self): + """check whether the input ospf spf intelligent timer start interval is valid""" + + if not self.spfstartinterval.isdigit(): + return False + if int(self.spfstartinterval) > 1000 or int(self.spfstartinterval) < 1: + return False + return True + + def is_valid_spf_hold_interval(self): + """check whether the input ospf spf intelligent timer hold interval is valid""" + + if not self.spfholdinterval.isdigit(): + return False + if int(self.spfholdinterval) > 5000 or int(self.spfholdinterval) < 1: + return False + return True + + def is_route_id_exist(self): + """is route id exist""" + + if not self.ospf_info: + return False + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] != self.ospf: + continue + if ospf_site["routerId"] == self.route_id: + return True + else: + continue + return False + + def get_exist_ospf_id(self): + """get exist ospf process id""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["processId"] + else: + continue + return None + + def get_exist_route(self): + """get exist route id""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["routerId"] + else: + continue + return None + + def get_exist_vrf(self): + """get exist vrf""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["vrfName"] + else: + continue + return None + + def get_exist_bandwidth(self): + """get exist bandwidth""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["bandwidthReference"] + else: + continue + return None + + def get_exist_lsa_a_interval(self): + """get exist lsa arrival interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaArrivalInterval"] + else: + continue + return None + + def get_exist_lsa_a_interval_flag(self): + """get exist lsa arrival interval flag""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaArrivalFlag"] + else: + continue + return None + + def get_exist_lsa_a_max_interval(self): + """get exist lsa arrival max interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaArrivalMaxInterval"] + else: + continue + return None + + def get_exist_lsa_a_start_interval(self): + """get exist lsa arrival start interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaArrivalStartInterval"] + else: + continue + return None + + def get_exist_lsa_a_hold_interval(self): + """get exist lsa arrival hold interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaArrivalHoldInterval"] + else: + continue + return None + + def getexistlsaointerval(self): + """get exist lsa originate interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaOriginateInterval"] + else: + continue + return None + + def getexistlsaointerval_flag(self): + """get exist lsa originate interval flag""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaOriginateIntervalFlag"] + else: + continue + return None + + def getexistlsaomaxinterval(self): + """get exist lsa originate max interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaOriginateMaxInterval"] + else: + continue + return None + + def getexistlsaostartinterval(self): + """get exist lsa originate start interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaOriginateStartInterval"] + else: + continue + return None + + def getexistlsaoholdinterval(self): + """get exist lsa originate hold interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["lsaOriginateHoldInterval"] + else: + continue + return None + + def get_exist_spf_interval(self): + """get exist spf second level timer interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["spfScheduleInterval"] + else: + continue + return None + + def get_exist_spf_milli_interval(self): + """get exist spf millisecond level timer interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["spfScheduleIntervalMillisecond"] + else: + continue + return None + + def get_exist_spf_max_interval(self): + """get exist spf max interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["spfScheduleMaxInterval"] + else: + continue + return None + + def get_exist_spf_start_interval(self): + """get exist spf start interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["spfScheduleStartInterval"] + else: + continue + return None + + def get_exist_spf_hold_interval(self): + """get exist spf hold interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["spfScheduleHoldInterval"] + else: + continue + return None + + def get_exist_spf_interval_type(self): + """get exist spf hold interval""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["spfScheduleIntervalType"] + else: + continue + return None + + def is_ospf_exist(self): + """is ospf exist""" + + if not self.ospf_info: + return False + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return True + else: + continue + return False + + def get_exist_description(self): + """is description exist""" + + if not self.ospf_info: + return None + + for ospf_site in self.ospf_info["ospfsite"]: + if ospf_site["processId"] == self.ospf: + return ospf_site["description"] + else: + continue + return None + + def check_params(self): + """Check all input params""" + + if self.ospf == '': + self.module.fail_json( + msg='Error: The ospf process id should not be null.') + if self.ospf: + if not self.is_valid_ospf_process_id(): + self.module.fail_json( + msg='Error: The ospf process id should between 1 - 4294967295.') + if self.route_id == '': + self.module.fail_json( + msg='Error: The ospf route id length should not be null.') + if self.route_id: + if not self.is_valid_ospf_route_id(): + self.module.fail_json( + msg='Error: The ospf route id length should between 0 - 20,i.e.10.1.1.1.') + if self.vrf == '': + self.module.fail_json( + msg='Error: The ospf vpn instance length should not be null.') + if self.vrf: + if not self.is_valid_vrf_name(): + self.module.fail_json( + msg='Error: The ospf vpn instance length should between 0 - 31,but can not contain " " or "?".') + if self.description == '': + self.module.fail_json( + msg='Error: The ospf description should not be null.') + if self.description: + if not self.is_valid_description(): + self.module.fail_json( + msg='Error: The ospf description length should between 1 - 80,but can not contain "?".') + if self.bandwidth == '': + self.module.fail_json( + msg='Error: The ospf bandwidth reference should not be null.') + if self.bandwidth: + if not self.is_valid_bandwidth(): + self.module.fail_json( + msg='Error: The ospf bandwidth reference should between 1 - 2147483648.') + if self.lsaalflag is True: + if not self.is_valid_lsa_arrival_interval(): + self.module.fail_json( + msg='Error: The ospf lsa arrival interval should between 0 - 10000.') + if self.lsaamaxinterval or self.lsaastartinterval or self.lsaaholdinterval: + self.module.fail_json( + msg='Error: Non-Intelligent Timer and Intelligent Timer Interval of ' + 'lsa-arrival-interval can not configured at the same time.') + if self.lsaalflag is False: + if self.lsaainterval: + self.module.fail_json( + msg='Error: The parameter of lsa arrival interval command is invalid, ' + 'because LSA arrival interval can not be config when the LSA arrival flag is not set.') + if self.lsaamaxinterval == '' or self.lsaastartinterval == '' or self.lsaaholdinterval == '': + self.module.fail_json( + msg='Error: The ospf lsa arrival intervals should not be null.') + if self.lsaamaxinterval: + if not self.isvalidlsamaxarrivalinterval(): + self.module.fail_json( + msg='Error: The ospf lsa arrival max interval should between 1 - 10000.') + if self.lsaastartinterval: + if not self.isvalidlsastartarrivalinterval(): + self.module.fail_json( + msg='Error: The ospf lsa arrival start interval should between 1 - 1000.') + if self.lsaaholdinterval: + if not self.isvalidlsaholdarrivalinterval(): + self.module.fail_json( + msg='Error: The ospf lsa arrival hold interval should between 1 - 5000.') + if self.lsaointervalflag is True: + if self.lsaointerval or self.lsaomaxinterval \ + or self.lsaostartinterval or self.lsaoholdinterval: + self.module.fail_json( + msg='Error: Interval for other-type and Instantly Flag ' + 'of lsa-originate-interval can not configured at the same time.') + if self.lsaointerval == '': + self.module.fail_json( + msg='Error: The ospf lsa originate interval should should not be null.') + if self.lsaointerval: + if not self.is_valid_lsa_originate_interval(): + self.module.fail_json( + msg='Error: The ospf lsa originate interval should between 0 - 10 s.') + if self.lsaomaxinterval == '' or self.lsaostartinterval == '' or self.lsaoholdinterval == '': + self.module.fail_json( + msg='Error: The ospf lsa originate intelligent intervals should should not be null.') + if self.lsaomaxinterval: + if not self.isvalidlsaoriginatemaxinterval(): + self.module.fail_json( + msg='Error: The ospf lsa originate max interval should between 1 - 10000 ms.') + if self.lsaostartinterval: + if not self.isvalidlsaostartinterval(): + self.module.fail_json( + msg='Error: The ospf lsa originate start interval should between 0 - 1000 ms.') + if self.lsaoholdinterval: + if not self.isvalidlsaoholdinterval(): + self.module.fail_json( + msg='Error: The ospf lsa originate hold interval should between 1 - 5000 ms.') + if self.spfintervaltype == '': + self.module.fail_json( + msg='Error: The ospf spf interval type should should not be null.') + if self.spfintervaltype == 'intelligent-timer': + if self.spfinterval is not None or self.spfintervalmi is not None: + self.module.fail_json( + msg='Error: Interval second and interval millisecond ' + 'of spf-schedule-interval can not configured if use intelligent timer.') + if self.spfmaxinterval == '' or self.spfstartinterval == '' or self.spfholdinterval == '': + self.module.fail_json( + msg='Error: The ospf spf intelligent timer intervals should should not be null.') + if self.spfmaxinterval and not self.is_valid_spf_max_interval(): + self.module.fail_json( + msg='Error: The ospf spf max interval of intelligent timer should between 1 - 20000 ms.') + if self.spfstartinterval and not self.is_valid_spf_start_interval(): + self.module.fail_json( + msg='Error: The ospf spf start interval of intelligent timer should between 1 - 1000 ms.') + if self.spfholdinterval and not self.is_valid_spf_hold_interval(): + self.module.fail_json( + msg='Error: The ospf spf hold interval of intelligent timer should between 1 - 5000 ms.') + if self.spfintervaltype == 'timer': + if self.spfintervalmi is not None: + self.module.fail_json( + msg='Error: Interval second and interval millisecond ' + 'of spf-schedule-interval can not configured at the same time.') + if self.spfmaxinterval or self.spfstartinterval or self.spfholdinterval: + self.module.fail_json( + msg='Error: Interval second and interval intelligent ' + 'of spf-schedule-interval can not configured at the same time.') + if self.spfinterval == '' or self.spfinterval is None: + self.module.fail_json( + msg='Error: The ospf spf timer intervals should should not be null.') + if not self.is_valid_spf_interval(): + self.module.fail_json( + msg='Error: Interval second should between 1 - 10 s.') + if self.spfintervaltype == 'millisecond': + if self.spfinterval is not None: + self.module.fail_json( + msg='Error: Interval millisecond and interval second ' + 'of spf-schedule-interval can not configured at the same time.') + if self.spfmaxinterval or self.spfstartinterval or self.spfholdinterval: + self.module.fail_json( + msg='Error: Interval millisecond and interval intelligent ' + 'of spf-schedule-interval can not configured at the same time.') + if self.spfintervalmi == '' or self.spfintervalmi is None: + self.module.fail_json( + msg='Error: The ospf spf millisecond intervals should should not be null.') + if not self.is_valid_spf_milli_interval(): + self.module.fail_json( + msg='Error: Interval millisecond should between 1 - 10000 ms.') + + def get_ospf_info(self): + """ get the detail information of ospf """ + + self.ospf_info["ospfsite"] = list() + + getxmlstr = CE_NC_GET_OSPF_VRF + xml_str = get_nc_config(self.module, getxmlstr) + if 'data/' in xml_str: + return + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + + # get the vpn address family and RD text + ospf_sites = root.findall( + "ospfv2/ospfv2comm/ospfSites/ospfSite") + if ospf_sites: + for ospf_site in ospf_sites: + ospf_ele_info = dict() + for ospf_site_ele in ospf_site: + if ospf_site_ele.tag in ["processId", "routerId", "vrfName", "bandwidthReference", + "description", "lsaArrivalInterval", "lsaArrivalMaxInterval", + "lsaArrivalStartInterval", "lsaArrivalHoldInterval", "lsaArrivalFlag", + "lsaOriginateInterval", "lsaOriginateMaxInterval", + "lsaOriginateStartInterval", "lsaOriginateHoldInterval", + "lsaOriginateIntervalFlag", "spfScheduleInterval", + "spfScheduleIntervalMillisecond", "spfScheduleMaxInterval", + "spfScheduleStartInterval", "spfScheduleHoldInterval", + "spfScheduleIntervalType"]: + ospf_ele_info[ + ospf_site_ele.tag] = ospf_site_ele.text + if ospf_ele_info["processId"] == self.ospf: + self.ospf_info["ospfsite"].append(ospf_ele_info) + + def get_proposed(self): + """get proposed info""" + + self.proposed["process_id"] = self.ospf + self.proposed["route_id"] = self.route_id + self.proposed["vrf"] = self.vrf + self.proposed["description"] = self.description + self.proposed["bandwidth"] = self.bandwidth + self.proposed["lsaalflag"] = self.lsaalflag + self.proposed["lsaainterval"] = self.lsaainterval + self.proposed["lsaamaxinterval"] = self.lsaamaxinterval + self.proposed["lsaastartinterval"] = self.lsaastartinterval + self.proposed["lsaaholdinterval"] = self.lsaaholdinterval + self.proposed["lsaointervalflag"] = self.lsaointervalflag + self.proposed["lsaointerval"] = self.lsaointerval + self.proposed["lsaomaxinterval"] = self.lsaomaxinterval + self.proposed["lsaostartinterval"] = self.lsaostartinterval + self.proposed["lsaoholdinterval"] = self.lsaoholdinterval + self.proposed["spfintervaltype"] = self.spfintervaltype + self.proposed["spfinterval"] = self.spfinterval + self.proposed["spfintervalmi"] = self.spfintervalmi + self.proposed["spfmaxinterval"] = self.spfmaxinterval + self.proposed["spfstartinterval"] = self.spfstartinterval + self.proposed["spfholdinterval"] = self.spfholdinterval + + def operate_ospf_info(self): + """operate ospf info""" + + config_route_id_xml = '' + vrf = self.get_exist_vrf() + if vrf is None: + vrf = '_public_' + description = self.get_exist_description() + if description is None: + description = '' + bandwidth_reference = self.get_exist_bandwidth() + if bandwidth_reference is None: + bandwidth_reference = '100' + lsa_in_interval = self.get_exist_lsa_a_interval() + if lsa_in_interval is None: + lsa_in_interval = '' + lsa_arrival_max_interval = self.get_exist_lsa_a_max_interval() + if lsa_arrival_max_interval is None: + lsa_arrival_max_interval = '1000' + lsa_arrival_start_interval = self.get_exist_lsa_a_start_interval() + if lsa_arrival_start_interval is None: + lsa_arrival_start_interval = '500' + lsa_arrival_hold_interval = self.get_exist_lsa_a_hold_interval() + if lsa_arrival_hold_interval is None: + lsa_arrival_hold_interval = '500' + lsa_originate_interval = self.getexistlsaointerval() + if lsa_originate_interval is None: + lsa_originate_interval = '5' + lsa_originate_max_interval = self.getexistlsaomaxinterval() + if lsa_originate_max_interval is None: + lsa_originate_max_interval = '5000' + lsa_originate_start_interval = self.getexistlsaostartinterval() + if lsa_originate_start_interval is None: + lsa_originate_start_interval = '500' + lsa_originate_hold_interval = self.getexistlsaoholdinterval() + if lsa_originate_hold_interval is None: + lsa_originate_hold_interval = '1000' + spf_interval = self.get_exist_spf_interval() + if spf_interval is None: + spf_interval = '' + spf_interval_milli = self.get_exist_spf_milli_interval() + if spf_interval_milli is None: + spf_interval_milli = '' + spf_max_interval = self.get_exist_spf_max_interval() + if spf_max_interval is None: + spf_max_interval = '5000' + spf_start_interval = self.get_exist_spf_start_interval() + if spf_start_interval is None: + spf_start_interval = '50' + spf_hold_interval = self.get_exist_spf_hold_interval() + if spf_hold_interval is None: + spf_hold_interval = '200' + + if self.route_id: + if self.state == 'present': + if self.route_id != self.get_exist_route(): + self.route_id_changed = True + config_route_id_xml = CE_NC_CREATE_ROUTE_ID % self.route_id + else: + if self.route_id != self.get_exist_route(): + self.module.fail_json( + msg='Error: The route id %s is not exist.' % self.route_id) + self.route_id_changed = True + configxmlstr = CE_NC_DELETE_OSPF % ( + self.ospf, self.get_exist_route(), self.get_exist_vrf()) + conf_str = build_config_xml(configxmlstr) + + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "OPERATE_VRF_AF") + self.changed = True + return + if self.vrf != '_public_': + if self.state == 'present': + if self.vrf != self.get_exist_vrf(): + self.vrf_changed = True + vrf = self.vrf + else: + if self.vrf != self.get_exist_vrf(): + self.module.fail_json( + msg='Error: The vrf %s is not exist.' % self.vrf) + self.vrf_changed = True + configxmlstr = CE_NC_DELETE_OSPF % ( + self.ospf, self.get_exist_route(), self.get_exist_vrf()) + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "OPERATE_VRF_AF") + self.changed = True + return + if self.bandwidth: + if self.state == 'present': + if self.bandwidth != self.get_exist_bandwidth(): + self.bandwidth_changed = True + bandwidth_reference = self.bandwidth + else: + if self.bandwidth != self.get_exist_bandwidth(): + self.module.fail_json( + msg='Error: The bandwidth %s is not exist.' % self.bandwidth) + if self.get_exist_bandwidth() != '100': + self.bandwidth_changed = True + bandwidth_reference = '100' + if self.description: + if self.state == 'present': + if self.description != self.get_exist_description(): + self.description_changed = True + description = self.description + else: + if self.description != self.get_exist_description(): + self.module.fail_json( + msg='Error: The description %s is not exist.' % self.description) + self.description_changed = True + description = '' + + if self.lsaalflag is False: + lsa_in_interval = '' + if self.state == 'present': + if self.lsaamaxinterval: + if self.lsaamaxinterval != self.get_exist_lsa_a_max_interval(): + self.lsa_arrival_changed = True + lsa_arrival_max_interval = self.lsaamaxinterval + if self.lsaastartinterval: + if self.lsaastartinterval != self.get_exist_lsa_a_start_interval(): + self.lsa_arrival_changed = True + lsa_arrival_start_interval = self.lsaastartinterval + if self.lsaaholdinterval: + if self.lsaaholdinterval != self.get_exist_lsa_a_hold_interval(): + self.lsa_arrival_changed = True + lsa_arrival_hold_interval = self.lsaaholdinterval + else: + if self.lsaamaxinterval: + if self.lsaamaxinterval != self.get_exist_lsa_a_max_interval(): + self.module.fail_json( + msg='Error: The lsaamaxinterval %s is not exist.' % self.lsaamaxinterval) + if self.get_exist_lsa_a_max_interval() != '1000': + lsa_arrival_max_interval = '1000' + self.lsa_arrival_changed = True + if self.lsaastartinterval: + if self.lsaastartinterval != self.get_exist_lsa_a_start_interval(): + self.module.fail_json( + msg='Error: The lsaastartinterval %s is not exist.' % self.lsaastartinterval) + if self.get_exist_lsa_a_start_interval() != '500': + lsa_arrival_start_interval = '500' + self.lsa_arrival_changed = True + if self.lsaaholdinterval: + if self.lsaaholdinterval != self.get_exist_lsa_a_hold_interval(): + self.module.fail_json( + msg='Error: The lsaaholdinterval %s is not exist.' % self.lsaaholdinterval) + if self.get_exist_lsa_a_hold_interval() != '500': + lsa_arrival_hold_interval = '500' + self.lsa_arrival_changed = True + else: + if self.state == 'present': + lsaalflag = "false" + if self.lsaalflag is True: + lsaalflag = "true" + if lsaalflag != self.get_exist_lsa_a_interval_flag(): + self.lsa_arrival_changed = True + if self.lsaainterval is None: + self.module.fail_json( + msg='Error: The lsaainterval is not supplied.') + else: + lsa_in_interval = self.lsaainterval + else: + if self.lsaainterval: + if self.lsaainterval != self.get_exist_lsa_a_interval(): + self.lsa_arrival_changed = True + lsa_in_interval = self.lsaainterval + else: + if self.lsaainterval: + if self.lsaainterval != self.get_exist_lsa_a_interval(): + self.module.fail_json( + msg='Error: The lsaainterval %s is not exist.' % self.lsaainterval) + self.lsaalflag = False + lsa_in_interval = '' + self.lsa_arrival_changed = True + + if self.lsaointervalflag is False: + if self.state == 'present': + if self.lsaomaxinterval: + if self.lsaomaxinterval != self.getexistlsaomaxinterval(): + self.lsa_originate_changed = True + lsa_originate_max_interval = self.lsaomaxinterval + if self.lsaostartinterval: + if self.lsaostartinterval != self.getexistlsaostartinterval(): + self.lsa_originate_changed = True + lsa_originate_start_interval = self.lsaostartinterval + if self.lsaoholdinterval: + if self.lsaoholdinterval != self.getexistlsaoholdinterval(): + self.lsa_originate_changed = True + lsa_originate_hold_interval = self.lsaoholdinterval + if self.lsaointerval: + if self.lsaointerval != self.getexistlsaointerval(): + self.lsa_originate_changed = True + lsa_originate_interval = self.lsaointerval + else: + if self.lsaomaxinterval: + if self.lsaomaxinterval != self.getexistlsaomaxinterval(): + self.module.fail_json( + msg='Error: The lsaomaxinterval %s is not exist.' % self.lsaomaxinterval) + if self.getexistlsaomaxinterval() != '5000': + lsa_originate_max_interval = '5000' + self.lsa_originate_changed = True + if self.lsaostartinterval: + if self.lsaostartinterval != self.getexistlsaostartinterval(): + self.module.fail_json( + msg='Error: The lsaostartinterval %s is not exist.' % self.lsaostartinterval) + if self.getexistlsaostartinterval() != '500': + lsa_originate_start_interval = '500' + self.lsa_originate_changed = True + if self.lsaoholdinterval: + if self.lsaoholdinterval != self.getexistlsaoholdinterval(): + self.module.fail_json( + msg='Error: The lsaoholdinterval %s is not exist.' % self.lsaoholdinterval) + if self.getexistlsaoholdinterval() != '1000': + lsa_originate_hold_interval = '1000' + self.lsa_originate_changed = True + if self.lsaointerval: + if self.lsaointerval != self.getexistlsaointerval(): + self.module.fail_json( + msg='Error: The lsaointerval %s is not exist.' % self.lsaointerval) + if self.getexistlsaointerval() != '5': + lsa_originate_interval = '5' + self.lsa_originate_changed = True + else: + if self.state == 'present': + if self.getexistlsaointerval_flag() != 'true': + self.lsa_originate_changed = True + lsa_originate_interval = '5' + lsa_originate_max_interval = '5000' + lsa_originate_start_interval = '500' + lsa_originate_hold_interval = '1000' + else: + if self.getexistlsaointerval_flag() == 'true': + self.lsaointervalflag = False + self.lsa_originate_changed = True + if self.spfintervaltype != self.get_exist_spf_interval_type(): + self.spf_changed = True + if self.spfintervaltype == 'timer': + if self.spfinterval: + if self.state == 'present': + if self.spfinterval != self.get_exist_spf_interval(): + self.spf_changed = True + spf_interval = self.spfinterval + spf_interval_milli = '' + else: + if self.spfinterval != self.get_exist_spf_interval(): + self.module.fail_json( + msg='Error: The spfinterval %s is not exist.' % self.spfinterval) + self.spfintervaltype = 'intelligent-timer' + spf_interval = '' + self.spf_changed = True + if self.spfintervaltype == 'millisecond': + if self.spfintervalmi: + if self.state == 'present': + if self.spfintervalmi != self.get_exist_spf_milli_interval(): + self.spf_changed = True + spf_interval_milli = self.spfintervalmi + spf_interval = '' + else: + if self.spfintervalmi != self.get_exist_spf_milli_interval(): + self.module.fail_json( + msg='Error: The spfintervalmi %s is not exist.' % self.spfintervalmi) + self.spfintervaltype = 'intelligent-timer' + spf_interval_milli = '' + self.spf_changed = True + if self.spfintervaltype == 'intelligent-timer': + spf_interval = '' + spf_interval_milli = '' + if self.spfmaxinterval: + if self.state == 'present': + if self.spfmaxinterval != self.get_exist_spf_max_interval(): + self.spf_changed = True + spf_max_interval = self.spfmaxinterval + else: + if self.spfmaxinterval != self.get_exist_spf_max_interval(): + self.module.fail_json( + msg='Error: The spfmaxinterval %s is not exist.' % self.spfmaxinterval) + if self.get_exist_spf_max_interval() != '5000': + self.spf_changed = True + spf_max_interval = '5000' + if self.spfstartinterval: + if self.state == 'present': + if self.spfstartinterval != self.get_exist_spf_start_interval(): + self.spf_changed = True + spf_start_interval = self.spfstartinterval + else: + if self.spfstartinterval != self.get_exist_spf_start_interval(): + self.module.fail_json( + msg='Error: The spfstartinterval %s is not exist.' % self.spfstartinterval) + if self.get_exist_spf_start_interval() != '50': + self.spf_changed = True + spf_start_interval = '50' + if self.spfholdinterval: + if self.state == 'present': + if self.spfholdinterval != self.get_exist_spf_hold_interval(): + self.spf_changed = True + spf_hold_interval = self.spfholdinterval + else: + if self.spfholdinterval != self.get_exist_spf_hold_interval(): + self.module.fail_json( + msg='Error: The spfholdinterval %s is not exist.' % self.spfholdinterval) + if self.get_exist_spf_hold_interval() != '200': + self.spf_changed = True + spf_hold_interval = '200' + + if not self.description_changed and not self.vrf_changed and not self.lsa_arrival_changed \ + and not self.lsa_originate_changed and not self.spf_changed \ + and not self.route_id_changed and not self.bandwidth_changed: + self.changed = False + return + else: + self.changed = True + lsaointervalflag = "false" + lsaalflag = "false" + if self.lsaointervalflag is True: + lsaointervalflag = "true" + if self.lsaalflag is True: + lsaalflag = "true" + configxmlstr = CE_NC_CREATE_OSPF_VRF % ( + self.ospf, config_route_id_xml, vrf, + description, bandwidth_reference, lsaalflag, + lsa_in_interval, lsa_arrival_max_interval, lsa_arrival_start_interval, + lsa_arrival_hold_interval, lsaointervalflag, lsa_originate_interval, + lsa_originate_max_interval, lsa_originate_start_interval, lsa_originate_hold_interval, + self.spfintervaltype, spf_interval, spf_interval_milli, + spf_max_interval, spf_start_interval, spf_hold_interval) + + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "OPERATE_VRF_AF") + + def get_existing(self): + """get existing info""" + + self.get_ospf_info() + self.existing['ospf_info'] = self.ospf_info["ospfsite"] + + def set_update_cmd(self): + """ set update command""" + if not self.changed: + return + + if self.state == 'present': + if self.vrf_changed: + if self.vrf != '_public_': + if self.route_id_changed: + self.updates_cmd.append( + 'ospf %s router-id %s vpn-instance %s' % (self.ospf, self.route_id, self.vrf)) + else: + self.updates_cmd.append( + 'ospf %s vpn-instance %s ' % (self.ospf, self.vrf)) + else: + if self.route_id_changed: + self.updates_cmd.append( + 'ospf %s router-id %s' % (self.ospf, self.route_id)) + else: + if self.route_id_changed: + if self.vrf != '_public_': + self.updates_cmd.append( + 'ospf %s router-id %s vpn-instance %s' % (self.ospf, self.route_id, self.get_exist_vrf())) + else: + self.updates_cmd.append( + 'ospf %s router-id %s' % (self.ospf, self.route_id)) + else: + if self.route_id_changed: + self.updates_cmd.append('undo ospf %s' % self.ospf) + return + + self.updates_cmd.append('ospf %s' % self.ospf) + + if self.description: + if self.state == 'present': + if self.description_changed: + self.updates_cmd.append( + 'description %s' % self.description) + else: + if self.description_changed: + self.updates_cmd.append('undo description') + if self.bandwidth_changed: + if self.state == 'present': + if self.get_exist_bandwidth() != '100': + self.updates_cmd.append( + 'bandwidth-reference %s' % (self.get_exist_bandwidth())) + else: + self.updates_cmd.append('undo bandwidth-reference') + if self.lsaalflag is True: + if self.lsa_arrival_changed: + if self.state == 'present': + self.updates_cmd.append( + 'lsa-arrival-interval %s' % (self.get_exist_lsa_a_interval())) + else: + self.updates_cmd.append( + 'undo lsa-arrival-interval') + + if self.lsaalflag is False: + if self.lsa_arrival_changed: + if self.state == 'present': + if self.get_exist_lsa_a_max_interval() != '1000' \ + or self.get_exist_lsa_a_start_interval() != '500'\ + or self.get_exist_lsa_a_hold_interval() != '500': + self.updates_cmd.append('lsa-arrival-interval intelligent-timer %s %s %s' + % (self.get_exist_lsa_a_max_interval(), + self.get_exist_lsa_a_start_interval(), + self.get_exist_lsa_a_hold_interval())) + else: + if self.get_exist_lsa_a_max_interval() == '1000' \ + and self.get_exist_lsa_a_start_interval() == '500'\ + and self.get_exist_lsa_a_hold_interval() == '500': + self.updates_cmd.append( + 'undo lsa-arrival-interval') + if self.lsaointervalflag is False: + if self.lsa_originate_changed: + if self.state == 'present': + if self.getexistlsaointerval() != '5' \ + or self.getexistlsaomaxinterval() != '5000' \ + or self.getexistlsaostartinterval() != '500' \ + or self.getexistlsaoholdinterval() != '1000': + self.updates_cmd.append('lsa-originate-interval other-type %s intelligent-timer %s %s %s' + % (self.getexistlsaointerval(), + self.getexistlsaomaxinterval(), + self.getexistlsaostartinterval(), + self.getexistlsaoholdinterval())) + else: + self.updates_cmd.append( + 'undo lsa-originate-interval') + if self.lsaointervalflag is True: + if self.lsa_originate_changed: + if self.state == 'present': + self.updates_cmd.append('lsa-originate-interval 0 ') + else: + self.updates_cmd.append( + 'undo lsa-originate-interval') + if self.spfintervaltype == 'millisecond': + if self.spf_changed: + if self.state == 'present': + self.updates_cmd.append( + 'spf-schedule-interval millisecond %s' % self.get_exist_spf_milli_interval()) + else: + self.updates_cmd.append( + 'undo spf-schedule-interval') + if self.spfintervaltype == 'timer': + if self.spf_changed: + if self.state == 'present': + self.updates_cmd.append( + 'spf-schedule-interval %s' % self.get_exist_spf_interval()) + else: + self.updates_cmd.append( + 'undo spf-schedule-interval') + if self.spfintervaltype == 'intelligent-timer': + if self.spf_changed: + if self.state == 'present': + if self.get_exist_spf_max_interval() != '5000' \ + or self.get_exist_spf_start_interval() != '50' \ + or self.get_exist_spf_hold_interval() != '200': + self.updates_cmd.append('spf-schedule-interval intelligent-timer %s %s %s' + % (self.get_exist_spf_max_interval(), + self.get_exist_spf_start_interval(), + self.get_exist_spf_hold_interval())) + else: + self.updates_cmd.append( + 'undo spf-schedule-interval') + + def get_end_state(self): + """get end state info""" + + self.get_ospf_info() + self.end_state['ospf_info'] = self.ospf_info["ospfsite"] + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + self.operate_ospf_info() + self.get_end_state() + self.set_update_cmd() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + ospf=dict(required=True, type='str'), + route_id=dict(required=False, type='str'), + vrf=dict(required=False, type='str', default='_public_'), + description=dict(required=False, type='str'), + bandwidth=dict(required=False, type='str'), + lsaalflag=dict(type='bool', default=False), + lsaainterval=dict(required=False, type='str'), + lsaamaxinterval=dict(required=False, type='str'), + lsaastartinterval=dict(required=False, type='str'), + lsaaholdinterval=dict(required=False, type='str'), + lsaointervalflag=dict(type='bool', default=False), + lsaointerval=dict(required=False, type='str'), + lsaomaxinterval=dict(required=False, type='str'), + lsaostartinterval=dict(required=False, type='str'), + lsaoholdinterval=dict(required=False, type='str'), + spfintervaltype=dict(required=False, default='intelligent-timer', + choices=['intelligent-timer', 'timer', 'millisecond']), + spfinterval=dict(required=False, type='str'), + spfintervalmi=dict(required=False, type='str'), + spfmaxinterval=dict(required=False, type='str'), + spfstartinterval=dict(required=False, type='str'), + spfholdinterval=dict(required=False, type='str'), + state=dict(required=False, choices=['present', 'absent'], default='present'), + ) + + argument_spec.update(ce_argument_spec) + module = OspfVrf(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_reboot.py b/plugins/modules/ce_reboot.py new file mode 100644 index 0000000..4194f2d --- /dev/null +++ b/plugins/modules/ce_reboot.py @@ -0,0 +1,166 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_reboot +short_description: Reboot a HUAWEI CloudEngine switches. +description: + - Reboot a HUAWEI CloudEngine switches. +author: Gong Jianjun (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +requirements: ["ncclient"] +options: + confirm: + description: + - Safeguard boolean. Set to true if you're sure you want to reboot. + type: bool + required: true + save_config: + description: + - Flag indicating whether to save the configuration. + required: false + type: bool + default: false +''' + +EXAMPLES = ''' +- name: reboot module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Reboot the device + ce_reboot: + confirm: true + save_config: true + provider: "{{ cli }}" +''' + +RETURN = ''' +rebooted: + description: Whether the device was instructed to reboot. + returned: success + type: bool + sample: true +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import execute_nc_action, ce_argument_spec + +try: + from ncclient.operations.errors import TimeoutExpiredError + HAS_NCCLIENT = True +except ImportError: + HAS_NCCLIENT = False + +CE_NC_XML_EXECUTE_REBOOT = """ + + + + %s + + + +""" + + +class Reboot(object): + """ Reboot a network device """ + + def __init__(self, **kwargs): + """ __init___ """ + + self.network_module = None + self.netconf = None + self.init_network_module(**kwargs) + + self.confirm = self.network_module.params['confirm'] + self.save_config = self.network_module.params['save_config'] + + def init_network_module(self, **kwargs): + """ init network module """ + + self.network_module = AnsibleModule(**kwargs) + + def netconf_set_action(self, xml_str): + """ netconf execute action """ + + try: + execute_nc_action(self.network_module, xml_str) + except TimeoutExpiredError: + pass + + def work(self): + """ start to work """ + + if not self.confirm: + self.network_module.fail_json( + msg='Error: Confirm must be set to true for this module to work.') + + xml_str = CE_NC_XML_EXECUTE_REBOOT % str(self.save_config).lower() + self.netconf_set_action(xml_str) + + +def main(): + """ main """ + + argument_spec = dict( + confirm=dict(required=True, type='bool'), + save_config=dict(default=False, type='bool') + ) + + argument_spec.update(ce_argument_spec) + module = Reboot(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_NCCLIENT: + module.network_module.fail_json(msg='Error: The ncclient library is required.') + + changed = False + rebooted = False + + module.work() + + changed = True + rebooted = True + + results = dict() + results['changed'] = changed + results['rebooted'] = rebooted + + module.network_module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_rollback.py b/plugins/modules/ce_rollback.py new file mode 100644 index 0000000..0b9db5a --- /dev/null +++ b/plugins/modules/ce_rollback.py @@ -0,0 +1,450 @@ +#!/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 . +# +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_rollback +short_description: Set a checkpoint or rollback to a checkpoint on HUAWEI CloudEngine switches. +description: + - This module offers the ability to set a configuration checkpoint + file or rollback to a configuration checkpoint file on HUAWEI CloudEngine switches. +author: + - Li Yanfeng (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + commit_id: + description: + - Specifies the label of the configuration rollback point to which system configurations are + expected to roll back. + The value is an integer that the system generates automatically. + label: + description: + - Specifies a user label for a configuration rollback point. + The value is a string of 1 to 256 case-sensitive ASCII characters, spaces not supported. + The value must start with a letter and cannot be presented in a single hyphen (-). + filename: + description: + - Specifies a configuration file for configuration rollback. + The value is a string of 5 to 64 case-sensitive characters in the format of *.zip, *.cfg, or *.dat, + spaces not supported. + last: + description: + - Specifies the number of configuration rollback points. + The value is an integer that ranges from 1 to 80. + oldest: + description: + - Specifies the number of configuration rollback points. + The value is an integer that ranges from 1 to 80. + action: + description: + - The operation of configuration rollback. + required: true + choices: ['rollback','clear','set','display','commit'] +''' +EXAMPLES = ''' +- name: rollback module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + +- name: Ensure commit_id is exist, and specifies the label of the configuration rollback point to + which system configurations are expected to roll back. + ce_rollback: + commit_id: 1000000748 + action: rollback + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: sometimes + type: dict + sample: {"commit_id": "1000000748", "action": "rollback"} +existing: + description: k/v pairs of existing rollback + returned: sometimes + type: dict + sample: {"commitId": "1000000748", "userLabel": "abc"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["rollback configuration to file a.cfg", + "set configuration commit 1000000783 label ddd", + "clear configuration commit 1000000783 label", + "display configuration commit list"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"commitId": "1000000748", "userLabel": "abc"} +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, exec_command, run_commands +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ComplexList + + +class RollBack(object): + """ + Manages rolls back the system from the current configuration state to a historical configuration state. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + self.commands = list() + # module input info + self.commit_id = self.module.params['commit_id'] + self.label = self.module.params['label'] + self.filename = self.module.params['filename'] + self.last = self.module.params['last'] + self.oldest = self.module.params['oldest'] + self.action = self.module.params['action'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + # configuration rollback points info + self.rollback_info = None + self.init_module() + + def init_module(self): + """ init module """ + + required_if = [('action', 'set', ['commit_id', 'label']), ('action', 'commit', ['label'])] + mutually_exclusive = None + required_one_of = None + if self.action == "rollback": + required_one_of = [['commit_id', 'label', 'filename', 'last']] + elif self.action == "clear": + required_one_of = [['commit_id', 'oldest']] + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True, required_if=required_if, mutually_exclusive=mutually_exclusive, required_one_of=required_one_of) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + self.commands.append("return") + self.commands.append("mmi-mode enable") + + if self.action == "commit": + self.commands.append("sys") + + self.commands.append(command) + self.updates_cmd.append(command) + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + run_commands(self.module, commands) + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_rollback_dict(self): + """ get rollback attributes dict.""" + + rollback_info = dict() + rollback_info["RollBackInfos"] = list() + + flags = list() + exp = "commit list" + flags.append(exp) + cfg_info = self.get_config(flags) + if not cfg_info: + return rollback_info + + cfg_line = cfg_info.split("\n") + for cfg in cfg_line: + if re.findall(r'^\d', cfg): + pre_rollback_info = cfg.split() + rollback_info["RollBackInfos"].append(dict(commitId=pre_rollback_info[1], userLabel=pre_rollback_info[2])) + + return rollback_info + + def get_filename_type(self, filename): + """Gets the type of filename, such as cfg, zip, dat...""" + + if filename is None: + return None + if ' ' in filename: + self.module.fail_json( + msg='Error: Configuration file name include spaces.') + + iftype = None + + if filename.endswith('.cfg'): + iftype = 'cfg' + elif filename.endswith('.zip'): + iftype = 'zip' + elif filename.endswith('.dat'): + iftype = 'dat' + else: + return None + return iftype.lower() + + def set_config(self): + + if self.action == "rollback": + if self.commit_id: + cmd = "rollback configuration to commit-id %s" % self.commit_id + self.cli_add_command(cmd) + if self.label: + cmd = "rollback configuration to label %s" % self.label + self.cli_add_command(cmd) + if self.filename: + cmd = "rollback configuration to file %s" % self.filename + self.cli_add_command(cmd) + if self.last: + cmd = "rollback configuration last %s" % self.last + self.cli_add_command(cmd) + elif self.action == "set": + if self.commit_id and self.label: + cmd = "set configuration commit %s label %s" % (self.commit_id, self.label) + self.cli_add_command(cmd) + elif self.action == "clear": + if self.commit_id: + cmd = "clear configuration commit %s label" % self.commit_id + self.cli_add_command(cmd) + if self.oldest: + cmd = "clear configuration commit oldest %s" % self.oldest + self.cli_add_command(cmd) + elif self.action == "commit": + if self.label: + cmd = "commit label %s" % self.label + self.cli_add_command(cmd) + + elif self.action == "display": + self.rollback_info = self.get_rollback_dict() + if self.commands: + self.commands.append('return') + self.commands.append('undo mmi-mode enable') + self.cli_load_config(self.commands) + self.changed = True + + def check_params(self): + """Check all input params""" + + # commit_id check + rollback_info = self.rollback_info["RollBackInfos"] + if self.commit_id: + if not self.commit_id.isdigit(): + self.module.fail_json( + msg='Error: The parameter of commit_id is invalid.') + + info_bool = False + for info in rollback_info: + if info.get("commitId") == self.commit_id: + info_bool = True + if not info_bool: + self.module.fail_json( + msg='Error: The parameter of commit_id is not exist.') + + if self.action == "clear": + info_bool = False + for info in rollback_info: + if info.get("commitId") == self.commit_id: + if info.get("userLabel") == "-": + info_bool = True + if info_bool: + self.module.fail_json( + msg='Error: This commit_id does not have a label.') + + # filename check + if self.filename: + if not self.get_filename_type(self.filename): + self.module.fail_json( + msg='Error: Invalid file name or file name extension ( *.cfg, *.zip, *.dat ).') + # last check + if self.last: + if not self.last.isdigit(): + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not digit.') + if int(self.last) <= 0 or int(self.last) > 80: + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not in the range from 1 to 80.') + + # oldest check + if self.oldest: + if not self.oldest.isdigit(): + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not digit.') + if int(self.oldest) <= 0 or int(self.oldest) > 80: + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not in the range from 1 to 80.') + + # label check + if self.label: + if self.label[0].isdigit(): + self.module.fail_json( + msg='Error: Commit label which should not start with a number.') + if len(self.label.replace(' ', '')) == 1: + if self.label == '-': + self.module.fail_json( + msg='Error: Commit label which should not be "-"') + if len(self.label.replace(' ', '')) < 1 or len(self.label) > 256: + self.module.fail_json( + msg='Error: Label of configuration checkpoints is a string of 1 to 256 characters.') + + if self.action == "rollback": + info_bool = False + for info in rollback_info: + if info.get("userLabel") == self.label: + info_bool = True + if not info_bool: + self.module.fail_json( + msg='Error: The parameter of userLabel is not exist.') + + if self.action == "commit": + info_bool = False + for info in rollback_info: + if info.get("userLabel") == self.label: + info_bool = True + if info_bool: + self.module.fail_json( + msg='Error: The parameter of userLabel is existing.') + + if self.action == "set": + info_bool = False + for info in rollback_info: + if info.get("commitId") == self.commit_id: + if info.get("userLabel") != "-": + info_bool = True + if info_bool: + self.module.fail_json( + msg='Error: The userLabel of this commitid is present and can be reset after deletion.') + + def get_proposed(self): + """get proposed info""" + + if self.commit_id: + self.proposed["commit_id"] = self.commit_id + if self.label: + self.proposed["label"] = self.label + if self.filename: + self.proposed["filename"] = self.filename + if self.last: + self.proposed["last"] = self.last + if self.oldest: + self.proposed["oldest"] = self.oldest + + def get_existing(self): + """get existing info""" + if not self.rollback_info: + self.existing["RollBackInfos"] = None + else: + self.existing["RollBackInfos"] = self.rollback_info["RollBackInfos"] + + def get_end_state(self): + """get end state info""" + + rollback_info = self.get_rollback_dict() + if not rollback_info: + self.end_state["RollBackInfos"] = None + else: + self.end_state["RollBackInfos"] = rollback_info["RollBackInfos"] + + def work(self): + """worker""" + + self.rollback_info = self.get_rollback_dict() + self.check_params() + self.get_proposed() + + self.set_config() + + self.get_existing() + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + commit_id=dict(required=False), + label=dict(required=False, type='str'), + filename=dict(required=False, type='str'), + last=dict(required=False, type='str'), + oldest=dict(required=False, type='str'), + action=dict(required=False, type='str', choices=[ + 'rollback', 'clear', 'set', 'commit', 'display']), + ) + argument_spec.update(ce_argument_spec) + module = RollBack(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_sflow.py b/plugins/modules/ce_sflow.py new file mode 100644 index 0000000..fc5171a --- /dev/null +++ b/plugins/modules/ce_sflow.py @@ -0,0 +1,1164 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_sflow +short_description: Manages sFlow configuration on HUAWEI CloudEngine switches. +description: + - Configure Sampled Flow (sFlow) to monitor traffic on an interface in real time, + detect abnormal traffic, and locate the source of attack traffic, + ensuring stable running of the network. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + agent_ip: + description: + - Specifies the IPv4/IPv6 address of an sFlow agent. + source_ip: + description: + - Specifies the source IPv4/IPv6 address of sFlow packets. + collector_id: + description: + - Specifies the ID of an sFlow collector. This ID is used when you specify + the collector in subsequent sFlow configuration. + choices: ['1', '2'] + collector_ip: + description: + - Specifies the IPv4/IPv6 address of the sFlow collector. + collector_ip_vpn: + description: + - Specifies the name of a VPN instance. + The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + When double quotation marks are used around the string, spaces are allowed in the string. + The value C(_public_) is reserved and cannot be used as the VPN instance name. + collector_datagram_size: + description: + - Specifies the maximum length of sFlow packets sent from an sFlow agent to an sFlow collector. + The value is an integer, in bytes. It ranges from 1024 to 8100. The default value is 1400. + collector_udp_port: + description: + - Specifies the UDP destination port number of sFlow packets. + The value is an integer that ranges from 1 to 65535. The default value is 6343. + collector_meth: + description: + - Configures the device to send sFlow packets through service interfaces, + enhancing the sFlow packet forwarding capability. + The enhanced parameter is optional. No matter whether you configure the enhanced mode, + the switch determines to send sFlow packets through service cards or management port + based on the routing information on the collector. + When the value is meth, the device forwards sFlow packets at the control plane. + When the value is enhanced, the device forwards sFlow packets at the forwarding plane to + enhance the sFlow packet forwarding capacity. + choices: ['meth', 'enhanced'] + collector_description: + description: + - Specifies the description of an sFlow collector. + The value is a string of 1 to 255 case-sensitive characters without spaces. + sflow_interface: + description: + - Full name of interface for Flow Sampling or Counter. + It must be a physical interface, Eth-Trunk, or Layer 2 subinterface. + sample_collector: + description: + - Indicates the ID list of the collector. + sample_rate: + description: + - Specifies the flow sampling rate in the format 1/rate. + The value is an integer and ranges from 1 to 4294967295. The default value is 8192. + sample_length: + description: + - Specifies the maximum length of sampled packets. + The value is an integer and ranges from 18 to 512, in bytes. The default value is 128. + sample_direction: + description: + - Enables flow sampling in the inbound or outbound direction. + choices: ['inbound', 'outbound', 'both'] + counter_collector: + description: + - Indicates the ID list of the counter collector. + counter_interval: + description: + - Indicates the counter sampling interval. + The value is an integer that ranges from 10 to 4294967295, in seconds. The default value is 20. + export_route: + description: + - Configures the sFlow packets sent by the switch not to carry routing information. + choices: ['enable', 'disable'] + rate_limit: + description: + - Specifies the rate of sFlow packets sent from a card to the control plane. + The value is an integer that ranges from 100 to 1500, in pps. + type: str + rate_limit_slot: + description: + - Specifies the slot where the rate of output sFlow packets is limited. + If this parameter is not specified, the rate of sFlow packets sent from + all cards to the control plane is limited. + The value is an integer or a string of characters. + type: str + forward_enp_slot: + description: + - Enable the Embedded Network Processor (ENP) chip function. + The switch uses the ENP chip to perform sFlow sampling, + and the maximum sFlow sampling interval is 65535. + If you set the sampling interval to be larger than 65535, + the switch automatically restores it to 65535. + The value is an integer or 'all'. + type: str + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +--- + +- name: sflow module test + hosts: ce128 + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Configuring sFlow Agent + ce_sflow: + agent_ip: 6.6.6.6 + provider: '{{ cli }}' + + - name: Configuring sFlow Collector + ce_sflow: + collector_id: 1 + collector_ip: 7.7.7.7 + collector_ip_vpn: vpn1 + collector_description: Collector1 + provider: '{{ cli }}' + + - name: Configure flow sampling. + ce_sflow: + sflow_interface: 10GE2/0/2 + sample_collector: 1 + sample_direction: inbound + provider: '{{ cli }}' + + - name: Configure counter sampling. + ce_sflow: + sflow_interface: 10GE2/0/2 + counter_collector: 1 + counter_interval: 1000 + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"agent_ip": "6.6.6.6", "state": "present"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"agent": {}} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"agent": {"family": "ipv4", "ipv4Addr": "1.2.3.4", "ipv6Addr": null}} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["sflow agent ip 6.6.6.6"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec, check_ip_addr + +CE_NC_GET_SFLOW = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %s + + + + + + + + + %s + + + + + + + + + + + +""" + + +def is_config_exist(cmp_cfg, test_cfg): + """is configuration exist?""" + + if not cmp_cfg or not test_cfg: + return False + + return bool(test_cfg in cmp_cfg) + + +def is_valid_ip_vpn(vpname): + """check ip vpn""" + + if not vpname: + return False + + if vpname == "_public_": + return False + + if len(vpname) < 1 or len(vpname) > 31: + return False + + return True + + +def get_ip_version(address): + """get ip version fast""" + + if not address: + return None + + if address.count(':') >= 2 and address.count(":") <= 7: + return "ipv6" + elif address.count('.') == 3: + return "ipv4" + else: + return None + + +def get_interface_type(interface): + """get the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class Sflow(object): + """Manages sFlow""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.agent_ip = self.module.params['agent_ip'] + self.agent_version = None + self.source_ip = self.module.params['source_ip'] + self.source_version = None + self.export_route = self.module.params['export_route'] + self.rate_limit = self.module.params['rate_limit'] + self.rate_limit_slot = self.module.params['rate_limit_slot'] + self.forward_enp_slot = self.module.params['forward_enp_slot'] + self.collector_id = self.module.params['collector_id'] + self.collector_ip = self.module.params['collector_ip'] + self.collector_version = None + self.collector_ip_vpn = self.module.params['collector_ip_vpn'] + self.collector_datagram_size = self.module.params['collector_datagram_size'] + self.collector_udp_port = self.module.params['collector_udp_port'] + self.collector_meth = self.module.params['collector_meth'] + self.collector_description = self.module.params['collector_description'] + self.sflow_interface = self.module.params['sflow_interface'] + self.sample_collector = self.module.params['sample_collector'] or list() + self.sample_rate = self.module.params['sample_rate'] + self.sample_length = self.module.params['sample_length'] + self.sample_direction = self.module.params['sample_direction'] + self.counter_collector = self.module.params['counter_collector'] or list() + self.counter_interval = self.module.params['counter_interval'] + self.state = self.module.params['state'] + + # state + self.config = "" # current config + self.sflow_dict = dict() + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + required_together = [("collector_id", "collector_ip")] + self.module = AnsibleModule( + argument_spec=self.spec, required_together=required_together, supports_check_mode=True) + + def check_response(self, con_obj, xml_name): + """Check if response message is already succeed""" + + xml_str = con_obj.xml + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def netconf_set_config(self, xml_str, xml_name): + """netconf set config""" + + rcv_xml = set_nc_config(self.module, xml_str) + if "" not in rcv_xml: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_sflow_dict(self): + """ sflow config dict""" + + sflow_dict = dict(source=list(), agent=dict(), collector=list(), + sampling=dict(), counter=dict(), export=dict()) + conf_str = CE_NC_GET_SFLOW % ( + self.sflow_interface, self.sflow_interface) + + if not self.collector_meth: + conf_str = conf_str.replace("", "") + + rcv_xml = get_nc_config(self.module, conf_str) + + if "" in rcv_xml: + return sflow_dict + + xml_str = rcv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + + # get source info + srcs = root.findall("sflow/sources/source") + if srcs: + for src in srcs: + attrs = dict() + for attr in src: + if attr.tag in ["family", "ipv4Addr", "ipv6Addr"]: + attrs[attr.tag] = attr.text + sflow_dict["source"].append(attrs) + + # get agent info + agent = root.find("sflow/agents/agent") + if agent: + for attr in agent: + if attr.tag in ["family", "ipv4Addr", "ipv6Addr"]: + sflow_dict["agent"][attr.tag] = attr.text + + # get collector info + collectors = root.findall("sflow/collectors/collector") + if collectors: + for collector in collectors: + attrs = dict() + for attr in collector: + if attr.tag in ["collectorID", "family", "ipv4Addr", "ipv6Addr", + "vrfName", "datagramSize", "port", "description", "meth"]: + attrs[attr.tag] = attr.text + sflow_dict["collector"].append(attrs) + + # get sampling info + sample = root.find("sflow/samplings/sampling") + if sample: + for attr in sample: + if attr.tag in ["ifName", "collectorID", "direction", "length", "rate"]: + sflow_dict["sampling"][attr.tag] = attr.text + + # get counter info + counter = root.find("sflow/counters/counter") + if counter: + for attr in counter: + if attr.tag in ["ifName", "collectorID", "interval"]: + sflow_dict["counter"][attr.tag] = attr.text + + # get export info + export = root.find("sflow/exports/export") + if export: + for attr in export: + if attr.tag == "ExportRoute": + sflow_dict["export"][attr.tag] = attr.text + + return sflow_dict + + def config_agent(self): + """configures sFlow agent""" + + xml_str = '' + if not self.agent_ip: + return xml_str + + self.agent_version = get_ip_version(self.agent_ip) + if not self.agent_version: + self.module.fail_json(msg="Error: agent_ip is invalid.") + + if self.state == "present": + if self.agent_ip != self.sflow_dict["agent"].get("ipv4Addr") \ + and self.agent_ip != self.sflow_dict["agent"].get("ipv6Addr"): + xml_str += '' + xml_str += '%s' % self.agent_version + if self.agent_version == "ipv4": + xml_str += '%s' % self.agent_ip + self.updates_cmd.append("sflow agent ip %s" % self.agent_ip) + else: + xml_str += '%s' % self.agent_ip + self.updates_cmd.append("sflow agent ipv6 %s" % self.agent_ip) + xml_str += '' + + else: + if self.agent_ip == self.sflow_dict["agent"].get("ipv4Addr") \ + or self.agent_ip == self.sflow_dict["agent"].get("ipv6Addr"): + xml_str += '' + self.updates_cmd.append("undo sflow agent") + + return xml_str + + def config_source(self): + """configures the source IP address for sFlow packets""" + + xml_str = '' + if not self.source_ip: + return xml_str + + self.source_version = get_ip_version(self.source_ip) + if not self.source_version: + self.module.fail_json(msg="Error: source_ip is invalid.") + + src_dict = dict() + for src in self.sflow_dict["source"]: + if src.get("family") == self.source_version: + src_dict = src + break + + if self.state == "present": + if self.source_ip != src_dict.get("ipv4Addr") \ + and self.source_ip != src_dict.get("ipv6Addr"): + xml_str += '' + xml_str += '%s' % self.source_version + if self.source_version == "ipv4": + xml_str += '%s' % self.source_ip + self.updates_cmd.append("sflow source ip %s" % self.source_ip) + else: + xml_str += '%s' % self.source_ip + self.updates_cmd.append( + "sflow source ipv6 %s" % self.source_ip) + xml_str += '' + else: + if self.source_ip == src_dict.get("ipv4Addr"): + xml_str += 'ipv4' + self.updates_cmd.append("undo sflow source ip %s" % self.source_ip) + elif self.source_ip == src_dict.get("ipv6Addr"): + xml_str += 'ipv6' + self.updates_cmd.append("undo sflow source ipv6 %s" % self.source_ip) + + return xml_str + + def config_collector(self): + """creates an sFlow collector and sets or modifies optional parameters for the sFlow collector""" + + xml_str = '' + if not self.collector_id: + return xml_str + + if self.state == "present" and not self.collector_ip: + return xml_str + + if self.collector_ip: + self.collector_version = get_ip_version(self.collector_ip) + if not self.collector_version: + self.module.fail_json(msg="Error: collector_ip is invalid.") + + # get collector dict + exist_dict = dict() + for collector in self.sflow_dict["collector"]: + if collector.get("collectorID") == self.collector_id: + exist_dict = collector + break + + change = False + if self.state == "present": + if not exist_dict: + change = True + elif self.collector_version != exist_dict.get("family"): + change = True + elif self.collector_version == "ipv4" and self.collector_ip != exist_dict.get("ipv4Addr"): + change = True + elif self.collector_version == "ipv6" and self.collector_ip != exist_dict.get("ipv6Addr"): + change = True + elif self.collector_ip_vpn and self.collector_ip_vpn != exist_dict.get("vrfName"): + change = True + elif not self.collector_ip_vpn and exist_dict.get("vrfName") != "_public_": + change = True + elif self.collector_udp_port and self.collector_udp_port != exist_dict.get("port"): + change = True + elif not self.collector_udp_port and exist_dict.get("port") != "6343": + change = True + elif self.collector_datagram_size and self.collector_datagram_size != exist_dict.get("datagramSize"): + change = True + elif not self.collector_datagram_size and exist_dict.get("datagramSize") != "1400": + change = True + elif self.collector_meth and self.collector_meth != exist_dict.get("meth"): + change = True + elif not self.collector_meth and exist_dict.get("meth") and exist_dict.get("meth") != "meth": + change = True + elif self.collector_description and self.collector_description != exist_dict.get("description"): + change = True + elif not self.collector_description and exist_dict.get("description"): + change = True + else: + pass + else: # absent + # collector not exist + if not exist_dict: + return xml_str + if self.collector_version and self.collector_version != exist_dict.get("family"): + return xml_str + if self.collector_version == "ipv4" and self.collector_ip != exist_dict.get("ipv4Addr"): + return xml_str + if self.collector_version == "ipv6" and self.collector_ip != exist_dict.get("ipv6Addr"): + return xml_str + if self.collector_ip_vpn and self.collector_ip_vpn != exist_dict.get("vrfName"): + return xml_str + if self.collector_udp_port and self.collector_udp_port != exist_dict.get("port"): + return xml_str + if self.collector_datagram_size and self.collector_datagram_size != exist_dict.get("datagramSize"): + return xml_str + if self.collector_meth and self.collector_meth != exist_dict.get("meth"): + return xml_str + if self.collector_description and self.collector_description != exist_dict.get("description"): + return xml_str + change = True + + if not change: + return xml_str + + # update or delete + if self.state == "absent": + xml_str += '%s' % self.collector_id + self.updates_cmd.append("undo collector %s" % self.collector_id) + else: + xml_str += '%s' % self.collector_id + cmd = "sflow collector %s" % self.collector_id + xml_str += '%s' % self.collector_version + if self.collector_version == "ipv4": + cmd += " ip %s" % self.collector_ip + xml_str += '%s' % self.collector_ip + else: + cmd += " ipv6 %s" % self.collector_ip + xml_str += '%s' % self.collector_ip + if self.collector_ip_vpn: + cmd += " vpn-instance %s" % self.collector_ip_vpn + xml_str += '%s' % self.collector_ip_vpn + if self.collector_datagram_size: + cmd += " length %s" % self.collector_datagram_size + xml_str += '%s' % self.collector_datagram_size + if self.collector_udp_port: + cmd += " udp-port %s" % self.collector_udp_port + xml_str += '%s' % self.collector_udp_port + if self.collector_description: + cmd += " description %s" % self.collector_description + xml_str += '%s' % self.collector_description + else: + xml_str += '' + if self.collector_meth: + if self.collector_meth == "enhanced": + cmd += " enhanced" + xml_str += '%s' % self.collector_meth + self.updates_cmd.append(cmd) + + xml_str += "" + + return xml_str + + def config_sampling(self): + """configure sflow sampling on an interface""" + + xml_str = '' + if not self.sflow_interface: + return xml_str + + if not self.sflow_dict["sampling"] and self.state == "absent": + return xml_str + + self.updates_cmd.append("interface %s" % self.sflow_interface) + if self.state == "present": + xml_str += '%s' % self.sflow_interface + else: + xml_str += '%s' % self.sflow_interface + + # sample_collector + if self.sample_collector: + if self.sflow_dict["sampling"].get("collectorID") \ + and self.sflow_dict["sampling"].get("collectorID") != "invalid": + existing = self.sflow_dict["sampling"].get("collectorID").split(',') + else: + existing = list() + + if self.state == "present": + diff = list(set(self.sample_collector) - set(existing)) + if diff: + self.updates_cmd.append( + "sflow sampling collector %s" % ' '.join(diff)) + new_set = list(self.sample_collector + existing) + xml_str += '%s' % ','.join(list(set(new_set))) + else: + same = list(set(self.sample_collector) & set(existing)) + if same: + self.updates_cmd.append( + "undo sflow sampling collector %s" % ' '.join(same)) + xml_str += '%s' % ','.join(list(set(same))) + + # sample_rate + if self.sample_rate: + exist = bool(self.sample_rate == self.sflow_dict["sampling"].get("rate")) + if self.state == "present" and not exist: + self.updates_cmd.append( + "sflow sampling rate %s" % self.sample_rate) + xml_str += '%s' % self.sample_rate + elif self.state == "absent" and exist: + self.updates_cmd.append( + "undo sflow sampling rate %s" % self.sample_rate) + xml_str += '%s' % self.sample_rate + + # sample_length + if self.sample_length: + exist = bool(self.sample_length == self.sflow_dict["sampling"].get("length")) + if self.state == "present" and not exist: + self.updates_cmd.append( + "sflow sampling length %s" % self.sample_length) + xml_str += '%s' % self.sample_length + elif self.state == "absent" and exist: + self.updates_cmd.append( + "undo sflow sampling length %s" % self.sample_length) + xml_str += '%s' % self.sample_length + + # sample_direction + if self.sample_direction: + direction = list() + if self.sample_direction == "both": + direction = ["inbound", "outbound"] + else: + direction.append(self.sample_direction) + existing = list() + if self.sflow_dict["sampling"].get("direction"): + if self.sflow_dict["sampling"].get("direction") == "both": + existing = ["inbound", "outbound"] + else: + existing.append( + self.sflow_dict["sampling"].get("direction")) + + if self.state == "present": + diff = list(set(direction) - set(existing)) + if diff: + new_set = list(set(direction + existing)) + self.updates_cmd.append( + "sflow sampling %s" % ' '.join(diff)) + if len(new_set) > 1: + new_dir = "both" + else: + new_dir = new_set[0] + xml_str += '%s' % new_dir + else: + same = list(set(existing) & set(direction)) + if same: + self.updates_cmd.append("undo sflow sampling %s" % ' '.join(same)) + if len(same) > 1: + del_dir = "both" + else: + del_dir = same[0] + xml_str += '%s' % del_dir + + if xml_str.endswith(""): + self.updates_cmd.pop() + return "" + + xml_str += '' + + return xml_str + + def config_counter(self): + """configures sflow counter on an interface""" + + xml_str = '' + if not self.sflow_interface: + return xml_str + + if not self.sflow_dict["counter"] and self.state == "absent": + return xml_str + + self.updates_cmd.append("interface %s" % self.sflow_interface) + if self.state == "present": + xml_str += '%s' % self.sflow_interface + else: + xml_str += '%s' % self.sflow_interface + + # counter_collector + if self.counter_collector: + if self.sflow_dict["counter"].get("collectorID") \ + and self.sflow_dict["counter"].get("collectorID") != "invalid": + existing = self.sflow_dict["counter"].get("collectorID").split(',') + else: + existing = list() + + if self.state == "present": + diff = list(set(self.counter_collector) - set(existing)) + if diff: + self.updates_cmd.append("sflow counter collector %s" % ' '.join(diff)) + new_set = list(self.counter_collector + existing) + xml_str += '%s' % ','.join(list(set(new_set))) + else: + same = list(set(self.counter_collector) & set(existing)) + if same: + self.updates_cmd.append( + "undo sflow counter collector %s" % ' '.join(same)) + xml_str += '%s' % ','.join(list(set(same))) + + # counter_interval + if self.counter_interval: + exist = bool(self.counter_interval == self.sflow_dict["counter"].get("interval")) + if self.state == "present" and not exist: + self.updates_cmd.append( + "sflow counter interval %s" % self.counter_interval) + xml_str += '%s' % self.counter_interval + elif self.state == "absent" and exist: + self.updates_cmd.append( + "undo sflow counter interval %s" % self.counter_interval) + xml_str += '%s' % self.counter_interval + + if xml_str.endswith(""): + self.updates_cmd.pop() + return "" + + xml_str += '' + + return xml_str + + def config_export(self): + """configure sflow export""" + + xml_str = '' + if not self.export_route: + return xml_str + + if self.export_route == "enable": + if self.sflow_dict["export"] and self.sflow_dict["export"].get("ExportRoute") == "disable": + xml_str = 'disable' + self.updates_cmd.append("undo sflow export extended-route-data disable") + else: # disable + if not self.sflow_dict["export"] or self.sflow_dict["export"].get("ExportRoute") != "disable": + xml_str = 'disable' + self.updates_cmd.append("sflow export extended-route-data disable") + + return xml_str + + def netconf_load_config(self, xml_str): + """load sflow config by netconf""" + + if not xml_str: + return + + xml_cfg = """ + + + %s + + """ % xml_str + + self.netconf_set_config(xml_cfg, "SET_SFLOW") + self.changed = True + + def check_params(self): + """Check all input params""" + + # check agent_ip + if self.agent_ip: + self.agent_ip = self.agent_ip.upper() + if not check_ip_addr(self.agent_ip): + self.module.fail_json(msg="Error: agent_ip is invalid.") + + # check source_ip + if self.source_ip: + self.source_ip = self.source_ip.upper() + if not check_ip_addr(self.source_ip): + self.module.fail_json(msg="Error: source_ip is invalid.") + + # check collector + if self.collector_id: + # check collector_ip and collector_ip_vpn + if self.collector_ip: + self.collector_ip = self.collector_ip.upper() + if not check_ip_addr(self.collector_ip): + self.module.fail_json( + msg="Error: collector_ip is invalid.") + if self.collector_ip_vpn and not is_valid_ip_vpn(self.collector_ip_vpn): + self.module.fail_json( + msg="Error: collector_ip_vpn is invalid.") + + # check collector_datagram_size ranges from 1024 to 8100 + if self.collector_datagram_size: + if not self.collector_datagram_size.isdigit(): + self.module.fail_json( + msg="Error: collector_datagram_size is not digit.") + if int(self.collector_datagram_size) < 1024 or int(self.collector_datagram_size) > 8100: + self.module.fail_json( + msg="Error: collector_datagram_size is not ranges from 1024 to 8100.") + + # check collector_udp_port ranges from 1 to 65535 + if self.collector_udp_port: + if not self.collector_udp_port.isdigit(): + self.module.fail_json( + msg="Error: collector_udp_port is not digit.") + if int(self.collector_udp_port) < 1 or int(self.collector_udp_port) > 65535: + self.module.fail_json( + msg="Error: collector_udp_port is not ranges from 1 to 65535.") + + # check collector_description 1 to 255 case-sensitive characters + if self.collector_description: + if self.collector_description.count(" "): + self.module.fail_json( + msg="Error: collector_description should without spaces.") + if len(self.collector_description) < 1 or len(self.collector_description) > 255: + self.module.fail_json( + msg="Error: collector_description is not ranges from 1 to 255.") + + # check sflow_interface + if self.sflow_interface: + intf_type = get_interface_type(self.sflow_interface) + if not intf_type: + self.module.fail_json(msg="Error: intf_type is invalid.") + if intf_type not in ['ge', '10ge', '25ge', '4x10ge', '40ge', '100ge', 'eth-trunk']: + self.module.fail_json( + msg="Error: interface %s is not support sFlow." % self.sflow_interface) + + # check sample_collector + if self.sample_collector: + self.sample_collector.sort() + if self.sample_collector not in [["1"], ["2"], ["1", "2"]]: + self.module.fail_json( + msg="Error: sample_collector is invalid.") + + # check sample_rate ranges from 1 to 4294967295 + if self.sample_rate: + if not self.sample_rate.isdigit(): + self.module.fail_json( + msg="Error: sample_rate is not digit.") + if int(self.sample_rate) < 1 or int(self.sample_rate) > 4294967295: + self.module.fail_json( + msg="Error: sample_rate is not ranges from 1 to 4294967295.") + + # check sample_length ranges from 18 to 512 + if self.sample_length: + if not self.sample_length.isdigit(): + self.module.fail_json( + msg="Error: sample_rate is not digit.") + if int(self.sample_length) < 18 or int(self.sample_length) > 512: + self.module.fail_json( + msg="Error: sample_length is not ranges from 18 to 512.") + + # check counter_collector + if self.counter_collector: + self.counter_collector.sort() + if self.counter_collector not in [["1"], ["2"], ["1", "2"]]: + self.module.fail_json( + msg="Error: counter_collector is invalid.") + + # counter_interval ranges from 10 to 4294967295 + if self.counter_interval: + if not self.counter_interval.isdigit(): + self.module.fail_json( + msg="Error: counter_interval is not digit.") + if int(self.counter_interval) < 10 or int(self.counter_interval) > 4294967295: + self.module.fail_json( + msg="Error: sample_length is not ranges from 10 to 4294967295.") + + if self.rate_limit or self.rate_limit_slot or self.forward_enp_slot: + self.module.fail_json(msg="Error: The following parameters cannot be configured" + "because XML mode is not supported:rate_limit,rate_limit_slot,forward_enp_slot.") + + def get_proposed(self): + """get proposed info""" + + # base config + if self.agent_ip: + self.proposed["agent_ip"] = self.agent_ip + if self.source_ip: + self.proposed["source_ip"] = self.source_ip + if self.export_route: + self.proposed["export_route"] = self.export_route + if self.rate_limit: + self.proposed["rate_limit"] = self.rate_limit + self.proposed["rate_limit_slot"] = self.rate_limit_slot + if self.forward_enp_slot: + self.proposed["forward_enp_slot"] = self.forward_enp_slot + if self.collector_id: + self.proposed["collector_id"] = self.collector_id + if self.collector_ip: + self.proposed["collector_ip"] = self.collector_ip + self.proposed["collector_ip_vpn"] = self.collector_ip_vpn + if self.collector_datagram_size: + self.proposed[ + "collector_datagram_size"] = self.collector_datagram_size + if self.collector_udp_port: + self.proposed["collector_udp_port"] = self.collector_udp_port + if self.collector_meth: + self.proposed["collector_meth"] = self.collector_meth + if self.collector_description: + self.proposed[ + "collector_description"] = self.collector_description + + # sample and counter config + if self.sflow_interface: + self.proposed["sflow_interface"] = self.sflow_interface + if self.sample_collector: + self.proposed["sample_collector"] = self.sample_collector + if self.sample_rate: + self.proposed["sample_rate"] = self.sample_rate + if self.sample_length: + self.proposed["sample_length"] = self.sample_length + if self.sample_direction: + self.proposed["sample_direction"] = self.sample_direction + if self.counter_collector: + self.proposed["counter_collector"] = self.counter_collector + if self.counter_interval: + self.proposed["counter_interval"] = self.counter_interval + + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.sflow_dict: + return + + if self.agent_ip: + self.existing["agent"] = self.sflow_dict["agent"] + if self.source_ip: + self.existing["source"] = self.sflow_dict["source"] + if self.collector_id: + self.existing["collector"] = self.sflow_dict["collector"] + if self.export_route: + self.existing["export"] = self.sflow_dict["export"] + + if self.sflow_interface: + self.existing["sampling"] = self.sflow_dict["sampling"] + self.existing["counter"] = self.sflow_dict["counter"] + + def get_end_state(self): + """get end state info""" + + sflow_dict = self.get_sflow_dict() + if not sflow_dict: + return + + if self.agent_ip: + self.end_state["agent"] = sflow_dict["agent"] + if self.source_ip: + self.end_state["source"] = sflow_dict["source"] + if self.collector_id: + self.end_state["collector"] = sflow_dict["collector"] + if self.export_route: + self.end_state["export"] = sflow_dict["export"] + + if self.sflow_interface: + self.end_state["sampling"] = sflow_dict["sampling"] + self.end_state["counter"] = sflow_dict["counter"] + if self.existing == self.end_state: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.sflow_dict = self.get_sflow_dict() + self.get_existing() + self.get_proposed() + + # deal present or absent + xml_str = '' + if self.export_route: + xml_str += self.config_export() + if self.agent_ip: + xml_str += self.config_agent() + if self.source_ip: + xml_str += self.config_source() + + if self.state == "present": + if self.collector_id and self.collector_ip: + xml_str += self.config_collector() + if self.sflow_interface: + xml_str += self.config_sampling() + xml_str += self.config_counter() + else: + if self.sflow_interface: + xml_str += self.config_sampling() + xml_str += self.config_counter() + if self.collector_id: + xml_str += self.config_collector() + + if xml_str: + self.netconf_load_config(xml_str) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + agent_ip=dict(required=False, type='str'), + source_ip=dict(required=False, type='str'), + export_route=dict(required=False, type='str', + choices=['enable', 'disable']), + rate_limit=dict(required=False, removed_in_version=2.13, type='str'), + rate_limit_slot=dict(required=False, removed_in_version=2.13, type='str'), + forward_enp_slot=dict(required=False, removed_in_version=2.13, type='str'), + collector_id=dict(required=False, type='str', choices=['1', '2']), + collector_ip=dict(required=False, type='str'), + collector_ip_vpn=dict(required=False, type='str'), + collector_datagram_size=dict(required=False, type='str'), + collector_udp_port=dict(required=False, type='str'), + collector_meth=dict(required=False, type='str', + choices=['meth', 'enhanced']), + collector_description=dict(required=False, type='str'), + sflow_interface=dict(required=False, type='str'), + sample_collector=dict(required=False, type='list'), + sample_rate=dict(required=False, type='str'), + sample_length=dict(required=False, type='str'), + sample_direction=dict(required=False, type='str', + choices=['inbound', 'outbound', 'both']), + counter_collector=dict(required=False, type='list'), + counter_interval=dict(required=False, type='str'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = Sflow(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_snmp_community.py b/plugins/modules/ce_snmp_community.py new file mode 100644 index 0000000..0ee53f2 --- /dev/null +++ b/plugins/modules/ce_snmp_community.py @@ -0,0 +1,976 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_snmp_community +short_description: Manages SNMP community configuration on HUAWEI CloudEngine switches. +description: + - Manages SNMP community configuration on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + acl_number: + description: + - Access control list number. + community_name: + description: + - Unique name to identify the community. + access_right: + description: + - Access right read or write. + choices: ['read','write'] + community_mib_view: + description: + - Mib view name. + group_name: + description: + - Unique name to identify the SNMPv3 group. + security_level: + description: + - Security level indicating whether to use authentication and encryption. + choices: ['noAuthNoPriv', 'authentication', 'privacy'] + read_view: + description: + - Mib view name for read. + write_view: + description: + - Mib view name for write. + notify_view: + description: + - Mib view name for notification. + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' + +- name: CloudEngine snmp community test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config SNMP community" + ce_snmp_community: + state: present + community_name: Wdz123456789 + access_right: write + provider: "{{ cli }}" + + - name: "Undo SNMP community" + ce_snmp_community: + state: absent + community_name: Wdz123456789 + access_right: write + provider: "{{ cli }}" + + - name: "Config SNMP group" + ce_snmp_community: + state: present + group_name: wdz_group + security_level: noAuthNoPriv + acl_number: 2000 + provider: "{{ cli }}" + + - name: "Undo SNMP group" + ce_snmp_community: + state: absent + group_name: wdz_group + security_level: noAuthNoPriv + acl_number: 2000 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"acl_number": "2000", "group_name": "wdz_group", + "security_level": "noAuthNoPriv", "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"snmp v3 group": {"snmp_group": ["wdz_group", "noAuthNoPriv", "2000"]}} +updates: + description: command sent to the device + returned: always + type: list + sample: ["snmp-agent group v3 wdz_group noauthentication acl 2000"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +# get snmp community +CE_GET_SNMP_COMMUNITY_HEADER = """ + + + + + + +""" +CE_GET_SNMP_COMMUNITY_TAIL = """ + + + + +""" +# merge snmp community +CE_MERGE_SNMP_COMMUNITY_HEADER = """ + + + + + %s + %s +""" +CE_MERGE_SNMP_COMMUNITY_TAIL = """ + + + + +""" +# create snmp community +CE_CREATE_SNMP_COMMUNITY_HEADER = """ + + + + + %s + %s +""" +CE_CREATE_SNMP_COMMUNITY_TAIL = """ + + + + +""" +# delete snmp community +CE_DELETE_SNMP_COMMUNITY_HEADER = """ + + + + + %s + %s +""" +CE_DELETE_SNMP_COMMUNITY_TAIL = """ + + + + +""" + +# get snmp v3 group +CE_GET_SNMP_V3_GROUP_HEADER = """ + + + + + + +""" +CE_GET_SNMP_V3_GROUP_TAIL = """ + + + + +""" +# merge snmp v3 group +CE_MERGE_SNMP_V3_GROUP_HEADER = """ + + + + + %s + %s +""" +CE_MERGE_SNMP_V3_GROUP_TAIL = """ + + + + +""" +# create snmp v3 group +CE_CREATE_SNMP_V3_GROUP_HEADER = """ + + + + + %s + %s +""" +CE_CREATE_SNMP_V3_GROUP_TAIL = """ + + + + +""" +# delete snmp v3 group +CE_DELETE_SNMP_V3_GROUP_HEADER = """ + + + + + %s + %s +""" +CE_DELETE_SNMP_V3_GROUP_TAIL = """ + + + + +""" + + +class SnmpCommunity(object): + """ Manages SNMP community configuration """ + + def netconf_get_config(self, **kwargs): + """ Get configure through netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ Set configure through netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = set_nc_config(module, conf_str) + + return xml_str + + def check_snmp_community_args(self, **kwargs): + """ Check snmp community args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + result["community_info"] = [] + state = module.params['state'] + community_name = module.params['community_name'] + access_right = module.params['access_right'] + acl_number = module.params['acl_number'] + community_mib_view = module.params['community_mib_view'] + + if community_name and access_right: + if len(community_name) > 32 or len(community_name) == 0: + module.fail_json( + msg='Error: The len of community_name %s is out of [1 - 32].' % community_name) + + if acl_number: + if acl_number.isdigit(): + if int(acl_number) > 2999 or int(acl_number) < 2000: + module.fail_json( + msg='Error: The value of acl_number %s is out of [2000 - 2999].' % acl_number) + else: + if not acl_number[0].isalpha() or len(acl_number) > 32 or len(acl_number) < 1: + module.fail_json( + msg='Error: The len of acl_number %s is out of [1 - 32] or is invalid.' % acl_number) + + if community_mib_view: + if len(community_mib_view) > 32 or len(community_mib_view) == 0: + module.fail_json( + msg='Error: The len of community_mib_view %s is out of [1 - 32].' % community_mib_view) + + conf_str = CE_GET_SNMP_COMMUNITY_HEADER + if acl_number: + conf_str += "" + if community_mib_view: + conf_str += "" + + conf_str += CE_GET_SNMP_COMMUNITY_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + community_info = root.findall("snmp/communitys/community") + if community_info: + for tmp in community_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["communityName", "accessRight", "aclNumber", "mibViewName"]: + tmp_dict[site.tag] = site.text + + result["community_info"].append(tmp_dict) + + if result["community_info"]: + community_name_list = list() + for tmp in result["community_info"]: + if "communityName" in tmp.keys(): + community_name_list.append(tmp["communityName"]) + + if community_name not in community_name_list: + need_cfg = True + else: + need_cfg_bool = True + + for tmp in result["community_info"]: + if tmp["communityName"] == community_name: + + cfg_bool_list = list() + + if access_right: + if "accessRight" in tmp.keys(): + need_cfg_access = False + if tmp["accessRight"] != access_right: + need_cfg_access = True + else: + need_cfg_access = True + + cfg_bool_list.append(need_cfg_access) + + if acl_number: + if "aclNumber" in tmp.keys(): + need_cfg_acl = False + if tmp["aclNumber"] != acl_number: + need_cfg_acl = True + else: + need_cfg_acl = True + + cfg_bool_list.append(need_cfg_acl) + + if community_mib_view: + if "mibViewName" in tmp.keys(): + need_cfg_mib = False + if tmp["mibViewName"] != community_mib_view: + need_cfg_mib = True + else: + need_cfg_mib = True + cfg_bool_list.append(need_cfg_mib) + + if True not in cfg_bool_list: + need_cfg_bool = False + + if state == "present": + if not need_cfg_bool: + need_cfg = False + else: + need_cfg = True + else: + if not need_cfg_bool: + need_cfg = True + else: + need_cfg = False + + result["need_cfg"] = need_cfg + return result + + def check_snmp_v3_group_args(self, **kwargs): + """ Check snmp v3 group args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + result["group_info"] = [] + state = module.params['state'] + group_name = module.params['group_name'] + security_level = module.params['security_level'] + acl_number = module.params['acl_number'] + read_view = module.params['read_view'] + write_view = module.params['write_view'] + notify_view = module.params['notify_view'] + + community_name = module.params['community_name'] + access_right = module.params['access_right'] + + if group_name and security_level: + + if community_name and access_right: + module.fail_json( + msg='Error: Community is used for v1/v2c, group_name is used for v3, do not ' + 'input at the same time.') + + if len(group_name) > 32 or len(group_name) == 0: + module.fail_json( + msg='Error: The len of group_name %s is out of [1 - 32].' % group_name) + + if acl_number: + if acl_number.isdigit(): + if int(acl_number) > 2999 or int(acl_number) < 2000: + module.fail_json( + msg='Error: The value of acl_number %s is out of [2000 - 2999].' % acl_number) + else: + if not acl_number[0].isalpha() or len(acl_number) > 32 or len(acl_number) < 1: + module.fail_json( + msg='Error: The len of acl_number %s is out of [1 - 32] or is invalid.' % acl_number) + + if read_view: + if len(read_view) > 32 or len(read_view) < 1: + module.fail_json( + msg='Error: The len of read_view %s is out of [1 - 32].' % read_view) + + if write_view: + if len(write_view) > 32 or len(write_view) < 1: + module.fail_json( + msg='Error: The len of write_view %s is out of [1 - 32].' % write_view) + + if notify_view: + if len(notify_view) > 32 or len(notify_view) < 1: + module.fail_json( + msg='Error: The len of notify_view %s is out of [1 - 32].' % notify_view) + + conf_str = CE_GET_SNMP_V3_GROUP_HEADER + if acl_number: + conf_str += "" + if read_view: + conf_str += "" + if write_view: + conf_str += "" + if notify_view: + conf_str += "" + + conf_str += CE_GET_SNMP_V3_GROUP_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + group_info = root.findall("snmp/snmpv3Groups/snmpv3Group") + if group_info: + for tmp in group_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["groupName", "securityLevel", "readViewName", "writeViewName", + "notifyViewName", "aclNumber"]: + tmp_dict[site.tag] = site.text + + result["group_info"].append(tmp_dict) + + if result["group_info"]: + group_name_list = list() + + for tmp in result["group_info"]: + if "groupName" in tmp.keys(): + group_name_list.append(tmp["groupName"]) + if group_name not in group_name_list: + if state == "present": + need_cfg = True + else: + need_cfg = False + else: + need_cfg_bool = True + for tmp in result["group_info"]: + if tmp["groupName"] == group_name: + + cfg_bool_list = list() + + if security_level: + if "securityLevel" in tmp.keys(): + need_cfg_group = False + if tmp["securityLevel"] != security_level: + need_cfg_group = True + else: + need_cfg_group = True + + cfg_bool_list.append(need_cfg_group) + + if acl_number: + if "aclNumber" in tmp.keys(): + need_cfg_acl = False + if tmp["aclNumber"] != acl_number: + need_cfg_acl = True + else: + need_cfg_acl = True + + cfg_bool_list.append(need_cfg_acl) + + if read_view: + if "readViewName" in tmp.keys(): + need_cfg_read = False + if tmp["readViewName"] != read_view: + need_cfg_read = True + else: + need_cfg_read = True + cfg_bool_list.append(need_cfg_read) + + if write_view: + if "writeViewName" in tmp.keys(): + need_cfg_write = False + if tmp["writeViewName"] != write_view: + need_cfg_write = True + else: + need_cfg_write = True + cfg_bool_list.append(need_cfg_write) + + if notify_view: + if "notifyViewName" in tmp.keys(): + need_cfg_notify = False + if tmp["notifyViewName"] != notify_view: + need_cfg_notify = True + else: + need_cfg_notify = True + cfg_bool_list.append(need_cfg_notify) + + if True not in cfg_bool_list: + need_cfg_bool = False + + if state == "present": + if not need_cfg_bool: + need_cfg = False + else: + need_cfg = True + else: + if not need_cfg_bool: + need_cfg = True + else: + need_cfg = False + + result["need_cfg"] = need_cfg + return result + + def merge_snmp_community(self, **kwargs): + """ Merge snmp community operation """ + + module = kwargs["module"] + community_name = module.params['community_name'] + access_right = module.params['access_right'] + acl_number = module.params['acl_number'] + community_mib_view = module.params['community_mib_view'] + + conf_str = CE_MERGE_SNMP_COMMUNITY_HEADER % ( + community_name, access_right) + if acl_number: + conf_str += "%s" % acl_number + if community_mib_view: + conf_str += "%s" % community_mib_view + + conf_str += CE_MERGE_SNMP_COMMUNITY_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge snmp community failed.') + + community_safe_name = "******" + + cmd = "snmp-agent community %s %s" % (access_right, community_safe_name) + + if acl_number: + cmd += " acl %s" % acl_number + if community_mib_view: + cmd += " mib-view %s" % community_mib_view + + return cmd + + def create_snmp_community(self, **kwargs): + """ Create snmp community operation """ + + module = kwargs["module"] + community_name = module.params['community_name'] + access_right = module.params['access_right'] + acl_number = module.params['acl_number'] + community_mib_view = module.params['community_mib_view'] + + conf_str = CE_CREATE_SNMP_COMMUNITY_HEADER % ( + community_name, access_right) + if acl_number: + conf_str += "%s" % acl_number + if community_mib_view: + conf_str += "%s" % community_mib_view + + conf_str += CE_CREATE_SNMP_COMMUNITY_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create snmp community failed.') + + community_safe_name = "******" + + cmd = "snmp-agent community %s %s" % (access_right, community_safe_name) + + if acl_number: + cmd += " acl %s" % acl_number + if community_mib_view: + cmd += " mib-view %s" % community_mib_view + + return cmd + + def delete_snmp_community(self, **kwargs): + """ Delete snmp community operation """ + + module = kwargs["module"] + community_name = module.params['community_name'] + access_right = module.params['access_right'] + acl_number = module.params['acl_number'] + community_mib_view = module.params['community_mib_view'] + + conf_str = CE_DELETE_SNMP_COMMUNITY_HEADER % ( + community_name, access_right) + if acl_number: + conf_str += "%s" % acl_number + if community_mib_view: + conf_str += "%s" % community_mib_view + + conf_str += CE_DELETE_SNMP_COMMUNITY_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create snmp community failed.') + + community_safe_name = "******" + cmd = "undo snmp-agent community %s %s" % ( + access_right, community_safe_name) + + return cmd + + def merge_snmp_v3_group(self, **kwargs): + """ Merge snmp v3 group operation """ + + module = kwargs["module"] + group_name = module.params['group_name'] + security_level = module.params['security_level'] + acl_number = module.params['acl_number'] + read_view = module.params['read_view'] + write_view = module.params['write_view'] + notify_view = module.params['notify_view'] + + conf_str = CE_MERGE_SNMP_V3_GROUP_HEADER % (group_name, security_level) + if acl_number: + conf_str += "%s" % acl_number + if read_view: + conf_str += "%s" % read_view + if write_view: + conf_str += "%s" % write_view + if notify_view: + conf_str += "%s" % notify_view + conf_str += CE_MERGE_SNMP_V3_GROUP_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge snmp v3 group failed.') + + if security_level == "noAuthNoPriv": + security_level_cli = "noauthentication" + elif security_level == "authentication": + security_level_cli = "authentication" + elif security_level == "privacy": + security_level_cli = "privacy" + + cmd = "snmp-agent group v3 %s %s" % (group_name, security_level_cli) + + if read_view: + cmd += " read-view %s" % read_view + if write_view: + cmd += " write-view %s" % write_view + if notify_view: + cmd += " notify-view %s" % notify_view + if acl_number: + cmd += " acl %s" % acl_number + + return cmd + + def create_snmp_v3_group(self, **kwargs): + """ Create snmp v3 group operation """ + + module = kwargs["module"] + group_name = module.params['group_name'] + security_level = module.params['security_level'] + acl_number = module.params['acl_number'] + read_view = module.params['read_view'] + write_view = module.params['write_view'] + notify_view = module.params['notify_view'] + + conf_str = CE_CREATE_SNMP_V3_GROUP_HEADER % ( + group_name, security_level) + if acl_number: + conf_str += "%s" % acl_number + if read_view: + conf_str += "%s" % read_view + if write_view: + conf_str += "%s" % write_view + if notify_view: + conf_str += "%s" % notify_view + conf_str += CE_CREATE_SNMP_V3_GROUP_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create snmp v3 group failed.') + + if security_level == "noAuthNoPriv": + security_level_cli = "noauthentication" + elif security_level == "authentication": + security_level_cli = "authentication" + elif security_level == "privacy": + security_level_cli = "privacy" + + cmd = "snmp-agent group v3 %s %s" % (group_name, security_level_cli) + + if read_view: + cmd += " read-view %s" % read_view + if write_view: + cmd += " write-view %s" % write_view + if notify_view: + cmd += " notify-view %s" % notify_view + if acl_number: + cmd += " acl %s" % acl_number + + return cmd + + def delete_snmp_v3_group(self, **kwargs): + """ Delete snmp v3 group operation """ + + module = kwargs["module"] + group_name = module.params['group_name'] + security_level = module.params['security_level'] + acl_number = module.params['acl_number'] + read_view = module.params['read_view'] + write_view = module.params['write_view'] + notify_view = module.params['notify_view'] + + conf_str = CE_DELETE_SNMP_V3_GROUP_HEADER % ( + group_name, security_level) + if acl_number: + conf_str += "%s" % acl_number + if read_view: + conf_str += "%s" % read_view + if write_view: + conf_str += "%s" % write_view + if notify_view: + conf_str += "%s" % notify_view + conf_str += CE_DELETE_SNMP_V3_GROUP_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete snmp v3 group failed.') + + if security_level == "noAuthNoPriv": + security_level_cli = "noauthentication" + elif security_level == "authentication": + security_level_cli = "authentication" + elif security_level == "privacy": + security_level_cli = "privacy" + + cmd = "undo snmp-agent group v3 %s %s" % ( + group_name, security_level_cli) + + return cmd + + +def main(): + """ main function """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + acl_number=dict(type='str'), + community_name=dict(type='str', no_log=True), + access_right=dict(choices=['read', 'write']), + community_mib_view=dict(type='str'), + group_name=dict(type='str'), + security_level=dict( + choices=['noAuthNoPriv', 'authentication', 'privacy']), + read_view=dict(type='str'), + write_view=dict(type='str'), + notify_view=dict(type='str') + ) + + argument_spec.update(ce_argument_spec) + required_together = [("community_name", "access_right"), ("security_level", "group_name")] + module = AnsibleModule( + argument_spec=argument_spec, + required_together=required_together, + supports_check_mode=True + ) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + state = module.params['state'] + acl_number = module.params['acl_number'] + community_name = module.params['community_name'] + community_mib_view = module.params['community_mib_view'] + access_right = module.params['access_right'] + group_name = module.params['group_name'] + security_level = module.params['security_level'] + read_view = module.params['read_view'] + write_view = module.params['write_view'] + notify_view = module.params['notify_view'] + + snmp_community_obj = SnmpCommunity() + + if not snmp_community_obj: + module.fail_json(msg='Error: Init module failed.') + + snmp_community_rst = snmp_community_obj.check_snmp_community_args( + module=module) + snmp_v3_group_rst = snmp_community_obj.check_snmp_v3_group_args( + module=module) + + # get proposed + proposed["state"] = state + if acl_number: + proposed["acl_number"] = acl_number + if community_name: + proposed["community_name"] = community_name + if community_mib_view: + proposed["community_mib_view"] = community_mib_view + if access_right: + proposed["access_right"] = access_right + if group_name: + proposed["group_name"] = group_name + if security_level: + proposed["security_level"] = security_level + if read_view: + proposed["read_view"] = read_view + if write_view: + proposed["write_view"] = write_view + if notify_view: + proposed["notify_view"] = notify_view + + # state exist snmp community config + exist_tmp = dict() + for item in snmp_community_rst: + if item != "need_cfg": + exist_tmp[item] = snmp_community_rst[item] + + if exist_tmp: + existing["snmp community"] = exist_tmp + # state exist snmp v3 group config + exist_tmp = dict() + for item in snmp_v3_group_rst: + if item != "need_cfg": + exist_tmp[item] = snmp_v3_group_rst[item] + + if exist_tmp: + existing["snmp v3 group"] = exist_tmp + + if state == "present": + if snmp_community_rst["need_cfg"]: + if len(snmp_community_rst["community_info"]) != 0: + cmd = snmp_community_obj.merge_snmp_community(module=module) + changed = True + updates.append(cmd) + else: + cmd = snmp_community_obj.create_snmp_community(module=module) + changed = True + updates.append(cmd) + + if snmp_v3_group_rst["need_cfg"]: + if len(snmp_v3_group_rst["group_info"]): + cmd = snmp_community_obj.merge_snmp_v3_group(module=module) + changed = True + updates.append(cmd) + else: + cmd = snmp_community_obj.create_snmp_v3_group(module=module) + changed = True + updates.append(cmd) + + else: + if snmp_community_rst["need_cfg"]: + cmd = snmp_community_obj.delete_snmp_community(module=module) + changed = True + updates.append(cmd) + if snmp_v3_group_rst["need_cfg"]: + cmd = snmp_community_obj.delete_snmp_v3_group(module=module) + changed = True + updates.append(cmd) + + # state end snmp community config + snmp_community_rst = snmp_community_obj.check_snmp_community_args( + module=module) + end_tmp = dict() + for item in snmp_community_rst: + if item != "need_cfg": + end_tmp[item] = snmp_community_rst[item] + end_tmp[item] = snmp_community_rst[item] + if end_tmp: + end_state["snmp community"] = end_tmp + # state end snmp v3 group config + snmp_v3_group_rst = snmp_community_obj.check_snmp_v3_group_args( + module=module) + end_tmp = dict() + for item in snmp_v3_group_rst: + if item != "need_cfg": + end_tmp[item] = snmp_v3_group_rst[item] + if end_tmp: + end_state["snmp v3 group"] = end_tmp + + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_snmp_contact.py b/plugins/modules/ce_snmp_contact.py new file mode 100644 index 0000000..8f83927 --- /dev/null +++ b/plugins/modules/ce_snmp_contact.py @@ -0,0 +1,269 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_snmp_contact +short_description: Manages SNMP contact configuration on HUAWEI CloudEngine switches. +description: + - Manages SNMP contact configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + contact: + description: + - Contact information. + required: true + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' + +- name: CloudEngine snmp contact test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config SNMP contact" + ce_snmp_contact: + state: present + contact: call Operator at 010-99999999 + provider: "{{ cli }}" + + - name: "Undo SNMP contact" + ce_snmp_contact: + state: absent + contact: call Operator at 010-99999999 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"contact": "call Operator at 010-99999999", + "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"contact": "call Operator at 010-99999999"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["snmp-agent sys-info contact call Operator at 010-99999999"] +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config, ce_argument_spec + + +class SnmpContact(object): + """ Manages SNMP contact configuration """ + + def __init__(self, **kwargs): + """ Class init """ + + # module + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # config + self.cur_cfg = dict() + + # module args + self.state = self.module.params['state'] + self.contact = self.module.params['contact'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def check_args(self): + """ Check invalid args """ + + if self.contact: + if len(self.contact) > 255 or len(self.contact) < 1: + self.module.fail_json( + msg='Error: The len of contact %s is out of [1 - 255].' % self.contact) + else: + self.module.fail_json( + msg='Error: The len of contact is 0.') + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_proposed(self): + """ Get proposed state """ + + self.proposed["state"] = self.state + + if self.contact: + self.proposed["contact"] = self.contact + + def get_existing(self): + """ Get existing state """ + + tmp_cfg = self.cli_get_config() + if tmp_cfg: + temp_data = tmp_cfg.split(r"contact ") + if len(temp_data) > 1: + self.cur_cfg["contact"] = temp_data[1] + self.existing["contact"] = temp_data[1] + + def get_end_state(self): + """ Get end state """ + + tmp_cfg = self.cli_get_config() + if tmp_cfg: + temp_data = tmp_cfg.split(r"contact ") + if len(temp_data) > 1: + self.end_state["contact"] = temp_data[1] + + def cli_load_config(self, commands): + """ Load configure by cli """ + + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_get_config(self): + """ Get configure by cli """ + + regular = "| include snmp | include contact" + flags = list() + flags.append(regular) + tmp_cfg = self.get_config(flags) + + return tmp_cfg + + def set_config(self): + """ Set configure by cli """ + + cmd = "snmp-agent sys-info contact %s" % self.contact + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def undo_config(self): + """ Undo configure by cli """ + + cmd = "undo snmp-agent sys-info contact" + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def work(self): + """ Main work function """ + + self.check_args() + self.get_proposed() + self.get_existing() + + if self.state == "present": + if "contact" in self.cur_cfg.keys() and self.contact == self.cur_cfg["contact"]: + pass + else: + self.set_config() + else: + if "contact" in self.cur_cfg.keys() and self.contact == self.cur_cfg["contact"]: + self.undo_config() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + contact=dict(type='str', required=True) + ) + + argument_spec.update(ce_argument_spec) + module = SnmpContact(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_snmp_location.py b/plugins/modules/ce_snmp_location.py new file mode 100644 index 0000000..9e6c984 --- /dev/null +++ b/plugins/modules/ce_snmp_location.py @@ -0,0 +1,270 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_snmp_location +short_description: Manages SNMP location configuration on HUAWEI CloudEngine switches. +description: + - Manages SNMP location configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + location: + description: + - Location information. + required: true + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' + +- name: CloudEngine snmp location test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config SNMP location" + ce_snmp_location: + state: present + location: nanjing China + provider: "{{ cli }}" + + - name: "Remove SNMP location" + ce_snmp_location: + state: absent + location: nanjing China + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"location": "nanjing China", + "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"location": "nanjing China"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["snmp-agent sys-info location nanjing China"] +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config, ce_argument_spec + + +class SnmpLocation(object): + """ Manages SNMP location configuration """ + + def __init__(self, **kwargs): + """ Class init """ + + # module + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # config + self.cur_cfg = dict() + + # module args + self.state = self.module.params['state'] + self.location = self.module.params['location'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def check_args(self): + """ Check invalid args """ + + if self.location: + if len(self.location) > 255 or len(self.location) < 1: + self.module.fail_json( + msg='Error: The len of location %s is out of [1 - 255].' % self.location) + else: + self.module.fail_json( + msg='Error: The len of location is 0.') + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_proposed(self): + """ Get proposed state """ + + self.proposed["state"] = self.state + + if self.location: + self.proposed["location"] = self.location + + def get_existing(self): + """ Get existing state """ + + tmp_cfg = self.cli_get_config() + if tmp_cfg: + temp_data = tmp_cfg.split(r"location ") + if len(temp_data) > 1: + self.cur_cfg["location"] = temp_data[1] + self.existing["location"] = temp_data[1] + + def get_end_state(self): + """ Get end state """ + + tmp_cfg = self.cli_get_config() + if tmp_cfg: + temp_data = tmp_cfg.split(r"location ") + if len(temp_data) > 1: + self.end_state["location"] = temp_data[1] + + def cli_load_config(self, commands): + """ Load config by cli """ + + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_get_config(self): + """ Get config by cli """ + + regular = "| include snmp | include location" + flags = list() + flags.append(regular) + tmp_cfg = self.get_config(flags) + + return tmp_cfg + + def set_config(self): + """ Set configure by cli """ + + cmd = "snmp-agent sys-info location %s" % self.location + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def undo_config(self): + """ Undo configure by cli """ + + cmd = "undo snmp-agent sys-info location" + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def work(self): + """ Main work function """ + + self.check_args() + self.get_proposed() + self.get_existing() + + if self.state == "present": + if "location" in self.cur_cfg.keys() and self.location == self.cur_cfg["location"]: + pass + else: + self.set_config() + else: + if "location" in self.cur_cfg.keys() and self.location == self.cur_cfg["location"]: + self.undo_config() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + location=dict(type='str', required=True) + ) + + argument_spec.update(ce_argument_spec) + module = SnmpLocation(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_snmp_target_host.py b/plugins/modules/ce_snmp_target_host.py new file mode 100644 index 0000000..f4f4d77 --- /dev/null +++ b/plugins/modules/ce_snmp_target_host.py @@ -0,0 +1,941 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_snmp_target_host +short_description: Manages SNMP target host configuration on HUAWEI CloudEngine switches. +description: + - Manages SNMP target host configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + version: + description: + - Version(s) Supported by SNMP Engine. + choices: ['none', 'v1', 'v2c', 'v3', 'v1v2c', 'v1v3', 'v2cv3', 'all'] + connect_port: + description: + - Udp port used by SNMP agent to connect the Network management. + host_name: + description: + - Unique name to identify target host entry. + address: + description: + - Network Address. + notify_type: + description: + - To configure notify type as trap or inform. + choices: ['trap','inform'] + vpn_name: + description: + - VPN instance Name. + recv_port: + description: + - UDP Port number used by network management to receive alarm messages. + security_model: + description: + - Security Model. + choices: ['v1','v2c', 'v3'] + security_name: + description: + - Security Name. + security_name_v3: + description: + - Security Name V3. + security_level: + description: + - Security level indicating whether to use authentication and encryption. + choices: ['noAuthNoPriv','authentication', 'privacy'] + is_public_net: + description: + - To enable or disable Public Net-manager for target Host. + default: no_use + choices: ['no_use','true','false'] + interface_name: + description: + - Name of the interface to send the trap message. +''' + +EXAMPLES = ''' + +- name: CloudEngine snmp target host test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config SNMP version" + ce_snmp_target_host: + state: present + version: v2cv3 + provider: "{{ cli }}" + + - name: "Config SNMP target host" + ce_snmp_target_host: + state: present + host_name: test1 + address: 1.1.1.1 + notify_type: trap + vpn_name: js + security_model: v2c + security_name: wdz + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"address": "10.135.182.158", "host_name": "test2", + "notify_type": "trap", "security_level": "authentication", + "security_model": "v3", "security_name_v3": "wdz", + "state": "present", "vpn_name": "js"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"target host info": [{"address": "10.135.182.158", "domain": "snmpUDPDomain", + "nmsName": "test2", "notifyType": "trap", + "securityLevel": "authentication", "securityModel": "v3", + "securityNameV3": "wdz", "vpnInstanceName": "js"}]} +updates: + description: command sent to the device + returned: always + type: list + sample: ["snmp-agent target-host host-name test2 trap address udp-domain 10.135.182.158 vpn-instance js params securityname wdz v3 authentication"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, \ + ce_argument_spec, load_config, check_ip_addr + +# get snmp version +CE_GET_SNMP_VERSION = """ + + + + + + + +""" +# merge snmp version +CE_MERGE_SNMP_VERSION = """ + + + + %s + + + +""" + +# get snmp target host +CE_GET_SNMP_TARGET_HOST_HEADER = """ + + + + + +""" +CE_GET_SNMP_TARGET_HOST_TAIL = """ + + + + +""" + +# merge snmp target host +CE_MERGE_SNMP_TARGET_HOST_HEADER = """ + + + + + %s +""" +CE_MERGE_SNMP_TARGET_HOST_TAIL = """ + + + + +""" + +# create snmp target host +CE_CREATE_SNMP_TARGET_HOST_HEADER = """ + + + + + %s +""" +CE_CREATE_SNMP_TARGET_HOST_TAIL = """ + + + + +""" + +# delete snmp target host +CE_DELETE_SNMP_TARGET_HOST_HEADER = """ + + + + + %s +""" +CE_DELETE_SNMP_TARGET_HOST_TAIL = """ + + + + +""" + +# get snmp listen port +CE_GET_SNMP_PORT = """ + + + + + + + +""" + +# merge snmp listen port +CE_MERGE_SNMP_PORT = """ + + + + %s + + + +""" + + +INTERFACE_TYPE = ['ethernet', 'eth-trunk', 'tunnel', 'null', 'loopback', + 'vlanif', '100ge', '40ge', 'mtunnel', '10ge', 'ge', 'meth', 'vbdif', 'nve'] + + +class SnmpTargetHost(object): + """ Manages SNMP target host configuration """ + + def __init__(self, **kwargs): + """ Class init """ + + # module + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + required_together = [("address", "notify_type"), ("address", "notify_type")] + required_if = [ + ["security_model", "v1", ["security_name"]], + ["security_model", "v2c", ["security_name"]], + ["security_model", "v3", ["security_name_v3"]] + ] + self.module = AnsibleModule( + argument_spec=argument_spec, + required_together=required_together, + required_if=required_if, + supports_check_mode=True + ) + + # module args + self.state = self.module.params['state'] + self.version = self.module.params['version'] + self.connect_port = self.module.params['connect_port'] + self.host_name = self.module.params['host_name'] + self.domain = "snmpUDPDomain" + self.address = self.module.params['address'] + self.notify_type = self.module.params['notify_type'] + self.vpn_name = self.module.params['vpn_name'] + self.recv_port = self.module.params['recv_port'] + self.security_model = self.module.params['security_model'] + self.security_name = self.module.params['security_name'] + self.security_name_v3 = self.module.params['security_name_v3'] + self.security_level = self.module.params['security_level'] + self.is_public_net = self.module.params['is_public_net'] + self.interface_name = self.module.params['interface_name'] + + # config + self.cur_cli_cfg = dict() + self.cur_netconf_cfg = dict() + self.end_netconf_cfg = dict() + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def netconf_get_config(self, conf_str): + """ Get configure by netconf """ + + xml_str = get_nc_config(self.module, conf_str) + + return xml_str + + def netconf_set_config(self, conf_str): + """ Set configure by netconf """ + + xml_str = set_nc_config(self.module, conf_str) + + return xml_str + + def check_cli_args(self): + """ Check invalid cli args """ + + if self.connect_port: + if int(self.connect_port) != 161 and (int(self.connect_port) > 65535 or int(self.connect_port) < 1025): + self.module.fail_json( + msg='Error: The value of connect_port %s is out of [161, 1025 - 65535].' % self.connect_port) + + def check_netconf_args(self, result): + """ Check invalid netconf args """ + + need_cfg = True + same_flag = True + delete_flag = False + result["target_host_info"] = [] + + if self.host_name: + + if len(self.host_name) > 32 or len(self.host_name) < 1: + self.module.fail_json( + msg='Error: The len of host_name is out of [1 - 32].') + + if self.vpn_name and self.is_public_net != 'no_use': + if self.is_public_net == "true": + self.module.fail_json( + msg='Error: Do not support vpn_name and is_public_net at the same time.') + + conf_str = CE_GET_SNMP_TARGET_HOST_HEADER + + if self.domain: + conf_str += "" + + if self.address: + if not check_ip_addr(ipaddr=self.address): + self.module.fail_json( + msg='Error: The host address [%s] is invalid.' % self.address) + conf_str += "
" + + if self.notify_type: + conf_str += "" + + if self.vpn_name: + if len(self.vpn_name) > 31 or len(self.vpn_name) < 1: + self.module.fail_json( + msg='Error: The len of vpn_name is out of [1 - 31].') + conf_str += "" + + if self.recv_port: + if int(self.recv_port) > 65535 or int(self.recv_port) < 0: + self.module.fail_json( + msg='Error: The value of recv_port is out of [0 - 65535].') + conf_str += "" + + if self.security_model: + conf_str += "" + + if self.security_name: + if len(self.security_name) > 32 or len(self.security_name) < 1: + self.module.fail_json( + msg='Error: The len of security_name is out of [1 - 32].') + conf_str += "" + + if self.security_name_v3: + if len(self.security_name_v3) > 32 or len(self.security_name_v3) < 1: + self.module.fail_json( + msg='Error: The len of security_name_v3 is out of [1 - 32].') + conf_str += "" + + if self.security_level: + conf_str += "" + + if self.is_public_net != 'no_use': + conf_str += "" + + if self.interface_name: + if len(self.interface_name) > 63 or len(self.interface_name) < 1: + self.module.fail_json( + msg='Error: The len of interface_name is out of [1 - 63].') + + find_flag = False + for item in INTERFACE_TYPE: + if item in self.interface_name.lower(): + find_flag = True + break + if not find_flag: + self.module.fail_json( + msg='Error: Please input full name of interface_name.') + + conf_str += "" + + conf_str += CE_GET_SNMP_TARGET_HOST_TAIL + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + if self.state == "present": + same_flag = False + else: + delete_flag = False + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + target_host_info = root.findall( + "snmp/targetHosts/targetHost") + if target_host_info: + for tmp in target_host_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["nmsName", "domain", "address", "notifyType", "vpnInstanceName", + "portNumber", "securityModel", "securityName", "securityNameV3", + "securityLevel", "isPublicNet", "interface-name"]: + tmp_dict[site.tag] = site.text + + result["target_host_info"].append(tmp_dict) + + if result["target_host_info"]: + for tmp in result["target_host_info"]: + + same_flag = True + + if "nmsName" in tmp.keys(): + if tmp["nmsName"] != self.host_name: + same_flag = False + else: + delete_flag = True + + if "domain" in tmp.keys(): + if tmp["domain"] != self.domain: + same_flag = False + + if "address" in tmp.keys(): + if tmp["address"] != self.address: + same_flag = False + + if "notifyType" in tmp.keys(): + if tmp["notifyType"] != self.notify_type: + same_flag = False + + if "vpnInstanceName" in tmp.keys(): + if tmp["vpnInstanceName"] != self.vpn_name: + same_flag = False + + if "portNumber" in tmp.keys(): + if tmp["portNumber"] != self.recv_port: + same_flag = False + + if "securityModel" in tmp.keys(): + if tmp["securityModel"] != self.security_model: + same_flag = False + + if "securityName" in tmp.keys(): + if tmp["securityName"] != self.security_name: + same_flag = False + + if "securityNameV3" in tmp.keys(): + if tmp["securityNameV3"] != self.security_name_v3: + same_flag = False + + if "securityLevel" in tmp.keys(): + if tmp["securityLevel"] != self.security_level: + same_flag = False + + if "isPublicNet" in tmp.keys(): + if tmp["isPublicNet"] != self.is_public_net: + same_flag = False + + if "interface-name" in tmp.keys(): + if tmp.get("interface-name") is not None: + if tmp["interface-name"].lower() != self.interface_name.lower(): + same_flag = False + else: + same_flag = False + + if same_flag: + break + + if self.state == "present": + need_cfg = True + if same_flag: + need_cfg = False + else: + need_cfg = False + if delete_flag: + need_cfg = True + + result["need_cfg"] = need_cfg + + def cli_load_config(self, commands): + """ Load configure by cli """ + + if not self.module.check_mode: + load_config(self.module, commands) + + def get_snmp_version(self): + """ Get snmp version """ + + version = None + conf_str = CE_GET_SNMP_VERSION + recv_xml = self.netconf_get_config(conf_str=conf_str) + + if "" in recv_xml: + pass + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + version_info = root.find("snmp/engine") + if version_info: + for site in version_info: + if site.tag in ["version"]: + version = site.text + + return version + + def xml_get_connect_port(self): + """ Get connect port by xml """ + tmp_cfg = None + conf_str = CE_GET_SNMP_PORT + recv_xml = self.netconf_get_config(conf_str=conf_str) + if "" in recv_xml: + pass + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + snmp_port_info = root.findall("snmp/systemCfg/snmpListenPort") + + if snmp_port_info: + tmp_cfg = snmp_port_info[0].text + return tmp_cfg + + def get_proposed(self): + """ Get proposed state """ + + self.proposed["state"] = self.state + + if self.version: + self.proposed["version"] = self.version + if self.connect_port: + self.proposed["connect_port"] = self.connect_port + if self.host_name: + self.proposed["host_name"] = self.host_name + if self.address: + self.proposed["address"] = self.address + if self.notify_type: + self.proposed["notify_type"] = self.notify_type + if self.vpn_name: + self.proposed["vpn_name"] = self.vpn_name + if self.recv_port: + self.proposed["recv_port"] = self.recv_port + if self.security_model: + self.proposed["security_model"] = self.security_model + if self.security_name: + self.proposed["security_name"] = "******" + if self.security_name_v3: + self.proposed["security_name_v3"] = self.security_name_v3 + if self.security_level: + self.proposed["security_level"] = self.security_level + if self.is_public_net != 'no_use': + self.proposed["is_public_net"] = self.is_public_net + if self.interface_name: + self.proposed["interface_name"] = self.interface_name + + def get_existing(self): + """ Get existing state """ + + if self.version: + version = self.get_snmp_version() + if version: + self.cur_cli_cfg["version"] = version + self.existing["version"] = version + + if self.connect_port: + tmp_cfg = self.xml_get_connect_port() + if tmp_cfg: + self.cur_cli_cfg["connect port"] = tmp_cfg + self.existing["connect port"] = tmp_cfg + + if self.host_name: + self.existing["target host info"] = self.cur_netconf_cfg[ + "target_host_info"] + + def get_end_state(self): + """ Get end state """ + + if self.version: + version = self.get_snmp_version() + if version: + self.end_state["version"] = version + + if self.connect_port: + tmp_cfg = self.xml_get_connect_port() + if tmp_cfg: + self.end_state["connect port"] = tmp_cfg + + if self.host_name: + self.end_state["target host info"] = self.end_netconf_cfg[ + "target_host_info"] + if self.existing == self.end_state: + self.changed = False + self.updates_cmd = list() + + def config_version_cli(self): + """ Config version by cli """ + + if "disable" in self.cur_cli_cfg["version"]: + cmd = "snmp-agent sys-info version %s" % self.version + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + else: + if self.version != self.cur_cli_cfg["version"]: + cmd = "snmp-agent sys-info version %s disable" % self.cur_cli_cfg[ + "version"] + self.updates_cmd.append(cmd) + cmd = "snmp-agent sys-info version %s" % self.version + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def undo_config_version_cli(self): + """ Undo config version by cli """ + + if "disable" in self.cur_cli_cfg["version"]: + pass + else: + cmd = "snmp-agent sys-info version %s disable" % self.cur_cli_cfg[ + "version"] + + cmds = list() + cmds.append(cmd) + + self.updates_cmd.append(cmd) + self.cli_load_config(cmds) + self.changed = True + + def config_connect_port_xml(self): + """ Config connect port by xml """ + + if "connect port" in self.cur_cli_cfg.keys(): + if self.cur_cli_cfg["connect port"] == self.connect_port: + pass + else: + cmd = "snmp-agent udp-port %s" % self.connect_port + + cmds = list() + cmds.append(cmd) + + self.updates_cmd.append(cmd) + conf_str = CE_MERGE_SNMP_PORT % self.connect_port + self.netconf_set_config(conf_str=conf_str) + self.changed = True + else: + cmd = "snmp-agent udp-port %s" % self.connect_port + + cmds = list() + cmds.append(cmd) + + self.updates_cmd.append(cmd) + conf_str = CE_MERGE_SNMP_PORT % self.connect_port + self.netconf_set_config(conf_str=conf_str) + self.changed = True + + def undo_config_connect_port_cli(self): + """ Undo config connect port by cli """ + + if "connect port" in self.cur_cli_cfg.keys(): + if not self.cur_cli_cfg["connect port"]: + pass + else: + cmd = "undo snmp-agent udp-port" + + cmds = list() + cmds.append(cmd) + + self.updates_cmd.append(cmd) + connect_port = "161" + conf_str = CE_MERGE_SNMP_PORT % connect_port + self.netconf_set_config(conf_str=conf_str) + self.changed = True + + def merge_snmp_target_host(self): + """ Merge snmp target host operation """ + + conf_str = CE_MERGE_SNMP_TARGET_HOST_HEADER % self.host_name + + if self.domain: + conf_str += "%s" % self.domain + if self.address: + conf_str += "
%s
" % self.address + if self.notify_type: + conf_str += "%s" % self.notify_type + if self.vpn_name: + conf_str += "%s" % self.vpn_name + if self.recv_port: + conf_str += "%s" % self.recv_port + if self.security_model: + conf_str += "%s" % self.security_model + if self.security_name: + conf_str += "%s" % self.security_name + if self.security_name_v3: + conf_str += "%s" % self.security_name_v3 + if self.security_level: + conf_str += "%s" % self.security_level + if self.is_public_net != 'no_use': + conf_str += "%s" % self.is_public_net + if self.interface_name: + conf_str += "%s" % self.interface_name + + conf_str += CE_MERGE_SNMP_TARGET_HOST_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge snmp target host failed.') + + cmd = "snmp-agent target-host host-name %s " % self.host_name + cmd += "%s " % self.notify_type + cmd += "address udp-domain %s " % self.address + + if self.recv_port: + cmd += "udp-port %s " % self.recv_port + if self.interface_name: + cmd += "source %s " % self.interface_name + if self.vpn_name: + cmd += "vpn-instance %s " % self.vpn_name + if self.is_public_net == "true": + cmd += "public-net " + if self.security_model in ["v1", "v2c"] and self.security_name: + cmd += "params securityname %s %s " % ( + "******", self.security_model) + if self.security_model == "v3" and self.security_name_v3: + cmd += "params securityname %s %s " % ( + self.security_name_v3, self.security_model) + if self.security_level and self.security_level in ["authentication", "privacy"]: + cmd += "%s" % self.security_level + + self.changed = True + self.updates_cmd.append(cmd) + + def delete_snmp_target_host(self): + """ Delete snmp target host operation """ + + conf_str = CE_DELETE_SNMP_TARGET_HOST_HEADER % self.host_name + + if self.domain: + conf_str += "%s" % self.domain + if self.address: + conf_str += "
%s
" % self.address + if self.notify_type: + conf_str += "%s" % self.notify_type + if self.vpn_name: + conf_str += "%s" % self.vpn_name + if self.recv_port: + conf_str += "%s" % self.recv_port + if self.security_model: + conf_str += "%s" % self.security_model + if self.security_name: + conf_str += "%s" % self.security_name + if self.security_name_v3: + conf_str += "%s" % self.security_name_v3 + if self.security_level: + conf_str += "%s" % self.security_level + if self.is_public_net != 'no_use': + conf_str += "%s" % self.is_public_net + if self.interface_name: + conf_str += "%s" % self.interface_name + + conf_str += CE_DELETE_SNMP_TARGET_HOST_TAIL + + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Delete snmp target host failed.') + + if not self.address: + cmd = "undo snmp-agent target-host host-name %s " % self.host_name + else: + if self.notify_type == "trap": + cmd = "undo snmp-agent target-host trap address udp-domain %s " % self.address + else: + cmd = "undo snmp-agent target-host inform address udp-domain %s " % self.address + if self.recv_port: + cmd += "udp-port %s " % self.recv_port + if self.interface_name: + cmd += "source %s " % self.interface_name + if self.vpn_name: + cmd += "vpn-instance %s " % self.vpn_name + if self.is_public_net == "true": + cmd += "public-net " + if self.security_model in ["v1", "v2c"] and self.security_name: + cmd += "params securityname %s" % "******" + if self.security_model == "v3" and self.security_name_v3: + cmd += "params securityname %s" % self.security_name_v3 + + self.changed = True + self.updates_cmd.append(cmd) + + def merge_snmp_version(self): + """ Merge snmp version operation """ + + conf_str = CE_MERGE_SNMP_VERSION % self.version + recv_xml = self.netconf_set_config(conf_str=conf_str) + + if "" not in recv_xml: + self.module.fail_json(msg='Error: Merge snmp version failed.') + + if self.version == "none": + cmd = "snmp-agent sys-info version %s disable" % self.cur_cli_cfg[ + "version"] + self.updates_cmd.append(cmd) + elif self.version == "v1v2c": + cmd = "snmp-agent sys-info version v1" + self.updates_cmd.append(cmd) + cmd = "snmp-agent sys-info version v2c" + self.updates_cmd.append(cmd) + elif self.version == "v1v3": + cmd = "snmp-agent sys-info version v1" + self.updates_cmd.append(cmd) + cmd = "snmp-agent sys-info version v3" + self.updates_cmd.append(cmd) + elif self.version == "v2cv3": + cmd = "snmp-agent sys-info version v2c" + self.updates_cmd.append(cmd) + cmd = "snmp-agent sys-info version v3" + self.updates_cmd.append(cmd) + else: + cmd = "snmp-agent sys-info version %s" % self.version + self.updates_cmd.append(cmd) + + self.changed = True + + def work(self): + """ Main work function """ + + self.check_cli_args() + self.check_netconf_args(self.cur_netconf_cfg) + self.get_proposed() + self.get_existing() + + if self.state == "present": + if self.version: + if self.version != self.cur_cli_cfg["version"]: + self.merge_snmp_version() + if self.connect_port: + self.config_connect_port_xml() + if self.cur_netconf_cfg["need_cfg"]: + self.merge_snmp_target_host() + + else: + if self.connect_port: + self.undo_config_connect_port_cli() + if self.cur_netconf_cfg["need_cfg"]: + self.delete_snmp_target_host() + + self.check_netconf_args(self.end_netconf_cfg) + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + version=dict(choices=['none', 'v1', 'v2c', 'v3', + 'v1v2c', 'v1v3', 'v2cv3', 'all']), + connect_port=dict(type='str'), + host_name=dict(type='str'), + address=dict(type='str'), + notify_type=dict(choices=['trap', 'inform']), + vpn_name=dict(type='str'), + recv_port=dict(type='str'), + security_model=dict(choices=['v1', 'v2c', 'v3']), + security_name=dict(type='str', no_log=True), + security_name_v3=dict(type='str'), + security_level=dict( + choices=['noAuthNoPriv', 'authentication', 'privacy']), + is_public_net=dict(type='str', default='no_use', choices=['no_use', 'true', 'false']), + interface_name=dict(type='str') + ) + + argument_spec.update(ce_argument_spec) + module = SnmpTargetHost(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_snmp_traps.py b/plugins/modules/ce_snmp_traps.py new file mode 100644 index 0000000..f10e7ae --- /dev/null +++ b/plugins/modules/ce_snmp_traps.py @@ -0,0 +1,560 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_snmp_traps +short_description: Manages SNMP traps configuration on HUAWEI CloudEngine switches. +description: + - Manages SNMP traps configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + feature_name: + description: + - Alarm feature name. + choices: ['aaa', 'arp', 'bfd', 'bgp', 'cfg', 'configuration', 'dad', 'devm', + 'dhcpsnp', 'dldp', 'driver', 'efm', 'erps', 'error-down', 'fcoe', + 'fei', 'fei_comm', 'fm', 'ifnet', 'info', 'ipsg', 'ipv6', 'isis', + 'l3vpn', 'lacp', 'lcs', 'ldm', 'ldp', 'ldt', 'lldp', 'mpls_lspm', + 'msdp', 'mstp', 'nd', 'netconf', 'nqa', 'nvo3', 'openflow', 'ospf', + 'ospfv3', 'pim', 'pim-std', 'qos', 'radius', 'rm', 'rmon', 'securitytrap', + 'smlktrap', 'snmp', 'ssh', 'stackmng', 'sysclock', 'sysom', 'system', + 'tcp', 'telnet', 'trill', 'trunk', 'tty', 'vbst', 'vfs', 'virtual-perception', + 'vrrp', 'vstm', 'all'] + trap_name: + description: + - Alarm trap name. + interface_type: + description: + - Interface type. + choices: ['Ethernet', 'Eth-Trunk', 'Tunnel', 'NULL', 'LoopBack', 'Vlanif', '100GE', + '40GE', 'MTunnel', '10GE', 'GE', 'MEth', 'Vbdif', 'Nve'] + interface_number: + description: + - Interface number. + port_number: + description: + - Source port number. +''' + +EXAMPLES = ''' + +- name: CloudEngine snmp traps test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config SNMP trap all enable" + ce_snmp_traps: + state: present + feature_name: all + provider: "{{ cli }}" + + - name: "Config SNMP trap interface" + ce_snmp_traps: + state: present + interface_type: 40GE + interface_number: 2/0/1 + provider: "{{ cli }}" + + - name: "Config SNMP trap port" + ce_snmp_traps: + state: present + port_number: 2222 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"feature_name": "all", + "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"snmp-agent trap": [], + "undo snmp-agent trap": []} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"snmp-agent trap": ["enable"], + "undo snmp-agent trap": []} +updates: + description: command sent to the device + returned: always + type: list + sample: ["snmp-agent trap enable"] +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import load_config, ce_argument_spec, run_commands +from ansible.module_utils.connection import exec_command + + +class SnmpTraps(object): + """ Manages SNMP trap configuration """ + + def __init__(self, **kwargs): + """ Class init """ + + # module + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule( + argument_spec=self.spec, + required_together=[("interface_type", "interface_number")], + supports_check_mode=True + ) + + # config + self.cur_cfg = dict() + self.cur_cfg["snmp-agent trap"] = [] + self.cur_cfg["undo snmp-agent trap"] = [] + + # module args + self.state = self.module.params['state'] + self.feature_name = self.module.params['feature_name'] + self.trap_name = self.module.params['trap_name'] + self.interface_type = self.module.params['interface_type'] + self.interface_number = self.module.params['interface_number'] + self.port_number = self.module.params['port_number'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.existing["snmp-agent trap"] = [] + self.existing["undo snmp-agent trap"] = [] + self.end_state = dict() + self.end_state["snmp-agent trap"] = [] + self.end_state["undo snmp-agent trap"] = [] + + commands = list() + cmd1 = 'display interface brief' + commands.append(cmd1) + self.interface = run_commands(self.module, commands) + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def check_args(self): + """ Check invalid args """ + + if self.port_number: + if self.port_number.isdigit(): + if int(self.port_number) < 1025 or int(self.port_number) > 65535: + self.module.fail_json( + msg='Error: The value of port_number is out of [1025 - 65535].') + else: + self.module.fail_json( + msg='Error: The port_number is not digit.') + + if self.interface_type and self.interface_number: + tmp_interface = self.interface_type + self.interface_number + if tmp_interface not in self.interface[0]: + self.module.fail_json( + msg='Error: The interface %s is not in the device.' % tmp_interface) + + def get_proposed(self): + """ Get proposed state """ + + self.proposed["state"] = self.state + + if self.feature_name: + self.proposed["feature_name"] = self.feature_name + + if self.trap_name: + self.proposed["trap_name"] = self.trap_name + + if self.interface_type: + self.proposed["interface_type"] = self.interface_type + + if self.interface_number: + self.proposed["interface_number"] = self.interface_number + + if self.port_number: + self.proposed["port_number"] = self.port_number + + def get_existing(self): + """ Get existing state """ + + tmp_cfg = self.cli_get_config() + if tmp_cfg: + temp_cfg_lower = tmp_cfg.lower() + temp_data = tmp_cfg.split("\n") + temp_data_lower = temp_cfg_lower.split("\n") + + for item in temp_data: + if "snmp-agent trap source-port " in item: + if self.port_number: + item_tmp = item.split("snmp-agent trap source-port ") + self.cur_cfg["trap source-port"] = item_tmp[1] + self.existing["trap source-port"] = item_tmp[1] + elif "snmp-agent trap source " in item: + if self.interface_type: + item_tmp = item.split("snmp-agent trap source ") + self.cur_cfg["trap source interface"] = item_tmp[1] + self.existing["trap source interface"] = item_tmp[1] + + if self.feature_name: + for item in temp_data_lower: + if item == "snmp-agent trap enable": + self.cur_cfg["snmp-agent trap"].append("enable") + self.existing["snmp-agent trap"].append("enable") + elif item == "snmp-agent trap disable": + self.cur_cfg["snmp-agent trap"].append("disable") + self.existing["snmp-agent trap"].append("disable") + elif "undo snmp-agent trap enable " in item: + item_tmp = item.split("undo snmp-agent trap enable ") + self.cur_cfg[ + "undo snmp-agent trap"].append(item_tmp[1]) + self.existing[ + "undo snmp-agent trap"].append(item_tmp[1]) + elif "snmp-agent trap enable " in item: + item_tmp = item.split("snmp-agent trap enable ") + self.cur_cfg["snmp-agent trap"].append(item_tmp[1]) + self.existing["snmp-agent trap"].append(item_tmp[1]) + else: + del self.existing["snmp-agent trap"] + del self.existing["undo snmp-agent trap"] + + def get_end_state(self): + """ Get end_state state """ + + tmp_cfg = self.cli_get_config() + if tmp_cfg: + temp_cfg_lower = tmp_cfg.lower() + temp_data = tmp_cfg.split("\n") + temp_data_lower = temp_cfg_lower.split("\n") + + for item in temp_data: + if "snmp-agent trap source-port " in item: + if self.port_number: + item_tmp = item.split("snmp-agent trap source-port ") + self.end_state["trap source-port"] = item_tmp[1] + elif "snmp-agent trap source " in item: + if self.interface_type: + item_tmp = item.split("snmp-agent trap source ") + self.end_state["trap source interface"] = item_tmp[1] + + if self.feature_name: + for item in temp_data_lower: + if item == "snmp-agent trap enable": + self.end_state["snmp-agent trap"].append("enable") + elif item == "snmp-agent trap disable": + self.end_state["snmp-agent trap"].append("disable") + elif "undo snmp-agent trap enable " in item: + item_tmp = item.split("undo snmp-agent trap enable ") + self.end_state[ + "undo snmp-agent trap"].append(item_tmp[1]) + elif "snmp-agent trap enable " in item: + item_tmp = item.split("snmp-agent trap enable ") + self.end_state["snmp-agent trap"].append(item_tmp[1]) + else: + del self.end_state["snmp-agent trap"] + del self.end_state["undo snmp-agent trap"] + if self.end_state == self.existing: + self.changed = False + self.updates_cmd = list() + + def cli_load_config(self, commands): + """ Load configure through cli """ + + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_get_config(self): + """ Get configure through cli """ + + regular = "| include snmp | include trap" + flags = list() + flags.append(regular) + tmp_cfg = self.get_config(flags) + + return tmp_cfg + + def set_trap_feature_name(self): + """ Set feature name for trap """ + + if self.feature_name == "all": + cmd = "snmp-agent trap enable" + else: + cmd = "snmp-agent trap enable feature-name %s" % self.feature_name + if self.trap_name: + cmd += " trap-name %s" % self.trap_name + + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def undo_trap_feature_name(self): + """ Undo feature name for trap """ + + if self.feature_name == "all": + cmd = "undo snmp-agent trap enable" + else: + cmd = "undo snmp-agent trap enable feature-name %s" % self.feature_name + if self.trap_name: + cmd += " trap-name %s" % self.trap_name + + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def set_trap_source_interface(self): + """ Set source interface for trap """ + + cmd = "snmp-agent trap source %s %s" % ( + self.interface_type, self.interface_number) + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def undo_trap_source_interface(self): + """ Undo source interface for trap """ + + cmd = "undo snmp-agent trap source" + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def set_trap_source_port(self): + """ Set source port for trap """ + + cmd = "snmp-agent trap source-port %s" % self.port_number + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def undo_trap_source_port(self): + """ Undo source port for trap """ + + cmd = "undo snmp-agent trap source-port" + self.updates_cmd.append(cmd) + + cmds = list() + cmds.append(cmd) + + self.cli_load_config(cmds) + self.changed = True + + def work(self): + """ The work function """ + + self.check_args() + self.get_proposed() + self.get_existing() + + find_flag = False + find_undo_flag = False + tmp_interface = None + + if self.state == "present": + if self.feature_name: + if self.trap_name: + tmp_cfg = "feature-name %s trap-name %s" % ( + self.feature_name, self.trap_name.lower()) + else: + tmp_cfg = "feature-name %s" % self.feature_name + + find_undo_flag = False + if self.cur_cfg["undo snmp-agent trap"]: + for item in self.cur_cfg["undo snmp-agent trap"]: + if item == tmp_cfg: + find_undo_flag = True + elif tmp_cfg in item: + find_undo_flag = True + elif self.feature_name == "all": + find_undo_flag = True + if find_undo_flag: + self.set_trap_feature_name() + + if not find_undo_flag: + find_flag = False + if self.cur_cfg["snmp-agent trap"]: + for item in self.cur_cfg["snmp-agent trap"]: + if item == "enable": + find_flag = True + elif item == tmp_cfg: + find_flag = True + if not find_flag: + self.set_trap_feature_name() + + if self.interface_type: + find_flag = False + tmp_interface = self.interface_type + self.interface_number + + if "trap source interface" in self.cur_cfg.keys(): + if self.cur_cfg["trap source interface"] == tmp_interface: + find_flag = True + + if not find_flag: + self.set_trap_source_interface() + + if self.port_number: + find_flag = False + + if "trap source-port" in self.cur_cfg.keys(): + if self.cur_cfg["trap source-port"] == self.port_number: + find_flag = True + + if not find_flag: + self.set_trap_source_port() + + else: + if self.feature_name: + if self.trap_name: + tmp_cfg = "feature-name %s trap-name %s" % ( + self.feature_name, self.trap_name.lower()) + else: + tmp_cfg = "feature-name %s" % self.feature_name + + find_flag = False + if self.cur_cfg["snmp-agent trap"]: + for item in self.cur_cfg["snmp-agent trap"]: + if item == tmp_cfg: + find_flag = True + elif item == "enable": + find_flag = True + elif tmp_cfg in item: + find_flag = True + else: + find_flag = True + + find_undo_flag = False + if self.cur_cfg["undo snmp-agent trap"]: + for item in self.cur_cfg["undo snmp-agent trap"]: + if item == tmp_cfg: + find_undo_flag = True + elif tmp_cfg in item: + find_undo_flag = True + + if find_undo_flag: + pass + elif find_flag: + self.undo_trap_feature_name() + + if self.interface_type: + if "trap source interface" in self.cur_cfg.keys(): + self.undo_trap_source_interface() + + if self.port_number: + if "trap source-port" in self.cur_cfg.keys(): + self.undo_trap_source_port() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + feature_name=dict(choices=['aaa', 'arp', 'bfd', 'bgp', 'cfg', 'configuration', 'dad', + 'devm', 'dhcpsnp', 'dldp', 'driver', 'efm', 'erps', 'error-down', + 'fcoe', 'fei', 'fei_comm', 'fm', 'ifnet', 'info', 'ipsg', 'ipv6', + 'isis', 'l3vpn', 'lacp', 'lcs', 'ldm', 'ldp', 'ldt', 'lldp', + 'mpls_lspm', 'msdp', 'mstp', 'nd', 'netconf', 'nqa', 'nvo3', + 'openflow', 'ospf', 'ospfv3', 'pim', 'pim-std', 'qos', 'radius', + 'rm', 'rmon', 'securitytrap', 'smlktrap', 'snmp', 'ssh', 'stackmng', + 'sysclock', 'sysom', 'system', 'tcp', 'telnet', 'trill', 'trunk', + 'tty', 'vbst', 'vfs', 'virtual-perception', 'vrrp', 'vstm', 'all']), + trap_name=dict(type='str'), + interface_type=dict(choices=['Ethernet', 'Eth-Trunk', 'Tunnel', 'NULL', 'LoopBack', 'Vlanif', + '100GE', '40GE', 'MTunnel', '10GE', 'GE', 'MEth', 'Vbdif', 'Nve']), + interface_number=dict(type='str'), + port_number=dict(type='str') + ) + + argument_spec.update(ce_argument_spec) + module = SnmpTraps(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_snmp_user.py b/plugins/modules/ce_snmp_user.py new file mode 100644 index 0000000..15d16b5 --- /dev/null +++ b/plugins/modules/ce_snmp_user.py @@ -0,0 +1,1045 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_snmp_user +short_description: Manages SNMP user configuration on HUAWEI CloudEngine switches. +description: + - Manages SNMP user configurations on CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + acl_number: + description: + - Access control list number. + usm_user_name: + description: + - Unique name to identify the USM user. + aaa_local_user: + description: + - Unique name to identify the local user. + remote_engine_id: + description: + - Remote engine id of the USM user. + user_group: + description: + - Name of the group where user belongs to. + auth_protocol: + description: + - Authentication protocol. + choices: ['noAuth', 'md5', 'sha'] + auth_key: + description: + - The authentication password. Password length, 8-255 characters. + priv_protocol: + description: + - Encryption protocol. + choices: ['noPriv', 'des56', '3des168', 'aes128', 'aes192', 'aes256'] + priv_key: + description: + - The encryption password. Password length 8-255 characters. +''' + +EXAMPLES = ''' + +- name: CloudEngine snmp user test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config SNMP usm user" + ce_snmp_user: + state: present + usm_user_name: wdz_snmp + remote_engine_id: 800007DB03389222111200 + acl_number: 2000 + user_group: wdz_group + provider: "{{ cli }}" + + - name: "Undo SNMP usm user" + ce_snmp_user: + state: absent + usm_user_name: wdz_snmp + remote_engine_id: 800007DB03389222111200 + acl_number: 2000 + user_group: wdz_group + provider: "{{ cli }}" + + - name: "Config SNMP local user" + ce_snmp_user: + state: present + aaa_local_user: wdz_user + auth_protocol: md5 + auth_key: huawei123 + priv_protocol: des56 + priv_key: huawei123 + provider: "{{ cli }}" + + - name: "Config SNMP local user" + ce_snmp_user: + state: absent + aaa_local_user: wdz_user + auth_protocol: md5 + auth_key: huawei123 + priv_protocol: des56 + priv_key: huawei123 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"acl_number": "2000", "remote_engine_id": "800007DB03389222111200", + "state": "present", "user_group": "wdz_group", + "usm_user_name": "wdz_snmp"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"snmp local user": {"local_user_info": []}, + "snmp usm user": {"usm_user_info": []}} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"snmp local user": {"local_user_info": []}, + "snmp usm user": {"usm_user_info": [{"aclNumber": "2000", "engineID": "800007DB03389222111200", + "groupName": "wdz_group", "userName": "wdz_snmp"}]}} +updates: + description: command sent to the device + returned: always + type: list + sample: ["snmp-agent remote-engineid 800007DB03389222111200 usm-user v3 wdz_snmp wdz_group acl 2000"] +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec + +# get snmp v3 USM user +CE_GET_SNMP_V3_USM_USER_HEADER = """ + + + + + + + +""" +CE_GET_SNMP_V3_USM_USER_TAIL = """ + + + + +""" +# merge snmp v3 USM user +CE_MERGE_SNMP_V3_USM_USER_HEADER = """ + + + + + %s + %s + %s +""" +CE_MERGE_SNMP_V3_USM_USER_TAIL = """ + + + + +""" +# create snmp v3 USM user +CE_CREATE_SNMP_V3_USM_USER_HEADER = """ + + + + + %s + %s + %s +""" +CE_CREATE_SNMP_V3_USM_USER_TAIL = """ + + + + +""" +# delete snmp v3 USM user +CE_DELETE_SNMP_V3_USM_USER_HEADER = """ + + + + + %s + %s + %s +""" +CE_DELETE_SNMP_V3_USM_USER_TAIL = """ + + + + +""" + +# get snmp v3 aaa local user +CE_GET_SNMP_V3_LOCAL_USER = """ + + + + + + + + + + + + + +""" +# merge snmp v3 aaa local user +CE_MERGE_SNMP_V3_LOCAL_USER = """ + + + + + %s + %s + %s + %s + %s + + + + +""" +# create snmp v3 aaa local user +CE_CREATE_SNMP_V3_LOCAL_USER = """ + + + + + %s + %s + %s + %s + %s + + + + +""" +# delete snmp v3 aaa local user +CE_DELETE_SNMP_V3_LOCAL_USER = """ + + + + + %s + %s + %s + %s + %s + + + + +""" +# display info +GET_SNMP_LOCAL_ENGINE = """ + + + + + + + +""" + + +class SnmpUser(object): + """ Manages SNMP user configuration """ + + def netconf_get_config(self, **kwargs): + """ Get configure by netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = get_nc_config(module, conf_str) + + return xml_str + + def netconf_set_config(self, **kwargs): + """ Set configure by netconf """ + + module = kwargs["module"] + conf_str = kwargs["conf_str"] + + xml_str = set_nc_config(module, conf_str) + + return xml_str + + def check_snmp_v3_usm_user_args(self, **kwargs): + """ Check snmp v3 usm user invalid args """ + + module = kwargs["module"] + result = dict() + need_cfg = False + state = module.params['state'] + usm_user_name = module.params['usm_user_name'] + remote_engine_id = module.params['remote_engine_id'] + + acl_number = module.params['acl_number'] + user_group = module.params['user_group'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + local_user_name = module.params['aaa_local_user'] + + if usm_user_name: + if len(usm_user_name) > 32 or len(usm_user_name) == 0: + module.fail_json( + msg='Error: The length of usm_user_name %s is out of [1 - 32].' % usm_user_name) + if remote_engine_id: + if len(remote_engine_id) > 64 or len(remote_engine_id) < 10: + module.fail_json( + msg='Error: The length of remote_engine_id %s is out of [10 - 64].' % remote_engine_id) + + conf_str = CE_GET_SNMP_V3_USM_USER_HEADER + + if acl_number: + if acl_number.isdigit(): + if int(acl_number) > 2999 or int(acl_number) < 2000: + module.fail_json( + msg='Error: The value of acl_number %s is out of [2000 - 2999].' % acl_number) + else: + if not acl_number[0].isalpha() or len(acl_number) > 32 or len(acl_number) < 1: + module.fail_json( + msg='Error: The length of acl_number %s is out of [1 - 32].' % acl_number) + + conf_str += "" + + if user_group: + if len(user_group) > 32 or len(user_group) == 0: + module.fail_json( + msg='Error: The length of user_group %s is out of [1 - 32].' % user_group) + + conf_str += "" + + if auth_protocol: + conf_str += "" + + if auth_key: + if len(auth_key) > 255 or len(auth_key) == 0: + module.fail_json( + msg='Error: The length of auth_key %s is out of [1 - 255].' % auth_key) + + conf_str += "" + + if priv_protocol: + if not auth_protocol: + module.fail_json( + msg='Error: Please input auth_protocol at the same time.') + + conf_str += "" + + if priv_key: + if len(priv_key) > 255 or len(priv_key) == 0: + module.fail_json( + msg='Error: The length of priv_key %s is out of [1 - 255].' % priv_key) + conf_str += "" + + result["usm_user_info"] = [] + + conf_str += CE_GET_SNMP_V3_USM_USER_TAIL + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + usm_user_info = root.findall("snmp/usmUsers/usmUser") + if usm_user_info: + for tmp in usm_user_info: + tmp_dict = dict() + tmp_dict["remoteEngineID"] = None + for site in tmp: + if site.tag in ["userName", "remoteEngineID", "engineID", "groupName", "authProtocol", + "authKey", "privProtocol", "privKey", "aclNumber"]: + tmp_dict[site.tag] = site.text + + result["usm_user_info"].append(tmp_dict) + + cur_cfg = dict() + if usm_user_name: + cur_cfg["userName"] = usm_user_name + if user_group: + cur_cfg["groupName"] = user_group + if auth_protocol: + cur_cfg["authProtocol"] = auth_protocol + if auth_key: + cur_cfg["authKey"] = auth_key + if priv_protocol: + cur_cfg["privProtocol"] = priv_protocol + if priv_key: + cur_cfg["privKey"] = priv_key + if acl_number: + cur_cfg["aclNumber"] = acl_number + + if remote_engine_id: + cur_cfg["engineID"] = remote_engine_id + cur_cfg["remoteEngineID"] = "true" + else: + cur_cfg["engineID"] = self.local_engine_id + cur_cfg["remoteEngineID"] = "false" + + if result["usm_user_info"]: + num = 0 + for tmp in result["usm_user_info"]: + if cur_cfg == tmp: + num += 1 + + if num == 0: + if state == "present": + need_cfg = True + else: + need_cfg = False + else: + if state == "present": + need_cfg = False + else: + need_cfg = True + + else: + if state == "present": + need_cfg = True + else: + need_cfg = False + + result["need_cfg"] = need_cfg + return result + + def check_snmp_v3_local_user_args(self, **kwargs): + """ Check snmp v3 local user invalid args """ + + module = kwargs["module"] + result = dict() + + need_cfg = False + state = module.params['state'] + local_user_name = module.params['aaa_local_user'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + usm_user_name = module.params['usm_user_name'] + + if local_user_name: + + if usm_user_name: + module.fail_json( + msg='Error: Please do not input usm_user_name and local_user_name at the same time.') + + if not auth_protocol or not auth_key or not priv_protocol or not priv_key: + module.fail_json( + msg='Error: Please input auth_protocol auth_key priv_protocol priv_key for local user.') + + if len(local_user_name) > 32 or len(local_user_name) == 0: + module.fail_json( + msg='Error: The length of local_user_name %s is out of [1 - 32].' % local_user_name) + + if len(auth_key) > 255 or len(auth_key) == 0: + module.fail_json( + msg='Error: The length of auth_key %s is out of [1 - 255].' % auth_key) + + if len(priv_key) > 255 or len(priv_key) == 0: + module.fail_json( + msg='Error: The length of priv_key %s is out of [1 - 255].' % priv_key) + + result["local_user_info"] = [] + + conf_str = CE_GET_SNMP_V3_LOCAL_USER + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + + if "" in recv_xml: + if state == "present": + need_cfg = True + + else: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + local_user_info = root.findall( + "snmp/localUsers/localUser") + if local_user_info: + for tmp in local_user_info: + tmp_dict = dict() + for site in tmp: + if site.tag in ["userName", "authProtocol", "authKey", "privProtocol", "privKey"]: + tmp_dict[site.tag] = site.text + + result["local_user_info"].append(tmp_dict) + + if result["local_user_info"]: + for tmp in result["local_user_info"]: + if "userName" in tmp.keys(): + if state == "present": + if tmp["userName"] != local_user_name: + need_cfg = True + else: + if tmp["userName"] == local_user_name: + need_cfg = True + if auth_protocol: + if "authProtocol" in tmp.keys(): + if state == "present": + if tmp["authProtocol"] != auth_protocol: + need_cfg = True + else: + if tmp["authProtocol"] == auth_protocol: + need_cfg = True + if auth_key: + if "authKey" in tmp.keys(): + if state == "present": + if tmp["authKey"] != auth_key: + need_cfg = True + else: + if tmp["authKey"] == auth_key: + need_cfg = True + if priv_protocol: + if "privProtocol" in tmp.keys(): + if state == "present": + if tmp["privProtocol"] != priv_protocol: + need_cfg = True + else: + if tmp["privProtocol"] == priv_protocol: + need_cfg = True + if priv_key: + if "privKey" in tmp.keys(): + if state == "present": + if tmp["privKey"] != priv_key: + need_cfg = True + else: + if tmp["privKey"] == priv_key: + need_cfg = True + + result["need_cfg"] = need_cfg + return result + + def merge_snmp_v3_usm_user(self, **kwargs): + """ Merge snmp v3 usm user operation """ + + module = kwargs["module"] + usm_user_name = module.params['usm_user_name'] + remote_engine_id = module.params['remote_engine_id'] + acl_number = module.params['acl_number'] + user_group = module.params['user_group'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + cmds = [] + + if remote_engine_id: + conf_str = CE_MERGE_SNMP_V3_USM_USER_HEADER % ( + usm_user_name, "true", remote_engine_id) + cmd = "snmp-agent remote-engineid %s usm-user v3 %s" % ( + remote_engine_id, usm_user_name) + else: + if not self.local_engine_id: + module.fail_json( + msg='Error: The local engine id is null, please input remote_engine_id.') + + conf_str = CE_MERGE_SNMP_V3_USM_USER_HEADER % ( + usm_user_name, "false", self.local_engine_id) + cmd = "snmp-agent usm-user v3 %s" % usm_user_name + + if user_group: + conf_str += "%s" % user_group + cmd += " %s" % user_group + + if acl_number: + conf_str += "%s" % acl_number + cmd += " acl %s" % acl_number + + cmds.append(cmd) + + if remote_engine_id: + cmd = "snmp-agent remote-engineid %s usm-user v3 %s" % ( + remote_engine_id, usm_user_name) + else: + cmd = "snmp-agent usm-user v3 %s" % usm_user_name + + if auth_protocol: + conf_str += "%s" % auth_protocol + + if auth_protocol != "noAuth": + cmd += " authentication-mode %s" % auth_protocol + + if auth_key: + conf_str += "%s" % auth_key + + if auth_protocol != "noAuth": + cmd += " cipher %s" % "******" + if auth_protocol or auth_key: + cmds.append(cmd) + + if remote_engine_id: + cmd = "snmp-agent remote-engineid %s usm-user v3 %s" % ( + remote_engine_id, usm_user_name) + else: + cmd = "snmp-agent usm-user v3 %s" % usm_user_name + + if priv_protocol: + conf_str += "%s" % priv_protocol + + if auth_protocol != "noAuth" and priv_protocol != "noPriv": + cmd += " privacy-mode %s" % priv_protocol + + if priv_key: + conf_str += "%s" % priv_key + + if auth_protocol != "noAuth" and priv_protocol != "noPriv": + cmd += " cipher %s" % "******" + if priv_key or priv_protocol: + cmds.append(cmd) + + conf_str += CE_MERGE_SNMP_V3_USM_USER_TAIL + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge snmp v3 usm user failed.') + + return cmds + + def create_snmp_v3_usm_user(self, **kwargs): + """ Create snmp v3 usm user operation """ + + module = kwargs["module"] + usm_user_name = module.params['usm_user_name'] + remote_engine_id = module.params['remote_engine_id'] + acl_number = module.params['acl_number'] + user_group = module.params['user_group'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + cmds = [] + + if remote_engine_id: + conf_str = CE_CREATE_SNMP_V3_USM_USER_HEADER % ( + usm_user_name, "true", remote_engine_id) + cmd = "snmp-agent remote-engineid %s usm-user v3 %s" % ( + remote_engine_id, usm_user_name) + else: + if not self.local_engine_id: + module.fail_json( + msg='Error: The local engine id is null, please input remote_engine_id.') + + conf_str = CE_CREATE_SNMP_V3_USM_USER_HEADER % ( + usm_user_name, "false", self.local_engine_id) + cmd = "snmp-agent usm-user v3 %s" % usm_user_name + + if user_group: + conf_str += "%s" % user_group + cmd += " %s" % user_group + + if acl_number: + conf_str += "%s" % acl_number + cmd += " acl %s" % acl_number + cmds.append(cmd) + + if remote_engine_id: + cmd = "snmp-agent remote-engineid %s usm-user v3 %s" % ( + remote_engine_id, usm_user_name) + else: + cmd = "snmp-agent usm-user v3 %s" % usm_user_name + + if auth_protocol: + conf_str += "%s" % auth_protocol + + if auth_protocol != "noAuth": + cmd += " authentication-mode %s" % auth_protocol + + if auth_key: + conf_str += "%s" % auth_key + + if auth_protocol != "noAuth": + cmd += " cipher %s" % "******" + + if auth_key or auth_protocol: + cmds.append(cmd) + + if remote_engine_id: + cmd = "snmp-agent remote-engineid %s usm-user v3 %s" % ( + remote_engine_id, usm_user_name) + else: + cmd = "snmp-agent usm-user v3 %s" % usm_user_name + + if priv_protocol: + conf_str += "%s" % priv_protocol + + if auth_protocol != "noAuth" and priv_protocol != "noPriv": + cmd += " privacy-mode %s" % priv_protocol + + if priv_key: + conf_str += "%s" % priv_key + + if auth_protocol != "noAuth" and priv_protocol != "noPriv": + cmd += " cipher %s" % "******" + + if priv_protocol or priv_key: + cmds.append(cmd) + + conf_str += CE_CREATE_SNMP_V3_USM_USER_TAIL + + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create snmp v3 usm user failed.') + + return cmds + + def delete_snmp_v3_usm_user(self, **kwargs): + """ Delete snmp v3 usm user operation """ + + module = kwargs["module"] + usm_user_name = module.params['usm_user_name'] + remote_engine_id = module.params['remote_engine_id'] + acl_number = module.params['acl_number'] + user_group = module.params['user_group'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + if remote_engine_id: + conf_str = CE_DELETE_SNMP_V3_USM_USER_HEADER % ( + usm_user_name, "true", remote_engine_id) + cmd = "undo snmp-agent remote-engineid %s usm-user v3 %s" % ( + remote_engine_id, usm_user_name) + else: + if not self.local_engine_id: + module.fail_json( + msg='Error: The local engine id is null, please input remote_engine_id.') + + conf_str = CE_DELETE_SNMP_V3_USM_USER_HEADER % ( + usm_user_name, "false", self.local_engine_id) + cmd = "undo snmp-agent usm-user v3 %s" % usm_user_name + + if user_group: + conf_str += "%s" % user_group + + if acl_number: + conf_str += "%s" % acl_number + + if auth_protocol: + conf_str += "%s" % auth_protocol + + if auth_key: + conf_str += "%s" % auth_key + + if priv_protocol: + conf_str += "%s" % priv_protocol + + if priv_key: + conf_str += "%s" % priv_key + + conf_str += CE_DELETE_SNMP_V3_USM_USER_TAIL + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete snmp v3 usm user failed.') + + return cmd + + def merge_snmp_v3_local_user(self, **kwargs): + """ Merge snmp v3 local user operation """ + + module = kwargs["module"] + local_user_name = module.params['aaa_local_user'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + conf_str = CE_MERGE_SNMP_V3_LOCAL_USER % ( + local_user_name, auth_protocol, auth_key, priv_protocol, priv_key) + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Merge snmp v3 local user failed.') + + cmd = "snmp-agent local-user v3 %s " % local_user_name + "authentication-mode %s " % auth_protocol + \ + "cipher ****** " + "privacy-mode %s " % priv_protocol + "cipher ******" + + return cmd + + def create_snmp_v3_local_user(self, **kwargs): + """ Create snmp v3 local user operation """ + + module = kwargs["module"] + local_user_name = module.params['aaa_local_user'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + conf_str = CE_CREATE_SNMP_V3_LOCAL_USER % ( + local_user_name, auth_protocol, auth_key, priv_protocol, priv_key) + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Create snmp v3 local user failed.') + + cmd = "snmp-agent local-user v3 %s " % local_user_name + "authentication-mode %s " % auth_protocol + \ + "cipher ****** " + "privacy-mode %s " % priv_protocol + "cipher ******" + + return cmd + + def delete_snmp_v3_local_user(self, **kwargs): + """ Delete snmp v3 local user operation """ + + module = kwargs["module"] + local_user_name = module.params['aaa_local_user'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + + conf_str = CE_DELETE_SNMP_V3_LOCAL_USER % ( + local_user_name, auth_protocol, auth_key, priv_protocol, priv_key) + recv_xml = self.netconf_set_config(module=module, conf_str=conf_str) + + if "" not in recv_xml: + module.fail_json(msg='Error: Delete snmp v3 local user failed.') + + cmd = "undo snmp-agent local-user v3 %s" % local_user_name + + return cmd + + def get_snmp_local_engine(self, **kwargs): + """ Get snmp local engine operation """ + + module = kwargs["module"] + + conf_str = GET_SNMP_LOCAL_ENGINE + recv_xml = self.netconf_get_config(module=module, conf_str=conf_str) + if "" in recv_xml: + xml_str = recv_xml.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + local_engine_info = root.findall("snmp/engine/engineID") + if local_engine_info: + self.local_engine_id = local_engine_info[0].text + + +def main(): + """ Module main function """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + acl_number=dict(type='str'), + usm_user_name=dict(type='str'), + remote_engine_id=dict(type='str'), + user_group=dict(type='str'), + auth_protocol=dict(choices=['noAuth', 'md5', 'sha']), + auth_key=dict(type='str', no_log=True), + priv_protocol=dict( + choices=['noPriv', 'des56', '3des168', 'aes128', 'aes192', 'aes256']), + priv_key=dict(type='str', no_log=True), + aaa_local_user=dict(type='str') + ) + + mutually_exclusive = [("usm_user_name", "local_user_name")] + argument_spec.update(ce_argument_spec) + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True + ) + + changed = False + proposed = dict() + existing = dict() + end_state = dict() + updates = [] + + state = module.params['state'] + acl_number = module.params['acl_number'] + usm_user_name = module.params['usm_user_name'] + remote_engine_id = module.params['remote_engine_id'] + user_group = module.params['user_group'] + auth_protocol = module.params['auth_protocol'] + auth_key = module.params['auth_key'] + priv_protocol = module.params['priv_protocol'] + priv_key = module.params['priv_key'] + aaa_local_user = module.params['aaa_local_user'] + + snmp_user_obj = SnmpUser() + + if not snmp_user_obj: + module.fail_json(msg='Error: Init module failed.') + + # get proposed + proposed["state"] = state + if acl_number: + proposed["acl_number"] = acl_number + if usm_user_name: + proposed["usm_user_name"] = usm_user_name + if remote_engine_id: + proposed["remote_engine_id"] = remote_engine_id + if user_group: + proposed["user_group"] = user_group + if auth_protocol: + proposed["auth_protocol"] = auth_protocol + if auth_key: + proposed["auth_key"] = auth_key + if priv_protocol: + proposed["priv_protocol"] = priv_protocol + if priv_key: + proposed["priv_key"] = priv_key + if aaa_local_user: + proposed["aaa_local_user"] = aaa_local_user + + snmp_user_obj.get_snmp_local_engine(module=module) + snmp_v3_usm_user_rst = snmp_user_obj.check_snmp_v3_usm_user_args( + module=module) + snmp_v3_local_user_rst = snmp_user_obj.check_snmp_v3_local_user_args( + module=module) + + # state exist snmp v3 user config + exist_tmp = dict() + for item in snmp_v3_usm_user_rst: + if item != "need_cfg": + exist_tmp[item] = snmp_v3_usm_user_rst[item] + if exist_tmp: + existing["snmp usm user"] = exist_tmp + + exist_tmp = dict() + for item in snmp_v3_local_user_rst: + if item != "need_cfg": + exist_tmp[item] = snmp_v3_local_user_rst[item] + if exist_tmp: + existing["snmp local user"] = exist_tmp + + if state == "present": + if snmp_v3_usm_user_rst["need_cfg"]: + if len(snmp_v3_usm_user_rst["usm_user_info"]) != 0: + cmd = snmp_user_obj.merge_snmp_v3_usm_user(module=module) + changed = True + updates.append(cmd) + else: + cmd = snmp_user_obj.create_snmp_v3_usm_user(module=module) + changed = True + updates.append(cmd) + + if snmp_v3_local_user_rst["need_cfg"]: + if len(snmp_v3_local_user_rst["local_user_info"]) != 0: + cmd = snmp_user_obj.merge_snmp_v3_local_user( + module=module) + changed = True + updates.append(cmd) + else: + cmd = snmp_user_obj.create_snmp_v3_local_user( + module=module) + changed = True + updates.append(cmd) + + else: + if snmp_v3_usm_user_rst["need_cfg"]: + cmd = snmp_user_obj.delete_snmp_v3_usm_user(module=module) + changed = True + updates.append(cmd) + if snmp_v3_local_user_rst["need_cfg"]: + cmd = snmp_user_obj.delete_snmp_v3_local_user(module=module) + changed = True + updates.append(cmd) + + # state exist snmp v3 user config + snmp_v3_usm_user_rst = snmp_user_obj.check_snmp_v3_usm_user_args( + module=module) + end_tmp = dict() + for item in snmp_v3_usm_user_rst: + if item != "need_cfg": + end_tmp[item] = snmp_v3_usm_user_rst[item] + if end_tmp: + end_state["snmp usm user"] = end_tmp + + snmp_v3_local_user_rst = snmp_user_obj.check_snmp_v3_local_user_args( + module=module) + end_tmp = dict() + for item in snmp_v3_local_user_rst: + if item != "need_cfg": + end_tmp[item] = snmp_v3_local_user_rst[item] + if end_tmp: + end_state["snmp local user"] = end_tmp + + results = dict() + results['proposed'] = proposed + results['existing'] = existing + results['changed'] = changed + results['end_state'] = end_state + results['updates'] = updates + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_startup.py b/plugins/modules/ce_startup.py new file mode 100644 index 0000000..209be2b --- /dev/null +++ b/plugins/modules/ce_startup.py @@ -0,0 +1,466 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_startup +short_description: Manages a system startup information on HUAWEI CloudEngine switches. +description: + - Manages a system startup information on HUAWEI CloudEngine switches. +author: + - Li Yanfeng (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + cfg_file: + description: + - Name of the configuration file that is applied for the next startup. + The value is a string of 5 to 255 characters. + default: present + software_file: + description: + - File name of the system software that is applied for the next startup. + The value is a string of 5 to 255 characters. + patch_file: + description: + - Name of the patch file that is applied for the next startup. + slot: + description: + - Position of the device.The value is a string of 1 to 32 characters. + The possible value of slot is all, slave-board, or the specific slotID. + action: + description: + - Display the startup information. + choices: ['display'] + +''' + +EXAMPLES = ''' +- name: startup module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Display startup information + ce_startup: + action: display + provider: "{{ cli }}" + + - name: Set startup patch file + ce_startup: + patch_file: 2.PAT + slot: all + provider: "{{ cli }}" + + - name: Set startup software file + ce_startup: + software_file: aa.cc + slot: 1 + provider: "{{ cli }}" + + - name: Set startup cfg file + ce_startup: + cfg_file: 2.cfg + slot: 1 + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"patch_file": "2.PAT", + "slot": "all"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: { + "configSysSoft": "flash:/CE12800-V200R002C20_issuB071.cc", + "curentPatchFile": "NULL", + "curentStartupFile": "NULL", + "curentSysSoft": "flash:/CE12800-V200R002C20_issuB071.cc", + "nextPatchFile": "flash:/1.PAT", + "nextStartupFile": "flash:/1.cfg", + "nextSysSoft": "flash:/CE12800-V200R002C20_issuB071.cc", + "position": "5" + } +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"StartupInfos": null} +updates: + description: command sent to the device + returned: always + type: list + sample: {"startup patch 2.PAT all"} +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, run_commands +from ansible.module_utils.connection import exec_command + + +class StartUp(object): + """ + Manages system startup information. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.cfg_file = self.module.params['cfg_file'] + self.software_file = self.module.params['software_file'] + self.patch_file = self.module.params['patch_file'] + self.slot = self.module.params['slot'] + self.action = self.module.params['action'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + # system startup info + self.startup_info = None + + def init_module(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_startup_dict(self): + """Retrieves the current config from the device or cache + """ + cmd = 'display startup' + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + startup_info = dict() + startup_info["StartupInfos"] = list() + if not cfg: + return startup_info + else: + re_find = re.findall(r'(.*)\s*' + r'\s*Configured\s*startup\s*system\s*software:\s*(.*)' + r'\s*Startup\s*system\s*software:\s*(.*)' + r'\s*Next\s*startup\s*system\s*software:\s*(.*)' + r'\s*Startup\s*saved-configuration\s*file:\s*(.*)' + r'\s*Next\s*startup\s*saved-configuration\s*file:\s*(.*)' + r'\s*Startup\s*paf\s*file:\s*(.*)' + r'\s*Next\s*startup\s*paf\s*file:\s*(.*)' + r'\s*Startup\s*patch\s*package:\s*(.*)' + r'\s*Next\s*startup\s*patch\s*package:\s*(.*)', cfg) + + if re_find: + for mem in re_find: + startup_info["StartupInfos"].append( + dict(nextStartupFile=mem[5], configSysSoft=mem[1], curentSysSoft=mem[2], + nextSysSoft=mem[3], curentStartupFile=mem[4], curentPatchFile=mem[8], + nextPatchFile=mem[9], postion=mem[0])) + return startup_info + return startup_info + + def get_cfg_filename_type(self, filename): + """Gets the type of cfg filename, such as cfg, zip, dat...""" + + if filename is None: + return None + if ' ' in filename: + self.module.fail_json( + msg='Error: Configuration file name include spaces.') + + iftype = None + + if filename.endswith('.cfg'): + iftype = 'cfg' + elif filename.endswith('.zip'): + iftype = 'zip' + elif filename.endswith('.dat'): + iftype = 'dat' + else: + return None + return iftype.lower() + + def get_pat_filename_type(self, filename): + """Gets the type of patch filename, such as cfg, zip, dat...""" + + if filename is None: + return None + if ' ' in filename: + self.module.fail_json( + msg='Error: Patch file name include spaces.') + + iftype = None + + if filename.endswith('.PAT'): + iftype = 'PAT' + else: + return None + return iftype.upper() + + def get_software_filename_type(self, filename): + """Gets the type of software filename, such as cfg, zip, dat...""" + + if filename is None: + return None + if ' ' in filename: + self.module.fail_json( + msg='Error: Software file name include spaces.') + + iftype = None + + if filename.endswith('.cc'): + iftype = 'cc' + else: + return None + return iftype.lower() + + def startup_next_cfg_file(self): + """set next cfg file""" + commands = list() + cmd = {'output': None, 'command': ''} + if self.slot: + cmd['command'] = "startup saved-configuration %s slot %s" % ( + self.cfg_file, self.slot) + commands.append(cmd) + self.updates_cmd.append( + "startup saved-configuration %s slot %s" % (self.cfg_file, self.slot)) + run_commands(self.module, commands) + self.changed = True + else: + cmd['command'] = "startup saved-configuration %s" % self.cfg_file + commands.append(cmd) + self.updates_cmd.append( + "startup saved-configuration %s" % self.cfg_file) + run_commands(self.module, commands) + self.changed = True + + def startup_next_software_file(self): + """set next software file""" + commands = list() + cmd = {'output': None, 'command': ''} + if self.slot: + if self.slot == "all" or self.slot == "slave-board": + cmd['command'] = "startup system-software %s %s" % ( + self.software_file, self.slot) + commands.append(cmd) + self.updates_cmd.append( + "startup system-software %s %s" % (self.software_file, self.slot)) + run_commands(self.module, commands) + self.changed = True + else: + cmd['command'] = "startup system-software %s slot %s" % ( + self.software_file, self.slot) + commands.append(cmd) + self.updates_cmd.append( + "startup system-software %s slot %s" % (self.software_file, self.slot)) + run_commands(self.module, commands) + self.changed = True + + if not self.slot: + cmd['command'] = "startup system-software %s" % self.software_file + commands.append(cmd) + self.updates_cmd.append( + "startup system-software %s" % self.software_file) + run_commands(self.module, commands) + self.changed = True + + def startup_next_pat_file(self): + """set next patch file""" + + commands = list() + cmd = {'output': None, 'command': ''} + if self.slot: + if self.slot == "all": + cmd['command'] = "startup patch %s %s" % ( + self.patch_file, self.slot) + commands.append(cmd) + self.updates_cmd.append( + "startup patch %s %s" % (self.patch_file, self.slot)) + run_commands(self.module, commands) + self.changed = True + else: + cmd['command'] = "startup patch %s slot %s" % ( + self.patch_file, self.slot) + commands.append(cmd) + self.updates_cmd.append( + "startup patch %s slot %s" % (self.patch_file, self.slot)) + run_commands(self.module, commands) + self.changed = True + + if not self.slot: + cmd['command'] = "startup patch %s" % self.patch_file + commands.append(cmd) + self.updates_cmd.append( + "startup patch %s" % self.patch_file) + run_commands(self.module, commands) + self.changed = True + + def check_params(self): + """Check all input params""" + + # cfg_file check + if self.cfg_file: + if not self.get_cfg_filename_type(self.cfg_file): + self.module.fail_json( + msg='Error: Invalid cfg file name or cfg file name extension ( *.cfg, *.zip, *.dat ).') + + # software_file check + if self.software_file: + if not self.get_software_filename_type(self.software_file): + self.module.fail_json( + msg='Error: Invalid software file name or software file name extension ( *.cc).') + + # patch_file check + if self.patch_file: + if not self.get_pat_filename_type(self.patch_file): + self.module.fail_json( + msg='Error: Invalid patch file name or patch file name extension ( *.PAT ).') + + # slot check + if self.slot: + if self.slot.isdigit(): + if int(self.slot) <= 0 or int(self.slot) > 16: + self.module.fail_json( + msg='Error: The number of slot is not in the range from 1 to 16.') + else: + if len(self.slot) <= 0 or len(self.slot) > 32: + self.module.fail_json( + msg='Error: The length of slot is not in the range from 1 to 32.') + + def get_proposed(self): + """get proposed info""" + + if self.cfg_file: + self.proposed["cfg_file"] = self.cfg_file + if self.software_file: + self.proposed["system_file"] = self.software_file + if self.patch_file: + self.proposed["patch_file"] = self.patch_file + if self.slot: + self.proposed["slot"] = self.slot + + def get_existing(self): + """get existing info""" + + if not self.startup_info: + self.existing["StartupInfos"] = None + else: + self.existing["StartupInfos"] = self.startup_info["StartupInfos"] + + def get_end_state(self): + """get end state info""" + if not self.startup_info: + self.end_state["StartupInfos"] = None + else: + self.end_state["StartupInfos"] = self.startup_info["StartupInfos"] + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.get_proposed() + + self.startup_info = self.get_startup_dict() + self.get_existing() + + startup_info = self.startup_info["StartupInfos"][0] + if self.cfg_file: + if self.cfg_file != startup_info["nextStartupFile"]: + self.startup_next_cfg_file() + + if self.software_file: + if self.software_file != startup_info["nextSysSoft"]: + self.startup_next_software_file() + if self.patch_file: + if self.patch_file != startup_info["nextPatchFile"]: + self.startup_next_pat_file() + if self.action == "display": + self.startup_info = self.get_startup_dict() + + self.startup_info = self.get_startup_dict() + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + cfg_file=dict(type='str'), + software_file=dict(type='str'), + patch_file=dict(type='str'), + slot=dict(type='str'), + action=dict(type='str', choices=['display']) + ) + argument_spec.update(ce_argument_spec) + module = StartUp(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_static_route.py b/plugins/modules/ce_static_route.py new file mode 100644 index 0000000..1d45560 --- /dev/null +++ b/plugins/modules/ce_static_route.py @@ -0,0 +1,830 @@ +#!/usr/bin/python + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_static_route +short_description: Manages static route configuration on HUAWEI CloudEngine switches. +description: + - Manages the static routes on HUAWEI CloudEngine switches. +author: Yang yang (@QijunPan) +notes: + - If no vrf is supplied, vrf is set to default. + - If I(state=absent), the route will be removed, regardless of the non-required parameters. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + prefix: + description: + - Destination ip address of static route. + required: true + mask: + description: + - Destination ip mask of static route. + required: true + aftype: + description: + - Destination ip address family type of static route. + required: true + choices: ['v4','v6'] + next_hop: + description: + - Next hop address of static route. + nhp_interface: + description: + - Next hop interface full name of static route. + vrf: + description: + - VPN instance of destination ip address. + destvrf: + description: + - VPN instance of next hop ip address. + tag: + description: + - Route tag value (numeric). + description: + description: + - Name of the route. Used with the name parameter on the CLI. + pref: + description: + - Preference or administrative difference of route (range 1-255). + state: + description: + - Specify desired state of the resource. + choices: ['present','absent'] + default: present +''' + +EXAMPLES = ''' +- name: static route module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Config a ipv4 static route, next hop is an address and that it has the proper description + ce_static_route: + prefix: 2.1.1.2 + mask: 24 + next_hop: 3.1.1.2 + description: 'Configured by Ansible' + aftype: v4 + provider: "{{ cli }}" + - name: Config a ipv4 static route ,next hop is an interface and that it has the proper description + ce_static_route: + prefix: 2.1.1.2 + mask: 24 + next_hop: 10GE1/0/1 + description: 'Configured by Ansible' + aftype: v4 + provider: "{{ cli }}" + - name: Config a ipv6 static route, next hop is an address and that it has the proper description + ce_static_route: + prefix: fc00:0:0:2001::1 + mask: 64 + next_hop: fc00:0:0:2004::1 + description: 'Configured by Ansible' + aftype: v6 + provider: "{{ cli }}" + - name: Config a ipv4 static route, next hop is an interface and that it has the proper description + ce_static_route: + prefix: fc00:0:0:2001::1 + mask: 64 + next_hop: 10GE1/0/1 + description: 'Configured by Ansible' + aftype: v6 + provider: "{{ cli }}" + - name: Config a VRF and set ipv4 static route, next hop is an address and that it has the proper description + ce_static_route: + vrf: vpna + prefix: 2.1.1.2 + mask: 24 + next_hop: 3.1.1.2 + description: 'Configured by Ansible' + aftype: v4 + provider: "{{ cli }}" +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"next_hop": "3.3.3.3", "pref": "100", + "prefix": "192.168.20.642", "mask": "24", "description": "testing", + "vrf": "_public_"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"next_hop": "3.3.3.3", "pref": "100", + "prefix": "192.168.20.0", "mask": "24", "description": "testing", + "tag" : "null"} +updates: + description: command list sent to the device + returned: always + type: list + sample: ["ip route-static 192.168.20.0 255.255.255.0 3.3.3.3 preference 100 description testing"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_STATIC_ROUTE = """ + + + + + + + + + + + + + + + + + + + + + +""" + +CE_NC_GET_STATIC_ROUTE_ABSENT = """ + + + + + + + + + + + + + + + + + + +""" + +CE_NC_SET_STATIC_ROUTE = """ + + + + + %s + %s + base + %s + %s + %s + %s + %s%s%s%s + + + + +""" +CE_NC_SET_DESCRIPTION = """ +%s +""" + +CE_NC_SET_PREFERENCE = """ +%s +""" + +CE_NC_SET_TAG = """ +%s +""" + +CE_NC_DELETE_STATIC_ROUTE = """ + + + + + %s + %s + base + %s + %s + %s + %s + %s + + + + +""" + + +def build_config_xml(xmlstr): + """build config xml""" + + return ' ' + xmlstr + ' ' + + +def is_valid_v4addr(addr): + """check if ipv4 addr is valid""" + if addr.find('.') != -1: + addr_list = addr.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + return False + + +def is_valid_v6addr(addr): + """check if ipv6 addr is valid""" + if addr.find(':') != -1: + addr_list = addr.split(':') + # The IPv6 binary system has a length of 128 bits and is grouped by 16 bits. + # Each group is separated by a colon ":" and can be divided into 8 groups, each group being represented by 4 hexadecimal + if len(addr_list) > 8: + return False + # You can use a double colon "::" to represent a group of 0 or more consecutive 0s, but only once. + if addr.count('::') > 1: + return False + # if do not use '::', the length of address should not be less than 8. + if addr.count('::') == 0 and len(addr_list) < 8: + return False + for group in addr_list: + if group.strip() == '': + continue + try: + # Each group is represented in 4-digit hexadecimal + int(group, base=16) + except ValueError: + return False + return True + return False + + +def is_valid_tag(tag): + """check if the tag is valid""" + + if not tag.isdigit(): + return False + + if int(tag) < 1 or int(tag) > 4294967295: + return False + + return True + + +def is_valid_preference(pref): + """check if the preference is valid""" + if pref.isdigit(): + return int(pref) > 0 and int(pref) < 256 + else: + return False + + +def is_valid_description(description): + """check if the description is valid""" + if description.find('?') != -1: + return False + if len(description) < 1 or len(description) > 255: + return False + return True + + +class StaticRoute(object): + """static route module""" + + def __init__(self, argument_spec, ): + self.spec = argument_spec + self.module = None + self.init_module() + + # static route info + self.prefix = self.module.params['prefix'] + self.mask = self.module.params['mask'] + self.aftype = self.module.params['aftype'] + self.next_hop = self.module.params['next_hop'] + self.nhp_interface = self.module.params['nhp_interface'] + if self.nhp_interface is None: + self.nhp_interface = "Invalid0" + self.tag = self.module.params['tag'] + self.description = self.module.params['description'] + self.state = self.module.params['state'] + self.pref = self.module.params['pref'] + + # vpn instance info + self.vrf = self.module.params['vrf'] + if self.vrf is None: + self.vrf = "_public_" + self.destvrf = self.module.params['destvrf'] + if self.destvrf is None: + self.destvrf = "_public_" + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + self.static_routes_info = dict() + + def init_module(self): + """init module""" + + required_one_of = [["next_hop", "nhp_interface"]] + self.module = AnsibleModule( + argument_spec=self.spec, required_one_of=required_one_of, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def convert_len_to_mask(self, masklen): + """convert mask length to ip address mask, i.e. 24 to 255.255.255.0""" + + mask_int = ["0"] * 4 + length = int(masklen) + + if length > 32: + self.module.fail_json(msg='IPv4 ipaddress mask length is invalid') + if length < 8: + mask_int[0] = str(int((0xFF << (8 - length % 8)) & 0xFF)) + if length >= 8: + mask_int[0] = '255' + mask_int[1] = str(int((0xFF << (16 - (length % 16))) & 0xFF)) + if length >= 16: + mask_int[1] = '255' + mask_int[2] = str(int((0xFF << (24 - (length % 24))) & 0xFF)) + if length >= 24: + mask_int[2] = '255' + mask_int[3] = str(int((0xFF << (32 - (length % 32))) & 0xFF)) + if length == 32: + mask_int[3] = '255' + + return '.'.join(mask_int) + + def convert_ip_prefix(self): + """convert prefix to real value i.e. 2.2.2.2/24 to 2.2.2.0/24""" + if self.aftype == "v4": + if self.prefix.find('.') == -1: + return False + if self.mask == '32': + return True + if self.mask == '0': + self.prefix = '0.0.0.0' + return True + addr_list = self.prefix.split('.') + length = len(addr_list) + if length > 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + byte_len = 8 + ip_len = int(self.mask) // byte_len + ip_bit = int(self.mask) % byte_len + else: + if self.prefix.find(':') == -1: + return False + if self.mask == '128': + return True + if self.mask == '0': + self.prefix = '::' + return True + addr_list = self.prefix.split(':') + length = len(addr_list) + if length > 6: + return False + byte_len = 16 + ip_len = int(self.mask) // byte_len + ip_bit = int(self.mask) % byte_len + + if self.aftype == "v4": + for i in range(ip_len + 1, length): + addr_list[i] = 0 + else: + for i in range(length - ip_len, length): + addr_list[i] = 0 + for j in range(0, byte_len - ip_bit): + if self.aftype == "v4": + addr_list[ip_len] = int(addr_list[ip_len]) & (0 << j) + else: + if addr_list[length - ip_len - 1] == "": + continue + addr_list[length - ip_len - + 1] = '0x%s' % addr_list[length - ip_len - 1] + addr_list[length - ip_len - + 1] = int(addr_list[length - ip_len - 1], 16) & (0 << j) + + if self.aftype == "v4": + self.prefix = '%s.%s.%s.%s' % (addr_list[0], addr_list[1], addr_list[2], addr_list[3]) + return True + else: + ipv6_addr_str = "" + for num in range(0, length - ip_len): + ipv6_addr_str += '%s:' % addr_list[num] + self.prefix = ipv6_addr_str + return True + + def set_update_cmd(self): + """set update command""" + if not self.changed: + return + if self.aftype == "v4": + aftype = "ip" + maskstr = self.convert_len_to_mask(self.mask) + else: + aftype = "ipv6" + maskstr = self.mask + if self.next_hop is None: + next_hop = '' + else: + next_hop = self.next_hop + if self.vrf == "_public_": + vrf = '' + else: + vrf = self.vrf + if self.destvrf == "_public_": + destvrf = '' + else: + destvrf = self.destvrf + if self.nhp_interface == "Invalid0": + nhp_interface = '' + else: + nhp_interface = self.nhp_interface + if self.state == "present": + if self.vrf != "_public_": + if self.destvrf != "_public_": + self.updates_cmd.append('%s route-static vpn-instance %s %s %s vpn-instance %s %s' + % (aftype, vrf, self.prefix, maskstr, destvrf, next_hop)) + else: + self.updates_cmd.append('%s route-static vpn-instance %s %s %s %s %s' + % (aftype, vrf, self.prefix, maskstr, nhp_interface, next_hop)) + elif self.destvrf != "_public_": + self.updates_cmd.append('%s route-static %s %s vpn-instance %s %s' + % (aftype, self.prefix, maskstr, self.destvrf, next_hop)) + else: + self.updates_cmd.append('%s route-static %s %s %s %s' + % (aftype, self.prefix, maskstr, nhp_interface, next_hop)) + if self.pref: + self.updates_cmd[0] += ' preference %s' % (self.pref) + if self.tag: + self.updates_cmd[0] += ' tag %s' % (self.tag) + if self.description: + self.updates_cmd[0] += ' description %s' % (self.description) + + if self.state == "absent": + if self.vrf != "_public_": + if self.destvrf != "_public_": + self.updates_cmd.append('undo %s route-static vpn-instance %s %s %s vpn-instance %s %s' + % (aftype, vrf, self.prefix, maskstr, destvrf, next_hop)) + else: + self.updates_cmd.append('undo %s route-static vpn-instance %s %s %s %s %s' + % (aftype, vrf, self.prefix, maskstr, nhp_interface, next_hop)) + elif self.destvrf != "_public_": + self.updates_cmd.append('undo %s route-static %s %s vpn-instance %s %s' + % (aftype, self.prefix, maskstr, self.destvrf, self.next_hop)) + else: + self.updates_cmd.append('undo %s route-static %s %s %s %s' + % (aftype, self.prefix, maskstr, nhp_interface, next_hop)) + + def operate_static_route(self, version, prefix, mask, nhp_interface, next_hop, vrf, destvrf, state): + """operate ipv4 static route""" + + description_xml = """\n""" + preference_xml = """\n""" + tag_xml = """\n""" + if next_hop is None: + next_hop = '0.0.0.0' + if nhp_interface is None: + nhp_interface = "Invalid0" + + if vrf is None: + vpn_instance = "_public_" + else: + vpn_instance = vrf + + if destvrf is None: + dest_vpn_instance = "_public_" + else: + dest_vpn_instance = destvrf + if self.description: + description_xml = CE_NC_SET_DESCRIPTION % self.description + if self.pref: + preference_xml = CE_NC_SET_PREFERENCE % self.pref + if self.tag: + tag_xml = CE_NC_SET_TAG % self.tag + + if state == "present": + configxmlstr = CE_NC_SET_STATIC_ROUTE % ( + vpn_instance, version, prefix, mask, nhp_interface, + dest_vpn_instance, next_hop, description_xml, preference_xml, tag_xml) + else: + configxmlstr = CE_NC_DELETE_STATIC_ROUTE % ( + vpn_instance, version, prefix, mask, nhp_interface, dest_vpn_instance, next_hop) + + conf_str = build_config_xml(configxmlstr) + + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "OPERATE_STATIC_ROUTE") + + def get_static_route(self, state): + """get ipv4 static route""" + + self.static_routes_info["sroute"] = list() + + if state == 'absent': + getxmlstr = CE_NC_GET_STATIC_ROUTE_ABSENT + else: + getxmlstr = CE_NC_GET_STATIC_ROUTE + + xml_str = get_nc_config(self.module, getxmlstr) + + if 'data/' in xml_str: + return + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + static_routes = root.findall( + "staticrt/staticrtbase/srRoutes/srRoute") + + if static_routes: + for static_route in static_routes: + static_info = dict() + for static_ele in static_route: + if static_ele.tag in ["vrfName", "afType", "topologyName", + "prefix", "maskLength", "destVrfName", + "nexthop", "ifName", "preference", "description"]: + static_info[ + static_ele.tag] = static_ele.text + if static_ele.tag == "tag": + if static_ele.text is not None: + static_info["tag"] = static_ele.text + else: + static_info["tag"] = "None" + self.static_routes_info["sroute"].append(static_info) + + def check_params(self): + """check all input params""" + + # check prefix and mask + if not self.mask.isdigit(): + self.module.fail_json(msg='Error: Mask is invalid.') + # ipv4 check + if self.aftype == "v4": + if int(self.mask) > 32 or int(self.mask) < 0: + self.module.fail_json( + msg='Error: Ipv4 mask must be an integer between 1 and 32.') + # next_hop check + if self.next_hop: + if not is_valid_v4addr(self.next_hop): + self.module.fail_json( + msg='Error: The %s is not a valid address' % self.next_hop) + # ipv6 check + if self.aftype == "v6": + if int(self.mask) > 128 or int(self.mask) < 0: + self.module.fail_json( + msg='Error: Ipv6 mask must be an integer between 1 and 128.') + if self.next_hop: + if not is_valid_v6addr(self.next_hop): + self.module.fail_json( + msg='Error: The %s is not a valid address' % self.next_hop) + + # description check + if self.description: + if not is_valid_description(self.description): + self.module.fail_json( + msg='Error: Dsecription length should be 1 - 35, and can not contain "?".') + # tag check + if self.tag: + if not is_valid_tag(self.tag): + self.module.fail_json( + msg='Error: Tag should be integer 1 - 4294967295.') + # preference check + if self.pref: + if not is_valid_preference(self.pref): + self.module.fail_json( + msg='Error: Preference should be integer 1 - 255.') + if self.nhp_interface != "Invalid0" and self.destvrf != "_public_": + self.module.fail_json( + msg='Error: Destination vrf dose no support next hop is interface.') + # convert prefix + if not self.convert_ip_prefix(): + self.module.fail_json( + msg='Error: The %s is not a valid address' % self.prefix) + + def set_ip_static_route(self): + """set ip static route""" + if not self.changed: + return + version = None + if self.aftype == "v4": + version = "ipv4unicast" + else: + version = "ipv6unicast" + self.operate_static_route(version, self.prefix, self.mask, self.nhp_interface, + self.next_hop, self.vrf, self.destvrf, self.state) + + def is_prefix_exist(self, static_route, version): + """is prefix mask nex_thop exist""" + if static_route is None: + return False + if self.next_hop and self.nhp_interface: + return static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["afType"] == version \ + and static_route["ifName"].lower() == self.nhp_interface.lower() \ + and static_route["nexthop"].lower() == self.next_hop.lower() + + if self.next_hop and not self.nhp_interface: + return static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["afType"] == version \ + and static_route["nexthop"].lower() == self.next_hop.lower() + + if not self.next_hop and self.nhp_interface: + return static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["afType"] == version \ + and static_route["ifName"].lower() == self.nhp_interface.lower() + + def get_ip_static_route(self): + """get ip static route""" + + if self.aftype == "v4": + version = "ipv4unicast" + else: + version = "ipv6unicast" + change = False + self.get_static_route(self.state) + if self.state == 'present': + for static_route in self.static_routes_info["sroute"]: + if self.is_prefix_exist(static_route, version): + if self.vrf: + if static_route["vrfName"] != self.vrf: + change = True + if self.tag: + if static_route["tag"] != self.tag: + change = True + if self.destvrf: + if static_route["destVrfName"] != self.destvrf: + change = True + if self.description: + if static_route["description"] != self.description: + change = True + if self.pref: + if static_route["preference"] != self.pref: + change = True + if self.nhp_interface: + if static_route["ifName"].lower() != self.nhp_interface.lower(): + change = True + if self.next_hop: + if static_route["nexthop"].lower() != self.next_hop.lower(): + change = True + return change + else: + continue + change = True + else: + for static_route in self.static_routes_info["sroute"]: + if static_route["nexthop"] and self.next_hop: + if static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["nexthop"].lower() == self.next_hop.lower() \ + and static_route["afType"] == version: + change = True + return change + if static_route["ifName"] and self.nhp_interface: + if static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["ifName"].lower() == self.nhp_interface.lower() \ + and static_route["afType"] == version: + change = True + return change + else: + continue + change = False + return change + + def get_proposed(self): + """get proposed information""" + + self.proposed['prefix'] = self.prefix + self.proposed['mask'] = self.mask + self.proposed['afType'] = self.aftype + self.proposed['next_hop'] = self.next_hop + self.proposed['ifName'] = self.nhp_interface + self.proposed['vrfName'] = self.vrf + self.proposed['destVrfName'] = self.destvrf + if self.tag: + self.proposed['tag'] = self.tag + if self.description: + self.proposed['description'] = self.description + if self.pref is None: + self.proposed['preference'] = 60 + else: + self.proposed['preference'] = self.pref + self.proposed['state'] = self.state + + def get_existing(self): + """get existing information""" + + change = self.get_ip_static_route() + self.existing['sroute'] = self.static_routes_info["sroute"] + self.changed = bool(change) + + def get_end_state(self): + """get end state information""" + + self.get_static_route(self.state) + self.end_state['sroute'] = self.static_routes_info["sroute"] + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + self.set_ip_static_route() + self.set_update_cmd() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """main""" + + argument_spec = dict( + prefix=dict(required=True, type='str'), + mask=dict(required=True, type='str'), + aftype=dict(choices=['v4', 'v6'], required=True), + next_hop=dict(required=False, type='str'), + nhp_interface=dict(required=False, type='str'), + vrf=dict(required=False, type='str'), + destvrf=dict(required=False, type='str'), + tag=dict(required=False, type='str'), + description=dict(required=False, type='str'), + pref=dict(required=False, type='str'), + state=dict(choices=['absent', 'present'], + default='present', required=False), + ) + argument_spec.update(ce_argument_spec) + interface = StaticRoute(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_static_route_bfd.py b/plugins/modules/ce_static_route_bfd.py new file mode 100644 index 0000000..92cd7e0 --- /dev/null +++ b/plugins/modules/ce_static_route_bfd.py @@ -0,0 +1,1596 @@ +#!/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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_static_route_bfd +short_description: Manages static route configuration on HUAWEI CloudEngine switches. +description: + - Manages the static routes on HUAWEI CloudEngine switches. +author: xuxiaowei0512 (@CloudEngine-Ansible) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. + - If no vrf is supplied, vrf is set to default. + - If I(state=absent), the route configuration will be removed, regardless of the non-required parameters. +options: + prefix: + description: + - Destination ip address of static route. + required: true + type: str + mask: + description: + - Destination ip mask of static route. + type: str + aftype: + description: + - Destination ip address family type of static route. + required: true + type: str + choices: ['v4','v6'] + next_hop: + description: + - Next hop address of static route. + type: str + nhp_interface: + description: + - Next hop interface full name of static route. + type: str + vrf: + description: + - VPN instance of destination ip address. + type: str + destvrf: + description: + - VPN instance of next hop ip address. + type: str + tag: + description: + - Route tag value (numeric). + type: int + description: + description: + - Name of the route. Used with the name parameter on the CLI. + type: str + pref: + description: + - Preference or administrative difference of route (range 1-255). + type: int + function_flag: + description: + - Used to distinguish between command line functions. + required: true + choices: ['globalBFD','singleBFD','dynamicBFD','staticBFD'] + type: str + min_tx_interval: + description: + - Set the minimum BFD session sending interval (range 50-1000). + type: int + min_rx_interval: + description: + - Set the minimum BFD receive interval (range 50-1000). + type: int + detect_multiplier: + description: + - Configure the BFD multiplier (range 3-50). + type: int + bfd_session_name: + description: + - bfd name (range 1-15). + type: str + commands: + description: + - Incoming command line is used to send sys,undo ip route-static default-bfd,commit. + type: list + state: + description: + - Specify desired state of the resource. + required: false + choices: ['present','absent'] + type: str + default: present +''' + +EXAMPLES = ''' + #ip route-static bfd interface-type interface-number nexthop-address [ local-address address ] + #[ min-rx-interval min-rx-interval | min-tx-interval min-tx-interval | detect-multiplier multiplier ] + - name: Config an ip route-static bfd 10GE1/0/1 3.3.3.3 min-rx-interval 50 min-tx-interval 50 detect-multiplier 5 + ce_static_route_bfd: + function_flag: 'singleBFD' + nhp_interface: 10GE1/0/1 + next_hop: 3.3.3.3 + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 5 + aftype: v4 + state: present + + #undo ip route-static bfd [ interface-type interface-number | vpn-instance vpn-instance-name ] nexthop-address + - name: undo ip route-static bfd 10GE1/0/1 3.3.3.4 + ce_static_route_bfd: + function_flag: 'singleBFD' + nhp_interface: 10GE1/0/1 + next_hop: 3.3.3.4 + aftype: v4 + state: absent + + #ip route-static default-bfd { min-rx-interval {min-rx-interval} | min-tx-interval {min-tx-interval} | detect-multiplier {multiplier}} + - name: Config an ip route-static default-bfd min-rx-interval 50 min-tx-interval 50 detect-multiplier 6 + ce_static_route_bfd: + function_flag: 'globalBFD' + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 6 + aftype: v4 + state: present + + - name: undo ip route-static default-bfd + ce_static_route_bfd: + function_flag: 'globalBFD' + aftype: v4 + state: absent + commands: 'sys,undo ip route-static default-bfd,commit' + + - name: Config an ipv4 static route 2.2.2.0/24 2.2.2.1 preference 1 tag 2 description test for staticBFD + ce_static_route_bfd: + function_flag: 'staticBFD' + prefix: 2.2.2.2 + mask: 24 + next_hop: 2.2.2.1 + tag: 2 + description: test + pref: 1 + aftype: v4 + bfd_session_name: btoa + state: present +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"function_flag": "staticBFD", "next_hop": "3.3.3.3", "pref": "100", + "prefix": "192.168.20.642", "mask": "24", "description": "testing", + "vrf": "_public_", "bfd_session_name": "btoa"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {"function_flag": "", "next_hop": "", "pref": "101", + "prefix": "192.168.20.0", "mask": "24", "description": "testing", + "tag" : "null", "bfd_session_name": "btoa"} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"function_flag": "staticBFD", "next_hop": "3.3.3.3", "pref": "100", + "prefix": "192.168.20.0", "mask": "24", "description": "testing", + "tag" : "null", "bfd_session_name": "btoa"} +updates: + description: command list sent to the device + returned: always + type: list + sample: ["ip route-static 192.168.20.0 255.255.255.0 3.3.3.3 preference 100 description testing"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import string_types +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config + +CE_NC_GET_STATIC_ROUTE_BFD_SESSIONNAME = """ + + + + + + + + + + + + + + + + + + + + + + +""" +# bfd enable +CE_NC_GET_STATIC_ROUTE_BFD_ENABLE = """ + + + + + + + + + + + + + + + + + + + + + + +""" + +CE_NC_GET_STATIC_ROUTE_BFD_ABSENT = """ + + + + + + %s + %s + %s + %s + + + + + +""" + +CE_NC_GET_STATIC_ROUTE_BFD = """ + + + + + + %s + %s + %s + %s + + + + + + + + + +""" +CE_NC_GET_STATIC_ROUTE_IPV4_GLOBAL_BFD = """ + + + + + + + + + + + +""" +CE_NC_GET_STATIC_ROUTE_ABSENT = """ + + + + + + + + + + + + + + + + + + +""" + +CE_NC_DELETE_STATIC_ROUTE_SINGLEBFD = """ + + + + + %s + %s + %s + %s + + + + +""" +CE_NC_SET_STATIC_ROUTE_SINGLEBFD = """ + + + + + %s + %s + %s + %s%s%s%s%s + + + + + +""" +CE_NC_SET_STATIC_ROUTE_SINGLEBFD_LOCALADRESS = """ +%s +""" +CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINTX = """ +%s +""" +CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINRX = """ +%s +""" +CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MUL = """ +%s +""" +CE_NC_SET_IPV4_STATIC_ROUTE_GLOBALBFD = """ + + + + %s%s%s + + + +""" + +CE_NC_SET_STATIC_ROUTE = """ + + + + + %s + %s + base + %s + %s + %s + %s + %s%s%s%s%s + + + + +""" +CE_NC_SET_DESCRIPTION = """ +%s +""" + +CE_NC_SET_PREFERENCE = """ +%s +""" + +CE_NC_SET_TAG = """ +%s +""" +CE_NC_SET_BFDSESSIONNAME = """ +%s +""" +CE_NC_SET_BFDENABLE = """ +true +""" +CE_NC_DELETE_STATIC_ROUTE = """ + + + + + %s + %s + base + %s + %s + %s + %s + %s + + + + +""" + + +def build_config_xml(xmlstr): + """build config xml""" + + return ' ' + xmlstr + ' ' + + +def is_valid_v4addr(addr): + """check if ipv4 addr is valid""" + if addr.find('.') != -1: + addr_list = addr.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + return False + + +def is_valid_v6addr(addr): + """check if ipv6 addr is valid""" + if addr.find(':') != -1: + addr_list = addr.split(':') + if len(addr_list) > 6: + return False + if addr_list[1] == "": + return False + return True + return False + + +def is_valid_tag(tag): + """check if the tag is valid""" + + if int(tag) < 1 or int(tag) > 4294967295: + return False + return True + + +def is_valid_bdf_interval(interval): + """check if the min_tx_interva,min-rx-interval is valid""" + + if interval < 50 or interval > 1000: + return False + return True + + +def is_valid_bdf_multiplier(multiplier): + """check if the detect_multiplier is valid""" + + if multiplier < 3 or multiplier > 50: + return False + return True + + +def is_valid_bdf_session_name(session_name): + """check if the bfd_session_name is valid""" + if session_name.find(' ') != -1: + return False + if len(session_name) < 1 or len(session_name) > 15: + return False + return True + + +def is_valid_preference(pref): + """check if the preference is valid""" + + if int(pref) > 0 and int(pref) < 256: + return True + return False + + +def is_valid_description(description): + """check if the description is valid""" + if description.find('?') != -1: + return False + if len(description) < 1 or len(description) > 255: + return False + return True + + +def compare_command(commands): + """check if the commands is valid""" + if len(commands) < 3: + return True + if commands[0] != 'sys' or commands[1] != 'undo ip route-static default-bfd' \ + or commands[2] != 'commit': + return True + + +def get_to_lines(stdout): + """data conversion""" + lines = list() + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + lines.append(item) + return lines + + +def get_change_state(oldvalue, newvalue, change): + """get change state""" + if newvalue is not None: + if oldvalue != str(newvalue): + change = True + else: + if oldvalue != newvalue: + change = True + return change + + +def get_xml(xml, value): + """operate xml""" + if value is None: + value = '' + else: + value = value + tempxml = xml % value + return tempxml + + +class StaticRouteBFD(object): + """static route module""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self._initmodule_() + + # static route info + self.function_flag = self.module.params['function_flag'] + self.aftype = self.module.params['aftype'] + self.state = self.module.params['state'] + if self.aftype == "v4": + self.version = "ipv4unicast" + else: + self.version = "ipv6unicast" + if self.function_flag != 'globalBFD': + self.nhp_interface = self.module.params['nhp_interface'] + if self.nhp_interface is None: + self.nhp_interface = "Invalid0" + + self.destvrf = self.module.params['destvrf'] + if self.destvrf is None: + self.destvrf = "_public_" + + self.next_hop = self.module.params['next_hop'] + self.prefix = self.module.params['prefix'] + + if self.function_flag != 'globalBFD' and self.function_flag != 'singleBFD': + self.mask = self.module.params['mask'] + self.tag = self.module.params['tag'] + self.description = self.module.params['description'] + self.pref = self.module.params['pref'] + if self.pref is None: + self.pref = 60 + # vpn instance info + self.vrf = self.module.params['vrf'] + if self.vrf is None: + self.vrf = "_public_" + # bfd session name + self.bfd_session_name = self.module.params['bfd_session_name'] + + if self.function_flag == 'globalBFD' or self.function_flag == 'singleBFD': + self.min_tx_interval = self.module.params['min_tx_interval'] + self.min_rx_interval = self.module.params['min_rx_interval'] + self.detect_multiplier = self.module.params['detect_multiplier'] + if self.function_flag == 'globalBFD' and self.state == 'absent': + self.commands = self.module.params['commands'] + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + self.static_routes_info = dict() + + def _initmodule_(self): + """init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=False) + + def _checkresponse_(self, xml_str, xml_name): + """check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def _convertlentomask_(self, masklen): + """convert mask length to ip address mask, i.e. 24 to 255.255.255.0""" + + mask_int = ["0"] * 4 + length = int(masklen) + + if length > 32: + self.module.fail_json(msg='IPv4 ipaddress mask length is invalid') + if length < 8: + mask_int[0] = str(int((0xFF << (8 - length % 8)) & 0xFF)) + if length >= 8: + mask_int[0] = '255' + mask_int[1] = str(int((0xFF << (16 - (length % 16))) & 0xFF)) + if length >= 16: + mask_int[1] = '255' + mask_int[2] = str(int((0xFF << (24 - (length % 24))) & 0xFF)) + if length >= 24: + mask_int[2] = '255' + mask_int[3] = str(int((0xFF << (32 - (length % 32))) & 0xFF)) + if length == 32: + mask_int[3] = '255' + + return '.'.join(mask_int) + + def _convertipprefix_(self): + """convert prefix to real value i.e. 2.2.2.2/24 to 2.2.2.0/24""" + if self.function_flag == 'singleBFD': + if self.aftype == "v4": + if self.prefix.find('.') == -1: + return False + addr_list = self.prefix.split('.') + length = len(addr_list) + if length > 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + else: + if self.prefix.find(':') == -1: + return False + else: + if self.aftype == "v4": + if self.prefix.find('.') == -1: + return False + if self.mask == '32': + self.prefix = self.prefix + return True + if self.mask == '0': + self.prefix = '0.0.0.0' + return True + addr_list = self.prefix.split('.') + length = len(addr_list) + if length > 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + byte_len = 8 + ip_len = int(self.mask) // byte_len + ip_bit = int(self.mask) % byte_len + else: + if self.prefix.find(':') == -1: + return False + if self.mask == '128': + self.prefix = self.prefix + return True + if self.mask == '0': + self.prefix = '::' + return True + addr_list = self.prefix.split(':') + length = len(addr_list) + if length > 6: + return False + byte_len = 16 + ip_len = int(self.mask) // byte_len + ip_bit = int(self.mask) % byte_len + + if self.aftype == "v4": + for i in range(ip_len + 1, length): + addr_list[i] = 0 + else: + for i in range(length - ip_len, length): + addr_list[i] = 0 + for j in range(0, byte_len - ip_bit): + if self.aftype == "v4": + addr_list[ip_len] = int(addr_list[ip_len]) & (0 << j) + else: + if addr_list[length - ip_len - 1] == "": + continue + addr_list[length - ip_len - + 1] = '0x%s' % addr_list[length - ip_len - 1] + addr_list[length - ip_len - + 1] = int(addr_list[length - ip_len - 1], 16) & (0 << j) + + if self.aftype == "v4": + self.prefix = '%s.%s.%s.%s' % (addr_list[0], addr_list[1], addr_list[2], addr_list[3]) + return True + if self.aftype == "v6": + ipv6_addr_str = "" + for num in range(0, length - ip_len): + ipv6_addr_str += '%s:' % addr_list[num] + self.prefix = ipv6_addr_str + + return True + + def set_update_cmd_globalbfd(self): + """set globalBFD update command""" + if not self.changed: + return + if self.state == "present": + self.updates_cmd.append('ip route-static default-bfd') + if self.min_tx_interval: + self.updates_cmd.append(' min-rx-interval %s' % (self.min_tx_interval)) + if self.min_rx_interval: + self.updates_cmd.append(' min-tx-interval %s' % (self.min_rx_interval)) + if self.detect_multiplier: + self.updates_cmd.append(' detect-multiplier %s' % (self.detect_multiplier)) + else: + self.updates_cmd.append('undo ip route-static default-bfd') + + def set_update_cmd_singlebfd(self): + """set singleBFD update command""" + if not self.changed: + return + if self.next_hop is None: + next_hop = '' + else: + next_hop = self.next_hop + + if self.destvrf == "_public_": + destvrf = '' + else: + destvrf = self.destvrf + + if self.nhp_interface == "Invalid0": + nhp_interface = '' + else: + nhp_interface = self.nhp_interface + if self.prefix == "0.0.0.0": + prefix = '' + else: + prefix = self.prefix + if self.state == "present": + if nhp_interface: + self.updates_cmd.append('ip route-static bfd %s %s' % (nhp_interface, next_hop)) + elif destvrf: + self.updates_cmd.append('ip route-static bfd vpn-instance %s %s' % (destvrf, next_hop)) + else: + self.updates_cmd.append('ip route-static bfd %s' % (next_hop)) + if prefix: + self.updates_cmd.append(' local-address %s' % (self.prefix)) + if self.min_tx_interval: + self.updates_cmd.append(' min-rx-interval %s' % (self.min_tx_interval)) + if self.min_rx_interval: + self.updates_cmd.append(' min-tx-interval %s' % (self.min_rx_interval)) + if self.detect_multiplier: + self.updates_cmd.append(' detect-multiplier %s' % (self.detect_multiplier)) + else: + if nhp_interface: + self.updates_cmd.append('undo ip route-static bfd %s %s' % (nhp_interface, next_hop)) + elif destvrf: + self.updates_cmd.append('undo ip route-static bfd vpn-instance %s %s' % (destvrf, next_hop)) + else: + self.updates_cmd.append('undo ip route-static bfd %s' % (next_hop)) + + def set_update_cmd(self): + """set update command""" + if not self.changed: + return + + if self.aftype == "v4": + maskstr = self._convertlentomask_(self.mask) + else: + maskstr = self.mask + static_bfd_flag = True + if self.bfd_session_name: + static_bfd_flag = False + if self.next_hop is None: + next_hop = '' + else: + next_hop = self.next_hop + if self.vrf == "_public_": + vrf = '' + else: + vrf = self.vrf + if self.destvrf == "_public_": + destvrf = '' + else: + destvrf = self.destvrf + if self.nhp_interface == "Invalid0": + nhp_interface = '' + else: + nhp_interface = self.nhp_interface + if self.state == "present": + if self.vrf != "_public_": + if self.destvrf != "_public_": + self.updates_cmd.append('ip route-static vpn-instance %s %s %s vpn-instance %s %s' + % (vrf, self.prefix, maskstr, destvrf, next_hop)) + else: + self.updates_cmd.append('ip route-static vpn-instance %s %s %s %s %s' + % (vrf, self.prefix, maskstr, nhp_interface, next_hop)) + elif self.destvrf != "_public_": + self.updates_cmd.append('ip route-static %s %s vpn-instance %s %s' + % (self.prefix, maskstr, self.destvrf, next_hop)) + else: + self.updates_cmd.append('ip route-static %s %s %s %s' + % (self.prefix, maskstr, nhp_interface, next_hop)) + if self.pref != 60: + self.updates_cmd.append(' preference %s' % (self.pref)) + if self.tag: + self.updates_cmd.append(' tag %s' % (self.tag)) + if not static_bfd_flag: + self.updates_cmd.append(' track bfd-session %s' % (self.bfd_session_name)) + else: + self.updates_cmd.append(' bfd enable') + if self.description: + self.updates_cmd.append(' description %s' % (self.description)) + + if self.state == "absent": + if self.vrf != "_public_": + if self.destvrf != "_public_": + self.updates_cmd.append('undo ip route-static vpn-instance %s %s %s vpn-instance %s %s' + % (vrf, self.prefix, maskstr, destvrf, next_hop)) + else: + self.updates_cmd.append('undo ip route-static vpn-instance %s %s %s %s %s' + % (vrf, self.prefix, maskstr, nhp_interface, next_hop)) + elif self.destvrf != "_public_": + self.updates_cmd.append('undo ip route-static %s %s vpn-instance %s %s' + % (self.prefix, maskstr, self.destvrf, self.next_hop)) + else: + self.updates_cmd.append('undo ip route-static %s %s %s %s' + % (self.prefix, maskstr, nhp_interface, next_hop)) + + def operate_static_route_globalbfd(self): + """set globalbfd update command""" + min_tx_interval = self.min_tx_interval + min_rx_interval = self.min_rx_interval + multiplier = self.detect_multiplier + min_tx_interval_xml = """\n""" + min_rx_interval_xml = """\n""" + multiplier_xml = """\n""" + if self.state == "present": + if min_tx_interval is not None: + min_tx_interval_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINTX % min_tx_interval + if min_rx_interval is not None: + min_rx_interval_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINRX % min_rx_interval + if multiplier is not None: + multiplier_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MUL % multiplier + + configxmlstr = CE_NC_SET_IPV4_STATIC_ROUTE_GLOBALBFD % ( + min_tx_interval_xml, min_rx_interval_xml, multiplier_xml) + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "OPERATE_STATIC_ROUTE_globalBFD") + + if self.state == "absent" and self.commands: + min_tx_interval_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINTX % 1000 + min_rx_interval_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINRX % 1000 + multiplier_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MUL % 3 + + configxmlstr = CE_NC_SET_IPV4_STATIC_ROUTE_GLOBALBFD % ( + min_tx_interval_xml, min_rx_interval_xml, multiplier_xml) + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "OPERATE_STATIC_ROUTE_globalBFD") + + def operate_static_route_singlebfd(self, version, prefix, nhp_interface, next_hop, destvrf, state): + """operate ipv4 static route singleBFD""" + min_tx_interval = self.min_tx_interval + min_rx_interval = self.min_rx_interval + multiplier = self.detect_multiplier + min_tx_interval_xml = """\n""" + min_rx_interval_xml = """\n""" + multiplier_xml = """\n""" + local_address_xml = """\n""" + if next_hop is None: + next_hop = '0.0.0.0' + + if destvrf is None: + dest_vpn_instance = "_public_" + else: + dest_vpn_instance = destvrf + + if nhp_interface is None: + nhp_interface = "Invalid0" + + if min_tx_interval is not None: + min_tx_interval_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINTX % min_tx_interval + if min_rx_interval is not None: + min_rx_interval_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MINRX % min_rx_interval + if multiplier is not None: + multiplier_xml = CE_NC_SET_IPV4_STATIC_ROUTE_BFDCOMMON_MUL % multiplier + + if prefix is not None: + local_address_xml = CE_NC_SET_STATIC_ROUTE_SINGLEBFD_LOCALADRESS % prefix + + if state == "present": + configxmlstr = CE_NC_SET_STATIC_ROUTE_SINGLEBFD % ( + version, nhp_interface, dest_vpn_instance, + next_hop, local_address_xml, min_tx_interval_xml, + min_rx_interval_xml, multiplier_xml) + + else: + configxmlstr = CE_NC_DELETE_STATIC_ROUTE_SINGLEBFD % ( + version, nhp_interface, dest_vpn_instance, next_hop) + + conf_str = build_config_xml(configxmlstr) + + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "OPERATE_STATIC_ROUTE_singleBFD") + + def operate_static_route(self, version, prefix, mask, nhp_interface, next_hop, vrf, destvrf, state): + """operate ipv4 static route""" + description_xml = """\n""" + preference_xml = """\n""" + tag_xml = """\n""" + bfd_xml = """\n""" + if next_hop is None: + next_hop = '0.0.0.0' + if nhp_interface is None: + nhp_interface = "Invalid0" + + if vrf is None: + vpn_instance = "_public_" + else: + vpn_instance = vrf + + if destvrf is None: + dest_vpn_instance = "_public_" + else: + dest_vpn_instance = destvrf + + description_xml = get_xml(CE_NC_SET_DESCRIPTION, self.description) + + preference_xml = get_xml(CE_NC_SET_PREFERENCE, self.pref) + + tag_xml = get_xml(CE_NC_SET_TAG, self.tag) + + if self.function_flag == 'staticBFD': + if self.bfd_session_name: + bfd_xml = CE_NC_SET_BFDSESSIONNAME % self.bfd_session_name + else: + bfd_xml = CE_NC_SET_BFDENABLE + if state == "present": + configxmlstr = CE_NC_SET_STATIC_ROUTE % ( + vpn_instance, version, prefix, mask, nhp_interface, + dest_vpn_instance, next_hop, description_xml, preference_xml, tag_xml, bfd_xml) + + else: + configxmlstr = CE_NC_DELETE_STATIC_ROUTE % ( + vpn_instance, version, prefix, mask, nhp_interface, dest_vpn_instance, next_hop) + + conf_str = build_config_xml(configxmlstr) + recv_xml = set_nc_config(self.module, conf_str) + self._checkresponse_(recv_xml, "OPERATE_STATIC_ROUTE") + + def get_change_state_global_bfd(self): + """get ipv4 global bfd change state""" + + self.get_global_bfd(self.state) + change = False + if self.state == "present": + if self.static_routes_info["sroute_global_bfd"]: + for static_route in self.static_routes_info["sroute_global_bfd"]: + if static_route is not None: + if self.min_tx_interval is not None: + if int(static_route["minTxInterval"]) != self.min_tx_interval: + change = True + if self.min_rx_interval is not None: + if int(static_route["minRxInterval"]) != self.min_rx_interval: + change = True + if self.detect_multiplier is not None: + if int(static_route["multiplier"]) != self.detect_multiplier: + change = True + return change + else: + continue + else: + change = True + else: + if self.commands: + if self.static_routes_info["sroute_global_bfd"]: + for static_route in self.static_routes_info["sroute_global_bfd"]: + if static_route is not None: + if int(static_route["minTxInterval"]) != 1000 or \ + int(static_route["minRxInterval"]) != 1000 or \ + int(static_route["multiplier"]) != 3: + change = True + return change + + def get_global_bfd(self, state): + """get ipv4 global bfd""" + + self.static_routes_info["sroute_global_bfd"] = list() + + getglobalbfdxmlstr = None + if self.aftype == 'v4': + getglobalbfdxmlstr = CE_NC_GET_STATIC_ROUTE_IPV4_GLOBAL_BFD + + if getglobalbfdxmlstr is not None: + xml_global_bfd_str = get_nc_config(self.module, getglobalbfdxmlstr) + + if 'data/' in xml_global_bfd_str: + return + + xml_global_bfd_str = xml_global_bfd_str.replace('\r', '').replace('\n', ''). \ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', ""). \ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_global_bfd_str) + static_routes_global_bfd = root.findall( + "staticrt/staticrtbase/srIPv4StaticSite") + + if static_routes_global_bfd: + for static_route in static_routes_global_bfd: + static_info = dict() + for static_ele in static_route: + if static_ele.tag == "minTxInterval": + if static_ele.text is not None: + static_info["minTxInterval"] = static_ele.text + if static_ele.tag == "minRxInterval": + if static_ele.text is not None: + static_info["minRxInterval"] = static_ele.text + if static_ele.tag == "multiplier": + if static_ele.text is not None: + static_info["multiplier"] = static_ele.text + + self.static_routes_info["sroute_global_bfd"].append(static_info) + + def get_change_state_single_bfd(self): + """get ipv4 single bfd change state""" + + self.get_single_bfd(self.state) + change = False + version = self.version + if self.state == 'present': + if self.static_routes_info["sroute_single_bfd"]: + for static_route in self.static_routes_info["sroute_single_bfd"]: + if static_route is not None and static_route['afType'] == version: + if self.nhp_interface: + if static_route["ifName"].lower() != self.nhp_interface.lower(): + change = True + if self.destvrf: + if static_route["destVrfName"].lower() != self.destvrf.lower(): + change = True + if self.next_hop: + if static_route["nexthop"].lower() != self.next_hop.lower(): + change = True + if self.prefix: + if static_route["localAddress"].lower() != self.prefix.lower(): + change = True + if self.min_tx_interval: + if int(static_route["minTxInterval"]) != self.min_tx_interval: + change = True + if self.min_rx_interval: + if int(static_route["minRxInterval"]) != self.min_rx_interval: + change = True + if self.detect_multiplier: + if int(static_route["multiplier"]) != self.detect_multiplier: + change = True + return change + + else: + continue + else: + change = True + else: + for static_route in self.static_routes_info["sroute_single_bfd"]: + # undo ip route-static bfd [ interface-type interface-number | + # vpn-instance vpn-instance-name ] nexthop-address + + if static_route["ifName"] and self.nhp_interface: + if static_route["ifName"].lower() == self.nhp_interface.lower() \ + and static_route["nexthop"].lower() == self.next_hop.lower() \ + and static_route["afType"] == version: + change = True + return change + + if static_route["destVrfName"] and self.destvrf: + if static_route["destVrfName"].lower() == self.destvrf.lower() \ + and static_route["nexthop"].lower() == self.next_hop.lower() \ + and static_route["afType"] == version: + change = True + return change + + if static_route["nexthop"] and self.next_hop: + if static_route["nexthop"].lower() == self.next_hop.lower() \ + and static_route["afType"] == version: + change = True + return change + else: + continue + change = False + return change + + def get_single_bfd(self, state): + """get ipv4 sigle bfd""" + self.static_routes_info["sroute_single_bfd"] = list() + if self.aftype == "v4": + version = "ipv4unicast" + else: + version = "ipv6unicast" + if state == 'absent': + getbfdxmlstr = CE_NC_GET_STATIC_ROUTE_BFD_ABSENT % ( + version, self.nhp_interface, self.destvrf, self.next_hop) + else: + getbfdxmlstr = CE_NC_GET_STATIC_ROUTE_BFD % ( + version, self.nhp_interface, self.destvrf, self.next_hop) + xml_bfd_str = get_nc_config(self.module, getbfdxmlstr) + + if 'data/' in xml_bfd_str: + return + xml_bfd_str = xml_bfd_str.replace('\r', '').replace('\n', ''). \ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', ""). \ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_bfd_str) + static_routes_bfd = root.findall( + "staticrt/staticrtbase/srBfdParas/srBfdPara") + if static_routes_bfd: + for static_route in static_routes_bfd: + static_info = dict() + for static_ele in static_route: + if static_ele.tag in ["afType", "destVrfName", "nexthop", "ifName"]: + static_info[static_ele.tag] = static_ele.text + if static_ele.tag == "localAddress": + if static_ele.text is not None: + static_info["localAddress"] = static_ele.text + else: + static_info["localAddress"] = "None" + if static_ele.tag == "minTxInterval": + if static_ele.text is not None: + static_info["minTxInterval"] = static_ele.text + if static_ele.tag == "minRxInterval": + if static_ele.text is not None: + static_info["minRxInterval"] = static_ele.text + if static_ele.tag == "multiplier": + if static_ele.text is not None: + static_info["multiplier"] = static_ele.text + self.static_routes_info["sroute_single_bfd"].append(static_info) + + def get_static_route(self, state): + """get ipv4 static route about BFD""" + self.static_routes_info["sroute"] = list() + # Increase the parameter used to distinguish whether the incoming bfdSessionName + static_bfd_flag = True + if self.bfd_session_name: + static_bfd_flag = False + + if state == 'absent': + getxmlstr = CE_NC_GET_STATIC_ROUTE_ABSENT + else: + # self.static_bfd_flag is true + if static_bfd_flag: + getxmlstr = CE_NC_GET_STATIC_ROUTE_BFD_ENABLE + + else: + getxmlstr = CE_NC_GET_STATIC_ROUTE_BFD_SESSIONNAME + xml_str = get_nc_config(self.module, getxmlstr) + if 'data/' in xml_str: + return + xml_str = xml_str.replace('\r', '').replace('\n', ''). \ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', ""). \ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + root = ElementTree.fromstring(xml_str) + static_routes = root.findall( + "staticrt/staticrtbase/srRoutes/srRoute") + + if static_routes: + for static_route in static_routes: + static_info = dict() + for static_ele in static_route: + if static_ele.tag in ["vrfName", "afType", "topologyName", + "prefix", "maskLength", "destVrfName", + "nexthop", "ifName", "preference", "description"]: + static_info[static_ele.tag] = static_ele.text + if static_ele.tag == "tag": + if static_ele.text is not None: + static_info["tag"] = static_ele.text + else: + static_info["tag"] = "None" + if static_bfd_flag: + if static_ele.tag == "bfdEnable": + if static_ele.text is not None: + static_info["bfdEnable"] = static_ele.text + else: + static_info["bfdEnable"] = "None" + else: + if static_ele.tag == "sessionName": + if static_ele.text is not None: + static_info["sessionName"] = static_ele.text + else: + static_info["sessionName"] = "None" + self.static_routes_info["sroute"].append(static_info) + + def _checkparams_(self): + """check all input params""" + if self.function_flag == 'singleBFD': + if not self.next_hop: + self.module.fail_json(msg='Error: missing required argument: next_hop.') + if self.state != 'absent': + if self.nhp_interface == "Invalid0" and (not self.prefix or self.prefix == '0.0.0.0'): + self.module.fail_json(msg='Error: If a nhp_interface is not configured, ' + 'the prefix must be configured.') + + if self.function_flag != 'globalBFD': + if self.function_flag == 'dynamicBFD' or self.function_flag == 'staticBFD': + if not self.mask: + self.module.fail_json(msg='Error: missing required argument: mask.') + # check prefix and mask + if not self.mask.isdigit(): + self.module.fail_json(msg='Error: Mask is invalid.') + if self.function_flag != 'singleBFD' or (self.function_flag == 'singleBFD' and self.destvrf != "_public_"): + if not self.prefix: + self.module.fail_json(msg='Error: missing required argument: prefix.') + # convert prefix + if not self._convertipprefix_(): + self.module.fail_json(msg='Error: The %s is not a valid address' % self.prefix) + + if self.nhp_interface != "Invalid0" and self.destvrf != "_public_": + self.module.fail_json(msg='Error: Destination vrf dose not support next hop is interface.') + + if not self.next_hop and self.nhp_interface == "Invalid0": + self.module.fail_json(msg='Error: one of the following is required: next_hop,nhp_interface.') + + if self.function_flag == 'dynamicBFD' or self.function_flag == 'staticBFD': + # description check + if self.description: + if not is_valid_description(self.description): + self.module.fail_json( + msg='Error: Dsecription length should be 1 - 35, and can not contain "?".') + # tag check + if self.tag is not None: + if not is_valid_tag(self.tag): + self.module.fail_json( + msg='Error: Tag should be integer 1 - 4294967295.') + # preference check + if self.pref is not None: + if not is_valid_preference(self.pref): + self.module.fail_json( + msg='Error: Preference should be integer 1 - 255.') + + if self.function_flag == 'staticBFD': + if self.bfd_session_name: + if not is_valid_bdf_session_name(self.bfd_session_name): + self.module.fail_json( + msg='Error: bfd_session_name length should be 1 - 15, and can not contain Space.') + + # ipv4 check + if self.aftype == "v4": + if self.function_flag == 'dynamicBFD' or self.function_flag == 'staticBFD': + if int(self.mask) > 32 or int(self.mask) < 0: + self.module.fail_json( + msg='Error: Ipv4 mask must be an integer between 1 and 32.') + # next_hop check + if self.function_flag != 'globalBFD': + if self.next_hop: + if not is_valid_v4addr(self.next_hop): + self.module.fail_json( + msg='Error: The %s is not a valid address.' % self.next_hop) + # ipv6 check + if self.aftype == "v6": + if self.function_flag == 'dynamicBFD' or self.function_flag == 'staticBFD': + if int(self.mask) > 128 or int(self.mask) < 0: + self.module.fail_json( + msg='Error: Ipv6 mask must be an integer between 1 and 128.') + if self.function_flag != 'globalBFD': + if self.next_hop: + if not is_valid_v6addr(self.next_hop): + self.module.fail_json( + msg='Error: The %s is not a valid address.' % self.next_hop) + + if self.function_flag == 'globalBFD' or self.function_flag == 'singleBFD': + # BFD prarams + if self.min_tx_interval: + if not is_valid_bdf_interval(self.min_tx_interval): + self.module.fail_json( + msg='Error: min_tx_interval should be integer 50 - 1000.') + if self.min_rx_interval: + if not is_valid_bdf_interval(self.min_rx_interval): + self.module.fail_json( + msg='Error: min_rx_interval should be integer 50 - 1000.') + if self.detect_multiplier: + if not is_valid_bdf_multiplier(self.detect_multiplier): + self.module.fail_json( + msg='Error: detect_multiplier should be integer 3 - 50.') + + if self.function_flag == 'globalBFD': + if self.state != 'absent': + if not self.min_tx_interval and not self.min_rx_interval and not self.detect_multiplier: + self.module.fail_json( + msg='Error: one of the following is required: min_tx_interval,' + 'detect_multiplier,min_rx_interval.') + else: + if not self.commands: + self.module.fail_json( + msg='Error: missing required argument: command.') + if compare_command(self.commands): + self.module.fail_json( + msg='Error: The command %s line is incorrect.' % (',').join(self.commands)) + + def set_ip_static_route_globalbfd(self): + """set ip static route globalBFD""" + if not self.changed: + return + if self.aftype == "v4": + self.operate_static_route_globalbfd() + + def set_ip_static_route_singlebfd(self): + """set ip static route singleBFD""" + if not self.changed: + return + version = None + if self.aftype == "v4": + version = "ipv4unicast" + else: + version = "ipv6unicast" + self.operate_static_route_singlebfd(version, self.prefix, self.nhp_interface, + self.next_hop, self.destvrf, self.state) + + def set_ip_static_route(self): + """set ip static route""" + if not self.changed: + return + version = None + if self.aftype == "v4": + version = "ipv4unicast" + else: + version = "ipv6unicast" + self.operate_static_route(version, self.prefix, self.mask, self.nhp_interface, + self.next_hop, self.vrf, self.destvrf, self.state) + + def is_prefix_exist(self, static_route, version): + """is prefix mask nex_thop exist""" + if static_route is None: + return False + if self.next_hop and self.nhp_interface: + return static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["afType"] == version \ + and static_route["ifName"].lower() == self.nhp_interface.lower() \ + and static_route["nexthop"].lower() == self.next_hop.lower() + + if self.next_hop and not self.nhp_interface: + return static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["afType"] == version \ + and static_route["nexthop"].lower() == self.next_hop.lower() + + if not self.next_hop and self.nhp_interface: + return static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["afType"] == version \ + and static_route["ifName"].lower() == self.nhp_interface.lower() + + def get_ip_static_route(self): + """get ip static route""" + change = False + version = self.version + self.get_static_route(self.state) + change_list = list() + if self.state == 'present': + for static_route in self.static_routes_info["sroute"]: + if self.is_prefix_exist(static_route, self.version): + info_dict = dict() + exist_dict = dict() + if self.vrf: + info_dict["vrfName"] = self.vrf + exist_dict["vrfName"] = static_route["vrfName"] + if self.destvrf: + info_dict["destVrfName"] = self.destvrf + exist_dict["destVrfName"] = static_route["destVrfName"] + if self.description: + info_dict["description"] = self.description + exist_dict["description"] = static_route["description"] + if self.tag: + info_dict["tag"] = self.tag + exist_dict["tag"] = static_route["tag"] + if self.pref: + info_dict["preference"] = str(self.pref) + exist_dict["preference"] = static_route["preference"] + if self.nhp_interface: + if self.nhp_interface.lower() == "invalid0": + info_dict["ifName"] = "Invalid0" + else: + info_dict["ifName"] = "Invalid0" + exist_dict["ifName"] = static_route["ifName"] + if self.next_hop: + info_dict["nexthop"] = self.next_hop + exist_dict["nexthop"] = static_route["nexthop"] + + if self.bfd_session_name: + info_dict["bfdEnable"] = 'true' + + else: + info_dict["bfdEnable"] = 'false' + exist_dict["bfdEnable"] = static_route["bfdEnable"] + + if exist_dict != info_dict: + change = True + else: + change = False + change_list.append(change) + + if False in change_list: + change = False + else: + change = True + return change + + else: + for static_route in self.static_routes_info["sroute"]: + if static_route["nexthop"] and self.next_hop: + if static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["nexthop"].lower() == self.next_hop.lower() \ + and static_route["afType"] == version: + change = True + return change + if static_route["ifName"] and self.nhp_interface: + if static_route["prefix"].lower() == self.prefix.lower() \ + and static_route["maskLength"] == self.mask \ + and static_route["ifName"].lower() == self.nhp_interface.lower() \ + and static_route["afType"] == version: + change = True + return change + else: + continue + change = False + return change + + def get_proposed(self): + """get proposed information""" + self.proposed['afType'] = self.aftype + self.proposed['state'] = self.state + if self.function_flag != 'globalBFD': + self.proposed['ifName'] = self.nhp_interface + self.proposed['destVrfName'] = self.destvrf + self.proposed['next_hop'] = self.next_hop + + if self.function_flag == 'singleBFD': + if self.prefix: + self.proposed['localAddress'] = self.prefix + + if self.function_flag == 'globalBFD' or self.function_flag == 'singleBFD': + self.proposed['minTxInterval'] = self.min_tx_interval + self.proposed['minRxInterval'] = self.min_rx_interval + self.proposed['multiplier'] = self.detect_multiplier + + if self.function_flag != 'globalBFD' and self.function_flag != 'singleBFD': + self.proposed['prefix'] = self.prefix + self.proposed['mask'] = self.mask + self.proposed['vrfName'] = self.vrf + if self.tag: + self.proposed['tag'] = self.tag + if self.description: + self.proposed['description'] = self.description + if self.pref is None: + self.proposed['preference'] = 60 + else: + self.proposed['preference'] = self.pref + + static_bfd_flag = True + if self.bfd_session_name: + static_bfd_flag = False + if not static_bfd_flag: + self.proposed['sessionName'] = self.bfd_session_name + else: + self.proposed['bfdEnable'] = 'true' + + def get_existing(self): + """get existing information""" + # globalBFD + if self.function_flag == 'globalBFD': + change = self.get_change_state_global_bfd() + self.existing['sroute_global_bfd'] = self.static_routes_info["sroute_global_bfd"] + # singleBFD + elif self.function_flag == 'singleBFD': + change = self.get_change_state_single_bfd() + self.existing['sroute_single_bfd'] = self.static_routes_info["sroute_single_bfd"] + # dynamicBFD / staticBFD + else: + change = self.get_ip_static_route() + self.existing['static_sroute'] = self.static_routes_info["sroute"] + self.changed = bool(change) + + def get_end_state(self): + """get end state information""" + + # globalBFD + if self.function_flag == 'globalBFD': + self.get_global_bfd(self.state) + self.end_state['sroute_global_bfd'] = self.static_routes_info["sroute_global_bfd"] + # singleBFD + elif self.function_flag == 'singleBFD': + self.static_routes_info["sroute_single_bfd"] = list() + self.get_single_bfd(self.state) + self.end_state['sroute_single_bfd'] = self.static_routes_info["sroute_single_bfd"] + # dynamicBFD / staticBFD + else: + self.get_static_route(self.state) + self.end_state['static_sroute'] = self.static_routes_info["sroute"] + + def work(self): + """worker""" + self._checkparams_() + self.get_existing() + self.get_proposed() + + if self.function_flag == 'globalBFD': + self.set_ip_static_route_globalbfd() + self.set_update_cmd_globalbfd() + elif self.function_flag == 'singleBFD': + self.set_ip_static_route_singlebfd() + self.set_update_cmd_singlebfd() + else: + self.set_ip_static_route() + self.set_update_cmd() + + self.get_end_state() + if self.existing == self.end_state: + self.changed = False + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """main""" + + argument_spec = dict( + prefix=dict(type='str'), + mask=dict(type='str'), + aftype=dict(choices=['v4', 'v6'], required=True), + next_hop=dict(type='str'), + nhp_interface=dict(type='str'), + vrf=dict(type='str'), + destvrf=dict(type='str'), + tag=dict(type='int'), + description=dict(type='str'), + pref=dict(type='int'), + # bfd + function_flag=dict(required=True, choices=['globalBFD', 'singleBFD', 'dynamicBFD', 'staticBFD']), + min_tx_interval=dict(type='int'), + min_rx_interval=dict(type='int'), + detect_multiplier=dict(type='int'), + # bfd session name + bfd_session_name=dict(type='str'), + commands=dict(type='list', required=False), + state=dict(choices=['absent', 'present'], default='present', required=False), + ) + interface = StaticRouteBFD(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_stp.py b/plugins/modules/ce_stp.py new file mode 100644 index 0000000..46e3918 --- /dev/null +++ b/plugins/modules/ce_stp.py @@ -0,0 +1,970 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_stp +short_description: Manages STP configuration on HUAWEI CloudEngine switches. +description: + - Manages STP configurations on HUAWEI CloudEngine switches. +author: + - wangdezhuang (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + state: + description: + - Specify desired state of the resource. + default: present + choices: ['present', 'absent'] + stp_mode: + description: + - Set an operation mode for the current MSTP process. + The mode can be STP, RSTP, or MSTP. + choices: ['stp', 'rstp', 'mstp'] + stp_enable: + description: + - Enable or disable STP on a switch. + choices: ['enable', 'disable'] + stp_converge: + description: + - STP convergence mode. + Fast means set STP aging mode to Fast. + Normal means set STP aging mode to Normal. + choices: ['fast', 'normal'] + bpdu_protection: + description: + - Configure BPDU protection on an edge port. + This function prevents network flapping caused by attack packets. + choices: ['enable', 'disable'] + tc_protection: + description: + - Configure the TC BPDU protection function for an MSTP process. + choices: ['enable', 'disable'] + tc_protection_interval: + description: + - Set the time the MSTP device takes to handle the maximum number of TC BPDUs + and immediately refresh forwarding entries. + The value is an integer ranging from 1 to 600, in seconds. + tc_protection_threshold: + description: + - Set the maximum number of TC BPDUs that the MSTP can handle. + The value is an integer ranging from 1 to 255. The default value is 1 on the switch. + interface: + description: + - Interface name. + If the value is C(all), will apply configuration to all interfaces. + if the value is a special name, only support input the full name. + edged_port: + description: + - Set the current port as an edge port. + choices: ['enable', 'disable'] + bpdu_filter: + description: + - Specify a port as a BPDU filter port. + choices: ['enable', 'disable'] + cost: + description: + - Set the path cost of the current port. + The default instance is 0. + root_protection: + description: + - Enable root protection on the current port. + choices: ['enable', 'disable'] + loop_protection: + description: + - Enable loop protection on the current port. + choices: ['enable', 'disable'] +''' + +EXAMPLES = ''' + +- name: CloudEngine stp test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Config stp mode" + ce_stp: + state: present + stp_mode: stp + provider: "{{ cli }}" + + - name: "Undo stp mode" + ce_stp: + state: absent + stp_mode: stp + provider: "{{ cli }}" + + - name: "Enable bpdu protection" + ce_stp: + state: present + bpdu_protection: enable + provider: "{{ cli }}" + + - name: "Disable bpdu protection" + ce_stp: + state: present + bpdu_protection: disable + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"bpdu_protection": "enable", + "state": "present"} +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: {"bpdu_protection": "disable"} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"bpdu_protection": "enable"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["stp bpdu-protection"] +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import exec_command, load_config, ce_argument_spec + + +def get_config(module, flags): + + """Retrieves the current config from the device or cache""" + + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(module, cmd) + if rc != 0: + module.fail_json(msg=err) + config = str(out).strip() + if config.startswith("display"): + configs = config.split("\n") + if len(configs) > 1: + return "\n".join(configs[1:]) + else: + return "" + else: + return config + + +class Stp(object): + """ Manages stp/rstp/mstp configuration """ + + def __init__(self, **kwargs): + """ Stp module init """ + + # module + argument_spec = kwargs["argument_spec"] + self.spec = argument_spec + self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True) + + # config + self.cur_cfg = dict() + self.stp_cfg = None + self.interface_stp_cfg = None + + # module args + self.state = self.module.params['state'] or None + self.stp_mode = self.module.params['stp_mode'] or None + self.stp_enable = self.module.params['stp_enable'] or None + self.stp_converge = self.module.params['stp_converge'] or None + self.interface = self.module.params['interface'] or None + self.edged_port = self.module.params['edged_port'] or None + self.bpdu_filter = self.module.params['bpdu_filter'] or None + self.cost = self.module.params['cost'] or None + self.bpdu_protection = self.module.params['bpdu_protection'] or None + self.tc_protection = self.module.params['tc_protection'] or None + self.tc_protection_interval = self.module.params['tc_protection_interval'] or None + self.tc_protection_threshold = self.module.params['tc_protection_threshold'] or None + self.root_protection = self.module.params['root_protection'] or None + self.loop_protection = self.module.params['loop_protection'] or None + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def cli_load_config(self, commands): + """ Cli load configuration """ + + if not self.module.check_mode: + load_config(self.module, commands) + + def cli_get_stp_config(self): + """ Cli get stp configuration """ + + flags = [r"| section include #\s*\n\s*stp", r"| section exclude #\s*\n+\s*stp process \d+"] + self.stp_cfg = get_config(self.module, flags) + + def cli_get_interface_stp_config(self): + """ Cli get interface's stp configuration """ + + if self.interface: + regular = r"| ignore-case section include ^#\s+interface %s\s+" % self.interface.replace(" ", "") + flags = list() + flags.append(regular) + tmp_cfg = get_config(self.module, flags) + + if not tmp_cfg: + self.module.fail_json( + msg='Error: The interface %s is not exist.' % self.interface) + + if "undo portswitch" in tmp_cfg: + self.module.fail_json( + msg='Error: The interface %s is not switch mode.' % self.interface) + + self.interface_stp_cfg = tmp_cfg + + def check_params(self): + """ Check module params """ + + if self.cost: + if self.cost.isdigit(): + if int(self.cost) < 1 or int(self.cost) > 200000000: + self.module.fail_json( + msg='Error: The value of cost is out of [1 - 200000000].') + else: + self.module.fail_json( + msg='Error: The cost is not digit.') + + if self.tc_protection_interval: + if self.tc_protection_interval.isdigit(): + if int(self.tc_protection_interval) < 1 or int(self.tc_protection_interval) > 600: + self.module.fail_json( + msg='Error: The value of tc_protection_interval is out of [1 - 600].') + else: + self.module.fail_json( + msg='Error: The tc_protection_interval is not digit.') + + if self.tc_protection_threshold: + if self.tc_protection_threshold.isdigit(): + if int(self.tc_protection_threshold) < 1 or int(self.tc_protection_threshold) > 255: + self.module.fail_json( + msg='Error: The value of tc_protection_threshold is out of [1 - 255].') + else: + self.module.fail_json( + msg='Error: The tc_protection_threshold is not digit.') + + if self.root_protection or self.loop_protection or self.cost: + if not self.interface: + self.module.fail_json( + msg='Error: Please input interface.') + elif self.interface == "all": + self.module.fail_json( + msg='Error: Interface can not be all when config root_protection or loop_protection or cost.') + + if self.root_protection and self.root_protection == "enable": + if self.loop_protection and self.loop_protection == "enable": + self.module.fail_json( + msg='Error: Can not enable root_protection and loop_protection at the same interface.') + + if self.edged_port or self.bpdu_filter: + if not self.interface: + self.module.fail_json( + msg='Error: Please input interface.') + + def get_proposed(self): + """ Get module proposed """ + + self.proposed["state"] = self.state + + if self.stp_mode: + self.proposed["stp_mode"] = self.stp_mode + if self.stp_enable: + self.proposed["stp_enable"] = self.stp_enable + if self.stp_converge: + self.proposed["stp_converge"] = self.stp_converge + if self.interface: + self.proposed["interface"] = self.interface + if self.edged_port: + self.proposed["edged_port"] = self.edged_port + if self.bpdu_filter: + self.proposed["bpdu_filter"] = self.bpdu_filter + if self.cost: + self.proposed["cost"] = self.cost + if self.bpdu_protection: + self.proposed["bpdu_protection"] = self.bpdu_protection + if self.tc_protection: + self.proposed["tc_protection"] = self.tc_protection + if self.tc_protection_interval: + self.proposed["tc_protection_interval"] = self.tc_protection_interval + if self.tc_protection_threshold: + self.proposed["tc_protection_threshold"] = self.tc_protection_threshold + if self.root_protection: + self.proposed["root_protection"] = self.root_protection + if self.loop_protection: + self.proposed["loop_protection"] = self.loop_protection + + def get_existing(self): + """ Get existing configuration """ + + self.cli_get_stp_config() + if self.interface and self.interface != "all": + self.cli_get_interface_stp_config() + + if self.stp_mode: + if "stp mode stp" in self.stp_cfg: + self.cur_cfg["stp_mode"] = "stp" + self.existing["stp_mode"] = "stp" + elif "stp mode rstp" in self.stp_cfg: + self.cur_cfg["stp_mode"] = "rstp" + self.existing["stp_mode"] = "rstp" + else: + self.cur_cfg["stp_mode"] = "mstp" + self.existing["stp_mode"] = "mstp" + + if self.stp_enable: + if "stp disable" in self.stp_cfg: + self.cur_cfg["stp_enable"] = "disable" + self.existing["stp_enable"] = "disable" + else: + self.cur_cfg["stp_enable"] = "enable" + self.existing["stp_enable"] = "enable" + + if self.stp_converge: + if "stp converge fast" in self.stp_cfg: + self.cur_cfg["stp_converge"] = "fast" + self.existing["stp_converge"] = "fast" + else: + self.cur_cfg["stp_converge"] = "normal" + self.existing["stp_converge"] = "normal" + + if self.edged_port: + if self.interface == "all": + if "stp edged-port default" in self.stp_cfg: + self.cur_cfg["edged_port"] = "enable" + self.existing["edged_port"] = "enable" + else: + self.cur_cfg["edged_port"] = "disable" + self.existing["edged_port"] = "disable" + else: + if "stp edged-port enable" in self.interface_stp_cfg: + self.cur_cfg["edged_port"] = "enable" + self.existing["edged_port"] = "enable" + else: + self.cur_cfg["edged_port"] = "disable" + self.existing["edged_port"] = "disable" + + if self.bpdu_filter: + if self.interface == "all": + if "stp bpdu-filter default" in self.stp_cfg: + self.cur_cfg["bpdu_filter"] = "enable" + self.existing["bpdu_filter"] = "enable" + else: + self.cur_cfg["bpdu_filter"] = "disable" + self.existing["bpdu_filter"] = "disable" + else: + if "stp bpdu-filter enable" in self.interface_stp_cfg: + self.cur_cfg["bpdu_filter"] = "enable" + self.existing["bpdu_filter"] = "enable" + else: + self.cur_cfg["bpdu_filter"] = "disable" + self.existing["bpdu_filter"] = "disable" + + if self.bpdu_protection: + if "stp bpdu-protection" in self.stp_cfg: + self.cur_cfg["bpdu_protection"] = "enable" + self.existing["bpdu_protection"] = "enable" + else: + self.cur_cfg["bpdu_protection"] = "disable" + self.existing["bpdu_protection"] = "disable" + + if self.tc_protection: + pre_cfg = self.stp_cfg.split("\n") + if "stp tc-protection" in pre_cfg: + self.cur_cfg["tc_protection"] = "enable" + self.existing["tc_protection"] = "enable" + else: + self.cur_cfg["tc_protection"] = "disable" + self.existing["tc_protection"] = "disable" + + if self.tc_protection_interval: + if "stp tc-protection interval" in self.stp_cfg: + tmp_value = re.findall(r'stp tc-protection interval (.*)', self.stp_cfg) + if not tmp_value: + self.module.fail_json( + msg='Error: Can not find tc-protection interval on the device.') + self.cur_cfg["tc_protection_interval"] = tmp_value[0] + self.existing["tc_protection_interval"] = tmp_value[0] + else: + self.cur_cfg["tc_protection_interval"] = "null" + self.existing["tc_protection_interval"] = "null" + + if self.tc_protection_threshold: + if "stp tc-protection threshold" in self.stp_cfg: + tmp_value = re.findall(r'stp tc-protection threshold (.*)', self.stp_cfg) + if not tmp_value: + self.module.fail_json( + msg='Error: Can not find tc-protection threshold on the device.') + self.cur_cfg["tc_protection_threshold"] = tmp_value[0] + self.existing["tc_protection_threshold"] = tmp_value[0] + else: + self.cur_cfg["tc_protection_threshold"] = "1" + self.existing["tc_protection_threshold"] = "1" + + if self.cost: + tmp_value = re.findall(r'stp instance (.*) cost (.*)', self.interface_stp_cfg) + if not tmp_value: + self.cur_cfg["cost"] = "null" + self.existing["cost"] = "null" + else: + self.cur_cfg["cost"] = tmp_value[0][1] + self.existing["cost"] = tmp_value[0][1] + + # root_protection and loop_protection should get configuration at the same time + if self.root_protection or self.loop_protection: + if "stp root-protection" in self.interface_stp_cfg: + self.cur_cfg["root_protection"] = "enable" + self.existing["root_protection"] = "enable" + else: + self.cur_cfg["root_protection"] = "disable" + self.existing["root_protection"] = "disable" + + if "stp loop-protection" in self.interface_stp_cfg: + self.cur_cfg["loop_protection"] = "enable" + self.existing["loop_protection"] = "enable" + else: + self.cur_cfg["loop_protection"] = "disable" + self.existing["loop_protection"] = "disable" + + def get_end_state(self): + """ Get end state """ + + self.cli_get_stp_config() + if self.interface and self.interface != "all": + self.cli_get_interface_stp_config() + + if self.stp_mode: + if "stp mode stp" in self.stp_cfg: + self.end_state["stp_mode"] = "stp" + elif "stp mode rstp" in self.stp_cfg: + self.end_state["stp_mode"] = "rstp" + else: + self.end_state["stp_mode"] = "mstp" + + if self.stp_enable: + if "stp disable" in self.stp_cfg: + self.end_state["stp_enable"] = "disable" + else: + self.end_state["stp_enable"] = "enable" + + if self.stp_converge: + if "stp converge fast" in self.stp_cfg: + self.end_state["stp_converge"] = "fast" + else: + self.end_state["stp_converge"] = "normal" + + if self.edged_port: + if self.interface == "all": + if "stp edged-port default" in self.stp_cfg: + self.end_state["edged_port"] = "enable" + else: + self.end_state["edged_port"] = "disable" + else: + if "stp edged-port enable" in self.interface_stp_cfg: + self.end_state["edged_port"] = "enable" + else: + self.end_state["edged_port"] = "disable" + + if self.bpdu_filter: + if self.interface == "all": + if "stp bpdu-filter default" in self.stp_cfg: + self.end_state["bpdu_filter"] = "enable" + else: + self.end_state["bpdu_filter"] = "disable" + else: + if "stp bpdu-filter enable" in self.interface_stp_cfg: + self.end_state["bpdu_filter"] = "enable" + else: + self.end_state["bpdu_filter"] = "disable" + + if self.bpdu_protection: + if "stp bpdu-protection" in self.stp_cfg: + self.end_state["bpdu_protection"] = "enable" + else: + self.end_state["bpdu_protection"] = "disable" + + if self.tc_protection: + pre_cfg = self.stp_cfg.split("\n") + if "stp tc-protection" in pre_cfg: + self.end_state["tc_protection"] = "enable" + else: + self.end_state["tc_protection"] = "disable" + + if self.tc_protection_interval: + if "stp tc-protection interval" in self.stp_cfg: + tmp_value = re.findall(r'stp tc-protection interval (.*)', self.stp_cfg) + if not tmp_value: + self.module.fail_json( + msg='Error: Can not find tc-protection interval on the device.') + self.end_state["tc_protection_interval"] = tmp_value[0] + else: + self.end_state["tc_protection_interval"] = "null" + + if self.tc_protection_threshold: + if "stp tc-protection threshold" in self.stp_cfg: + tmp_value = re.findall(r'stp tc-protection threshold (.*)', self.stp_cfg) + if not tmp_value: + self.module.fail_json( + msg='Error: Can not find tc-protection threshold on the device.') + self.end_state["tc_protection_threshold"] = tmp_value[0] + else: + self.end_state["tc_protection_threshold"] = "1" + + if self.cost: + tmp_value = re.findall(r'stp instance (.*) cost (.*)', self.interface_stp_cfg) + if not tmp_value: + self.end_state["cost"] = "null" + else: + self.end_state["cost"] = tmp_value[0][1] + + if self.root_protection or self.loop_protection: + if "stp root-protection" in self.interface_stp_cfg: + self.end_state["root_protection"] = "enable" + else: + self.end_state["root_protection"] = "disable" + + if "stp loop-protection" in self.interface_stp_cfg: + self.end_state["loop_protection"] = "enable" + else: + self.end_state["loop_protection"] = "disable" + + if self.existing == self.end_state: + self.changed = False + self.updates_cmd = list() + + def present_stp(self): + """ Present stp configuration """ + + cmds = list() + + # config stp global + if self.stp_mode: + if self.stp_mode != self.cur_cfg["stp_mode"]: + cmd = "stp mode %s" % self.stp_mode + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.stp_enable: + if self.stp_enable != self.cur_cfg["stp_enable"]: + cmd = "stp %s" % self.stp_enable + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.stp_converge: + if self.stp_converge != self.cur_cfg["stp_converge"]: + cmd = "stp converge %s" % self.stp_converge + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.edged_port: + if self.interface == "all": + if self.edged_port != self.cur_cfg["edged_port"]: + if self.edged_port == "enable": + cmd = "stp edged-port default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp edged-port default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.bpdu_filter: + if self.interface == "all": + if self.bpdu_filter != self.cur_cfg["bpdu_filter"]: + if self.bpdu_filter == "enable": + cmd = "stp bpdu-filter default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp bpdu-filter default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.bpdu_protection: + if self.bpdu_protection != self.cur_cfg["bpdu_protection"]: + if self.bpdu_protection == "enable": + cmd = "stp bpdu-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp bpdu-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.tc_protection: + if self.tc_protection != self.cur_cfg["tc_protection"]: + if self.tc_protection == "enable": + cmd = "stp tc-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp tc-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.tc_protection_interval: + if self.tc_protection_interval != self.cur_cfg["tc_protection_interval"]: + cmd = "stp tc-protection interval %s" % self.tc_protection_interval + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.tc_protection_threshold: + if self.tc_protection_threshold != self.cur_cfg["tc_protection_threshold"]: + cmd = "stp tc-protection threshold %s" % self.tc_protection_threshold + cmds.append(cmd) + self.updates_cmd.append(cmd) + + # config interface stp + if self.interface and self.interface != "all": + tmp_changed = False + + cmd = "interface %s" % self.interface + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.edged_port: + if self.edged_port != self.cur_cfg["edged_port"]: + if self.edged_port == "enable": + cmd = "stp edged-port enable" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp edged-port" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.bpdu_filter: + if self.bpdu_filter != self.cur_cfg["bpdu_filter"]: + if self.bpdu_filter == "enable": + cmd = "stp bpdu-filter enable" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp bpdu-filter" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.root_protection: + if self.root_protection == "enable" and self.cur_cfg["loop_protection"] == "enable": + self.module.fail_json( + msg='Error: The interface has enable loop_protection, can not enable root_protection.') + if self.root_protection != self.cur_cfg["root_protection"]: + if self.root_protection == "enable": + cmd = "stp root-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp root-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.loop_protection: + if self.loop_protection == "enable" and self.cur_cfg["root_protection"] == "enable": + self.module.fail_json( + msg='Error: The interface has enable root_protection, can not enable loop_protection.') + if self.loop_protection != self.cur_cfg["loop_protection"]: + if self.loop_protection == "enable": + cmd = "stp loop-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp loop-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.cost: + if self.cost != self.cur_cfg["cost"]: + cmd = "stp cost %s" % self.cost + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if not tmp_changed: + cmd = "interface %s" % self.interface + self.updates_cmd.remove(cmd) + cmds.remove(cmd) + + if cmds: + self.cli_load_config(cmds) + self.changed = True + + def absent_stp(self): + """ Absent stp configuration """ + + cmds = list() + + if self.stp_mode: + if self.stp_mode == self.cur_cfg["stp_mode"]: + if self.stp_mode != "mstp": + cmd = "undo stp mode" + cmds.append(cmd) + self.updates_cmd.append(cmd) + self.changed = True + + if self.stp_enable: + if self.stp_enable != self.cur_cfg["stp_enable"]: + cmd = "stp %s" % self.stp_enable + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.stp_converge: + if self.stp_converge == self.cur_cfg["stp_converge"]: + cmd = "undo stp converge" + cmds.append(cmd) + self.updates_cmd.append(cmd) + self.changed = True + + if self.edged_port: + if self.interface == "all": + if self.edged_port != self.cur_cfg["edged_port"]: + if self.edged_port == "enable": + cmd = "stp edged-port default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp edged-port default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.bpdu_filter: + if self.interface == "all": + if self.bpdu_filter != self.cur_cfg["bpdu_filter"]: + if self.bpdu_filter == "enable": + cmd = "stp bpdu-filter default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp bpdu-filter default" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.bpdu_protection: + if self.bpdu_protection != self.cur_cfg["bpdu_protection"]: + if self.bpdu_protection == "enable": + cmd = "stp bpdu-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp bpdu-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.tc_protection: + if self.tc_protection != self.cur_cfg["tc_protection"]: + if self.tc_protection == "enable": + cmd = "stp tc-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + else: + cmd = "undo stp tc-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.tc_protection_interval: + if self.tc_protection_interval == self.cur_cfg["tc_protection_interval"]: + cmd = "undo stp tc-protection interval" + cmds.append(cmd) + self.updates_cmd.append(cmd) + self.changed = True + + if self.tc_protection_threshold: + if self.tc_protection_threshold == self.cur_cfg["tc_protection_threshold"]: + if self.tc_protection_threshold != "1": + cmd = "undo stp tc-protection threshold" + cmds.append(cmd) + self.updates_cmd.append(cmd) + self.changed = True + + # undo interface stp + if self.interface and self.interface != "all": + tmp_changed = False + + cmd = "interface %s" % self.interface + cmds.append(cmd) + self.updates_cmd.append(cmd) + + if self.edged_port: + if self.edged_port != self.cur_cfg["edged_port"]: + if self.edged_port == "enable": + cmd = "stp edged-port enable" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp edged-port" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.bpdu_filter: + if self.bpdu_filter != self.cur_cfg["bpdu_filter"]: + if self.bpdu_filter == "enable": + cmd = "stp bpdu-filter enable" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp bpdu-filter" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.root_protection: + if self.root_protection == "enable" and self.cur_cfg["loop_protection"] == "enable": + self.module.fail_json( + msg='Error: The interface has enable loop_protection, can not enable root_protection.') + if self.root_protection != self.cur_cfg["root_protection"]: + if self.root_protection == "enable": + cmd = "stp root-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp root-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.loop_protection: + if self.loop_protection == "enable" and self.cur_cfg["root_protection"] == "enable": + self.module.fail_json( + msg='Error: The interface has enable root_protection, can not enable loop_protection.') + if self.loop_protection != self.cur_cfg["loop_protection"]: + if self.loop_protection == "enable": + cmd = "stp loop-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + else: + cmd = "undo stp loop-protection" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if self.cost: + if self.cost == self.cur_cfg["cost"]: + cmd = "undo stp cost" + cmds.append(cmd) + self.updates_cmd.append(cmd) + tmp_changed = True + + if not tmp_changed: + cmd = "interface %s" % self.interface + self.updates_cmd.remove(cmd) + cmds.remove(cmd) + + if cmds: + self.cli_load_config(cmds) + self.changed = True + + def work(self): + """ Work function """ + + self.check_params() + self.get_proposed() + self.get_existing() + + if self.state == "present": + self.present_stp() + else: + self.absent_stp() + + self.get_end_state() + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + self.results['updates'] = self.updates_cmd + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + stp_mode=dict(choices=['stp', 'rstp', 'mstp']), + stp_enable=dict(choices=['enable', 'disable']), + stp_converge=dict(choices=['fast', 'normal']), + bpdu_protection=dict(choices=['enable', 'disable']), + tc_protection=dict(choices=['enable', 'disable']), + tc_protection_interval=dict(type='str'), + tc_protection_threshold=dict(type='str'), + interface=dict(type='str'), + edged_port=dict(choices=['enable', 'disable']), + bpdu_filter=dict(choices=['enable', 'disable']), + cost=dict(type='str'), + root_protection=dict(choices=['enable', 'disable']), + loop_protection=dict(choices=['enable', 'disable']) + ) + + argument_spec.update(ce_argument_spec) + module = Stp(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_switchport.py b/plugins/modules/ce_switchport.py new file mode 100644 index 0000000..4b3582a --- /dev/null +++ b/plugins/modules/ce_switchport.py @@ -0,0 +1,998 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_switchport +short_description: Manages Layer 2 switchport interfaces on HUAWEI CloudEngine switches. +description: + - Manages Layer 2 switchport interfaces on HUAWEI CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - When C(state=absent), VLANs can be added/removed from trunk links and + the existing access VLAN can be 'unconfigured' to just having VLAN 1 on that interface. + - When working with trunks VLANs the keywords add/remove are always sent + in the C(port trunk allow-pass vlan) command. Use verbose mode to see commands sent. + - When C(state=unconfigured), the interface will result with having a default Layer 2 interface, i.e. vlan 1 in access mode. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - Full name of the interface, i.e. 40GE1/0/22. + required: true + mode: + description: + - The link type of an interface. + choices: ['access','trunk', 'hybrid', 'dot1qtunnel'] + default_vlan: + description: + - If C(mode=access, or mode=dot1qtunnel), used as the access VLAN ID, in the range from 1 to 4094. + pvid_vlan: + description: + - If C(mode=trunk, or mode=hybrid), used as the trunk native VLAN ID, in the range from 1 to 4094. + trunk_vlans: + description: + - If C(mode=trunk), used as the VLAN range to ADD or REMOVE + from the trunk, such as 2-10 or 2,5,10-15, etc. + untagged_vlans: + description: + - If C(mode=hybrid), used as the VLAN range to ADD or REMOVE + from the trunk, such as 2-10 or 2,5,10-15, etc. + tagged_vlans: + description: + - If C(mode=hybrid), used as the VLAN range to ADD or REMOVE + from the trunk, such as 2-10 or 2,5,10-15, etc. + state: + description: + - Manage the state of the resource. + default: present + choices: ['present', 'absent', 'unconfigured'] +''' + +EXAMPLES = ''' +- name: switchport module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + - name: Ensure 10GE1/0/22 is in its default switchport state + ce_switchport: + interface: 10GE1/0/22 + state: unconfigured + provider: '{{ cli }}' + + - name: Ensure 10GE1/0/22 is configured for access vlan 20 + ce_switchport: + interface: 10GE1/0/22 + mode: access + default_vlan: 20 + provider: '{{ cli }}' + + - name: Ensure 10GE1/0/22 only has vlans 5-10 as trunk vlans + ce_switchport: + interface: 10GE1/0/22 + mode: trunk + pvid_vlan: 10 + trunk_vlans: 5-10 + provider: '{{ cli }}' + + - name: Ensure 10GE1/0/22 is a trunk port and ensure 2-50 are being tagged (doesn't mean others aren't also being tagged) + ce_switchport: + interface: 10GE1/0/22 + mode: trunk + pvid_vlan: 10 + trunk_vlans: 2-50 + provider: '{{ cli }}' + + - name: Ensure these VLANs are not being tagged on the trunk + ce_switchport: + interface: 10GE1/0/22 + mode: trunk + trunk_vlans: 51-4000 + state: absent + provider: '{{ cli }}' +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"default_vlan": "20", "interface": "10GE1/0/22", "mode": "access"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {"default_vlan": "10", "interface": "10GE1/0/22", + "mode": "access", "switchport": "enable"} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"default_vlan": "20", "interface": "10GE1/0/22", + "mode": "access", "switchport": "enable"} +updates: + description: command string sent to the device + returned: always + type: list + sample: ["10GE1/0/22", "port default vlan 20"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from xml.etree import ElementTree as ET +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_PORT_ATTR = """ + + + + + %s + + + + + + + + + + + +""" + +CE_NC_SET_PORT = """ + + + + %s + + %s + %s + %s + %s + + + + +""" + +CE_NC_SET_PORT_MODE = """ + + + + %s + + %s + + + + +""" + +CE_NC_SET_DEFAULT_PORT = """ + + + + + %s + + access + 1 + + + + + + + +""" + + +SWITCH_PORT_TYPE = ('ge', '10ge', '25ge', + '4x10ge', '40ge', '100ge', 'eth-trunk') + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +def is_portswitch_enalbed(iftype): + """"[undo] portswitch""" + + return bool(iftype in SWITCH_PORT_TYPE) + + +def vlan_bitmap_undo(bitmap): + """convert vlan bitmap to undo bitmap""" + + vlan_bit = ['F'] * 1024 + + if not bitmap or len(bitmap) == 0: + return ''.join(vlan_bit) + + bit_len = len(bitmap) + for num in range(bit_len): + undo = (~int(bitmap[num], 16)) & 0xF + vlan_bit[num] = hex(undo)[2] + + return ''.join(vlan_bit) + + +def is_vlan_bitmap_empty(bitmap): + """check vlan bitmap empty""" + + if not bitmap or len(bitmap) == 0: + return True + + bit_len = len(bitmap) + for num in range(bit_len): + if bitmap[num] != '0': + return False + + return True + + +class SwitchPort(object): + """ + Manages Layer 2 switchport interfaces. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # interface and vlan info + self.interface = self.module.params['interface'] + self.mode = self.module.params['mode'] + self.state = self.module.params['state'] + self.default_vlan = self.module.params['default_vlan'] + self.pvid_vlan = self.module.params['pvid_vlan'] + self.trunk_vlans = self.module.params['trunk_vlans'] + self.untagged_vlans = self.module.params['untagged_vlans'] + self.tagged_vlans = self.module.params['tagged_vlans'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + self.intf_info = dict() # interface vlan info + self.intf_type = None # loopback tunnel ... + + def init_module(self): + """ init module """ + + required_if = [('state', 'absent', ['mode']), ('state', 'present', ['mode'])] + mutually_exclusive = [['default_vlan', 'trunk_vlans'], + ['default_vlan', 'pvid_vlan'], + ['default_vlan', 'untagged_vlans'], + ['trunk_vlans', 'untagged_vlans'], + ['trunk_vlans', 'tagged_vlans'], + ['default_vlan', 'tagged_vlans']] + + self.module = AnsibleModule( + argument_spec=self.spec, required_if=required_if, supports_check_mode=True, mutually_exclusive=mutually_exclusive) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_interface_dict(self, ifname): + """ get one interface attributes dict.""" + + intf_info = dict() + conf_str = CE_NC_GET_PORT_ATTR % ifname + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return intf_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + tree = ET.fromstring(xml_str) + l2Enable = tree.find('ethernet/ethernetIfs/ethernetIf/l2Enable') + intf_info["l2Enable"] = l2Enable.text + port_type = tree.find('ethernet/ethernetIfs/ethernetIf/l2Attribute') + for pre in port_type: + intf_info[pre.tag] = pre.text + intf_info["ifName"] = ifname + if intf_info["trunkVlans"] is None: + intf_info["trunkVlans"] = "" + if intf_info["untagVlans"] is None: + intf_info["untagVlans"] = "" + return intf_info + + def is_l2switchport(self): + """Check layer2 switch port""" + + return bool(self.intf_info["l2Enable"] == "enable") + + def merge_access_vlan(self, ifname, default_vlan): + """Merge access interface vlan""" + + change = False + conf_str = "" + + self.updates_cmd.append("interface %s" % ifname) + if self.state == "present": + if self.intf_info["linkType"] == "access": + if default_vlan and self.intf_info["pvid"] != default_vlan: + self.updates_cmd.append( + "port default vlan %s" % default_vlan) + conf_str = CE_NC_SET_PORT % (ifname, "access", default_vlan, "", "") + change = True + else: # not access + self.updates_cmd.append("port link-type access") + if default_vlan: + self.updates_cmd.append( + "port default vlan %s" % default_vlan) + conf_str = CE_NC_SET_PORT % (ifname, "access", default_vlan, "", "") + else: + conf_str = CE_NC_SET_PORT % (ifname, "access", "1", "", "") + change = True + elif self.state == "absent": + if self.intf_info["linkType"] == "access": + if default_vlan and self.intf_info["pvid"] == default_vlan and default_vlan != "1": + self.updates_cmd.append( + "undo port default vlan %s" % default_vlan) + conf_str = CE_NC_SET_PORT % (ifname, "access", "1", "", "") + change = True + + if not change: + self.updates_cmd.pop() # remove interface + return + conf_str = "" + conf_str + "" + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "MERGE_ACCESS_PORT") + self.changed = True + + def merge_trunk_vlan(self, ifname, pvid_vlan, trunk_vlans): + """Merge trunk interface vlan""" + + change = False + xmlstr = "" + pvid = "" + trunk = "" + self.updates_cmd.append("interface %s" % ifname) + if trunk_vlans: + vlan_list = self.vlan_range_to_list(trunk_vlans) + vlan_map = self.vlan_list_to_bitmap(vlan_list) + if self.state == "present": + if self.intf_info["linkType"] == "trunk": + if pvid_vlan and self.intf_info["pvid"] != pvid_vlan: + self.updates_cmd.append( + "port trunk pvid vlan %s" % pvid_vlan) + pvid = pvid_vlan + change = True + + if trunk_vlans: + add_vlans = self.vlan_bitmap_add( + self.intf_info["trunkVlans"], vlan_map) + if not is_vlan_bitmap_empty(add_vlans): + self.updates_cmd.append( + "port trunk allow-pass %s" + % trunk_vlans.replace(',', ' ').replace('-', ' to ')) + trunk = "%s:%s" % (add_vlans, add_vlans) + change = True + if pvid or trunk: + xmlstr += CE_NC_SET_PORT % (ifname, "trunk", pvid, trunk, "") + if not pvid: + xmlstr = xmlstr.replace("", "") + if not trunk: + xmlstr = xmlstr.replace("", "") + + else: # not trunk + self.updates_cmd.append("port link-type trunk") + change = True + if pvid_vlan: + self.updates_cmd.append( + "port trunk pvid vlan %s" % pvid_vlan) + pvid = pvid_vlan + if trunk_vlans: + self.updates_cmd.append( + "port trunk allow-pass %s" + % trunk_vlans.replace(',', ' ').replace('-', ' to ')) + trunk = "%s:%s" % (vlan_map, vlan_map) + if pvid or trunk: + xmlstr += CE_NC_SET_PORT % (ifname, "trunk", pvid, trunk, "") + if not pvid: + xmlstr = xmlstr.replace("", "") + if not trunk: + xmlstr = xmlstr.replace("", "") + + if not pvid_vlan and not trunk_vlans: + xmlstr += CE_NC_SET_PORT_MODE % (ifname, "trunk") + self.updates_cmd.append( + "undo port trunk allow-pass vlan 1") + elif self.state == "absent": + if self.intf_info["linkType"] == "trunk": + if pvid_vlan and self.intf_info["pvid"] == pvid_vlan and pvid_vlan != '1': + self.updates_cmd.append( + "undo port trunk pvid vlan %s" % pvid_vlan) + pvid = "1" + change = True + if trunk_vlans: + del_vlans = self.vlan_bitmap_del( + self.intf_info["trunkVlans"], vlan_map) + if not is_vlan_bitmap_empty(del_vlans): + self.updates_cmd.append( + "undo port trunk allow-pass %s" + % trunk_vlans.replace(',', ' ').replace('-', ' to ')) + undo_map = vlan_bitmap_undo(del_vlans) + trunk = "%s:%s" % (undo_map, del_vlans) + change = True + if pvid or trunk: + xmlstr += CE_NC_SET_PORT % (ifname, "trunk", pvid, trunk, "") + if not pvid: + xmlstr = xmlstr.replace("", "") + if not trunk: + xmlstr = xmlstr.replace("", "") + + if not change: + self.updates_cmd.pop() + return + conf_str = "" + xmlstr + "" + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "MERGE_TRUNK_PORT") + self.changed = True + + def merge_hybrid_vlan(self, ifname, pvid_vlan, tagged_vlans, untagged_vlans): + """Merge hybrid interface vlan""" + + change = False + xmlstr = "" + pvid = "" + tagged = "" + untagged = "" + self.updates_cmd.append("interface %s" % ifname) + if tagged_vlans: + vlan_targed_list = self.vlan_range_to_list(tagged_vlans) + vlan_targed_map = self.vlan_list_to_bitmap(vlan_targed_list) + if untagged_vlans: + vlan_untarged_list = self.vlan_range_to_list(untagged_vlans) + vlan_untarged_map = self.vlan_list_to_bitmap(vlan_untarged_list) + if self.state == "present": + if self.intf_info["linkType"] == "hybrid": + if pvid_vlan and self.intf_info["pvid"] != pvid_vlan: + self.updates_cmd.append( + "port hybrid pvid vlan %s" % pvid_vlan) + pvid = pvid_vlan + change = True + if tagged_vlans: + add_vlans = self.vlan_bitmap_add( + self.intf_info["trunkVlans"], vlan_targed_map) + if not is_vlan_bitmap_empty(add_vlans): + self.updates_cmd.append( + "port hybrid tagged vlan %s" + % tagged_vlans.replace(',', ' ').replace('-', ' to ')) + tagged = "%s:%s" % (add_vlans, add_vlans) + change = True + if untagged_vlans: + add_vlans = self.vlan_bitmap_add( + self.intf_info["untagVlans"], vlan_untarged_map) + if not is_vlan_bitmap_empty(add_vlans): + self.updates_cmd.append( + "port hybrid untagged vlan %s" + % untagged_vlans.replace(',', ' ').replace('-', ' to ')) + untagged = "%s:%s" % (add_vlans, add_vlans) + change = True + if pvid or tagged or untagged: + xmlstr += CE_NC_SET_PORT % (ifname, "hybrid", pvid, tagged, untagged) + if not pvid: + xmlstr = xmlstr.replace("", "") + if not tagged: + xmlstr = xmlstr.replace("", "") + if not untagged: + xmlstr = xmlstr.replace("", "") + else: + self.updates_cmd.append("port link-type hybrid") + change = True + if pvid_vlan: + self.updates_cmd.append( + "port hybrid pvid vlan %s" % pvid_vlan) + pvid = pvid_vlan + if tagged_vlans: + self.updates_cmd.append( + "port hybrid tagged vlan %s" + % tagged_vlans.replace(',', ' ').replace('-', ' to ')) + tagged = "%s:%s" % (vlan_targed_map, vlan_targed_map) + if untagged_vlans: + self.updates_cmd.append( + "port hybrid untagged vlan %s" + % untagged_vlans.replace(',', ' ').replace('-', ' to ')) + untagged = "%s:%s" % (vlan_untarged_map, vlan_untarged_map) + if pvid or tagged or untagged: + xmlstr += CE_NC_SET_PORT % (ifname, "hybrid", pvid, tagged, untagged) + if not pvid: + xmlstr = xmlstr.replace("", "") + if not tagged: + xmlstr = xmlstr.replace("", "") + if not untagged: + xmlstr = xmlstr.replace("", "") + if not pvid_vlan and not tagged_vlans and not untagged_vlans: + xmlstr += CE_NC_SET_PORT_MODE % (ifname, "hybrid") + self.updates_cmd.append( + "undo port hybrid untagged vlan 1") + elif self.state == "absent": + if self.intf_info["linkType"] == "hybrid": + if pvid_vlan and self.intf_info["pvid"] == pvid_vlan and pvid_vlan != '1': + self.updates_cmd.append( + "undo port hybrid pvid vlan %s" % pvid_vlan) + pvid = "1" + change = True + if tagged_vlans: + del_vlans = self.vlan_bitmap_del( + self.intf_info["trunkVlans"], vlan_targed_map) + if not is_vlan_bitmap_empty(del_vlans): + self.updates_cmd.append( + "undo port hybrid tagged vlan %s" + % tagged_vlans.replace(',', ' ').replace('-', ' to ')) + undo_map = vlan_bitmap_undo(del_vlans) + tagged = "%s:%s" % (undo_map, del_vlans) + change = True + if untagged_vlans: + del_vlans = self.vlan_bitmap_del( + self.intf_info["untagVlans"], vlan_untarged_map) + if not is_vlan_bitmap_empty(del_vlans): + self.updates_cmd.append( + "undo port hybrid untagged vlan %s" + % untagged_vlans.replace(',', ' ').replace('-', ' to ')) + undo_map = vlan_bitmap_undo(del_vlans) + untagged = "%s:%s" % (undo_map, del_vlans) + change = True + if pvid or tagged or untagged: + xmlstr += CE_NC_SET_PORT % (ifname, "hybrid", pvid, tagged, untagged) + if not pvid: + xmlstr = xmlstr.replace("", "") + if not tagged: + xmlstr = xmlstr.replace("", "") + if not untagged: + xmlstr = xmlstr.replace("", "") + + if not change: + self.updates_cmd.pop() + return + + conf_str = "" + xmlstr + "" + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "MERGE_HYBRID_PORT") + self.changed = True + + def merge_dot1qtunnel_vlan(self, ifname, default_vlan): + """Merge dot1qtunnel""" + + change = False + conf_str = "" + + self.updates_cmd.append("interface %s" % ifname) + if self.state == "present": + if self.intf_info["linkType"] == "dot1qtunnel": + if default_vlan and self.intf_info["pvid"] != default_vlan: + self.updates_cmd.append( + "port default vlan %s" % default_vlan) + conf_str = CE_NC_SET_PORT % (ifname, "dot1qtunnel", default_vlan, "", "") + change = True + else: + self.updates_cmd.append("port link-type dot1qtunnel") + if default_vlan: + self.updates_cmd.append( + "port default vlan %s" % default_vlan) + conf_str = CE_NC_SET_PORT % (ifname, "dot1qtunnel", default_vlan, "", "") + else: + conf_str = CE_NC_SET_PORT % (ifname, "dot1qtunnel", "1", "", "") + change = True + elif self.state == "absent": + if self.intf_info["linkType"] == "dot1qtunnel": + if default_vlan and self.intf_info["pvid"] == default_vlan and default_vlan != "1": + self.updates_cmd.append( + "undo port default vlan %s" % default_vlan) + conf_str = CE_NC_SET_PORT % (ifname, "dot1qtunnel", "1", "", "") + change = True + if not change: + self.updates_cmd.pop() # remove interface + return + conf_str = "" + conf_str + "" + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "MERGE_DOT1QTUNNEL_PORT") + self.changed = True + + def default_switchport(self, ifname): + """Set interface default or unconfigured""" + + change = False + if self.intf_info["linkType"] != "access": + self.updates_cmd.append("interface %s" % ifname) + self.updates_cmd.append("port link-type access") + self.updates_cmd.append("port default vlan 1") + change = True + else: + if self.intf_info["pvid"] != "1": + self.updates_cmd.append("interface %s" % ifname) + self.updates_cmd.append("port default vlan 1") + change = True + + if not change: + return + + conf_str = CE_NC_SET_DEFAULT_PORT % ifname + rcv_xml = set_nc_config(self.module, conf_str) + self.check_response(rcv_xml, "DEFAULT_INTF_VLAN") + self.changed = True + + def vlan_series(self, vlanid_s): + """ convert vlan range to vlan list """ + + vlan_list = [] + peerlistlen = len(vlanid_s) + if peerlistlen != 2: + self.module.fail_json(msg='Error: Format of vlanid is invalid.') + for num in range(peerlistlen): + if not vlanid_s[num].isdigit(): + self.module.fail_json( + msg='Error: Format of vlanid is invalid.') + if int(vlanid_s[0]) > int(vlanid_s[1]): + self.module.fail_json(msg='Error: Format of vlanid is invalid.') + elif int(vlanid_s[0]) == int(vlanid_s[1]): + vlan_list.append(str(vlanid_s[0])) + return vlan_list + for num in range(int(vlanid_s[0]), int(vlanid_s[1])): + vlan_list.append(str(num)) + vlan_list.append(vlanid_s[1]) + + return vlan_list + + def vlan_region(self, vlanid_list): + """ convert vlan range to vlan list """ + + vlan_list = [] + peerlistlen = len(vlanid_list) + for num in range(peerlistlen): + if vlanid_list[num].isdigit(): + vlan_list.append(vlanid_list[num]) + else: + vlan_s = self.vlan_series(vlanid_list[num].split('-')) + vlan_list.extend(vlan_s) + + return vlan_list + + def vlan_range_to_list(self, vlan_range): + """ convert vlan range to vlan list """ + + vlan_list = self.vlan_region(vlan_range.split(',')) + + return vlan_list + + def vlan_list_to_bitmap(self, vlanlist): + """ convert vlan list to vlan bitmap """ + + vlan_bit = ['0'] * 1024 + bit_int = [0] * 1024 + + vlan_list_len = len(vlanlist) + for num in range(vlan_list_len): + tagged_vlans = int(vlanlist[num]) + if tagged_vlans <= 0 or tagged_vlans > 4094: + self.module.fail_json( + msg='Error: Vlan id is not in the range from 1 to 4094.') + j = tagged_vlans // 4 + bit_int[j] |= 0x8 >> (tagged_vlans % 4) + vlan_bit[j] = hex(bit_int[j])[2] + + vlan_xml = ''.join(vlan_bit) + + return vlan_xml + + def vlan_bitmap_add(self, oldmap, newmap): + """vlan add bitmap""" + + vlan_bit = ['0'] * 1024 + + if len(newmap) != 1024: + self.module.fail_json(msg='Error: New vlan bitmap is invalid.') + + if len(oldmap) != 1024 and len(oldmap) != 0: + self.module.fail_json(msg='Error: old vlan bitmap is invalid.') + + if len(oldmap) == 0: + return newmap + + for num in range(1024): + new_tmp = int(newmap[num], 16) + old_tmp = int(oldmap[num], 16) + add = (~(new_tmp & old_tmp)) & new_tmp + vlan_bit[num] = hex(add)[2] + + vlan_xml = ''.join(vlan_bit) + + return vlan_xml + + def vlan_bitmap_del(self, oldmap, delmap): + """vlan del bitmap""" + + vlan_bit = ['0'] * 1024 + + if not oldmap or len(oldmap) == 0: + return ''.join(vlan_bit) + + if len(oldmap) != 1024 or len(delmap) != 1024: + self.module.fail_json(msg='Error: vlan bitmap is invalid.') + + for num in range(1024): + tmp = int(delmap[num], 16) & int(oldmap[num], 16) + vlan_bit[num] = hex(tmp)[2] + + vlan_xml = ''.join(vlan_bit) + + return vlan_xml + + def check_params(self): + """Check all input params""" + + # interface type check + if self.interface: + self.intf_type = get_interface_type(self.interface) + if not self.intf_type: + self.module.fail_json( + msg='Error: Interface name of %s is error.' % self.interface) + + if not self.intf_type or not is_portswitch_enalbed(self.intf_type): + self.module.fail_json(msg='Error: Interface %s is error.') + + # check default_vlan + if self.default_vlan: + if not self.default_vlan.isdigit(): + self.module.fail_json(msg='Error: Access vlan id is invalid.') + if int(self.default_vlan) <= 0 or int(self.default_vlan) > 4094: + self.module.fail_json( + msg='Error: Access vlan id is not in the range from 1 to 4094.') + + # check pvid_vlan + if self.pvid_vlan: + if not self.pvid_vlan.isdigit(): + self.module.fail_json(msg='Error: Pvid vlan id is invalid.') + if int(self.pvid_vlan) <= 0 or int(self.pvid_vlan) > 4094: + self.module.fail_json( + msg='Error: Pvid vlan id is not in the range from 1 to 4094.') + + # get interface info + self.intf_info = self.get_interface_dict(self.interface) + if not self.intf_info: + self.module.fail_json(msg='Error: Interface does not exist.') + + if not self.is_l2switchport(): + self.module.fail_json( + msg='Error: Interface is not layer2 switch port.') + if self.state == "unconfigured": + if any([self.mode, self.default_vlan, self.pvid_vlan, self.trunk_vlans, self.untagged_vlans, self.tagged_vlans]): + self.module.fail_json( + msg='Error: When state is unconfigured, only interface name exists.') + else: + if self.mode == "access": + if any([self.pvid_vlan, self.trunk_vlans, self.untagged_vlans, self.tagged_vlans]): + self.module.fail_json( + msg='Error: When mode is access, only default_vlan can be supported.') + elif self.mode == "trunk": + if any([self.default_vlan, self.untagged_vlans, self.tagged_vlans]): + self.module.fail_json( + msg='Error: When mode is trunk, only pvid_vlan and trunk_vlans can exist.') + elif self.mode == "hybrid": + if any([self.default_vlan, self.trunk_vlans]): + self.module.fail_json( + msg='Error: When mode is hybrid, default_vlan and trunk_vlans cannot exist') + else: + if any([self.pvid_vlan, self.trunk_vlans, self.untagged_vlans, self.tagged_vlans]): + self.module.fail_json( + msg='Error: When mode is dot1qtunnel, only default_vlan can be supported.') + + def get_proposed(self): + """get proposed info""" + + self.proposed['state'] = self.state + self.proposed['interface'] = self.interface + self.proposed['mode'] = self.mode + if self.mode: + if self.mode == "access": + self.proposed['access_pvid'] = self.default_vlan + elif self.mode == "trunk": + self.proposed['pvid_vlan'] = self.pvid_vlan + self.proposed['trunk_vlans'] = self.trunk_vlans + elif self.mode == "hybrid": + self.proposed['pvid_vlan'] = self.pvid_vlan + self.proposed['untagged_vlans'] = self.untagged_vlans + self.proposed['tagged_vlans'] = self.tagged_vlans + else: + self.proposed['dot1qtunnel_pvid'] = self.default_vlan + + def get_existing(self): + """get existing info""" + + if self.intf_info: + self.existing["interface"] = self.intf_info["ifName"] + self.existing["switchport"] = self.intf_info["l2Enable"] + self.existing["mode"] = self.intf_info["linkType"] + if self.intf_info["linkType"] == "access": + self.existing['access_pvid'] = self.intf_info["pvid"] + elif self.intf_info["linkType"] == "trunk": + self.existing['trunk_pvid'] = self.intf_info["pvid"] + self.existing['trunk_vlans'] = self.intf_info["trunkVlans"] + elif self.intf_info["linkType"] == "hybrid": + self.existing['hybrid_pvid'] = self.intf_info["pvid"] + self.existing['hybrid_untagged_vlans'] = self.intf_info["untagVlans"] + self.existing['hybrid_tagged_vlans'] = self.intf_info["trunkVlans"] + else: + self.existing['dot1qtunnel_pvid'] = self.intf_info["pvid"] + + def get_end_state(self): + """get end state info""" + + end_info = self.get_interface_dict(self.interface) + if end_info: + self.end_state["interface"] = end_info["ifName"] + self.end_state["switchport"] = end_info["l2Enable"] + self.end_state["mode"] = end_info["linkType"] + if end_info["linkType"] == "access": + self.end_state['access_pvid'] = end_info["pvid"] + elif end_info["linkType"] == "trunk": + self.end_state['trunk_pvid'] = end_info["pvid"] + self.end_state['trunk_vlans'] = end_info["trunkVlans"] + elif end_info["linkType"] == "hybrid": + self.end_state['hybrid_pvid'] = end_info["pvid"] + self.end_state['hybrid_untagged_vlans'] = end_info["untagVlans"] + self.end_state['hybrid_tagged_vlans'] = end_info["trunkVlans"] + else: + self.end_state['dot1qtunnel_pvid'] = end_info["pvid"] + if self.end_state == self.existing: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + if not self.intf_info: + self.module.fail_json(msg='Error: interface does not exist.') + self.get_existing() + self.get_proposed() + + # present or absent + if self.state == "present" or self.state == "absent": + if self.mode == "access": + self.merge_access_vlan(self.interface, self.default_vlan) + elif self.mode == "trunk": + self.merge_trunk_vlan( + self.interface, self.pvid_vlan, self.trunk_vlans) + elif self.mode == "hybrid": + self.merge_hybrid_vlan(self.interface, self.pvid_vlan, self.tagged_vlans, self.untagged_vlans) + else: + self.merge_dot1qtunnel_vlan(self.interface, self.default_vlan) + + # unconfigured + else: + self.default_switchport(self.interface) + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + interface=dict(required=True, type='str'), + mode=dict(choices=['access', 'trunk', 'dot1qtunnel', 'hybrid'], required=False), + default_vlan=dict(type='str', required=False), + pvid_vlan=dict(type='str', required=False), + trunk_vlans=dict(type='str', required=False), + untagged_vlans=dict(type='str', required=False), + tagged_vlans=dict(type='str', required=False), + state=dict(choices=['absent', 'present', 'unconfigured'], + default='present') + ) + + argument_spec.update(ce_argument_spec) + switchport = SwitchPort(argument_spec) + switchport.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vlan.py b/plugins/modules/ce_vlan.py new file mode 100644 index 0000000..c1c4baa --- /dev/null +++ b/plugins/modules/ce_vlan.py @@ -0,0 +1,688 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vlan +short_description: Manages VLAN resources and attributes on Huawei CloudEngine switches. +description: + - Manages VLAN configurations on Huawei CloudEngine switches. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + vlan_id: + description: + - Single VLAN ID, in the range from 1 to 4094. + vlan_range: + description: + - Range of VLANs such as C(2-10) or C(2,5,10-15), etc. + name: + description: + - Name of VLAN, minimum of 1 character, maximum of 31 characters. + description: + description: + - Specify VLAN description, minimum of 1 character, maximum of 80 characters. + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: vlan module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Ensure a range of VLANs are not present on the switch + ce_vlan: + vlan_range: "2-10,20,50,55-60,100-150" + state: absent + provider: "{{ cli }}" + + - name: Ensure VLAN 50 exists with the name WEB + ce_vlan: + vlan_id: 50 + name: WEB + state: absent + provider: "{{ cli }}" + + - name: Ensure VLAN is NOT on the device + ce_vlan: + vlan_id: 50 + state: absent + provider: "{{ cli }}" + +''' + +RETURN = ''' +proposed_vlans_list: + description: list of VLANs being proposed + returned: always + type: list + sample: ["100"] +existing_vlans_list: + description: list of existing VLANs on the switch prior to making changes + returned: always + type: list + sample: ["1", "2", "3", "4", "5", "20"] +end_state_vlans_list: + description: list of VLANs after the module is executed + returned: always + type: list + sample: ["1", "2", "3", "4", "5", "20", "100"] +proposed: + description: k/v pairs of parameters passed into module (does not include + vlan_id or vlan_range) + returned: always + type: dict + sample: {"vlan_id":"20", "name": "VLAN_APP", "description": "vlan for app" } +existing: + description: k/v pairs of existing vlan or null when using vlan_range + returned: always + type: dict + sample: {"vlan_id":"20", "name": "VLAN_APP", "description": "" } +end_state: + description: k/v pairs of the VLAN after executing module or null + when using vlan_range + returned: always + type: dict + sample: {"vlan_id":"20", "name": "VLAN_APP", "description": "vlan for app" } +updates: + description: command string sent to the device + returned: always + type: list + sample: ["vlan 20", "name VLAN20"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, execute_nc_action, ce_argument_spec + +CE_NC_CREATE_VLAN = """ + + + + + %s + %s + %s + + + + + + +""" + +CE_NC_DELETE_VLAN = """ + + + + + %s + + + + +""" + +CE_NC_MERGE_VLAN_DES = """ + + + + + %s + %s + + + + + + +""" + +CE_NC_MERGE_VLAN_NAME = """ + + + + + %s + %s + + + + + + +""" + + +CE_NC_MERGE_VLAN = """ + + + + + %s + %s + %s + + + + + + +""" + +CE_NC_GET_VLAN = """ + + + + + %s + + + + + + +""" + +CE_NC_GET_VLANS = """ + + + + + + + + + + +""" + +CE_NC_CREATE_VLAN_BATCH = """ + + + + %s:%s + + + +""" + +CE_NC_DELETE_VLAN_BATCH = """ + + + + %s:%s + + + +""" + + +class Vlan(object): + """ + Manages VLAN resources and attributes + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # vlan config info + self.vlan_id = self.module.params['vlan_id'] + self.vlan_range = self.module.params['vlan_range'] + self.name = self.module.params['name'] + self.description = self.module.params['description'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.vlan_exist = False + self.vlan_attr_exist = None + self.vlans_list_exist = list() + self.vlans_list_change = list() + self.updates_cmd = list() + self.results = dict() + self.vlan_attr_end = dict() + + def init_module(self): + """ + init ansible NetworkModule. + """ + + required_one_of = [["vlan_id", "vlan_range"]] + mutually_exclusive = [["vlan_id", "vlan_range"]] + + self.module = AnsibleModule( + argument_spec=self.spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def config_vlan(self, vlan_id, name='', description=''): + """Create vlan.""" + + if name is None: + name = '' + if description is None: + description = '' + + conf_str = CE_NC_CREATE_VLAN % (vlan_id, name, description) + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "CREATE_VLAN") + self.changed = True + + def merge_vlan(self, vlan_id, name, description): + """Merge vlan.""" + + conf_str = None + + if not name and description: + conf_str = CE_NC_MERGE_VLAN_DES % (vlan_id, description) + if not description and name: + conf_str = CE_NC_MERGE_VLAN_NAME % (vlan_id, name) + if description and name: + conf_str = CE_NC_MERGE_VLAN % (vlan_id, name, description) + + if not conf_str: + return + + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "MERGE_VLAN") + self.changed = True + + def create_vlan_batch(self, vlan_list): + """Create vlan batch.""" + + if not vlan_list: + return + + vlan_bitmap = self.vlan_list_to_bitmap(vlan_list) + xmlstr = CE_NC_CREATE_VLAN_BATCH % (vlan_bitmap, vlan_bitmap) + + recv_xml = execute_nc_action(self.module, xmlstr) + self.check_response(recv_xml, "CREATE_VLAN_BATCH") + self.updates_cmd.append('vlan batch %s' % ( + self.vlan_range.replace(',', ' ').replace('-', ' to '))) + self.changed = True + + def delete_vlan_batch(self, vlan_list): + """Delete vlan batch.""" + + if not vlan_list: + return + + vlan_bitmap = self.vlan_list_to_bitmap(vlan_list) + xmlstr = CE_NC_DELETE_VLAN_BATCH % (vlan_bitmap, vlan_bitmap) + + recv_xml = execute_nc_action(self.module, xmlstr) + self.check_response(recv_xml, "DELETE_VLAN_BATCH") + self.updates_cmd.append('undo vlan batch %s' % ( + self.vlan_range.replace(',', ' ').replace('-', ' to '))) + self.changed = True + + def undo_config_vlan(self, vlanid): + """Delete vlan.""" + + conf_str = CE_NC_DELETE_VLAN % vlanid + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "DELETE_VLAN") + self.changed = True + self.updates_cmd.append('undo vlan %s' % self.vlan_id) + + def get_vlan_attr(self, vlan_id): + """ get vlan attributes.""" + + conf_str = CE_NC_GET_VLAN % vlan_id + xml_str = get_nc_config(self.module, conf_str) + attr = dict() + + if "" in xml_str: + return attr + else: + re_find_id = re.findall(r'.*(.*).*\s*', xml_str) + re_find_name = re.findall(r'.*(.*).*\s*', xml_str) + re_find_desc = re.findall(r'.*(.*).*\s*', xml_str) + + if re_find_id: + if re_find_name: + attr = dict(vlan_id=re_find_id[0], name=re_find_name[0], + description=re_find_desc[0]) + else: + attr = dict(vlan_id=re_find_id[0], name=None, + description=re_find_desc[0]) + return attr + + def get_vlans_name(self): + """ get all vlan vid and its name list, + sample: [ ("20", "VLAN_NAME_20"), ("30", "VLAN_NAME_30") ]""" + + conf_str = CE_NC_GET_VLANS + xml_str = get_nc_config(self.module, conf_str) + vlan_list = list() + + if "" in xml_str: + return vlan_list + else: + vlan_list = re.findall( + r'.*(.*).*\s*(.*).*', xml_str) + return vlan_list + + def get_vlans_list(self): + """ get all vlan vid list, sample: [ "20", "30", "31" ]""" + + conf_str = CE_NC_GET_VLANS + xml_str = get_nc_config(self.module, conf_str) + vlan_list = list() + + if "" in xml_str: + return vlan_list + else: + vlan_list = re.findall( + r'.*(.*).*', xml_str) + return vlan_list + + def vlan_series(self, vlanid_s): + """ convert vlan range to list """ + + vlan_list = [] + peerlistlen = len(vlanid_s) + if peerlistlen != 2: + self.module.fail_json(msg='Error: Format of vlanid is invalid.') + for num in range(peerlistlen): + if not vlanid_s[num].isdigit(): + self.module.fail_json( + msg='Error: Format of vlanid is invalid.') + if int(vlanid_s[0]) > int(vlanid_s[1]): + self.module.fail_json(msg='Error: Format of vlanid is invalid.') + elif int(vlanid_s[0]) == int(vlanid_s[1]): + vlan_list.append(str(vlanid_s[0])) + return vlan_list + for num in range(int(vlanid_s[0]), int(vlanid_s[1])): + vlan_list.append(str(num)) + vlan_list.append(vlanid_s[1]) + + return vlan_list + + def vlan_region(self, vlanid_list): + """ convert vlan range to vlan list """ + + vlan_list = [] + peerlistlen = len(vlanid_list) + for num in range(peerlistlen): + if vlanid_list[num].isdigit(): + vlan_list.append(vlanid_list[num]) + else: + vlan_s = self.vlan_series(vlanid_list[num].split('-')) + vlan_list.extend(vlan_s) + + return vlan_list + + def vlan_range_to_list(self, vlan_range): + """ convert vlan range to vlan list """ + + vlan_list = self.vlan_region(vlan_range.split(',')) + + return vlan_list + + def vlan_list_to_bitmap(self, vlanlist): + """ convert vlan list to vlan bitmap """ + + vlan_bit = ['0'] * 1024 + bit_int = [0] * 1024 + + vlan_list_len = len(vlanlist) + for num in range(vlan_list_len): + tagged_vlans = int(vlanlist[num]) + if tagged_vlans <= 0 or tagged_vlans > 4094: + self.module.fail_json( + msg='Error: Vlan id is not in the range from 1 to 4094.') + j = tagged_vlans // 4 + bit_int[j] |= 0x8 >> (tagged_vlans % 4) + vlan_bit[j] = hex(bit_int[j])[2] + + vlan_xml = ''.join(vlan_bit) + + return vlan_xml + + def check_params(self): + """Check all input params""" + + if not self.vlan_id and self.description: + self.module.fail_json( + msg='Error: Vlan description could be set only at one vlan.') + + if not self.vlan_id and self.name: + self.module.fail_json( + msg='Error: Vlan name could be set only at one vlan.') + + # check vlan id + if self.vlan_id: + if not self.vlan_id.isdigit(): + self.module.fail_json( + msg='Error: Vlan id is not digit.') + if int(self.vlan_id) <= 0 or int(self.vlan_id) > 4094: + self.module.fail_json( + msg='Error: Vlan id is not in the range from 1 to 4094.') + + # check vlan description + if self.description: + if len(self.description) > 81 or len(self.description.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: vlan description is not in the range from 1 to 80.') + + # check vlan name + if self.name: + if len(self.name) > 31 or len(self.name.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: Vlan name is not in the range from 1 to 31.') + + def get_proposed(self): + """ + get proposed config. + """ + + if self.vlans_list_change: + if self.state == 'present': + proposed_vlans_tmp = list(self.vlans_list_change) + proposed_vlans_tmp.extend(self.vlans_list_exist) + self.results['proposed_vlans_list'] = list( + set(proposed_vlans_tmp)) + else: + self.results['proposed_vlans_list'] = list( + set(self.vlans_list_exist) - set(self.vlans_list_change)) + self.results['proposed_vlans_list'].sort() + else: + self.results['proposed_vlans_list'] = self.vlans_list_exist + + if self.vlan_id: + if self.state == "present": + self.results['proposed'] = dict( + vlan_id=self.vlan_id, + name=self.name, + description=self.description + ) + else: + self.results['proposed'] = None + else: + self.results['proposed'] = None + + def get_existing(self): + """ + get existing config. + """ + + self.results['existing_vlans_list'] = self.vlans_list_exist + + if self.vlan_id: + if self.vlan_attr_exist: + self.results['existing'] = dict( + vlan_id=self.vlan_attr_exist['vlan_id'], + name=self.vlan_attr_exist['name'], + description=self.vlan_attr_exist['description'] + ) + else: + self.results['existing'] = None + else: + self.results['existing'] = None + + def get_end_state(self): + """ + get end state config. + """ + + self.results['end_state_vlans_list'] = self.get_vlans_list() + + if self.vlan_id: + if self.vlan_attr_end: + self.results['end_state'] = dict( + vlan_id=self.vlan_attr_end['vlan_id'], + name=self.vlan_attr_end['name'], + description=self.vlan_attr_end['description'] + ) + else: + self.results['end_state'] = None + + else: + self.results['end_state'] = None + + def work(self): + """ + worker. + """ + + # check param + self.check_params() + + # get all vlan info + self.vlans_list_exist = self.get_vlans_list() + + # get vlan attributes + if self.vlan_id: + self.vlans_list_change.append(self.vlan_id) + self.vlan_attr_exist = self.get_vlan_attr(self.vlan_id) + if self.vlan_attr_exist: + self.vlan_exist = True + + if self.vlan_range: + new_vlans_tmp = self.vlan_range_to_list(self.vlan_range) + if self.state == 'present': + self.vlans_list_change = list( + set(new_vlans_tmp) - set(self.vlans_list_exist)) + else: + self.vlans_list_change = [ + val for val in new_vlans_tmp if val in self.vlans_list_exist] + + if self.state == 'present': + if self.vlan_id: + if not self.vlan_exist: + # create a new vlan + self.config_vlan(self.vlan_id, self.name, self.description) + elif self.description and self.description != self.vlan_attr_exist['description']: + # merge vlan description + self.merge_vlan(self.vlan_id, self.name, self.description) + elif self.name and self.name != self.vlan_attr_exist['name']: + # merge vlan name + self.merge_vlan(self.vlan_id, self.name, self.description) + + # update command for results + if self.changed: + self.updates_cmd.append('vlan %s' % self.vlan_id) + if self.name: + self.updates_cmd.append('name %s' % self.name) + if self.description: + self.updates_cmd.append( + 'description %s' % self.description) + elif self.vlan_range and self.vlans_list_change: + self.create_vlan_batch(self.vlans_list_change) + else: # absent + if self.vlan_id: + if self.vlan_exist: + # delete the vlan + self.undo_config_vlan(self.vlan_id) + elif self.vlan_range and self.vlans_list_change: + self.delete_vlan_batch(self.vlans_list_change) + + # result + if self.vlan_id: + self.vlan_attr_end = self.get_vlan_attr(self.vlan_id) + + self.get_existing() + self.get_proposed() + self.get_end_state() + + self.results['changed'] = self.changed + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """ module main """ + + argument_spec = dict( + vlan_id=dict(required=False), + vlan_range=dict(required=False, type='str'), + name=dict(required=False, type='str'), + description=dict(required=False, type='str'), + state=dict(choices=['absent', 'present'], + default='present', required=False), + ) + + argument_spec.update(ce_argument_spec) + vlancfg = Vlan(argument_spec) + vlancfg.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vrf.py b/plugins/modules/ce_vrf.py new file mode 100644 index 0000000..2a58a61 --- /dev/null +++ b/plugins/modules/ce_vrf.py @@ -0,0 +1,353 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vrf +short_description: Manages VPN instance on HUAWEI CloudEngine switches. +description: + - Manages VPN instance of HUAWEI CloudEngine switches. +author: Yang yang (@QijunPan) +notes: + - If I(state=absent), the route will be removed, regardless of the non-required options. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + vrf: + description: + - VPN instance, the length of vrf name is 1 - 31, i.e. "test", but can not be C(_public_). + required: true + description: + description: + - Description of the vrf, the string length is 1 - 242 . + state: + description: + - Manage the state of the resource. + choices: ['present','absent'] + default: present +''' + +EXAMPLES = ''' +- name: vrf module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Config a vpn install named vpna, description is test + ce_vrf: + vrf: vpna + description: test + state: present + provider: "{{ cli }}" + - name: Delete a vpn install named vpna + ce_vrf: + vrf: vpna + state: absent + provider: "{{ cli }}" +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"vrf": "vpna", + "description": "test", + "state": "present"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: {"vrf": "vpna", + "description": "test", + "present": "present"} +updates: + description: command list sent to the device + returned: always + type: list + sample: ["ip vpn-instance vpna", + "description test"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_VRF = """ + + + + + + + + + + + + +""" + +CE_NC_CREATE_VRF = """ + + + + + %s + %s + + + + +""" + +CE_NC_DELETE_VRF = """ + + + + + %s + %s + + + + +""" + + +def build_config_xml(xmlstr): + """build_config_xml""" + + return ' ' + xmlstr + ' ' + + +class Vrf(object): + """Manage vpn instance""" + + def __init__(self, argument_spec, ): + self.spec = argument_spec + self.module = None + self.init_module() + + # vpn instance info + self.vrf = self.module.params['vrf'] + self.description = self.module.params['description'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init_module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def set_update_cmd(self): + """ set update command""" + if not self.changed: + return + if self.state == "present": + self.updates_cmd.append('ip vpn-instance %s' % (self.vrf)) + if self.description: + self.updates_cmd.append('description %s' % (self.description)) + else: + self.updates_cmd.append('undo ip vpn-instance %s' % (self.vrf)) + + def get_vrf(self): + """ check if vrf is need to change""" + + getxmlstr = CE_NC_GET_VRF + xml_str = get_nc_config(self.module, getxmlstr) + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + vpn_instances = root.findall( + "l3vpn/l3vpncomm/l3vpnInstances/l3vpnInstance") + if vpn_instances: + for vpn_instance in vpn_instances: + if vpn_instance.find('vrfName').text == self.vrf: + if vpn_instance.find('vrfDescription').text == self.description: + if self.state == "present": + return False + else: + return True + else: + return True + return self.state == "present" + else: + return self.state == "present" + + def check_params(self): + """Check all input params""" + + # vrf and description check + if self.vrf == '_public_': + self.module.fail_json( + msg='Error: The vrf name _public_ is reserved.') + if len(self.vrf) < 1 or len(self.vrf) > 31: + self.module.fail_json( + msg='Error: The vrf name length must between 1 and 242.') + if self.description: + if len(self.description) < 1 or len(self.description) > 242: + self.module.fail_json( + msg='Error: The vrf description length must between 1 and 242.') + + def operate_vrf(self): + """config/delete vrf""" + if not self.changed: + return + if self.state == "present": + if self.description is None: + configxmlstr = CE_NC_CREATE_VRF % (self.vrf, '') + else: + configxmlstr = CE_NC_CREATE_VRF % (self.vrf, self.description) + else: + configxmlstr = CE_NC_DELETE_VRF % (self.vrf, self.description) + + conf_str = build_config_xml(configxmlstr) + + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "OPERATE_VRF") + + def get_proposed(self): + """get_proposed""" + + if self.state == 'present': + self.proposed['vrf'] = self.vrf + if self.description: + self.proposed['description'] = self.description + + else: + self.proposed = dict() + self.proposed['state'] = self.state + + def get_existing(self): + """get_existing""" + + change = self.get_vrf() + if change: + if self.state == 'present': + self.existing = dict() + else: + self.existing['vrf'] = self.vrf + if self.description: + self.existing['description'] = self.description + self.changed = True + else: + if self.state == 'absent': + self.existing = dict() + else: + self.existing['vrf'] = self.vrf + if self.description: + self.existing['description'] = self.description + self.changed = False + + def get_end_state(self): + """get_end_state""" + + change = self.get_vrf() + if not change: + if self.state == 'present': + self.end_state['vrf'] = self.vrf + if self.description: + self.end_state['description'] = self.description + else: + self.end_state = dict() + else: + if self.state == 'present': + self.end_state = dict() + else: + self.end_state['vrf'] = self.vrf + if self.description: + self.end_state['description'] = self.description + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + self.operate_vrf() + self.set_update_cmd() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """main""" + + argument_spec = dict( + vrf=dict(required=True, type='str'), + description=dict(required=False, type='str'), + state=dict(choices=['absent', 'present'], + default='present', required=False), + ) + argument_spec.update(ce_argument_spec) + interface = Vrf(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vrf_af.py b/plugins/modules/ce_vrf_af.py new file mode 100644 index 0000000..8a479e2 --- /dev/null +++ b/plugins/modules/ce_vrf_af.py @@ -0,0 +1,848 @@ +#!/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 . +# +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} +DOCUMENTATION = ''' +--- +module: ce_vrf_af +short_description: Manages VPN instance address family on HUAWEI CloudEngine switches. +description: + - Manages VPN instance address family of HUAWEI CloudEngine switches. +author: Yang yang (@QijunPan) +notes: + - If I(state=absent), the vrf will be removed, regardless of the non-required parameters. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + vrf: + description: + - VPN instance. + required: true + vrf_aftype: + description: + - VPN instance address family. + choices: ['v4','v6'] + default: v4 + route_distinguisher: + description: + - VPN instance route distinguisher,the RD used to distinguish same route prefix from different vpn. + The RD must be setted before setting vpn_target_value. + vpn_target_state: + description: + - Manage the state of the vpn target. + choices: ['present','absent'] + vpn_target_type: + description: + - VPN instance vpn target type. + choices: ['export_extcommunity', 'import_extcommunity'] + vpn_target_value: + description: + - VPN instance target value. Such as X.X.X.X:number<0-65535> or number<0-65535>:number<0-4294967295> + or number<0-65535>.number<0-65535>:number<0-65535> or number<65536-4294967295>:number<0-65535> + but not support 0:0 and 0.0:0. + evpn: + description: + - Is extend vpn or normal vpn. + type: bool + default: 'no' + state: + description: + - Manage the state of the af. + choices: ['present','absent'] + default: present +''' + +EXAMPLES = ''' +- name: vrf af module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Config vpna, set address family is ipv4 + ce_vrf_af: + vrf: vpna + vrf_aftype: v4 + state: present + provider: "{{ cli }}" + - name: Config vpna, delete address family is ipv4 + ce_vrf_af: + vrf: vpna + vrf_aftype: v4 + state: absent + provider: "{{ cli }}" + - name: Config vpna, set address family is ipv4,rd=1:1,set vpn_target_type=export_extcommunity,vpn_target_value=2:2 + ce_vrf_af: + vrf: vpna + vrf_aftype: v4 + route_distinguisher: 1:1 + vpn_target_type: export_extcommunity + vpn_target_value: 2:2 + vpn_target_state: present + state: present + provider: "{{ cli }}" + - name: Config vpna, set address family is ipv4,rd=1:1,delete vpn_target_type=export_extcommunity,vpn_target_value=2:2 + ce_vrf_af: + vrf: vpna + vrf_aftype: v4 + route_distinguisher: 1:1 + vpn_target_type: export_extcommunity + vpn_target_value: 2:2 + vpn_target_state: absent + state: present + provider: "{{ cli }}" +''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"vrf": "vpna", + "vrf_aftype": "v4", + "state": "present", + "vpn_targe_state":"absent", + "evpn": "none", + "vpn_target_type": "none", + "vpn_target_value": "none"} +existing: + description: k/v pairs of existing switchport + returned: always + type: dict + sample: { + "route_distinguisher": [ + "1:1", + "2:2" + ], + "vpn_target_type": [], + "vpn_target_value": [], + "vrf": "vpna", + "vrf_aftype": [ + "ipv4uni", + "ipv6uni" + ] + } +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict + sample: { + "route_distinguisher": [ + "1:1", + "2:2" + ], + "vpn_target_type": [ + "import_extcommunity", + "3:3" + ], + "vpn_target_value": [], + "vrf": "vpna", + "vrf_aftype": [ + "ipv4uni", + "ipv6uni" + ] + } +updates: + description: command list sent to the device + returned: always + type: list + sample: [ + "ip vpn-instance vpna", + "vpn-target 3:3 import_extcommunity" + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + + +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_VRF = """ + + + + + + + + + + + + +""" + +CE_NC_GET_VRF_AF = """ + + + + + + %s + + + + %s + + + + + + + +""" + +CE_NC_DELETE_VRF_AF = """ + + + + + %s + + + %s + + + + + + +""" + +CE_NC_CREATE_VRF_AF = """ + + + + + %s + + + %s + %s%s + + + + + +""" +CE_NC_CREATE_VRF_TARGET = """ + + + %s + %s + + +""" + +CE_NC_DELETE_VRF_TARGET = """ + + + %s + %s + + +""" + +CE_NC_GET_VRF_TARGET = """ + + + + + + +""" + +CE_NC_CREATE_EXTEND_VRF_TARGET = """ + + + %s + %s + evpn + + +""" + +CE_NC_DELETE_EXTEND_VRF_TARGET = """ + + + %s + %s + evpn + + +""" + +CE_NC_GET_EXTEND_VRF_TARGET = """ + + + + + + + +""" + + +def build_config_xml(xmlstr): + """build_config_xml""" + + return ' ' + xmlstr + ' ' + + +def is_valid_value(vrf_targe_value): + """check if the vrf target value is valid""" + + each_num = None + if len(vrf_targe_value) > 21 or len(vrf_targe_value) < 3: + return False + if vrf_targe_value.find(':') == -1: + return False + elif vrf_targe_value == '0:0': + return False + elif vrf_targe_value == '0.0:0': + return False + else: + value_list = vrf_targe_value.split(':') + if value_list[0].find('.') != -1: + if not value_list[1].isdigit(): + return False + if int(value_list[1]) > 65535: + return False + value = value_list[0].split('.') + if len(value) == 4: + for each_num in value: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + elif len(value) == 2: + for each_num in value: + if not each_num.isdigit(): + return False + if int(each_num) > 65535: + return False + return True + else: + return False + elif not value_list[0].isdigit(): + return False + elif not value_list[1].isdigit(): + return False + elif int(value_list[0]) < 65536 and int(value_list[1]) < 4294967296: + return True + elif int(value_list[0]) > 65535 and int(value_list[0]) < 4294967296: + return bool(int(value_list[1]) < 65536) + else: + return False + + +class VrfAf(object): + """manage the vrf address family and export/import target""" + + def __init__(self, argument_spec, ): + self.spec = argument_spec + self.module = None + self.init_module() + + # vpn instance info + self.vrf = self.module.params['vrf'] + self.vrf_aftype = self.module.params['vrf_aftype'] + if self.vrf_aftype == 'v4': + self.vrf_aftype = 'ipv4uni' + else: + self.vrf_aftype = 'ipv6uni' + self.route_distinguisher = self.module.params['route_distinguisher'] + self.evpn = self.module.params['evpn'] + self.vpn_target_type = self.module.params['vpn_target_type'] + self.vpn_target_value = self.module.params['vpn_target_value'] + self.vpn_target_state = self.module.params['vpn_target_state'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + self.vpn_target_changed = False + self.vrf_af_type_changed = False + self.vrf_rd_changed = False + self.vrf_af_info = dict() + + def init_module(self): + """init_module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def is_vrf_af_exist(self): + """is vrf address family exist""" + + if not self.vrf_af_info: + return False + + for vrf_af_ele in self.vrf_af_info["vpnInstAF"]: + if vrf_af_ele["afType"] == self.vrf_aftype: + return True + else: + continue + return False + + def get_exist_rd(self): + """get exist route distinguisher """ + + if not self.vrf_af_info: + return None + + for vrf_af_ele in self.vrf_af_info["vpnInstAF"]: + if vrf_af_ele["afType"] == self.vrf_aftype: + if vrf_af_ele["vrfRD"] is None: + return None + else: + return vrf_af_ele["vrfRD"] + else: + continue + return None + + def is_vrf_rd_exist(self): + """is vrf route distinguisher exist""" + + if not self.vrf_af_info: + return False + + for vrf_af_ele in self.vrf_af_info["vpnInstAF"]: + if vrf_af_ele["afType"] == self.vrf_aftype: + if vrf_af_ele["vrfRD"] is None: + return False + if self.route_distinguisher is not None: + return bool(vrf_af_ele["vrfRD"] == self.route_distinguisher) + else: + return True + else: + continue + return False + + def is_vrf_rt_exist(self): + """is vpn target exist""" + + if not self.vrf_af_info: + return False + + for vrf_af_ele in self.vrf_af_info["vpnInstAF"]: + if vrf_af_ele["afType"] == self.vrf_aftype: + if self.evpn is False: + if not vrf_af_ele.get("vpnTargets"): + return False + for vpn_target in vrf_af_ele.get("vpnTargets"): + if vpn_target["vrfRTType"] == self.vpn_target_type \ + and vpn_target["vrfRTValue"] == self.vpn_target_value: + return True + else: + continue + else: + if not vrf_af_ele.get("evpnTargets"): + return False + for evpn_target in vrf_af_ele.get("evpnTargets"): + if evpn_target["vrfRTType"] == self.vpn_target_type \ + and evpn_target["vrfRTValue"] == self.vpn_target_value: + return True + else: + continue + else: + continue + return False + + def set_update_cmd(self): + """ set update command""" + if not self.changed: + return + if self.vpn_target_type: + if self.vpn_target_type == "export_extcommunity": + vpn_target_type = "export-extcommunity" + else: + vpn_target_type = "import-extcommunity" + if self.state == "present": + self.updates_cmd.append('ip vpn-instance %s' % (self.vrf)) + if self.vrf_aftype == 'ipv4uni': + self.updates_cmd.append('ipv4-family') + elif self.vrf_aftype == 'ipv6uni': + self.updates_cmd.append('ipv6-family') + if self.route_distinguisher: + if not self.is_vrf_rd_exist(): + self.updates_cmd.append( + 'route-distinguisher %s' % self.route_distinguisher) + else: + if self.get_exist_rd() is not None: + self.updates_cmd.append( + 'undo route-distinguisher %s' % self.get_exist_rd()) + if self.vpn_target_state == "present": + if not self.is_vrf_rt_exist(): + if self.evpn is False: + self.updates_cmd.append( + 'vpn-target %s %s' % (self.vpn_target_value, vpn_target_type)) + else: + self.updates_cmd.append( + 'vpn-target %s %s evpn' % (self.vpn_target_value, vpn_target_type)) + elif self.vpn_target_state == "absent": + if self.is_vrf_rt_exist(): + if self.evpn is False: + self.updates_cmd.append( + 'undo vpn-target %s %s' % (self.vpn_target_value, vpn_target_type)) + else: + self.updates_cmd.append( + 'undo vpn-target %s %s evpn' % (self.vpn_target_value, vpn_target_type)) + else: + self.updates_cmd.append('ip vpn-instance %s' % (self.vrf)) + if self.vrf_aftype == 'ipv4uni': + self.updates_cmd.append('undo ipv4-family') + elif self.vrf_aftype == 'ipv6uni': + self.updates_cmd.append('undo ipv6-family') + + def get_vrf(self): + """ check if vrf is need to change""" + + getxmlstr = CE_NC_GET_VRF + xmlstr_new_1 = (self.vrf.lower()) + + xml_str = get_nc_config(self.module, getxmlstr) + re_find_1 = re.findall( + r'.*(.*).*', xml_str.lower()) + + if re_find_1 is None: + return False + + return xmlstr_new_1 in re_find_1 + + def get_vrf_af(self): + """ check if vrf is need to change""" + + self.vrf_af_info["vpnInstAF"] = list() + if self.evpn is True: + getxmlstr = CE_NC_GET_VRF_AF % ( + self.vrf, CE_NC_GET_EXTEND_VRF_TARGET) + else: + getxmlstr = CE_NC_GET_VRF_AF % (self.vrf, CE_NC_GET_VRF_TARGET) + + xml_str = get_nc_config(self.module, getxmlstr) + + if 'data/' in xml_str: + return self.state == 'present' + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + + # get the vpn address family and RD text + vrf_addr_types = root.findall( + "l3vpn/l3vpncomm/l3vpnInstances/l3vpnInstance/vpnInstAFs/vpnInstAF") + if vrf_addr_types: + for vrf_addr_type in vrf_addr_types: + vrf_af_info = dict() + for vrf_addr_type_ele in vrf_addr_type: + if vrf_addr_type_ele.tag in ["vrfName", "afType", "vrfRD"]: + vrf_af_info[vrf_addr_type_ele.tag] = vrf_addr_type_ele.text + if vrf_addr_type_ele.tag == 'vpnTargets': + vrf_af_info["vpnTargets"] = list() + for rtargets in vrf_addr_type_ele: + rt_dict = dict() + for rtarget in rtargets: + if rtarget.tag in ["vrfRTValue", "vrfRTType"]: + rt_dict[rtarget.tag] = rtarget.text + vrf_af_info["vpnTargets"].append(rt_dict) + if vrf_addr_type_ele.tag == 'exVpnTargets': + vrf_af_info["evpnTargets"] = list() + for rtargets in vrf_addr_type_ele: + rt_dict = dict() + for rtarget in rtargets: + if rtarget.tag in ["vrfRTValue", "vrfRTType"]: + rt_dict[rtarget.tag] = rtarget.text + vrf_af_info["evpnTargets"].append(rt_dict) + self.vrf_af_info["vpnInstAF"].append(vrf_af_info) + + def check_params(self): + """Check all input params""" + + # vrf and description check + if self.vrf == '_public_': + self.module.fail_json( + msg='Error: The vrf name _public_ is reserved.') + if not self.get_vrf(): + self.module.fail_json( + msg='Error: The vrf name do not exist.') + if self.state == 'present': + if self.route_distinguisher: + if not is_valid_value(self.route_distinguisher): + self.module.fail_json(msg='Error:The vrf route distinguisher length must between 3 ~ 21,' + 'i.e. X.X.X.X:number<0-65535> or number<0-65535>:number<0-4294967295>' + 'or number<0-65535>.number<0-65535>:number<0-65535>' + 'or number<65536-4294967295>:number<0-65535>' + ' but not be 0:0 or 0.0:0.') + if not self.vpn_target_state: + if self.vpn_target_value or self.vpn_target_type: + self.module.fail_json( + msg='Error: The vpn target state should be exist.') + if self.vpn_target_state: + if not self.vpn_target_value or not self.vpn_target_type: + self.module.fail_json( + msg='Error: The vpn target value and type should be exist.') + if self.vpn_target_value: + if not is_valid_value(self.vpn_target_value): + self.module.fail_json(msg='Error:The vrf target value length must between 3 ~ 21,' + 'i.e. X.X.X.X:number<0-65535> or number<0-65535>:number<0-4294967295>' + 'or number<0-65535>.number<0-65535>:number<0-65535>' + 'or number<65536-4294967295>:number<0-65535>' + ' but not be 0:0 or 0.0:0.') + + def operate_vrf_af(self): + """config/delete vrf""" + + vrf_target_operate = '' + if self.route_distinguisher is None: + route_d = '' + else: + route_d = self.route_distinguisher + + if self.state == 'present': + if self.vrf_aftype: + if self.is_vrf_af_exist(): + self.vrf_af_type_changed = False + else: + self.vrf_af_type_changed = True + configxmlstr = CE_NC_CREATE_VRF_AF % ( + self.vrf, self.vrf_aftype, route_d, vrf_target_operate) + else: + self.vrf_af_type_changed = bool(self.is_vrf_af_exist()) + + if self.vpn_target_state == 'present': + if self.evpn is False and not self.is_vrf_rt_exist(): + vrf_target_operate = CE_NC_CREATE_VRF_TARGET % ( + self.vpn_target_type, self.vpn_target_value) + configxmlstr = CE_NC_CREATE_VRF_AF % ( + self.vrf, self.vrf_aftype, route_d, vrf_target_operate) + self.vpn_target_changed = True + if self.evpn is True and not self.is_vrf_rt_exist(): + vrf_target_operate = CE_NC_CREATE_EXTEND_VRF_TARGET % ( + self.vpn_target_type, self.vpn_target_value) + configxmlstr = CE_NC_CREATE_VRF_AF % ( + self.vrf, self.vrf_aftype, route_d, vrf_target_operate) + self.vpn_target_changed = True + elif self.vpn_target_state == 'absent': + if self.evpn is False and self.is_vrf_rt_exist(): + vrf_target_operate = CE_NC_DELETE_VRF_TARGET % ( + self.vpn_target_type, self.vpn_target_value) + configxmlstr = CE_NC_CREATE_VRF_AF % ( + self.vrf, self.vrf_aftype, route_d, vrf_target_operate) + self.vpn_target_changed = True + if self.evpn is True and self.is_vrf_rt_exist(): + vrf_target_operate = CE_NC_DELETE_EXTEND_VRF_TARGET % ( + self.vpn_target_type, self.vpn_target_value) + configxmlstr = CE_NC_CREATE_VRF_AF % ( + self.vrf, self.vrf_aftype, route_d, vrf_target_operate) + self.vpn_target_changed = True + else: + if self.route_distinguisher: + if not self.is_vrf_rd_exist(): + configxmlstr = CE_NC_CREATE_VRF_AF % ( + self.vrf, self.vrf_aftype, route_d, vrf_target_operate) + self.vrf_rd_changed = True + else: + self.vrf_rd_changed = False + else: + if self.is_vrf_rd_exist(): + configxmlstr = CE_NC_CREATE_VRF_AF % ( + self.vrf, self.vrf_aftype, route_d, vrf_target_operate) + self.vrf_rd_changed = True + else: + self.vrf_rd_changed = False + if not self.vrf_rd_changed and not self.vrf_af_type_changed and not self.vpn_target_changed: + self.changed = False + else: + self.changed = True + else: + if self.is_vrf_af_exist(): + configxmlstr = CE_NC_DELETE_VRF_AF % ( + self.vrf, self.vrf_aftype) + self.changed = True + else: + self.changed = False + + if not self.changed: + return + + conf_str = build_config_xml(configxmlstr) + + recv_xml = set_nc_config(self.module, conf_str) + self.check_response(recv_xml, "OPERATE_VRF_AF") + + def get_proposed(self): + """get_proposed""" + + if self.state == 'present': + self.proposed['vrf'] = self.vrf + if self.vrf_aftype is None: + self.proposed['vrf_aftype'] = 'ipv4uni' + else: + self.proposed['vrf_aftype'] = self.vrf_aftype + if self.route_distinguisher is not None: + self.proposed['route_distinguisher'] = self.route_distinguisher + else: + self.proposed['route_distinguisher'] = list() + if self.vpn_target_state == 'present': + self.proposed['evpn'] = self.evpn + self.proposed['vpn_target_type'] = self.vpn_target_type + self.proposed['vpn_target_value'] = self.vpn_target_value + else: + self.proposed['vpn_target_type'] = list() + self.proposed['vpn_target_value'] = list() + else: + self.proposed = dict() + self.proposed['state'] = self.state + self.proposed['vrf'] = self.vrf + self.proposed['vrf_aftype'] = list() + self.proposed['route_distinguisher'] = list() + self.proposed['vpn_target_value'] = list() + self.proposed['vpn_target_type'] = list() + + def get_existing(self): + """get_existing""" + + self.get_vrf_af() + self.existing['vrf'] = self.vrf + self.existing['vrf_aftype'] = list() + self.existing['route_distinguisher'] = list() + self.existing['vpn_target_value'] = list() + self.existing['vpn_target_type'] = list() + self.existing['evpn_target_value'] = list() + self.existing['evpn_target_type'] = list() + if self.vrf_af_info["vpnInstAF"] is None: + return + for vrf_af_ele in self.vrf_af_info["vpnInstAF"]: + self.existing['vrf_aftype'].append(vrf_af_ele["afType"]) + self.existing['route_distinguisher'].append( + vrf_af_ele["vrfRD"]) + if vrf_af_ele.get("vpnTargets"): + for vpn_target in vrf_af_ele.get("vpnTargets"): + self.existing['vpn_target_type'].append( + vpn_target["vrfRTType"]) + self.existing['vpn_target_value'].append( + vpn_target["vrfRTValue"]) + if vrf_af_ele.get("evpnTargets"): + for evpn_target in vrf_af_ele.get("evpnTargets"): + self.existing['evpn_target_type'].append( + evpn_target["vrfRTType"]) + self.existing['evpn_target_value'].append( + evpn_target["vrfRTValue"]) + + def get_end_state(self): + """get_end_state""" + + self.get_vrf_af() + self.end_state['vrf'] = self.vrf + self.end_state['vrf_aftype'] = list() + self.end_state['route_distinguisher'] = list() + self.end_state['vpn_target_value'] = list() + self.end_state['vpn_target_type'] = list() + self.end_state['evpn_target_value'] = list() + self.end_state['evpn_target_type'] = list() + if self.vrf_af_info["vpnInstAF"] is None: + return + for vrf_af_ele in self.vrf_af_info["vpnInstAF"]: + self.end_state['vrf_aftype'].append(vrf_af_ele["afType"]) + self.end_state['route_distinguisher'].append(vrf_af_ele["vrfRD"]) + if vrf_af_ele.get("vpnTargets"): + for vpn_target in vrf_af_ele.get("vpnTargets"): + self.end_state['vpn_target_type'].append( + vpn_target["vrfRTType"]) + self.end_state['vpn_target_value'].append( + vpn_target["vrfRTValue"]) + if vrf_af_ele.get("evpnTargets"): + for evpn_target in vrf_af_ele.get("evpnTargets"): + self.end_state['evpn_target_type'].append( + evpn_target["vrfRTType"]) + self.end_state['evpn_target_value'].append( + evpn_target["vrfRTValue"]) + + def work(self): + """worker""" + + self.check_params() + self.get_existing() + self.get_proposed() + self.operate_vrf_af() + self.set_update_cmd() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """main""" + + argument_spec = dict( + vrf=dict(required=True, type='str'), + vrf_aftype=dict(choices=['v4', 'v6'], + default='v4', required=False), + route_distinguisher=dict(required=False, type='str'), + evpn=dict(type='bool', default=False), + vpn_target_type=dict( + choices=['export_extcommunity', 'import_extcommunity'], required=False), + vpn_target_value=dict(required=False, type='str'), + vpn_target_state=dict(choices=['absent', 'present'], required=False), + state=dict(choices=['absent', 'present'], + default='present', required=False), + ) + argument_spec.update(ce_argument_spec) + interface = VrfAf(argument_spec) + interface.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vrf_interface.py b/plugins/modules/ce_vrf_interface.py new file mode 100644 index 0000000..4dc2d81 --- /dev/null +++ b/plugins/modules/ce_vrf_interface.py @@ -0,0 +1,518 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vrf_interface +short_description: Manages interface specific VPN configuration on HUAWEI CloudEngine switches. +description: + - Manages interface specific VPN configuration of HUAWEI CloudEngine switches. +author: Zhijin Zhou (@QijunPan) +notes: + - Ensure that a VPN instance has been created and the IPv4 address family has been enabled for the VPN instance. + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + vrf: + description: + - VPN instance, the length of vrf name is 1 ~ 31, i.e. "test", but can not be C(_public_). + required: true + vpn_interface: + description: + - An interface that can binding VPN instance, i.e. 40GE1/0/22, Vlanif10. + Must be fully qualified interface name. + Interface types, such as 10GE, 40GE, 100GE, LoopBack, MEth, Tunnel, Vlanif.... + required: true + state: + description: + - Manage the state of the resource. + required: false + choices: ['present','absent'] + default: present +''' + +EXAMPLES = ''' +- name: VRF interface test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: "Configure a VPN instance for the interface" + ce_vrf_interface: + vpn_interface: 40GE1/0/2 + vrf: test + state: present + provider: "{{ cli }}" + + - name: "Disable the association between a VPN instance and an interface" + ce_vrf_interface: + vpn_interface: 40GE1/0/2 + vrf: test + state: absent + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: { + "state": "present", + "vpn_interface": "40GE2/0/17", + "vrf": "jss" + } +existing: + description: k/v pairs of existing attributes on the interface + returned: verbose mode + type: dict + sample: { + "vpn_interface": "40GE2/0/17", + "vrf": null + } +end_state: + description: k/v pairs of end attributes on the interface + returned: verbose mode + type: dict + sample: { + "vpn_interface": "40GE2/0/17", + "vrf": "jss" + } +updates: + description: command list sent to the device + returned: always + type: list + sample: [ + "ip binding vpn-instance jss", + ] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec, get_nc_config, set_nc_config + +CE_NC_GET_VRF = """ + + + + + + %s + + + + + +""" + +CE_NC_GET_VRF_INTERFACE = """ + + + + + + + + + + + + + + + + +""" + +CE_NC_MERGE_VRF_INTERFACE = """ + + + + + + %s + + + %s + + + + + + + +""" + +CE_NC_GET_INTF = """ + + + + + %s + + + + + +""" + +CE_NC_DEL_INTF_VPN = """ + + + + + + %s + + + %s + + + + + + + +""" + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class VrfInterface(object): + """Manage vpn instance""" + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # vpn instance info + self.vrf = self.module.params['vrf'] + self.vpn_interface = self.module.params['vpn_interface'] + self.vpn_interface = self.vpn_interface.upper().replace(' ', '') + self.state = self.module.params['state'] + self.intf_info = dict() + self.intf_info['isL2SwitchPort'] = None + self.intf_info['vrfName'] = None + self.conf_exist = False + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init_module""" + + required_one_of = [("vrf", "vpn_interface")] + self.module = AnsibleModule( + argument_spec=self.spec, required_one_of=required_one_of, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_update_cmd(self): + """ get updated command""" + + if self.conf_exist: + return + + if self.state == 'absent': + self.updates_cmd.append( + "undo ip binding vpn-instance %s" % self.vrf) + return + + if self.vrf != self.intf_info['vrfName']: + self.updates_cmd.append("ip binding vpn-instance %s" % self.vrf) + + return + + def check_params(self): + """Check all input params""" + + if not self.is_vrf_exist(): + self.module.fail_json( + msg='Error: The VPN instance is not existed.') + + if self.state == 'absent': + if self.vrf != self.intf_info['vrfName']: + self.module.fail_json( + msg='Error: The VPN instance is not bound to the interface.') + + if self.intf_info['isL2SwitchPort'] == 'true': + self.module.fail_json( + msg='Error: L2Switch Port can not binding a VPN instance.') + + # interface type check + if self.vpn_interface: + intf_type = get_interface_type(self.vpn_interface) + if not intf_type: + self.module.fail_json( + msg='Error: interface name of %s' + ' is error.' % self.vpn_interface) + + # vrf check + if self.vrf == '_public_': + self.module.fail_json( + msg='Error: The vrf name _public_ is reserved.') + if len(self.vrf) < 1 or len(self.vrf) > 31: + self.module.fail_json( + msg='Error: The vrf name length must be between 1 and 31.') + + def get_interface_vpn_name(self, vpninfo, vpn_name): + """ get vpn instance name""" + + l3vpn_if = vpninfo.findall("l3vpnIf") + for l3vpn_ifinfo in l3vpn_if: + for ele in l3vpn_ifinfo: + if ele.tag in ['ifName']: + if ele.text.lower() == self.vpn_interface.lower(): + self.intf_info['vrfName'] = vpn_name + + def get_interface_vpn(self): + """ get the VPN instance associated with the interface""" + + xml_str = CE_NC_GET_VRF_INTERFACE + con_obj = get_nc_config(self.module, xml_str) + if "" in con_obj: + return + + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get global vrf interface info + root = ElementTree.fromstring(xml_str) + vpns = root.findall( + "l3vpn/l3vpncomm/l3vpnInstances/l3vpnInstance") + if vpns: + for vpnele in vpns: + vpn_name = None + for vpninfo in vpnele: + if vpninfo.tag == 'vrfName': + vpn_name = vpninfo.text + if vpninfo.tag == 'l3vpnIfs': + self.get_interface_vpn_name(vpninfo, vpn_name) + + return + + def is_vrf_exist(self): + """ judge whether the VPN instance is existed""" + + conf_str = CE_NC_GET_VRF % self.vrf + con_obj = get_nc_config(self.module, conf_str) + if "" in con_obj: + return False + + return True + + def get_intf_conf_info(self): + """ get related configuration of the interface""" + + conf_str = CE_NC_GET_INTF % self.vpn_interface + con_obj = get_nc_config(self.module, conf_str) + if "" in con_obj: + return + + # get interface base info + xml_str = con_obj.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + interface = root.find("ifm/interfaces/interface") + if interface: + for eles in interface: + if eles.tag in ["isL2SwitchPort"]: + self.intf_info[eles.tag] = eles.text + + self.get_interface_vpn() + return + + def get_existing(self): + """get existing config""" + + self.existing = dict(vrf=self.intf_info['vrfName'], + vpn_interface=self.vpn_interface) + + def get_proposed(self): + """get_proposed""" + + self.proposed = dict(vrf=self.vrf, + vpn_interface=self.vpn_interface, + state=self.state) + + def get_end_state(self): + """get_end_state""" + + self.intf_info['vrfName'] = None + self.get_intf_conf_info() + + self.end_state = dict(vrf=self.intf_info['vrfName'], + vpn_interface=self.vpn_interface) + + def show_result(self): + """ show result""" + + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + def judge_if_config_exist(self): + """ judge whether configuration has existed""" + + if self.state == 'absent': + return False + + delta = set(self.proposed.items()).difference( + self.existing.items()) + delta = dict(delta) + if len(delta) == 1 and delta['state']: + return True + + return False + + def config_interface_vrf(self): + """ configure VPN instance of the interface""" + + if not self.conf_exist and self.state == 'present': + + xml_str = CE_NC_MERGE_VRF_INTERFACE % ( + self.vrf, self.vpn_interface) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "VRF_INTERFACE_CONFIG") + self.changed = True + elif self.state == 'absent': + xml_str = CE_NC_DEL_INTF_VPN % (self.vrf, self.vpn_interface) + ret_xml = set_nc_config(self.module, xml_str) + self.check_response(ret_xml, "DEL_VRF_INTERFACE_CONFIG") + self.changed = True + + def work(self): + """execute task""" + + self.get_intf_conf_info() + self.check_params() + self.get_existing() + self.get_proposed() + self.conf_exist = self.judge_if_config_exist() + + self.config_interface_vrf() + + self.get_update_cmd() + self.get_end_state() + self.show_result() + + +def main(): + """main""" + + argument_spec = dict( + vrf=dict(required=True, type='str'), + vpn_interface=dict(required=True, type='str'), + state=dict(choices=['absent', 'present'], + default='present', required=False), + ) + argument_spec.update(ce_argument_spec) + vrf_intf = VrfInterface(argument_spec) + vrf_intf.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vrrp.py b/plugins/modules/ce_vrrp.py new file mode 100644 index 0000000..4ee85ea --- /dev/null +++ b/plugins/modules/ce_vrrp.py @@ -0,0 +1,1328 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vrrp +short_description: Manages VRRP interfaces on HUAWEI CloudEngine devices. +description: + - Manages VRRP interface attributes on HUAWEI CloudEngine devices. +author: + - Li Yanfeng (@numone213) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + interface: + description: + - Name of an interface. The value is a string of 1 to 63 characters. + vrid: + description: + - VRRP backup group ID. + The value is an integer ranging from 1 to 255. + default: present + virtual_ip : + description: + - Virtual IP address. The value is a string of 0 to 255 characters. + vrrp_type: + description: + - Type of a VRRP backup group. + type: str + choices: ['normal', 'member', 'admin'] + admin_ignore_if_down: + description: + - mVRRP ignores an interface Down event. + type: bool + default: 'false' + admin_vrid: + description: + - Tracked mVRRP ID. The value is an integer ranging from 1 to 255. + admin_interface: + description: + - Tracked mVRRP interface name. The value is a string of 1 to 63 characters. + admin_flowdown: + description: + - Disable the flowdown function for service VRRP. + type: bool + default: 'false' + priority: + description: + - Configured VRRP priority. + The value ranges from 1 to 254. The default value is 100. A larger value indicates a higher priority. + version: + description: + - VRRP version. The default version is v2. + type: str + choices: ['v2','v3'] + advertise_interval: + description: + - Configured interval between sending advertisements, in milliseconds. + Only the master router sends VRRP advertisements. The default value is 1000 milliseconds. + preempt_timer_delay: + description: + - Preemption delay. + The value is an integer ranging from 0 to 3600. The default value is 0. + gratuitous_arp_interval: + description: + - Interval at which gratuitous ARP packets are sent, in seconds. + The value ranges from 30 to 1200.The default value is 300. + recover_delay: + description: + - Delay in recovering after an interface goes Up. + The delay is used for interface flapping suppression. + The value is an integer ranging from 0 to 3600. + The default value is 0 seconds. + holding_multiplier: + description: + - The configured holdMultiplier.The value is an integer ranging from 3 to 10. The default value is 3. + auth_mode: + description: + - Authentication type used for VRRP packet exchanges between virtual routers. + The values are noAuthentication, simpleTextPassword, md5Authentication. + The default value is noAuthentication. + type: str + choices: ['simple','md5','none'] + is_plain: + description: + - Select the display mode of an authentication key. + By default, an authentication key is displayed in ciphertext. + type: bool + default: 'false' + auth_key: + description: + - This object is set based on the authentication type. + When noAuthentication is specified, the value is empty. + When simpleTextPassword or md5Authentication is specified, the value is a string of 1 to 8 characters + in plaintext and displayed as a blank text for security. + fast_resume: + description: + - mVRRP's fast resume mode. + type: str + choices: ['enable','disable'] + state: + description: + - Specify desired state of the resource. + type: str + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: vrrp module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + tasks: + - name: Set vrrp version + ce_vrrp: + version: v3 + provider: "{{ cli }}" + - name: Set vrrp gratuitous-arp interval + ce_vrrp: + gratuitous_arp_interval: 40 + mlag_id: 4 + provider: "{{ cli }}" + - name: Set vrrp recover-delay + ce_vrrp: + recover_delay: 10 + provider: "{{ cli }}" + - name: Set vrrp vrid virtual-ip + ce_vrrp: + interface: 40GE2/0/8 + vrid: 1 + virtual_ip: 10.14.2.7 + provider: "{{ cli }}" + - name: Set vrrp vrid admin + ce_vrrp: + interface: 40GE2/0/8 + vrid: 1 + vrrp_type: admin + provider: "{{ cli }}" + - name: Set vrrp vrid fast_resume + ce_vrrp: + interface: 40GE2/0/8 + vrid: 1 + fast_resume: enable + provider: "{{ cli }}" + - name: Set vrrp vrid holding-multiplier + ce_vrrp: + interface: 40GE2/0/8 + vrid: 1 + holding_multiplier: 4 + provider: "{{ cli }}" + - name: Set vrrp vrid preempt timer delay + ce_vrrp: + interface: 40GE2/0/8 + vrid: 1 + preempt_timer_delay: 10 + provider: "{{ cli }}" + - name: Set vrrp vrid admin-vrrp + ce_vrrp: + interface: 40GE2/0/8 + vrid: 1 + admin_interface: 40GE2/0/9 + admin_vrid: 2 + vrrp_type: member + provider: "{{ cli }}" + - name: Set vrrp vrid authentication-mode + ce_vrrp: + interface: 40GE2/0/8 + vrid: 1 + is_plain: true + auth_mode: simple + auth_key: aaa + provider: "{{ cli }}" +''' + +RETURN = ''' +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: { + "auth_key": "aaa", + "auth_mode": "simple", + "interface": "40GE2/0/8", + "is_plain": true, + "state": "present", + "vrid": "1" + } +existing: + description: k/v pairs of existing aaa server + returned: always + type: dict + sample: { + "auth_mode": "none", + "interface": "40GE2/0/8", + "is_plain": "false", + "vrid": "1", + "vrrp_type": "normal" + } +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: { + "auth_mode": "simple", + "interface": "40GE2/0/8", + "is_plain": "true", + "vrid": "1", + "vrrp_type": "normal" + } +updates: + description: command sent to the device + returned: always + type: list + sample: { "interface 40GE2/0/8", + "vrrp vrid 1 authentication-mode simple plain aaa"} +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + + +CE_NC_GET_VRRP_GROUP_INFO = """ + + + + + %s + %s + + + + +""" + +CE_NC_SET_VRRP_GROUP_INFO_HEAD = """ + + + + + %s + %s +""" +CE_NC_SET_VRRP_GROUP_INFO_TAIL = """ + + + + +""" +CE_NC_GET_VRRP_GLOBAL_INFO = """ + + + + + + + + + + +""" + +CE_NC_SET_VRRP_GLOBAL_HEAD = """ + + + +""" +CE_NC_SET_VRRP_GLOBAL_TAIL = """ + + + +""" + +CE_NC_GET_VRRP_VIRTUAL_IP_INFO = """ + + + + + %s + %s + + + + + + + + + +""" +CE_NC_CREATE_VRRP_VIRTUAL_IP_INFO = """ + + + + + %s + %s + + + %s + + + + + + +""" +CE_NC_DELETE_VRRP_VIRTUAL_IP_INFO = """ + + + + + %s + %s + + + %s + + + + + + +""" + + +def is_valid_address(address): + """check ip-address is valid""" + + if address.find('.') != -1: + addr_list = address.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('NULL'): + iftype = 'null' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + else: + return None + + return iftype.lower() + + +class Vrrp(object): + """ + Manages Manages vrrp information. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.interface = self.module.params['interface'] + self.vrid = self.module.params['vrid'] + self.virtual_ip = self.module.params['virtual_ip'] + self.vrrp_type = self.module.params['vrrp_type'] + self.admin_ignore_if_down = 'false' if self.module.params['admin_ignore_if_down'] is False else 'true' + self.admin_vrid = self.module.params['admin_vrid'] + self.admin_interface = self.module.params['admin_interface'] + self.admin_flowdown = 'false' if self.module.params['admin_flowdown'] is False else 'true' + self.priority = self.module.params['priority'] + self.version = self.module.params['version'] + self.advertise_interval = self.module.params['advertise_interval'] + self.preempt_timer_delay = self.module.params['preempt_timer_delay'] + self.gratuitous_arp_interval = self.module.params[ + 'gratuitous_arp_interval'] + self.recover_delay = self.module.params['recover_delay'] + self.holding_multiplier = self.module.params['holding_multiplier'] + self.auth_mode = self.module.params['auth_mode'] + self.is_plain = 'false' if self.module.params['is_plain'] is False else 'true' + self.auth_key = self.module.params['auth_key'] + self.fast_resume = self.module.params['fast_resume'] + self.state = self.module.params['state'] + + # vrrp info + self.vrrp_global_info = None + self.virtual_ip_info = None + self.vrrp_group_info = None + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + def init_module(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def get_virtual_ip_info(self): + """ get vrrp virtual ip info.""" + virtual_ip_info = dict() + conf_str = CE_NC_GET_VRRP_VIRTUAL_IP_INFO % (self.vrid, self.interface) + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return virtual_ip_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + virtual_ip_info["vrrpVirtualIpInfos"] = list() + root = ElementTree.fromstring(xml_str) + vrrp_virtual_ip_infos = root.findall( + "vrrp/vrrpGroups/vrrpGroup/virtualIps/virtualIp") + if vrrp_virtual_ip_infos: + for vrrp_virtual_ip_info in vrrp_virtual_ip_infos: + virtual_ip_dict = dict() + for ele in vrrp_virtual_ip_info: + if ele.tag in ["virtualIpAddress"]: + virtual_ip_dict[ele.tag] = ele.text + virtual_ip_info["vrrpVirtualIpInfos"].append( + virtual_ip_dict) + return virtual_ip_info + + def get_vrrp_global_info(self): + """ get vrrp global info.""" + + vrrp_global_info = dict() + conf_str = CE_NC_GET_VRRP_GLOBAL_INFO + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return vrrp_global_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + global_info = root.findall( + "vrrp/vrrpGlobalCfg") + + if global_info: + for tmp in global_info: + for site in tmp: + if site.tag in ["gratuitousArpTimeOut", "gratuitousArpFlag", "recoverDelay", "version"]: + vrrp_global_info[site.tag] = site.text + return vrrp_global_info + + def get_vrrp_group_info(self): + """ get vrrp group info.""" + + vrrp_group_info = dict() + conf_str = CE_NC_GET_VRRP_GROUP_INFO % (self.interface, self.vrid) + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return vrrp_group_info + else: + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + root = ElementTree.fromstring(xml_str) + global_info = root.findall( + "vrrp/vrrpGroups/vrrpGroup") + + if global_info: + for tmp in global_info: + for site in tmp: + if site.tag in ["ifName", "vrrpId", "priority", "advertiseInterval", "preemptMode", "delayTime", + "authenticationMode", "authenticationKey", "vrrpType", "adminVrrpId", + "adminIfName", "adminIgnoreIfDown", "isPlain", "unflowdown", "fastResume", + "holdMultiplier"]: + vrrp_group_info[site.tag] = site.text + return vrrp_group_info + + def check_params(self): + """Check all input params""" + + # interface check + if self.interface: + intf_type = get_interface_type(self.interface) + if not intf_type: + self.module.fail_json( + msg='Error: Interface name of %s ' + 'is error.' % self.interface) + + # vrid check + if self.vrid: + if not self.vrid.isdigit(): + self.module.fail_json( + msg='Error: The value of vrid is an integer.') + if int(self.vrid) < 1 or int(self.vrid) > 255: + self.module.fail_json( + msg='Error: The value of vrid ranges from 1 to 255.') + + # virtual_ip check + if self.virtual_ip: + if not is_valid_address(self.virtual_ip): + self.module.fail_json( + msg='Error: The %s is not a valid ip address.' % self.virtual_ip) + + # admin_vrid check + if self.admin_vrid: + if not self.admin_vrid.isdigit(): + self.module.fail_json( + msg='Error: The value of admin_vrid is an integer.') + if int(self.admin_vrid) < 1 or int(self.admin_vrid) > 255: + self.module.fail_json( + msg='Error: The value of admin_vrid ranges from 1 to 255.') + + # admin_interface check + if self.admin_interface: + intf_type = get_interface_type(self.admin_interface) + if not intf_type: + self.module.fail_json( + msg='Error: Admin interface name of %s ' + 'is error.' % self.admin_interface) + + # priority check + if self.priority: + if not self.priority.isdigit(): + self.module.fail_json( + msg='Error: The value of priority is an integer.') + if int(self.priority) < 1 or int(self.priority) > 254: + self.module.fail_json( + msg='Error: The value of priority ranges from 1 to 254. The default value is 100.') + + # advertise_interval check + if self.advertise_interval: + if not self.advertise_interval.isdigit(): + self.module.fail_json( + msg='Error: The value of advertise_interval is an integer.') + if int(self.advertise_interval) < 1000 or int(self.advertise_interval) > 255000: + self.module.fail_json( + msg='Error: The value of advertise_interval ranges from 1000 to 255000 milliseconds. The default value is 1000 milliseconds.') + if int(self.advertise_interval) % 1000 != 0: + self.module.fail_json( + msg='Error: The advertisement interval value of VRRP must be a multiple of 1000 milliseconds.') + # preempt_timer_delay check + if self.preempt_timer_delay: + if not self.preempt_timer_delay.isdigit(): + self.module.fail_json( + msg='Error: The value of preempt_timer_delay is an integer.') + if int(self.preempt_timer_delay) < 1 or int(self.preempt_timer_delay) > 3600: + self.module.fail_json( + msg='Error: The value of preempt_timer_delay ranges from 1 to 3600. The default value is 0.') + + # holding_multiplier check + if self.holding_multiplier: + if not self.holding_multiplier.isdigit(): + self.module.fail_json( + msg='Error: The value of holding_multiplier is an integer.') + if int(self.holding_multiplier) < 3 or int(self.holding_multiplier) > 10: + self.module.fail_json( + msg='Error: The value of holding_multiplier ranges from 3 to 10. The default value is 3.') + + # auth_key check + if self.auth_key: + if len(self.auth_key) > 16 \ + or len(self.auth_key.replace(' ', '')) < 1: + self.module.fail_json( + msg='Error: The length of auth_key is not in the range from 1 to 16.') + + def is_virtual_ip_change(self): + """whether virtual ip change""" + + if not self.virtual_ip_info: + return True + + for info in self.virtual_ip_info["vrrpVirtualIpInfos"]: + if info["virtualIpAddress"] == self.virtual_ip: + return False + return True + + def is_virtual_ip_exist(self): + """whether virtual ip info exist""" + + if not self.virtual_ip_info: + return False + + for info in self.virtual_ip_info["vrrpVirtualIpInfos"]: + if info["virtualIpAddress"] == self.virtual_ip: + return True + return False + + def is_vrrp_global_info_change(self): + """whether vrrp global attribute info change""" + + if not self.vrrp_global_info: + return True + + if self.gratuitous_arp_interval: + if self.vrrp_global_info["gratuitousArpFlag"] == "false": + self.module.fail_json(msg="Error: gratuitousArpFlag is false.") + if self.vrrp_global_info["gratuitousArpTimeOut"] != self.gratuitous_arp_interval: + return True + if self.recover_delay: + if self.vrrp_global_info["recoverDelay"] != self.recover_delay: + return True + if self.version: + if self.vrrp_global_info["version"] != self.version: + return True + return False + + def is_vrrp_global_info_exist(self): + """whether vrrp global attribute info exist""" + + if self.gratuitous_arp_interval or self.recover_delay or self.version: + if self.gratuitous_arp_interval: + if self.vrrp_global_info["gratuitousArpFlag"] == "false": + self.module.fail_json( + msg="Error: gratuitousArpFlag is false.") + if self.vrrp_global_info["gratuitousArpTimeOut"] != self.gratuitous_arp_interval: + return False + if self.recover_delay: + if self.vrrp_global_info["recoverDelay"] != self.recover_delay: + return False + if self.version: + if self.vrrp_global_info["version"] != self.version: + return False + return True + + return False + + def is_vrrp_group_info_change(self): + """whether vrrp group attribute info change""" + if self.vrrp_type: + if self.vrrp_group_info["vrrpType"] != self.vrrp_type: + return True + if self.admin_ignore_if_down: + if self.vrrp_group_info["adminIgnoreIfDown"] != self.admin_ignore_if_down: + return True + if self.admin_vrid: + if self.vrrp_group_info["adminVrrpId"] != self.admin_vrid: + return True + if self.admin_interface: + if self.vrrp_group_info["adminIfName"] != self.admin_interface: + return True + if self.admin_flowdown: + if self.vrrp_group_info["unflowdown"] != self.admin_flowdown: + return True + if self.priority: + if self.vrrp_group_info["priority"] != self.priority: + return True + if self.fast_resume: + fast_resume = "false" + if self.fast_resume == "enable": + fast_resume = "true" + if self.vrrp_group_info["fastResume"] != fast_resume: + return True + if self.advertise_interval: + if self.vrrp_group_info["advertiseInterval"] != self.advertise_interval: + return True + if self.preempt_timer_delay: + if self.vrrp_group_info["delayTime"] != self.preempt_timer_delay: + return True + if self.holding_multiplier: + if self.vrrp_group_info["holdMultiplier"] != self.holding_multiplier: + return True + if self.auth_mode: + if self.vrrp_group_info["authenticationMode"] != self.auth_mode: + return True + if self.auth_key: + return True + if self.is_plain: + if self.vrrp_group_info["isPlain"] != self.is_plain: + return True + + return False + + def is_vrrp_group_info_exist(self): + """whether vrrp group attribute info exist""" + + if self.vrrp_type: + if self.vrrp_group_info["vrrpType"] != self.vrrp_type: + return False + if self.admin_ignore_if_down: + if self.vrrp_group_info["adminIgnoreIfDown"] != self.admin_ignore_if_down: + return False + if self.admin_vrid: + if self.vrrp_group_info["adminVrrpId"] != self.admin_vrid: + return False + if self.admin_interface: + if self.vrrp_group_info["adminIfName"] != self.admin_interface: + return False + if self.admin_flowdown: + if self.vrrp_group_info["unflowdown"] != self.admin_flowdown: + return False + if self.priority: + if self.vrrp_group_info["priority"] != self.priority: + return False + if self.fast_resume: + fast_resume = "false" + if self.fast_resume == "enable": + fast_resume = "true" + if self.vrrp_group_info["fastResume"] != fast_resume: + return False + if self.advertise_interval: + if self.vrrp_group_info["advertiseInterval"] != self.advertise_interval: + return False + if self.preempt_timer_delay: + if self.vrrp_group_info["delayTime"] != self.preempt_timer_delay: + return False + if self.holding_multiplier: + if self.vrrp_group_info["holdMultiplier"] != self.holding_multiplier: + return False + if self.auth_mode: + if self.vrrp_group_info["authenticationMode"] != self.auth_mode: + return False + if self.is_plain: + if self.vrrp_group_info["isPlain"] != self.is_plain: + return False + return True + + def create_virtual_ip(self): + """create virtual ip info""" + + if self.is_virtual_ip_change(): + conf_str = CE_NC_CREATE_VRRP_VIRTUAL_IP_INFO % ( + self.vrid, self.interface, self.virtual_ip) + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: create virtual ip info failed.') + + self.updates_cmd.append("interface %s" % self.interface) + self.updates_cmd.append( + "vrrp vrid %s virtual-ip %s" % (self.vrid, self.virtual_ip)) + self.changed = True + + def delete_virtual_ip(self): + """delete virtual ip info""" + + if self.is_virtual_ip_exist(): + conf_str = CE_NC_DELETE_VRRP_VIRTUAL_IP_INFO % ( + self.vrid, self.interface, self.virtual_ip) + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: delete virtual ip info failed.') + + self.updates_cmd.append("interface %s" % self.interface) + self.updates_cmd.append( + "undo vrrp vrid %s virtual-ip %s " % (self.vrid, self.virtual_ip)) + self.changed = True + + def set_vrrp_global(self): + """set vrrp global attribute info""" + + if self.is_vrrp_global_info_change(): + conf_str = CE_NC_SET_VRRP_GLOBAL_HEAD + if self.gratuitous_arp_interval: + conf_str += "%s" % self.gratuitous_arp_interval + if self.recover_delay: + conf_str += "%s" % self.recover_delay + if self.version: + conf_str += "%s" % self.version + conf_str += CE_NC_SET_VRRP_GLOBAL_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set vrrp global attribute info failed.') + + if self.gratuitous_arp_interval: + self.updates_cmd.append( + "vrrp gratuitous-arp interval %s" % self.gratuitous_arp_interval) + + if self.recover_delay: + self.updates_cmd.append( + "vrrp recover-delay %s" % self.recover_delay) + + if self.version: + version = "3" + if self.version == "v2": + version = "2" + self.updates_cmd.append("vrrp version %s" % version) + self.changed = True + + def delete_vrrp_global(self): + """delete vrrp global attribute info""" + + if self.is_vrrp_global_info_exist(): + conf_str = CE_NC_SET_VRRP_GLOBAL_HEAD + if self.gratuitous_arp_interval: + if self.gratuitous_arp_interval == "120": + self.module.fail_json( + msg='Error: The default value of gratuitous_arp_interval is 120.') + gratuitous_arp_interval = "120" + conf_str += "%s" % gratuitous_arp_interval + if self.recover_delay: + if self.recover_delay == "0": + self.module.fail_json( + msg='Error: The default value of recover_delay is 0.') + recover_delay = "0" + conf_str += "%s" % recover_delay + if self.version: + if self.version == "v2": + self.module.fail_json( + msg='Error: The default value of version is v2.') + version = "v2" + conf_str += "%s" % version + conf_str += CE_NC_SET_VRRP_GLOBAL_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set vrrp global attribute info failed.') + if self.gratuitous_arp_interval: + self.updates_cmd.append("undo vrrp gratuitous-arp interval") + + if self.recover_delay: + self.updates_cmd.append("undo vrrp recover-delay") + + if self.version == "v3": + self.updates_cmd.append("undo vrrp version") + self.changed = True + + def set_vrrp_group(self): + """set vrrp group attribute info""" + + if self.is_vrrp_group_info_change(): + conf_str = CE_NC_SET_VRRP_GROUP_INFO_HEAD % ( + self.interface, self.vrid) + if self.vrrp_type: + conf_str += "%s" % self.vrrp_type + if self.admin_vrid: + conf_str += "%s" % self.admin_vrid + if self.admin_interface: + conf_str += "%s" % self.admin_interface + if self.admin_flowdown: + conf_str += "%s" % self.admin_flowdown + if self.priority: + conf_str += "%s" % self.priority + if self.vrrp_type == "admin": + if self.admin_ignore_if_down: + conf_str += "%s" % self.admin_ignore_if_down + if self.fast_resume: + fast_resume = "false" + if self.fast_resume == "enable": + fast_resume = "true" + conf_str += "%s" % fast_resume + if self.advertise_interval: + conf_str += "%s" % self.advertise_interval + if self.preempt_timer_delay: + conf_str += "%s" % self.preempt_timer_delay + if self.holding_multiplier: + conf_str += "%s" % self.holding_multiplier + if self.auth_mode: + conf_str += "%s" % self.auth_mode + if self.auth_key: + conf_str += "%s" % self.auth_key + if self.auth_mode == "simple": + conf_str += "%s" % self.is_plain + + conf_str += CE_NC_SET_VRRP_GROUP_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set vrrp group attribute info failed.') + if self.interface and self.vrid: + self.updates_cmd.append("interface %s" % self.interface) + if self.vrrp_type == "admin": + if self.admin_ignore_if_down == "true": + self.updates_cmd.append( + "vrrp vrid %s admin ignore-if-down" % self.vrid) + else: + self.updates_cmd.append( + "vrrp vrid %s admin" % self.vrid) + + if self.priority: + self.updates_cmd.append( + "vrrp vrid %s priority %s" % (self.vrid, self.priority)) + + if self.fast_resume == "enable": + self.updates_cmd.append( + "vrrp vrid %s fast-resume" % self.vrid) + if self.fast_resume == "disable": + self.updates_cmd.append( + "undo vrrp vrid %s fast-resume" % self.vrid) + + if self.advertise_interval: + advertise_interval = int(self.advertise_interval) / 1000 + self.updates_cmd.append("vrrp vrid %s timer advertise %s" % ( + self.vrid, int(advertise_interval))) + + if self.preempt_timer_delay: + self.updates_cmd.append("vrrp vrid %s preempt timer delay %s" % (self.vrid, + self.preempt_timer_delay)) + + if self.holding_multiplier: + self.updates_cmd.append( + "vrrp vrid %s holding-multiplier %s" % (self.vrid, self.holding_multiplier)) + + if self.admin_vrid and self.admin_interface: + if self.admin_flowdown == "true": + self.updates_cmd.append("vrrp vrid %s track admin-vrrp interface %s vrid %s unflowdown" % + (self.vrid, self.admin_interface, self.admin_vrid)) + else: + self.updates_cmd.append("vrrp vrid %s track admin-vrrp interface %s vrid %s" % + (self.vrid, self.admin_interface, self.admin_vrid)) + + if self.auth_mode and self.auth_key: + if self.auth_mode == "simple": + if self.is_plain == "true": + self.updates_cmd.append("vrrp vrid %s authentication-mode simple plain %s" % + (self.vrid, self.auth_key)) + else: + self.updates_cmd.append("vrrp vrid %s authentication-mode simple cipher %s" % + (self.vrid, self.auth_key)) + if self.auth_mode == "md5": + self.updates_cmd.append( + "vrrp vrid %s authentication-mode md5 %s" % (self.vrid, self.auth_key)) + self.changed = True + + def delete_vrrp_group(self): + """delete vrrp group attribute info""" + + if self.is_vrrp_group_info_exist(): + conf_str = CE_NC_SET_VRRP_GROUP_INFO_HEAD % ( + self.interface, self.vrid) + if self.vrrp_type: + vrrp_type = self.vrrp_type + if self.vrrp_type == "admin": + vrrp_type = "normal" + if self.vrrp_type == "member" and self.admin_vrid and self.admin_interface: + vrrp_type = "normal" + conf_str += "%s" % vrrp_type + if self.priority: + if self.priority == "100": + self.module.fail_json( + msg='Error: The default value of priority is 100.') + priority = "100" + conf_str += "%s" % priority + + if self.fast_resume: + fast_resume = "false" + if self.fast_resume == "enable": + fast_resume = "true" + conf_str += "%s" % fast_resume + if self.advertise_interval: + if self.advertise_interval == "1000": + self.module.fail_json( + msg='Error: The default value of advertise_interval is 1000.') + advertise_interval = "1000" + conf_str += "%s" % advertise_interval + if self.preempt_timer_delay: + if self.preempt_timer_delay == "0": + self.module.fail_json( + msg='Error: The default value of preempt_timer_delay is 0.') + preempt_timer_delay = "0" + conf_str += "%s" % preempt_timer_delay + if self.holding_multiplier: + if self.holding_multiplier == "0": + self.module.fail_json( + msg='Error: The default value of holding_multiplier is 3.') + holding_multiplier = "3" + conf_str += "%s" % holding_multiplier + if self.auth_mode: + auth_mode = self.auth_mode + if self.auth_mode == "md5" or self.auth_mode == "simple": + auth_mode = "none" + conf_str += "%s" % auth_mode + + conf_str += CE_NC_SET_VRRP_GROUP_INFO_TAIL + recv_xml = set_nc_config(self.module, conf_str) + if "" not in recv_xml: + self.module.fail_json( + msg='Error: set vrrp global attribute info failed.') + if self.interface and self.vrid: + self.updates_cmd.append("interface %s" % self.interface) + if self.vrrp_type == "admin": + self.updates_cmd.append( + "undo vrrp vrid %s admin" % self.vrid) + + if self.priority: + self.updates_cmd.append( + "undo vrrp vrid %s priority" % self.vrid) + + if self.fast_resume: + self.updates_cmd.append( + "undo vrrp vrid %s fast-resume" % self.vrid) + + if self.advertise_interval: + self.updates_cmd.append( + "undo vrrp vrid %s timer advertise" % self.vrid) + + if self.preempt_timer_delay: + self.updates_cmd.append( + "undo vrrp vrid %s preempt timer delay" % self.vrid) + + if self.holding_multiplier: + self.updates_cmd.append( + "undo vrrp vrid %s holding-multiplier" % self.vrid) + + if self.admin_vrid and self.admin_interface: + self.updates_cmd.append( + "undo vrrp vrid %s track admin-vrrp" % self.vrid) + + if self.auth_mode: + self.updates_cmd.append( + "undo vrrp vrid %s authentication-mode" % self.vrid) + self.changed = True + + def get_proposed(self): + """get proposed info""" + + if self.interface: + self.proposed["interface"] = self.interface + if self.vrid: + self.proposed["vrid"] = self.vrid + if self.virtual_ip: + self.proposed["virtual_ip"] = self.virtual_ip + if self.vrrp_type: + self.proposed["vrrp_type"] = self.vrrp_type + if self.admin_vrid: + self.proposed["admin_vrid"] = self.admin_vrid + if self.admin_interface: + self.proposed["admin_interface"] = self.admin_interface + if self.admin_flowdown: + self.proposed["unflowdown"] = self.admin_flowdown + if self.admin_ignore_if_down: + self.proposed["admin_ignore_if_down"] = self.admin_ignore_if_down + if self.priority: + self.proposed["priority"] = self.priority + if self.version: + self.proposed["version"] = self.version + if self.advertise_interval: + self.proposed["advertise_interval"] = self.advertise_interval + if self.preempt_timer_delay: + self.proposed["preempt_timer_delay"] = self.preempt_timer_delay + if self.gratuitous_arp_interval: + self.proposed[ + "gratuitous_arp_interval"] = self.gratuitous_arp_interval + if self.recover_delay: + self.proposed["recover_delay"] = self.recover_delay + if self.holding_multiplier: + self.proposed["holding_multiplier"] = self.holding_multiplier + if self.auth_mode: + self.proposed["auth_mode"] = self.auth_mode + if self.is_plain: + self.proposed["is_plain"] = self.is_plain + if self.auth_key: + self.proposed["auth_key"] = self.auth_key + if self.fast_resume: + self.proposed["fast_resume"] = self.fast_resume + if self.state: + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if self.gratuitous_arp_interval: + self.existing["gratuitous_arp_interval"] = self.vrrp_global_info[ + "gratuitousArpTimeOut"] + if self.version: + self.existing["version"] = self.vrrp_global_info["version"] + if self.recover_delay: + self.existing["recover_delay"] = self.vrrp_global_info[ + "recoverDelay"] + + if self.virtual_ip: + if self.virtual_ip_info: + self.existing["interface"] = self.interface + self.existing["vrid"] = self.vrid + self.existing["virtual_ip_info"] = self.virtual_ip_info[ + "vrrpVirtualIpInfos"] + + if self.vrrp_group_info: + self.existing["interface"] = self.vrrp_group_info["ifName"] + self.existing["vrid"] = self.vrrp_group_info["vrrpId"] + self.existing["vrrp_type"] = self.vrrp_group_info["vrrpType"] + if self.vrrp_type == "admin": + self.existing["admin_ignore_if_down"] = self.vrrp_group_info[ + "adminIgnoreIfDown"] + if self.admin_vrid and self.admin_interface: + self.existing["admin_vrid"] = self.vrrp_group_info[ + "adminVrrpId"] + self.existing["admin_interface"] = self.vrrp_group_info[ + "adminIfName"] + self.existing["admin_flowdown"] = self.vrrp_group_info[ + "unflowdown"] + if self.priority: + self.existing["priority"] = self.vrrp_group_info["priority"] + if self.advertise_interval: + self.existing["advertise_interval"] = self.vrrp_group_info[ + "advertiseInterval"] + if self.preempt_timer_delay: + self.existing["preempt_timer_delay"] = self.vrrp_group_info[ + "delayTime"] + if self.holding_multiplier: + self.existing["holding_multiplier"] = self.vrrp_group_info[ + "holdMultiplier"] + if self.fast_resume: + fast_resume_exist = "disable" + fast_resume = self.vrrp_group_info["fastResume"] + if fast_resume == "true": + fast_resume_exist = "enable" + self.existing["fast_resume"] = fast_resume_exist + if self.auth_mode: + self.existing["auth_mode"] = self.vrrp_group_info[ + "authenticationMode"] + self.existing["is_plain"] = self.vrrp_group_info["isPlain"] + + def get_end_state(self): + """get end state info""" + + if self.gratuitous_arp_interval or self.version or self.recover_delay: + self.vrrp_global_info = self.get_vrrp_global_info() + if self.interface and self.vrid: + if self.virtual_ip: + self.virtual_ip_info = self.get_virtual_ip_info() + if self.virtual_ip_info: + self.vrrp_group_info = self.get_vrrp_group_info() + + if self.gratuitous_arp_interval: + self.end_state["gratuitous_arp_interval"] = self.vrrp_global_info[ + "gratuitousArpTimeOut"] + if self.version: + self.end_state["version"] = self.vrrp_global_info["version"] + if self.recover_delay: + self.end_state["recover_delay"] = self.vrrp_global_info[ + "recoverDelay"] + + if self.virtual_ip: + if self.virtual_ip_info: + self.end_state["interface"] = self.interface + self.end_state["vrid"] = self.vrid + self.end_state["virtual_ip_info"] = self.virtual_ip_info[ + "vrrpVirtualIpInfos"] + + if self.vrrp_group_info: + self.end_state["interface"] = self.vrrp_group_info["ifName"] + self.end_state["vrid"] = self.vrrp_group_info["vrrpId"] + self.end_state["vrrp_type"] = self.vrrp_group_info["vrrpType"] + if self.vrrp_type == "admin": + self.end_state["admin_ignore_if_down"] = self.vrrp_group_info[ + "adminIgnoreIfDown"] + if self.admin_vrid and self.admin_interface: + self.end_state["admin_vrid"] = self.vrrp_group_info[ + "adminVrrpId"] + self.end_state["admin_interface"] = self.vrrp_group_info[ + "adminIfName"] + self.end_state["admin_flowdown"] = self.vrrp_group_info[ + "unflowdown"] + if self.priority: + self.end_state["priority"] = self.vrrp_group_info["priority"] + if self.advertise_interval: + self.end_state["advertise_interval"] = self.vrrp_group_info[ + "advertiseInterval"] + if self.preempt_timer_delay: + self.end_state["preempt_timer_delay"] = self.vrrp_group_info[ + "delayTime"] + if self.holding_multiplier: + self.end_state["holding_multiplier"] = self.vrrp_group_info[ + "holdMultiplier"] + if self.fast_resume: + fast_resume_end = "disable" + fast_resume = self.vrrp_group_info["fastResume"] + if fast_resume == "true": + fast_resume_end = "enable" + self.end_state["fast_resume"] = fast_resume_end + if self.auth_mode: + self.end_state["auth_mode"] = self.vrrp_group_info[ + "authenticationMode"] + self.end_state["is_plain"] = self.vrrp_group_info["isPlain"] + if self.existing == self.end_state: + self.changed = False + + def work(self): + """worker""" + + self.check_params() + if self.gratuitous_arp_interval or self.version or self.recover_delay: + self.vrrp_global_info = self.get_vrrp_global_info() + if self.interface and self.vrid: + self.virtual_ip_info = self.get_virtual_ip_info() + if self.virtual_ip_info: + self.vrrp_group_info = self.get_vrrp_group_info() + self.get_proposed() + self.get_existing() + + if self.gratuitous_arp_interval or self.version or self.recover_delay: + if self.state == "present": + self.set_vrrp_global() + else: + self.delete_vrrp_global() + else: + if not self.interface or not self.vrid: + self.module.fail_json( + msg='Error: interface, vrid must be config at the same time.') + + if self.interface and self.vrid: + if self.virtual_ip: + if self.state == "present": + self.create_virtual_ip() + else: + self.delete_virtual_ip() + else: + if not self.vrrp_group_info: + self.module.fail_json( + msg='Error: The VRRP group does not exist.') + if self.admin_ignore_if_down == "true": + if self.vrrp_type != "admin": + self.module.fail_json( + msg='Error: vrrpType must be admin when admin_ignore_if_down is true.') + if self.admin_interface or self.admin_vrid: + if self.vrrp_type != "member": + self.module.fail_json( + msg='Error: it binds a VRRP group to an mVRRP group, vrrp_type must be "member".') + if not self.vrrp_type or not self.interface or not self.vrid: + self.module.fail_json( + msg='Error: admin_interface admin_vrid vrrp_type interface vrid must ' + 'be config at the same time.') + if self.auth_mode == "md5" and self.is_plain == "true": + self.module.fail_json( + msg='Error: is_plain can not be True when auth_mode is md5.') + + if self.state == "present": + self.set_vrrp_group() + else: + self.delete_vrrp_group() + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """ Module main """ + + argument_spec = dict( + interface=dict(type='str'), + vrid=dict(type='str'), + virtual_ip=dict(type='str'), + vrrp_type=dict(type='str', choices=['normal', 'member', 'admin']), + admin_ignore_if_down=dict(type='bool', default=False), + admin_vrid=dict(type='str'), + admin_interface=dict(type='str'), + admin_flowdown=dict(type='bool', default=False), + priority=dict(type='str'), + version=dict(type='str', choices=['v2', 'v3']), + advertise_interval=dict(type='str'), + preempt_timer_delay=dict(type='str'), + gratuitous_arp_interval=dict(type='str'), + recover_delay=dict(type='str'), + holding_multiplier=dict(type='str'), + auth_mode=dict(type='str', choices=['simple', 'md5', 'none']), + is_plain=dict(type='bool', default=False), + auth_key=dict(type='str'), + fast_resume=dict(type='str', choices=['enable', 'disable']), + state=dict(type='str', default='present', + choices=['present', 'absent']) + ) + + argument_spec.update(ce_argument_spec) + module = Vrrp(argument_spec=argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vxlan_arp.py b/plugins/modules/ce_vxlan_arp.py new file mode 100644 index 0000000..403775d --- /dev/null +++ b/plugins/modules/ce_vxlan_arp.py @@ -0,0 +1,689 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vxlan_arp +short_description: Manages ARP attributes of VXLAN on HUAWEI CloudEngine devices. +description: + - Manages ARP attributes of VXLAN on HUAWEI CloudEngine devices. +author: QijunPan (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + evn_bgp: + description: + - Enables EVN BGP. + choices: ['enable', 'disable'] + evn_source_ip: + description: + - Specifies the source address of an EVN BGP peer. + The value is in dotted decimal notation. + evn_peer_ip: + description: + - Specifies the IP address of an EVN BGP peer. + The value is in dotted decimal notation. + evn_server: + description: + - Configures the local device as the router reflector (RR) on the EVN network. + choices: ['enable', 'disable'] + evn_reflect_client: + description: + - Configures the local device as the route reflector (RR) and its peer as the client. + choices: ['enable', 'disable'] + vbdif_name: + description: + - Full name of VBDIF interface, i.e. Vbdif100. + arp_collect_host: + description: + - Enables EVN BGP or BGP EVPN to collect host information. + choices: ['enable', 'disable'] + host_collect_protocol: + description: + - Enables EVN BGP or BGP EVPN to advertise host information. + choices: ['bgp','none'] + bridge_domain_id: + description: + - Specifies a BD(bridge domain) ID. + The value is an integer ranging from 1 to 16777215. + arp_suppress: + description: + - Enables ARP broadcast suppression in a BD. + choices: ['enable', 'disable'] + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +- name: vxlan arp module test + hosts: ce128 + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configure EVN BGP on Layer 2 and Layer 3 VXLAN gateways to establish EVN BGP peer relationships. + ce_vxlan_arp: + evn_bgp: enable + evn_source_ip: 6.6.6.6 + evn_peer_ip: 7.7.7.7 + provider: "{{ cli }}" + - name: Configure a Layer 3 VXLAN gateway as a BGP RR. + ce_vxlan_arp: + evn_bgp: enable + evn_server: enable + provider: "{{ cli }}" + - name: Enable EVN BGP on a Layer 3 VXLAN gateway to collect host information. + ce_vxlan_arp: + vbdif_name: Vbdif100 + arp_collect_host: enable + provider: "{{ cli }}" + - name: Enable Layer 2 and Layer 3 VXLAN gateways to use EVN BGP to advertise host information. + ce_vxlan_arp: + host_collect_protocol: bgp + provider: "{{ cli }}" + - name: Enable ARP broadcast suppression on a Layer 2 VXLAN gateway. + ce_vxlan_arp: + bridge_domain_id: 100 + arp_suppress: enable + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"evn_bgp": "enable", "evn_source_ip": "6.6.6.6", "evn_peer_ip":"7.7.7.7", state: "present"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"evn_bgp": "disable", "evn_source_ip": null, "evn_peer_ip": []} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"evn_bgp": "enable", "evn_source_ip": "6.6.6.6", "evn_peer_ip": ["7.7.7.7"]} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["evn bgp", + "source-address 6.6.6.6", + "peer 7.7.7.7"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec +from ansible.module_utils.connection import exec_command + + +def is_config_exist(cmp_cfg, test_cfg): + """is configuration exist""" + + if not cmp_cfg or not test_cfg: + return False + + return bool(test_cfg in cmp_cfg) + + +def is_valid_v4addr(addr): + """check is ipv4 addr is valid""" + + if addr.count('.') == 3: + addr_list = addr.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +def get_evn_peers(config): + """get evn peer ip list""" + + get = re.findall(r"peer ([0-9]+.[0-9]+.[0-9]+.[0-9]+)", config) + if not get: + return None + else: + return list(set(get)) + + +def get_evn_srouce(config): + """get evn peer ip list""" + + get = re.findall( + r"source-address ([0-9]+.[0-9]+.[0-9]+.[0-9]+)", config) + if not get: + return None + else: + return get[0] + + +def get_evn_reflect_client(config): + """get evn reflect client list""" + + get = re.findall( + r"peer ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\s*reflect-client", config) + if not get: + return None + else: + return list(get) + + +class VxlanArp(object): + """ + Manages arp attributes of VXLAN. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.evn_bgp = self.module.params['evn_bgp'] + self.evn_source_ip = self.module.params['evn_source_ip'] + self.evn_peer_ip = self.module.params['evn_peer_ip'] + self.evn_server = self.module.params['evn_server'] + self.evn_reflect_client = self.module.params['evn_reflect_client'] + self.vbdif_name = self.module.params['vbdif_name'] + self.arp_collect_host = self.module.params['arp_collect_host'] + self.host_collect_protocol = self.module.params[ + 'host_collect_protocol'] + self.bridge_domain_id = self.module.params['bridge_domain_id'] + self.arp_suppress = self.module.params['arp_suppress'] + self.state = self.module.params['state'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.config = "" # current config + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init module""" + + required_together = [("vbdif_name", "arp_collect_host"), ("bridge_domain_id", "arp_suppress")] + self.module = AnsibleModule(argument_spec=self.spec, + required_together=required_together, + supports_check_mode=True) + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_current_config(self): + """get current configuration""" + + flags = list() + exp = r"| ignore-case section include evn bgp|host collect protocol bgp" + if self.vbdif_name: + exp += r"|^#\s+interface %s\s+" % self.vbdif_name.lower().capitalize().replace(" ", "") + + if self.bridge_domain_id: + exp += r"|^#\s+bridge-domain %s\s+" % self.bridge_domain_id + + flags.append(exp) + cfg_str = self.get_config(flags) + config = cfg_str.split("\n") + + exist_config = "" + for cfg in config: + if not cfg.startswith("display"): + exist_config += cfg + return exist_config + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) # show updates result + + def config_bridge_domain(self): + """manage bridge domain configuration""" + + if not self.bridge_domain_id: + return + + # bridge-domain bd-id + # [undo] arp broadcast-suppress enable + + cmd = "bridge-domain %s" % self.bridge_domain_id + if not is_config_exist(self.config, cmd): + self.module.fail_json(msg="Error: Bridge domain %s is not exist." % self.bridge_domain_id) + + cmd = "arp broadcast-suppress enable" + exist = is_config_exist(self.config, cmd) + if self.arp_suppress == "enable" and not exist: + self.cli_add_command("bridge-domain %s" % self.bridge_domain_id) + self.cli_add_command(cmd) + self.cli_add_command("quit") + elif self.arp_suppress == "disable" and exist: + self.cli_add_command("bridge-domain %s" % self.bridge_domain_id) + self.cli_add_command(cmd, undo=True) + self.cli_add_command("quit") + + def config_evn_bgp(self): + """enables EVN BGP and configure evn bgp command""" + + evn_bgp_view = False + evn_bgp_enable = False + + cmd = "evn bgp" + exist = is_config_exist(self.config, cmd) + if self.evn_bgp == "enable" or exist: + evn_bgp_enable = True + + # [undo] evn bgp + if self.evn_bgp: + if self.evn_bgp == "enable" and not exist: + self.cli_add_command(cmd) + evn_bgp_view = True + elif self.evn_bgp == "disable" and exist: + self.cli_add_command(cmd, undo=True) + return + + # [undo] source-address ip-address + if evn_bgp_enable and self.evn_source_ip: + cmd = "source-address %s" % self.evn_source_ip + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd) + elif self.state == "absent" and exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd, undo=True) + + # [undo] peer ip-address + # [undo] peer ipv4-address reflect-client + if evn_bgp_enable and self.evn_peer_ip: + cmd = "peer %s" % self.evn_peer_ip + exist = is_config_exist(self.config, cmd) + if self.state == "present": + if not exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd) + if self.evn_reflect_client == "enable": + self.cli_add_command( + "peer %s reflect-client" % self.evn_peer_ip) + else: + if self.evn_reflect_client: + cmd = "peer %s reflect-client" % self.evn_peer_ip + exist = is_config_exist(self.config, cmd) + if self.evn_reflect_client == "enable" and not exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd) + elif self.evn_reflect_client == "disable" and exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd, undo=True) + else: + if exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd, undo=True) + + # [undo] server enable + if evn_bgp_enable and self.evn_server: + cmd = "server enable" + exist = is_config_exist(self.config, cmd) + if self.evn_server == "enable" and not exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd) + elif self.evn_server == "disable" and exist: + if not evn_bgp_view: + self.cli_add_command("evn bgp") + evn_bgp_view = True + self.cli_add_command(cmd, undo=True) + + if evn_bgp_view: + self.cli_add_command("quit") + + def config_vbdif(self): + """configure command at the VBDIF interface view""" + + # interface vbdif bd-id + # [undo] arp collect host enable + + cmd = "interface %s" % self.vbdif_name.lower().capitalize() + exist = is_config_exist(self.config, cmd) + + if not exist: + self.module.fail_json( + msg="Error: Interface %s does not exist." % self.vbdif_name) + + cmd = "arp collect host enable" + exist = is_config_exist(self.config, cmd) + if self.arp_collect_host == "enable" and not exist: + self.cli_add_command("interface %s" % + self.vbdif_name.lower().capitalize()) + self.cli_add_command(cmd) + self.cli_add_command("quit") + elif self.arp_collect_host == "disable" and exist: + self.cli_add_command("interface %s" % + self.vbdif_name.lower().capitalize()) + self.cli_add_command(cmd, undo=True) + self.cli_add_command("quit") + + def config_host_collect_protocal(self): + """Enable EVN BGP or BGP EVPN to advertise host information""" + + # [undo] host collect protocol bgp + cmd = "host collect protocol bgp" + exist = is_config_exist(self.config, cmd) + + if self.state == "present": + if self.host_collect_protocol == "bgp" and not exist: + self.cli_add_command(cmd) + elif self.host_collect_protocol == "none" and exist: + self.cli_add_command(cmd, undo=True) + else: + if self.host_collect_protocol == "bgp" and exist: + self.cli_add_command(cmd, undo=True) + + def is_valid_vbdif(self, ifname): + """check is interface vbdif is valid""" + + if not ifname.upper().startswith('VBDIF'): + return False + bdid = self.vbdif_name.replace(" ", "").upper().replace("VBDIF", "") + if not bdid.isdigit(): + return False + if int(bdid) < 1 or int(bdid) > 16777215: + return False + + return True + + def check_params(self): + """Check all input params""" + + # bridge domain id check + if self.bridge_domain_id: + if not self.bridge_domain_id.isdigit(): + self.module.fail_json( + msg="Error: Bridge domain id is not digit.") + if int(self.bridge_domain_id) < 1 or int(self.bridge_domain_id) > 16777215: + self.module.fail_json( + msg="Error: Bridge domain id is not in the range from 1 to 16777215.") + + # evn_source_ip check + if self.evn_source_ip: + if not is_valid_v4addr(self.evn_source_ip): + self.module.fail_json(msg="Error: evn_source_ip is invalid.") + + # evn_peer_ip check + if self.evn_peer_ip: + if not is_valid_v4addr(self.evn_peer_ip): + self.module.fail_json(msg="Error: evn_peer_ip is invalid.") + + # vbdif_name check + if self.vbdif_name: + self.vbdif_name = self.vbdif_name.replace( + " ", "").lower().capitalize() + if not self.is_valid_vbdif(self.vbdif_name): + self.module.fail_json(msg="Error: vbdif_name is invalid.") + + # evn_reflect_client and evn_peer_ip must set at the same time + if self.evn_reflect_client and not self.evn_peer_ip: + self.module.fail_json( + msg="Error: evn_reflect_client and evn_peer_ip must set at the same time.") + + # evn_server and evn_reflect_client can not set at the same time + if self.evn_server == "enable" and self.evn_reflect_client == "enable": + self.module.fail_json( + msg="Error: evn_server and evn_reflect_client can not set at the same time.") + + def get_proposed(self): + """get proposed info""" + + if self.evn_bgp: + self.proposed["evn_bgp"] = self.evn_bgp + if self.evn_source_ip: + self.proposed["evn_source_ip"] = self.evn_source_ip + if self.evn_peer_ip: + self.proposed["evn_peer_ip"] = self.evn_peer_ip + if self.evn_server: + self.proposed["evn_server"] = self.evn_server + if self.evn_reflect_client: + self.proposed["evn_reflect_client"] = self.evn_reflect_client + if self.arp_collect_host: + self.proposed["arp_collect_host"] = self.arp_collect_host + if self.host_collect_protocol: + self.proposed["host_collect_protocol"] = self.host_collect_protocol + if self.arp_suppress: + self.proposed["arp_suppress"] = self.arp_suppress + if self.vbdif_name: + self.proposed["vbdif_name"] = self.evn_peer_ip + if self.bridge_domain_id: + self.proposed["bridge_domain_id"] = self.bridge_domain_id + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + evn_bgp_exist = is_config_exist(self.config, "evn bgp") + if evn_bgp_exist: + self.existing["evn_bgp"] = "enable" + else: + self.existing["evn_bgp"] = "disable" + + if evn_bgp_exist: + if is_config_exist(self.config, "server enable"): + self.existing["evn_server"] = "enable" + else: + self.existing["evn_server"] = "disable" + + self.existing["evn_source_ip"] = get_evn_srouce(self.config) + self.existing["evn_peer_ip"] = get_evn_peers(self.config) + self.existing["evn_reflect_client"] = get_evn_reflect_client( + self.config) + + if is_config_exist(self.config, "arp collect host enable"): + self.existing["host_collect_protocol"] = "enable" + else: + self.existing["host_collect_protocol"] = "disable" + + if is_config_exist(self.config, "host collect protocol bgp"): + self.existing["host_collect_protocol"] = "bgp" + else: + self.existing["host_collect_protocol"] = None + + if is_config_exist(self.config, "arp broadcast-suppress enable"): + self.existing["arp_suppress"] = "enable" + else: + self.existing["arp_suppress"] = "disable" + + def get_end_state(self): + """get end state info""" + + config = self.get_current_config() + evn_bgp_exist = is_config_exist(config, "evn bgp") + if evn_bgp_exist: + self.end_state["evn_bgp"] = "enable" + else: + self.end_state["evn_bgp"] = "disable" + + if evn_bgp_exist: + if is_config_exist(config, "server enable"): + self.end_state["evn_server"] = "enable" + else: + self.end_state["evn_server"] = "disable" + + self.end_state["evn_source_ip"] = get_evn_srouce(config) + self.end_state["evn_peer_ip"] = get_evn_peers(config) + self.end_state[ + "evn_reflect_client"] = get_evn_reflect_client(config) + + if is_config_exist(config, "arp collect host enable"): + self.end_state["host_collect_protocol"] = "enable" + else: + self.end_state["host_collect_protocol"] = "disable" + + if is_config_exist(config, "host collect protocol bgp"): + self.end_state["host_collect_protocol"] = "bgp" + else: + self.end_state["host_collect_protocol"] = None + + if is_config_exist(config, "arp broadcast-suppress enable"): + self.end_state["arp_suppress"] = "enable" + else: + self.end_state["arp_suppress"] = "disable" + + def work(self): + """worker""" + + self.check_params() + self.config = self.get_current_config() + self.get_existing() + self.get_proposed() + + # deal present or absent + if self.evn_bgp or self.evn_server or self.evn_peer_ip or self.evn_source_ip: + self.config_evn_bgp() + + if self.vbdif_name and self.arp_collect_host: + self.config_vbdif() + + if self.host_collect_protocol: + self.config_host_collect_protocal() + + if self.bridge_domain_id and self.arp_suppress: + self.config_bridge_domain() + + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + evn_bgp=dict(required=False, type='str', + choices=['enable', 'disable']), + evn_source_ip=dict(required=False, type='str'), + evn_peer_ip=dict(required=False, type='str'), + evn_server=dict(required=False, type='str', + choices=['enable', 'disable']), + evn_reflect_client=dict( + required=False, type='str', choices=['enable', 'disable']), + vbdif_name=dict(required=False, type='str'), + arp_collect_host=dict(required=False, type='str', + choices=['enable', 'disable']), + host_collect_protocol=dict( + required=False, type='str', choices=['bgp', 'none']), + bridge_domain_id=dict(required=False, type='str'), + arp_suppress=dict(required=False, type='str', + choices=['enable', 'disable']), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = VxlanArp(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vxlan_gateway.py b/plugins/modules/ce_vxlan_gateway.py new file mode 100644 index 0000000..c88b85c --- /dev/null +++ b/plugins/modules/ce_vxlan_gateway.py @@ -0,0 +1,937 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vxlan_gateway +short_description: Manages gateway for the VXLAN network on HUAWEI CloudEngine devices. +description: + - Configuring Centralized All-Active Gateways or Distributed Gateway for + the VXLAN Network on HUAWEI CloudEngine devices. +author: QijunPan (@QijunPan) +notes: + - Ensure All-Active Gateways or Distributed Gateway for the VXLAN Network can not configure at the same time. + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + dfs_id: + description: + - Specifies the ID of a DFS group. + The value must be 1. + dfs_source_ip: + description: + - Specifies the IPv4 address bound to a DFS group. + The value is in dotted decimal notation. + dfs_source_vpn: + description: + - Specifies the name of a VPN instance bound to a DFS group. + The value is a string of 1 to 31 case-sensitive characters without spaces. + If the character string is quoted by double quotation marks, the character string can contain spaces. + The value C(_public_) is reserved and cannot be used as the VPN instance name. + dfs_udp_port: + description: + - Specifies the UDP port number of the DFS group. + The value is an integer that ranges from 1025 to 65535. + dfs_all_active: + description: + - Creates all-active gateways. + choices: ['enable', 'disable'] + dfs_peer_ip: + description: + - Configure the IP address of an all-active gateway peer. + The value is in dotted decimal notation. + dfs_peer_vpn: + description: + - Specifies the name of the VPN instance that is associated with all-active gateway peer. + The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + When double quotation marks are used around the string, spaces are allowed in the string. + The value C(_public_) is reserved and cannot be used as the VPN instance name. + vpn_instance: + description: + - Specifies the name of a VPN instance. + The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + When double quotation marks are used around the string, spaces are allowed in the string. + The value C(_public_) is reserved and cannot be used as the VPN instance name. + vpn_vni: + description: + - Specifies a VNI ID. + Binds a VXLAN network identifier (VNI) to a virtual private network (VPN) instance. + The value is an integer ranging from 1 to 16000000. + vbdif_name: + description: + - Full name of VBDIF interface, i.e. Vbdif100. + vbdif_bind_vpn: + description: + - Specifies the name of the VPN instance that is associated with the interface. + The value is a string of 1 to 31 case-sensitive characters, spaces not supported. + When double quotation marks are used around the string, spaces are allowed in the string. + The value C(_public_) is reserved and cannot be used as the VPN instance name. + vbdif_mac: + description: + - Specifies a MAC address for a VBDIF interface. + The value is in the format of H-H-H. Each H is a 4-digit hexadecimal number, such as C(00e0) or C(fc01). + If an H contains less than four digits, 0s are added ahead. For example, C(e0) is equal to C(00e0). + A MAC address cannot be all 0s or 1s or a multicast MAC address. + arp_distribute_gateway: + description: + - Enable the distributed gateway function on VBDIF interface. + choices: ['enable','disable'] + arp_direct_route: + description: + - Enable VLINK direct route on VBDIF interface. + choices: ['enable','disable'] + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +- name: vxlan gateway module test + hosts: ce128 + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Configuring Centralized All-Active Gateways for the VXLAN Network + ce_vxlan_gateway: + dfs_id: 1 + dfs_source_ip: 6.6.6.6 + dfs_all_active: enable + dfs_peer_ip: 7.7.7.7 + provider: "{{ cli }}" + - name: Bind the VPN instance to a Layer 3 gateway, enable distributed gateway, and configure host route advertisement. + ce_vxlan_gateway: + vbdif_name: Vbdif100 + vbdif_bind_vpn: vpn1 + arp_distribute_gateway: enable + arp_direct_route: enable + provider: "{{ cli }}" + - name: Assign a VNI to a VPN instance. + ce_vxlan_gateway: + vpn_instance: vpn1 + vpn_vni: 100 + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"dfs_id": "1", "dfs_source_ip": "6.6.6.6", "dfs_all_active":"enable", "dfs_peer_ip": "7.7.7.7"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"dfs_id": "1", "dfs_source_ip": null, "evn_peer_ip": [], "dfs_all_active": "disable"} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"dfs_id": "1", "evn_source_ip": "6.6.6.6", "evn_source_vpn": null, + "evn_peers": [{"ip": "7.7.7.7", "vpn": ""}], "dfs_all_active": "enable"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["dfs-group 1", + "source ip 6.6.6.6", + "active-active-gateway", + "peer 7.7.7.7"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec +from ansible.module_utils.connection import exec_command + + +def is_config_exist(cmp_cfg, test_cfg): + """is configuration exist?""" + + if not cmp_cfg or not test_cfg: + return False + + return bool(test_cfg in cmp_cfg) + + +def is_valid_v4addr(addr): + """check is ipv4 addr""" + + if not addr: + return False + + if addr.count('.') == 3: + addr_list = addr.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +def mac_format(mac): + """convert mac format to xxxx-xxxx-xxxx""" + + if not mac: + return None + + if mac.count("-") != 2: + return None + + addrs = mac.split("-") + for i in range(3): + if not addrs[i] or not addrs[i].isalnum(): + return None + if len(addrs[i]) < 1 or len(addrs[i]) > 4: + return None + try: + addrs[i] = int(addrs[i], 16) + except ValueError: + return None + + try: + return "%04x-%04x-%04x" % (addrs[0], addrs[1], addrs[2]) + except ValueError: + return None + except TypeError: + return None + + +def get_dfs_source_ip(config): + """get dfs source ip address""" + + get = re.findall(r"source ip ([0-9]+.[0-9]+.[0-9]+.[0-9]+)", config) + if not get: + return None + else: + return get[0] + + +def get_dfs_source_vpn(config): + """get dfs source ip vpn instance name""" + + get = re.findall( + r"source ip [0-9]+.[0-9]+.[0-9]+.[0-9]+ vpn-instance (\S+)", config) + if not get: + return None + else: + return get[0] + + +def get_dfs_udp_port(config): + """get dfs udp port""" + + get = re.findall(r"udp port (\d+)", config) + if not get: + return None + else: + return get[0] + + +def get_dfs_peers(config): + """get evn peer ip list""" + + get = re.findall( + r"peer ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\s?(vpn-instance)?\s?(\S*)", config) + if not get: + return None + else: + peers = list() + for item in get: + peers.append(dict(ip=item[0], vpn=item[2])) + return peers + + +def get_ip_vpn(config): + """get ip vpn instance""" + + get = re.findall(r"ip vpn-instance (\S+)", config) + if not get: + return None + else: + return get[0] + + +def get_ip_vpn_vni(config): + """get ip vpn vxlan vni""" + + get = re.findall(r"vxlan vni (\d+)", config) + if not get: + return None + else: + return get[0] + + +def get_vbdif_vpn(config): + """get ip vpn name of interface vbdif""" + + get = re.findall(r"ip binding vpn-instance (\S+)", config) + if not get: + return None + else: + return get[0] + + +def get_vbdif_mac(config): + """get mac address of interface vbdif""" + + get = re.findall( + r" mac-address ([0-9a-fA-F]{1,4}-[0-9a-fA-F]{1,4}-[0-9a-fA-F]{1,4})", config) + if not get: + return None + else: + return get[0] + + +class VxlanGateway(object): + """ + Manages Gateway for the VXLAN Network. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.dfs_id = self.module.params['dfs_id'] + self.dfs_source_ip = self.module.params['dfs_source_ip'] + self.dfs_source_vpn = self.module.params['dfs_source_vpn'] + self.dfs_udp_port = self.module.params['dfs_udp_port'] + self.dfs_all_active = self.module.params['dfs_all_active'] + self.dfs_peer_ip = self.module.params['dfs_peer_ip'] + self.dfs_peer_vpn = self.module.params['dfs_peer_vpn'] + self.vpn_instance = self.module.params['vpn_instance'] + self.vpn_vni = self.module.params['vpn_vni'] + self.vbdif_name = self.module.params['vbdif_name'] + self.vbdif_mac = self.module.params['vbdif_mac'] + self.vbdif_bind_vpn = self.module.params['vbdif_bind_vpn'] + self.arp_distribute_gateway = self.module.params['arp_distribute_gateway'] + self.arp_direct_route = self.module.params['arp_direct_route'] + self.state = self.module.params['state'] + + # host info + self.host = self.module.params['host'] + self.username = self.module.params['username'] + self.port = self.module.params['port'] + + # state + self.config = "" # current config + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_current_config(self): + """get current configuration""" + + flags = list() + exp = r" | ignore-case section include ^#\s+dfs-group" + if self.vpn_instance: + exp += r"|^#\s+ip vpn-instance %s" % self.vpn_instance + if self.vbdif_name: + exp += r"|^#\s+interface %s" % self.vbdif_name + flags.append(exp) + return self.get_config(flags) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) # show updates result + + def config_dfs_group(self): + """manage Dynamic Fabric Service (DFS) group configuration""" + + if not self.dfs_id: + return + + dfs_view = False + view_cmd = "dfs-group %s" % self.dfs_id + exist = is_config_exist(self.config, view_cmd) + if self.state == "present" and not exist: + self.cli_add_command(view_cmd) + dfs_view = True + + # undo dfs-group dfs-group-id + if self.state == "absent" and exist: + if not self.dfs_source_ip and not self.dfs_udp_port and not self.dfs_all_active and not self.dfs_peer_ip: + self.cli_add_command(view_cmd, undo=True) + return + + # [undo] source ip ip-address [ vpn-instance vpn-instance-name ] + if self.dfs_source_ip: + cmd = "source ip %s" % self.dfs_source_ip + if self.dfs_source_vpn: + cmd += " vpn-instance %s" % self.dfs_source_vpn + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(cmd) + if self.state == "absent" and exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(cmd, undo=True) + + # [undo] udp port port-number + if self.dfs_udp_port: + cmd = "udp port %s" % self.dfs_udp_port + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(cmd) + elif self.state == "absent" and exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(cmd, undo=True) + + # [undo] active-active-gateway + # [undo]peer[ vpn-instance vpn-instance-name ] + aa_cmd = "active-active-gateway" + aa_exist = is_config_exist(self.config, aa_cmd) + aa_view = False + if self.dfs_all_active == "disable": + if aa_exist: + cmd = "peer %s" % self.dfs_peer_ip + if self.dfs_source_vpn: + cmd += " vpn-instance %s" % self.dfs_peer_vpn + exist = is_config_exist(self.config, cmd) + if self.state == "absent" and exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(aa_cmd) + self.cli_add_command(cmd, undo=True) + self.cli_add_command("quit") + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(aa_cmd, undo=True) + elif self.dfs_all_active == "enable": + if not aa_exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(aa_cmd) + aa_view = True + + if self.dfs_peer_ip: + cmd = "peer %s" % self.dfs_peer_ip + if self.dfs_peer_vpn: + cmd += " vpn-instance %s" % self.dfs_peer_vpn + exist = is_config_exist(self.config, cmd) + + if self.state == "present" and not exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + if not aa_view: + self.cli_add_command(aa_cmd) + self.cli_add_command(cmd) + self.cli_add_command("quit") + elif self.state == "absent" and exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + if not aa_view: + self.cli_add_command(aa_cmd) + self.cli_add_command(cmd, undo=True) + self.cli_add_command("quit") + else: # not input dfs_all_active + if aa_exist and self.dfs_peer_ip: + cmd = "peer %s" % self.dfs_peer_ip + if self.dfs_peer_vpn: + cmd += " vpn-instance %s" % self.dfs_peer_vpn + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(aa_cmd) + self.cli_add_command(cmd) + self.cli_add_command("quit") + elif self.state == "absent" and exist: + if not dfs_view: + self.cli_add_command(view_cmd) + dfs_view = True + self.cli_add_command(aa_cmd) + self.cli_add_command(cmd, undo=True) + self.cli_add_command("quit") + else: + pass + elif not aa_exist and self.dfs_peer_ip and self.state == "present": + self.module.fail_json( + msg="Error: All-active gateways is not enable.") + else: + pass + + if dfs_view: + self.cli_add_command("quit") + + def config_ip_vpn(self): + """configure command at the ip vpn view""" + + if not self.vpn_instance or not self.vpn_vni: + return + + # ip vpn-instance vpn-instance-name + view_cmd = "ip vpn-instance %s" % self.vpn_instance + exist = is_config_exist(self.config, view_cmd) + if not exist: + self.module.fail_json( + msg="Error: ip vpn instance %s is not exist." % self.vpn_instance) + + # [undo] vxlan vni vni-id + cmd = "vxlan vni %s" % self.vpn_vni + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + self.cli_add_command(view_cmd) + self.cli_add_command(cmd) + self.cli_add_command("quit") + elif self.state == "absent" and exist: + self.cli_add_command(view_cmd) + self.cli_add_command(cmd, undo=True) + self.cli_add_command("quit") + + def config_vbdif(self): + """configure command at the VBDIF interface view""" + + if not self.vbdif_name: + return + + vbdif_cmd = "interface %s" % self.vbdif_name.lower().capitalize() + exist = is_config_exist(self.config, vbdif_cmd) + + if not exist: + self.module.fail_json( + msg="Error: Interface %s is not exist." % self.vbdif_name) + + # interface vbdif bd-id + # [undo] ip binding vpn-instance vpn-instance-name + vbdif_view = False + if self.vbdif_bind_vpn: + cmd = "ip binding vpn-instance %s" % self.vbdif_bind_vpn + exist = is_config_exist(self.config, cmd) + + if self.state == "present" and not exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command(cmd) + elif self.state == "absent" and exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command(cmd, undo=True) + + # [undo] arp distribute-gateway enable + if self.arp_distribute_gateway: + cmd = "arp distribute-gateway enable" + exist = is_config_exist(self.config, cmd) + if self.arp_distribute_gateway == "enable" and not exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command(cmd) + elif self.arp_distribute_gateway == "disable" and exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command(cmd, undo=True) + + # [undo] arp direct-route enable + if self.arp_direct_route: + cmd = "arp direct-route enable" + exist = is_config_exist(self.config, cmd) + if self.arp_direct_route == "enable" and not exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command(cmd) + elif self.arp_direct_route == "disable" and exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command(cmd, undo=True) + + # mac-address mac-address + # undo mac-address + if self.vbdif_mac: + cmd = "mac-address %s" % self.vbdif_mac + exist = is_config_exist(self.config, cmd) + if self.state == "present" and not exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command(cmd) + elif self.state == "absent" and exist: + if not vbdif_view: + self.cli_add_command(vbdif_cmd) + vbdif_view = True + self.cli_add_command("undo mac-address") + + # quit + if vbdif_view: + self.cli_add_command("quit") + + def is_valid_vbdif(self, ifname): + """check is interface vbdif""" + + if not ifname.upper().startswith('VBDIF'): + return False + bdid = self.vbdif_name.replace(" ", "").upper().replace("VBDIF", "") + if not bdid.isdigit(): + return False + if int(bdid) < 1 or int(bdid) > 16777215: + return False + + return True + + def is_valid_ip_vpn(self, vpname): + """check ip vpn""" + + if not vpname: + return False + + if vpname == "_public_": + self.module.fail_json( + msg="Error: The value C(_public_) is reserved and cannot be used as the VPN instance name.") + + if len(vpname) < 1 or len(vpname) > 31: + self.module.fail_json( + msg="Error: IP vpn name length is not in the range from 1 to 31.") + + return True + + def check_params(self): + """Check all input params""" + + # dfs id check + if self.dfs_id: + if not self.dfs_id.isdigit(): + self.module.fail_json(msg="Error: DFS id is not digit.") + if int(self.dfs_id) != 1: + self.module.fail_json(msg="Error: DFS is not 1.") + + # dfs_source_ip check + if self.dfs_source_ip: + if not is_valid_v4addr(self.dfs_source_ip): + self.module.fail_json(msg="Error: dfs_source_ip is invalid.") + # dfs_source_vpn check + if self.dfs_source_vpn and not self.is_valid_ip_vpn(self.dfs_source_vpn): + self.module.fail_json(msg="Error: dfs_source_vpn is invalid.") + + # dfs_source_vpn and dfs_source_ip must set at the same time + if self.dfs_source_vpn and not self.dfs_source_ip: + self.module.fail_json( + msg="Error: dfs_source_vpn and dfs_source_ip must set at the same time.") + + # dfs_udp_port check + if self.dfs_udp_port: + if not self.dfs_udp_port.isdigit(): + self.module.fail_json( + msg="Error: dfs_udp_port id is not digit.") + if int(self.dfs_udp_port) < 1025 or int(self.dfs_udp_port) > 65535: + self.module.fail_json( + msg="dfs_udp_port is not ranges from 1025 to 65535.") + + # dfs_peer_ip check + if self.dfs_peer_ip: + if not is_valid_v4addr(self.dfs_peer_ip): + self.module.fail_json(msg="Error: dfs_peer_ip is invalid.") + # dfs_peer_vpn check + if self.dfs_peer_vpn and not self.is_valid_ip_vpn(self.dfs_peer_vpn): + self.module.fail_json(msg="Error: dfs_peer_vpn is invalid.") + + # dfs_peer_vpn and dfs_peer_ip must set at the same time + if self.dfs_peer_vpn and not self.dfs_peer_ip: + self.module.fail_json( + msg="Error: dfs_peer_vpn and dfs_peer_ip must set at the same time.") + + # vpn_instance check + if self.vpn_instance and not self.is_valid_ip_vpn(self.vpn_instance): + self.module.fail_json(msg="Error: vpn_instance is invalid.") + + # vpn_vni check + if self.vpn_vni: + if not self.vpn_vni.isdigit(): + self.module.fail_json(msg="Error: vpn_vni id is not digit.") + if int(self.vpn_vni) < 1 or int(self.vpn_vni) > 16000000: + self.module.fail_json( + msg="vpn_vni is not ranges from 1 to 16000000.") + + # vpn_instance and vpn_vni must set at the same time + if bool(self.vpn_instance) != bool(self.vpn_vni): + self.module.fail_json( + msg="Error: vpn_instance and vpn_vni must set at the same time.") + + # vbdif_name check + if self.vbdif_name: + self.vbdif_name = self.vbdif_name.replace(" ", "").lower().capitalize() + if not self.is_valid_vbdif(self.vbdif_name): + self.module.fail_json(msg="Error: vbdif_name is invalid.") + + # vbdif_mac check + if self.vbdif_mac: + mac = mac_format(self.vbdif_mac) + if not mac: + self.module.fail_json(msg="Error: vbdif_mac is invalid.") + self.vbdif_mac = mac + + # vbdif_bind_vpn check + if self.vbdif_bind_vpn and not self.is_valid_ip_vpn(self.vbdif_bind_vpn): + self.module.fail_json(msg="Error: vbdif_bind_vpn is invalid.") + + # All-Active Gateways or Distributed Gateway config can not set at the + # same time. + if self.dfs_id: + if self.vpn_vni or self.arp_distribute_gateway == "enable": + self.module.fail_json(msg="Error: All-Active Gateways or Distributed Gateway config " + "can not set at the same time.") + + def get_proposed(self): + """get proposed info""" + + if self.dfs_id: + self.proposed["dfs_id"] = self.dfs_id + self.proposed["dfs_source_ip"] = self.dfs_source_ip + self.proposed["dfs_source_vpn"] = self.dfs_source_vpn + self.proposed["dfs_udp_port"] = self.dfs_udp_port + self.proposed["dfs_all_active"] = self.dfs_all_active + self.proposed["dfs_peer_ip"] = self.dfs_peer_ip + self.proposed["dfs_peer_vpn"] = self.dfs_peer_vpn + + if self.vpn_instance: + self.proposed["vpn_instance"] = self.vpn_instance + self.proposed["vpn_vni"] = self.vpn_vni + + if self.vbdif_name: + self.proposed["vbdif_name"] = self.vbdif_name + self.proposed["vbdif_mac"] = self.vbdif_mac + self.proposed["vbdif_bind_vpn"] = self.vbdif_bind_vpn + self.proposed[ + "arp_distribute_gateway"] = self.arp_distribute_gateway + self.proposed["arp_direct_route"] = self.arp_direct_route + + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if not self.config: + return + + if is_config_exist(self.config, "dfs-group 1"): + self.existing["dfs_id"] = "1" + self.existing["dfs_source_ip"] = get_dfs_source_ip(self.config) + self.existing["dfs_source_vpn"] = get_dfs_source_vpn(self.config) + self.existing["dfs_udp_port"] = get_dfs_udp_port(self.config) + if is_config_exist(self.config, "active-active-gateway"): + self.existing["dfs_all_active"] = "enable" + self.existing["dfs_peers"] = get_dfs_peers(self.config) + else: + self.existing["dfs_all_active"] = "disable" + + if self.vpn_instance: + self.existing["vpn_instance"] = get_ip_vpn(self.config) + self.existing["vpn_vni"] = get_ip_vpn_vni(self.config) + + if self.vbdif_name: + self.existing["vbdif_name"] = self.vbdif_name + self.existing["vbdif_mac"] = get_vbdif_mac(self.config) + self.existing["vbdif_bind_vpn"] = get_vbdif_vpn(self.config) + if is_config_exist(self.config, "arp distribute-gateway enable"): + self.existing["arp_distribute_gateway"] = "enable" + else: + self.existing["arp_distribute_gateway"] = "disable" + if is_config_exist(self.config, "arp direct-route enable"): + self.existing["arp_direct_route"] = "enable" + else: + self.existing["arp_direct_route"] = "disable" + + def get_end_state(self): + """get end state info""" + + config = self.get_current_config() + if not config: + return + + if is_config_exist(config, "dfs-group 1"): + self.end_state["dfs_id"] = "1" + self.end_state["dfs_source_ip"] = get_dfs_source_ip(config) + self.end_state["dfs_source_vpn"] = get_dfs_source_vpn(config) + self.end_state["dfs_udp_port"] = get_dfs_udp_port(config) + if is_config_exist(config, "active-active-gateway"): + self.end_state["dfs_all_active"] = "enable" + self.end_state["dfs_peers"] = get_dfs_peers(config) + else: + self.end_state["dfs_all_active"] = "disable" + + if self.vpn_instance: + self.end_state["vpn_instance"] = get_ip_vpn(config) + self.end_state["vpn_vni"] = get_ip_vpn_vni(config) + + if self.vbdif_name: + self.end_state["vbdif_name"] = self.vbdif_name + self.end_state["vbdif_mac"] = get_vbdif_mac(config) + self.end_state["vbdif_bind_vpn"] = get_vbdif_vpn(config) + if is_config_exist(config, "arp distribute-gateway enable"): + self.end_state["arp_distribute_gateway"] = "enable" + else: + self.end_state["arp_distribute_gateway"] = "disable" + if is_config_exist(config, "arp direct-route enable"): + self.end_state["arp_direct_route"] = "enable" + else: + self.end_state["arp_direct_route"] = "disable" + + def work(self): + """worker""" + + self.check_params() + self.config = self.get_current_config() + self.get_existing() + self.get_proposed() + + # deal present or absent + if self.dfs_id: + self.config_dfs_group() + + if self.vpn_instance: + self.config_ip_vpn() + + if self.vbdif_name: + self.config_vbdif() + + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + dfs_id=dict(required=False, type='str'), + dfs_source_ip=dict(required=False, type='str'), + dfs_source_vpn=dict(required=False, type='str'), + dfs_udp_port=dict(required=False, type='str'), + dfs_all_active=dict(required=False, type='str', + choices=['enable', 'disable']), + dfs_peer_ip=dict(required=False, type='str'), + dfs_peer_vpn=dict(required=False, type='str'), + vpn_instance=dict(required=False, type='str'), + vpn_vni=dict(required=False, type='str'), + vbdif_name=dict(required=False, type='str'), + vbdif_mac=dict(required=False, type='str'), + vbdif_bind_vpn=dict(required=False, type='str'), + arp_distribute_gateway=dict( + required=False, type='str', choices=['enable', 'disable']), + arp_direct_route=dict(required=False, type='str', + choices=['enable', 'disable']), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = VxlanGateway(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vxlan_global.py b/plugins/modules/ce_vxlan_global.py new file mode 100644 index 0000000..c4ebe01 --- /dev/null +++ b/plugins/modules/ce_vxlan_global.py @@ -0,0 +1,540 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vxlan_global +short_description: Manages global attributes of VXLAN and bridge domain on HUAWEI CloudEngine devices. +description: + - Manages global attributes of VXLAN and bridge domain on HUAWEI CloudEngine devices. +author: QijunPan (@QijunPan) +notes: + - Recommended connection is C(network_cli). + - This module also works with C(local) connections for legacy playbooks. +options: + bridge_domain_id: + description: + - Specifies a bridge domain ID. + The value is an integer ranging from 1 to 16777215. + tunnel_mode_vxlan: + description: + - Set the tunnel mode to VXLAN when configuring the VXLAN feature. + choices: ['enable', 'disable'] + nvo3_prevent_loops: + description: + - Loop prevention of VXLAN traffic in non-enhanced mode. + When the device works in non-enhanced mode, + inter-card forwarding of VXLAN traffic may result in loops. + choices: ['enable', 'disable'] + nvo3_acl_extend: + description: + - Enabling or disabling the VXLAN ACL extension function. + choices: ['enable', 'disable'] + nvo3_gw_enhanced: + description: + - Configuring the Layer 3 VXLAN Gateway to Work in Non-loopback Mode. + choices: ['l2', 'l3'] + nvo3_service_extend: + description: + - Enabling or disabling the VXLAN service extension function. + choices: ['enable', 'disable'] + nvo3_eth_trunk_hash: + description: + - Eth-Trunk from load balancing VXLAN packets in optimized mode. + choices: ['enable','disable'] + nvo3_ecmp_hash: + description: + - Load balancing of VXLAN packets through ECMP in optimized mode. + choices: ['enable', 'disable'] + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +- name: vxlan global module test + hosts: ce128 + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Create bridge domain and set tunnel mode to VXLAN + ce_vxlan_global: + bridge_domain_id: 100 + nvo3_acl_extend: enable + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"bridge_domain_id": "100", "nvo3_acl_extend": "enable", state="present"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"bridge_domain": {"80", "90"}, "nvo3_acl_extend": "disable"} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"bridge_domain_id": {"80", "90", "100"}, "nvo3_acl_extend": "enable"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["bridge-domain 100", + "ip tunnel mode vxlan"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import load_config +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import ce_argument_spec +from ansible.module_utils.connection import exec_command + + +def is_config_exist(cmp_cfg, test_cfg): + """is configuration exist?""" + + if not cmp_cfg or not test_cfg: + return False + + return bool(test_cfg in cmp_cfg) + + +def get_nvo3_gw_enhanced(cmp_cfg): + """get the Layer 3 VXLAN Gateway to Work in Non-loopback Mode """ + + get = re.findall( + r"assign forward nvo3-gateway enhanced (l[2|3])", cmp_cfg) + if not get: + return None + else: + return get[0] + + +class VxlanGlobal(object): + """ + Manages global attributes of VXLAN and bridge domain. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.tunnel_mode_vxlan = self.module.params['tunnel_mode_vxlan'] + self.nvo3_prevent_loops = self.module.params['nvo3_prevent_loops'] + self.nvo3_acl_extend = self.module.params['nvo3_acl_extend'] + self.nvo3_gw_enhanced = self.module.params['nvo3_gw_enhanced'] + self.nvo3_service_extend = self.module.params['nvo3_service_extend'] + self.nvo3_eth_trunk_hash = self.module.params['nvo3_eth_trunk_hash'] + self.nvo3_ecmp_hash = self.module.params['nvo3_ecmp_hash'] + self.bridge_domain_id = self.module.params['bridge_domain_id'] + self.state = self.module.params['state'] + + # state + self.config = "" # current config + self.bd_info = list() + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def init_module(self): + """init module""" + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def cli_load_config(self, commands): + """load config by cli""" + + if not self.module.check_mode: + load_config(self.module, commands) + + def get_config(self, flags=None): + """Retrieves the current config from the device or cache + """ + flags = [] if flags is None else flags + + cmd = 'display current-configuration ' + cmd += ' '.join(flags) + cmd = cmd.strip() + + rc, out, err = exec_command(self.module, cmd) + if rc != 0: + self.module.fail_json(msg=err) + cfg = str(out).strip() + + return cfg + + def get_current_config(self): + """get current configuration""" + + flags = list() + exp = " include-default | include vxlan|assign | exclude undo" + flags.append(exp) + return self.get_config(flags) + + def cli_add_command(self, command, undo=False): + """add command to self.update_cmd and self.commands""" + + if undo and command.lower() not in ["quit", "return"]: + cmd = "undo " + command + else: + cmd = command + + self.commands.append(cmd) # set to device + if command.lower() not in ["quit", "return"]: + self.updates_cmd.append(cmd) # show updates result + + def get_bd_list(self): + """get bridge domain list""" + flags = list() + bd_info = list() + exp = " include-default | include bridge-domain | exclude undo" + flags.append(exp) + bd_str = self.get_config(flags) + if not bd_str: + return bd_info + bd_num = re.findall(r'bridge-domain\s*([0-9]+)', bd_str) + bd_info.extend(bd_num) + return bd_info + + def config_bridge_domain(self): + """manage bridge domain""" + + if not self.bridge_domain_id: + return + + cmd = "bridge-domain %s" % self.bridge_domain_id + exist = self.bridge_domain_id in self.bd_info + if self.state == "present": + if not exist: + self.cli_add_command(cmd) + self.cli_add_command("quit") + else: + if exist: + self.cli_add_command(cmd, undo=True) + + def config_tunnel_mode(self): + """config tunnel mode vxlan""" + + # ip tunnel mode vxlan + if self.tunnel_mode_vxlan: + cmd = "ip tunnel mode vxlan" + exist = is_config_exist(self.config, cmd) + if self.tunnel_mode_vxlan == "enable": + if not exist: + self.cli_add_command(cmd) + else: + if exist: + self.cli_add_command(cmd, undo=True) + + def config_assign_forward(self): + """config assign forward command""" + + # [undo] assign forward nvo3-gateway enhanced {l2|l3) + if self.nvo3_gw_enhanced: + cmd = "assign forward nvo3-gateway enhanced %s" % self.nvo3_gw_enhanced + exist = is_config_exist(self.config, cmd) + if self.state == "present": + if not exist: + self.cli_add_command(cmd) + else: + if exist: + self.cli_add_command(cmd, undo=True) + + # [undo] assign forward nvo3 f-linecard compatibility enable + if self.nvo3_prevent_loops: + cmd = "assign forward nvo3 f-linecard compatibility enable" + exist = is_config_exist(self.config, cmd) + if self.nvo3_prevent_loops == "enable": + if not exist: + self.cli_add_command(cmd) + else: + if exist: + self.cli_add_command(cmd, undo=True) + + # [undo] assign forward nvo3 acl extend enable + if self.nvo3_acl_extend: + cmd = "assign forward nvo3 acl extend enable" + exist = is_config_exist(self.config, cmd) + if self.nvo3_acl_extend == "enable": + if not exist: + self.cli_add_command(cmd) + else: + if exist: + self.cli_add_command(cmd, undo=True) + + # [undo] assign forward nvo3 service extend enable + if self.nvo3_service_extend: + cmd = "assign forward nvo3 service extend enable" + exist = is_config_exist(self.config, cmd) + if self.nvo3_service_extend == "enable": + if not exist: + self.cli_add_command(cmd) + else: + if exist: + self.cli_add_command(cmd, undo=True) + + # assign forward nvo3 eth-trunk hash {enable|disable} + if self.nvo3_eth_trunk_hash: + cmd = "assign forward nvo3 eth-trunk hash enable" + exist = is_config_exist(self.config, cmd) + if self.nvo3_eth_trunk_hash == "enable": + if not exist: + self.cli_add_command(cmd) + else: + if exist: + self.cli_add_command(cmd, undo=True) + + # [undo] assign forward nvo3 ecmp hash enable + if self.nvo3_ecmp_hash: + cmd = "assign forward nvo3 ecmp hash enable" + exist = is_config_exist(self.config, cmd) + if self.nvo3_ecmp_hash == "enable": + if not exist: + self.cli_add_command(cmd) + else: + if exist: + self.cli_add_command(cmd, undo=True) + + def check_params(self): + """Check all input params""" + + # bridge domain id check + if self.bridge_domain_id: + if not self.bridge_domain_id.isdigit(): + self.module.fail_json( + msg="Error: bridge domain id is not digit.") + if int(self.bridge_domain_id) < 1 or int(self.bridge_domain_id) > 16777215: + self.module.fail_json( + msg="Error: bridge domain id is not in the range from 1 to 16777215.") + + def get_proposed(self): + """get proposed info""" + + if self.tunnel_mode_vxlan: + self.proposed["tunnel_mode_vxlan"] = self.tunnel_mode_vxlan + if self.nvo3_prevent_loops: + self.proposed["nvo3_prevent_loops"] = self.nvo3_prevent_loops + if self.nvo3_acl_extend: + self.proposed["nvo3_acl_extend"] = self.nvo3_acl_extend + if self.nvo3_gw_enhanced: + self.proposed["nvo3_gw_enhanced"] = self.nvo3_gw_enhanced + if self.nvo3_service_extend: + self.proposed["nvo3_service_extend"] = self.nvo3_service_extend + if self.nvo3_eth_trunk_hash: + self.proposed["nvo3_eth_trunk_hash"] = self.nvo3_eth_trunk_hash + if self.nvo3_ecmp_hash: + self.proposed["nvo3_ecmp_hash"] = self.nvo3_ecmp_hash + if self.bridge_domain_id: + self.proposed["bridge_domain_id"] = self.bridge_domain_id + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + self.existing["bridge_domain"] = self.bd_info + + cmd = "ip tunnel mode vxlan" + exist = is_config_exist(self.config, cmd) + if exist: + self.existing["tunnel_mode_vxlan"] = "enable" + else: + self.existing["tunnel_mode_vxlan"] = "disable" + + cmd = "assign forward nvo3 f-linecard compatibility enable" + exist = is_config_exist(self.config, cmd) + if exist: + self.existing["nvo3_prevent_loops"] = "enable" + else: + self.existing["nvo3_prevent_loops"] = "disable" + + cmd = "assign forward nvo3 acl extend enable" + exist = is_config_exist(self.config, cmd) + if exist: + self.existing["nvo3_acl_extend"] = "enable" + else: + self.existing["nvo3_acl_extend"] = "disable" + + self.existing["nvo3_gw_enhanced"] = get_nvo3_gw_enhanced( + self.config) + + cmd = "assign forward nvo3 service extend enable" + exist = is_config_exist(self.config, cmd) + if exist: + self.existing["nvo3_service_extend"] = "enable" + else: + self.existing["nvo3_service_extend"] = "disable" + + cmd = "assign forward nvo3 eth-trunk hash enable" + exist = is_config_exist(self.config, cmd) + if exist: + self.existing["nvo3_eth_trunk_hash"] = "enable" + else: + self.existing["nvo3_eth_trunk_hash"] = "disable" + + cmd = "assign forward nvo3 ecmp hash enable" + exist = is_config_exist(self.config, cmd) + if exist: + self.existing["nvo3_ecmp_hash"] = "enable" + else: + self.existing["nvo3_ecmp_hash"] = "disable" + + def get_end_state(self): + """get end state info""" + + config = self.get_current_config() + + self.end_state["bridge_domain"] = self.get_bd_list() + + cmd = "ip tunnel mode vxlan" + exist = is_config_exist(config, cmd) + if exist: + self.end_state["tunnel_mode_vxlan"] = "enable" + else: + self.end_state["tunnel_mode_vxlan"] = "disable" + + cmd = "assign forward nvo3 f-linecard compatibility enable" + exist = is_config_exist(config, cmd) + if exist: + self.end_state["nvo3_prevent_loops"] = "enable" + else: + self.end_state["nvo3_prevent_loops"] = "disable" + + cmd = "assign forward nvo3 acl extend enable" + exist = is_config_exist(config, cmd) + if exist: + self.end_state["nvo3_acl_extend"] = "enable" + else: + self.end_state["nvo3_acl_extend"] = "disable" + + self.end_state["nvo3_gw_enhanced"] = get_nvo3_gw_enhanced(config) + + cmd = "assign forward nvo3 service extend enable" + exist = is_config_exist(config, cmd) + if exist: + self.end_state["nvo3_service_extend"] = "enable" + else: + self.end_state["nvo3_service_extend"] = "disable" + + cmd = "assign forward nvo3 eth-trunk hash enable" + exist = is_config_exist(config, cmd) + if exist: + self.end_state["nvo3_eth_trunk_hash"] = "enable" + else: + self.end_state["nvo3_eth_trunk_hash"] = "disable" + + cmd = "assign forward nvo3 ecmp hash enable" + exist = is_config_exist(config, cmd) + if exist: + self.end_state["nvo3_ecmp_hash"] = "enable" + else: + self.end_state["nvo3_ecmp_hash"] = "disable" + if self.existing == self.end_state: + self.changed = True + + def work(self): + """worker""" + + self.check_params() + self.config = self.get_current_config() + self.bd_info = self.get_bd_list() + self.get_existing() + self.get_proposed() + + # deal present or absent + self.config_bridge_domain() + self.config_tunnel_mode() + self.config_assign_forward() + if self.commands: + self.cli_load_config(self.commands) + self.changed = True + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + tunnel_mode_vxlan=dict(required=False, type='str', + choices=['enable', 'disable']), + nvo3_prevent_loops=dict(required=False, type='str', + choices=['enable', 'disable']), + nvo3_acl_extend=dict(required=False, type='str', + choices=['enable', 'disable']), + nvo3_gw_enhanced=dict(required=False, type='str', + choices=['l2', 'l3']), + nvo3_service_extend=dict(required=False, type='str', + choices=['enable', 'disable']), + nvo3_eth_trunk_hash=dict(required=False, type='str', + choices=['enable', 'disable']), + nvo3_ecmp_hash=dict(required=False, type='str', + choices=['enable', 'disable']), + bridge_domain_id=dict(required=False, type='str'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = VxlanGlobal(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vxlan_tunnel.py b/plugins/modules/ce_vxlan_tunnel.py new file mode 100644 index 0000000..a6701a7 --- /dev/null +++ b/plugins/modules/ce_vxlan_tunnel.py @@ -0,0 +1,941 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vxlan_tunnel +short_description: Manages VXLAN tunnel configuration on HUAWEI CloudEngine devices. +description: + - This module offers the ability to set the VNI and mapped to the BD, + and configure an ingress replication list on HUAWEI CloudEngine devices. +author: + - Li Yanfeng (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + bridge_domain_id: + description: + - Specifies a bridge domain ID. The value is an integer ranging from 1 to 16777215. + vni_id: + description: + - Specifies a VXLAN network identifier (VNI) ID. The value is an integer ranging from 1 to 16000000. + nve_name: + description: + - Specifies the number of an NVE interface. The value ranges from 1 to 2. + nve_mode: + description: + - Specifies the working mode of an NVE interface. + choices: ['mode-l2','mode-l3'] + peer_list_ip: + description: + - Specifies the IP address of a remote VXLAN tunnel endpoints (VTEP). + The value is in dotted decimal notation. + protocol_type: + description: + - The operation type of routing protocol. + choices: ['bgp','null'] + source_ip: + description: + - Specifies an IP address for a source VTEP. The value is in dotted decimal notation. + state: + description: + - Manage the state of the resource. + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +- name: vxlan tunnel module test + hosts: ce128 + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Make sure nve_name is exist, ensure vni_id and protocol_type is configured on Nve1 interface. + ce_vxlan_tunnel: + nve_name: Nve1 + vni_id: 100 + protocol_type: bgp + state: present + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {nve_interface_name": "Nve1", nve_mode": "mode-l2", "source_ip": "0.0.0.0"} +existing: + description: + - k/v pairs of existing rollback + returned: always + type: dict + sample: {nve_interface_name": "Nve1", nve_mode": "mode-l3", "source_ip": "0.0.0.0"} + +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface Nve1", + "mode l3"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {nve_interface_name": "Nve1", nve_mode": "mode-l3", "source_ip": "0.0.0.0"} +''' +import re +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_VNI_BD_INFO = """ + + + + + + + + + + +""" + +CE_NC_GET_NVE_INFO = """ + + + + + %s + + + + +""" + +CE_NC_MERGE_VNI_BD_ID = """ + + + + + %s + %s + + + + +""" + +CE_NC_DELETE_VNI_BD_ID = """ + + + + + %s + %s + + + + +""" + +CE_NC_MERGE_NVE_MODE = """ + + + + + %s + %s + + + + +""" + +CE_NC_MERGE_NVE_SOURCE_IP_PROTOCOL = """ + + + + + %s + %s + + + + +""" + +CE_NC_MERGE_VNI_PEER_ADDRESS_IP_HEAD = """ + + + + + %s + + + %s +""" + +CE_NC_MERGE_VNI_PEER_ADDRESS_IP_END = """ + + + + + + +""" +CE_NC_MERGE_VNI_PEER_ADDRESS_IP_MERGE = """ + + + %s + + +""" + +CE_NC_DELETE_VNI_PEER_ADDRESS_IP_HEAD = """ + + + + + %s + + + %s +""" +CE_NC_DELETE_VNI_PEER_ADDRESS_IP_END = """ + + + + + + +""" +CE_NC_DELETE_VNI_PEER_ADDRESS_IP_DELETE = """ + + + %s + + +""" + +CE_NC_DELETE_PEER_ADDRESS_IP_HEAD = """ + + + + + %s + + + %s +""" +CE_NC_DELETE_PEER_ADDRESS_IP_END = """ + + + + + + +""" +CE_NC_MERGE_VNI_PROTOCOL = """ + + + + + %s + + + %s + %s + + + + + + +""" + +CE_NC_DELETE_VNI_PROTOCOL = """ + + + + + %s + + + %s + %s + + + + + + +""" + + +def is_valid_address(address): + """check ip-address is valid""" + + if address.find('.') != -1: + addr_list = address.split('.') + if len(addr_list) != 4: + return False + for each_num in addr_list: + if not each_num.isdigit(): + return False + if int(each_num) > 255: + return False + return True + + return False + + +class VxlanTunnel(object): + """ + Manages vxlan tunnel configuration. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.bridge_domain_id = self.module.params['bridge_domain_id'] + self.vni_id = self.module.params['vni_id'] + self.nve_name = self.module.params['nve_name'] + self.nve_mode = self.module.params['nve_mode'] + self.peer_list_ip = self.module.params['peer_list_ip'] + self.protocol_type = self.module.params['protocol_type'] + self.source_ip = self.module.params['source_ip'] + self.state = self.module.params['state'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + # configuration nve info + self.vni2bd_info = None + self.nve_info = None + + def init_module(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_vni2bd_dict(self): + """ get vni2bd attributes dict.""" + + vni2bd_info = dict() + # get vni bd info + conf_str = CE_NC_GET_VNI_BD_INFO + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return vni2bd_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + # get vni to bridge domain id info + root = ElementTree.fromstring(xml_str) + vni2bd_info["vni2BdInfos"] = list() + vni2bds = root.findall("nvo3/nvo3Vni2Bds/nvo3Vni2Bd") + + if vni2bds: + for vni2bd in vni2bds: + vni_dict = dict() + for ele in vni2bd: + if ele.tag in ["vniId", "bdId"]: + vni_dict[ele.tag] = ele.text + vni2bd_info["vni2BdInfos"].append(vni_dict) + + return vni2bd_info + + def check_nve_interface(self, nve_name): + """is nve interface exist""" + + if not self.nve_info: + return False + + if self.nve_info["ifName"] == nve_name: + return True + return False + + def get_nve_dict(self, nve_name): + """ get nve interface attributes dict.""" + + nve_info = dict() + # get nve info + conf_str = CE_NC_GET_NVE_INFO % nve_name + xml_str = get_nc_config(self.module, conf_str) + if "" in xml_str: + return nve_info + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get nve info + root = ElementTree.fromstring(xml_str) + + nvo3 = root.find("nvo3/nvo3Nves/nvo3Nve") + if nvo3: + for nve in nvo3: + if nve.tag in ["srcAddr", "ifName", "nveType"]: + nve_info[nve.tag] = nve.text + + # get nve vni info + nve_info["vni_peer_protocols"] = list() + + vni_members = root.findall( + "nvo3/nvo3Nves/nvo3Nve/vniMembers/vniMember") + if vni_members: + for member in vni_members: + vni_dict = dict() + for ele in member: + if ele.tag in ["vniId", "protocol"]: + vni_dict[ele.tag] = ele.text + nve_info["vni_peer_protocols"].append(vni_dict) + + # get vni peer address ip info + nve_info["vni_peer_ips"] = list() + + re_find = re.findall(r'(.*?)\s*' + r'(.*?)\s*' + r'(.*?)', xml_str) + + if re_find: + for vni_peers in re_find: + vni_info = dict() + vni_peer = re.findall(r'(.*?)', vni_peers[2]) + if vni_peer: + vni_info["vniId"] = vni_peers[0] + vni_peer_list = list() + for peer in vni_peer: + vni_peer_list.append(peer) + vni_info["peerAddr"] = vni_peer_list + nve_info["vni_peer_ips"].append(vni_info) + + return nve_info + + def check_nve_name(self): + """Gets Nve interface name""" + + if self.nve_name is None: + return False + if self.nve_name in ["Nve1", "Nve2"]: + return True + return False + + def is_vni_bd_exist(self, vni_id, bd_id): + """is vni to bridge-domain-id exist""" + + if not self.vni2bd_info: + return False + + for vni2bd in self.vni2bd_info["vni2BdInfos"]: + if vni2bd["vniId"] == vni_id and vni2bd["bdId"] == bd_id: + return True + return False + + def is_vni_bd_change(self, vni_id, bd_id): + """is vni to bridge-domain-id change""" + + if not self.vni2bd_info: + return True + + for vni2bd in self.vni2bd_info["vni2BdInfos"]: + if vni2bd["vniId"] == vni_id and vni2bd["bdId"] == bd_id: + return False + return True + + def is_nve_mode_exist(self, nve_name, mode): + """is nve interface mode exist""" + + if not self.nve_info: + return False + + if self.nve_info["ifName"] == nve_name and self.nve_info["nveType"] == mode: + return True + return False + + def is_nve_mode_change(self, nve_name, mode): + """is nve interface mode change""" + + if not self.nve_info: + return True + + if self.nve_info["ifName"] == nve_name and self.nve_info["nveType"] == mode: + return False + return True + + def is_nve_source_ip_exist(self, nve_name, source_ip): + """is vni to bridge-domain-id exist""" + + if not self.nve_info: + return False + + if self.nve_info["ifName"] == nve_name and self.nve_info["srcAddr"] == source_ip: + return True + return False + + def is_nve_source_ip_change(self, nve_name, source_ip): + """is vni to bridge-domain-id change""" + + if not self.nve_info: + return True + + if self.nve_info["ifName"] == nve_name and self.nve_info["srcAddr"] == source_ip: + return False + return True + + def is_vni_protocol_exist(self, nve_name, vni_id, protocol_type): + """is vni protocol exist""" + + if not self.nve_info: + return False + if self.nve_info["ifName"] == nve_name: + for member in self.nve_info["vni_peer_protocols"]: + if member["vniId"] == vni_id and member["protocol"] == protocol_type: + return True + return False + + def is_vni_protocol_change(self, nve_name, vni_id, protocol_type): + """is vni protocol change""" + + if not self.nve_info: + return True + if self.nve_info["ifName"] == nve_name: + for member in self.nve_info["vni_peer_protocols"]: + if member["vniId"] == vni_id and member["protocol"] == protocol_type: + return False + return True + + def is_vni_peer_list_exist(self, nve_name, vni_id, peer_ip): + """is vni peer list exist""" + + if not self.nve_info: + return False + if self.nve_info["ifName"] == nve_name: + for member in self.nve_info["vni_peer_ips"]: + if member["vniId"] == vni_id and peer_ip in member["peerAddr"]: + return True + return False + + def is_vni_peer_list_change(self, nve_name, vni_id, peer_ip_list): + """is vni peer list change""" + + if not self.nve_info: + return True + + if self.nve_info["ifName"] == nve_name: + if not self.nve_info["vni_peer_ips"]: + return True + + nve_peer_info = list() + for nve_peer in self.nve_info["vni_peer_ips"]: + if nve_peer["vniId"] == vni_id: + nve_peer_info.append(nve_peer) + + if not nve_peer_info: + return True + + nve_peer_list = nve_peer_info[0]["peerAddr"] + for peer in peer_ip_list: + if peer not in nve_peer_list: + return True + + return False + + def config_merge_vni2bd(self, bd_id, vni_id): + """config vni to bd id""" + + if self.is_vni_bd_change(vni_id, bd_id): + cfg_xml = CE_NC_MERGE_VNI_BD_ID % (vni_id, bd_id) + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "MERGE_VNI_BD") + self.updates_cmd.append("bridge-domain %s" % bd_id) + self.updates_cmd.append("vxlan vni %s" % vni_id) + self.changed = True + + def config_merge_mode(self, nve_name, mode): + """config nve mode""" + + if self.is_nve_mode_change(nve_name, mode): + cfg_xml = CE_NC_MERGE_NVE_MODE % (nve_name, mode) + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "MERGE_MODE") + self.updates_cmd.append("interface %s" % nve_name) + if mode == "mode-l3": + self.updates_cmd.append("mode l3") + else: + self.updates_cmd.append("undo mode l3") + self.changed = True + + def config_merge_source_ip(self, nve_name, source_ip): + """config nve source ip""" + + if self.is_nve_source_ip_change(nve_name, source_ip): + cfg_xml = CE_NC_MERGE_NVE_SOURCE_IP_PROTOCOL % ( + nve_name, source_ip) + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "MERGE_SOURCE_IP") + self.updates_cmd.append("interface %s" % nve_name) + self.updates_cmd.append("source %s" % source_ip) + self.changed = True + + def config_merge_vni_peer_ip(self, nve_name, vni_id, peer_ip_list): + """config vni peer ip""" + + if self.is_vni_peer_list_change(nve_name, vni_id, peer_ip_list): + cfg_xml = CE_NC_MERGE_VNI_PEER_ADDRESS_IP_HEAD % ( + nve_name, vni_id) + for peer_ip in peer_ip_list: + cfg_xml += CE_NC_MERGE_VNI_PEER_ADDRESS_IP_MERGE % peer_ip + cfg_xml += CE_NC_MERGE_VNI_PEER_ADDRESS_IP_END + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "MERGE_VNI_PEER_IP") + self.updates_cmd.append("interface %s" % nve_name) + + for peer_ip in peer_ip_list: + cmd_output = "vni %s head-end peer-list %s" % (vni_id, peer_ip) + self.updates_cmd.append(cmd_output) + self.changed = True + + def config_merge_vni_protocol_type(self, nve_name, vni_id, protocol_type): + """config vni protocol type""" + + if self.is_vni_protocol_change(nve_name, vni_id, protocol_type): + cfg_xml = CE_NC_MERGE_VNI_PROTOCOL % ( + nve_name, vni_id, protocol_type) + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "MERGE_VNI_PEER_PROTOCOL") + self.updates_cmd.append("interface %s" % nve_name) + + if protocol_type == "bgp": + self.updates_cmd.append( + "vni %s head-end peer-list protocol %s" % (vni_id, protocol_type)) + else: + self.updates_cmd.append( + "undo vni %s head-end peer-list protocol bgp" % vni_id) + self.changed = True + + def config_delete_vni2bd(self, bd_id, vni_id): + """remove vni to bd id""" + + if not self.is_vni_bd_exist(vni_id, bd_id): + return + cfg_xml = CE_NC_DELETE_VNI_BD_ID % (vni_id, bd_id) + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "DELETE_VNI_BD") + self.updates_cmd.append( + "bridge-domain %s" % bd_id) + self.updates_cmd.append( + "undo vxlan vni %s" % vni_id) + + self.changed = True + + def config_delete_mode(self, nve_name, mode): + """nve mode""" + + if mode == "mode-l3": + if not self.is_nve_mode_exist(nve_name, mode): + return + cfg_xml = CE_NC_MERGE_NVE_MODE % (nve_name, "mode-l2") + + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "DELETE_MODE") + self.updates_cmd.append("interface %s" % nve_name) + self.updates_cmd.append("undo mode l3") + self.changed = True + else: + self.module.fail_json( + msg='Error: Can not configure undo mode l2.') + + def config_delete_source_ip(self, nve_name, source_ip): + """nve source ip""" + + if not self.is_nve_source_ip_exist(nve_name, source_ip): + return + ipaddr = "0.0.0.0" + cfg_xml = CE_NC_MERGE_NVE_SOURCE_IP_PROTOCOL % ( + nve_name, ipaddr) + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "DELETE_SOURCE_IP") + self.updates_cmd.append("interface %s" % nve_name) + self.updates_cmd.append("undo source %s" % source_ip) + self.changed = True + + def config_delete_vni_peer_ip(self, nve_name, vni_id, peer_ip_list): + """remove vni peer ip""" + + for peer_ip in peer_ip_list: + if not self.is_vni_peer_list_exist(nve_name, vni_id, peer_ip): + self.module.fail_json(msg='Error: The %s does not exist' % peer_ip) + + config = False + + nve_peer_info = list() + for nve_peer in self.nve_info["vni_peer_ips"]: + if nve_peer["vniId"] == vni_id: + nve_peer_info = nve_peer.get("peerAddr") + for peer in nve_peer_info: + if peer not in peer_ip_list: + config = True + + if not config: + cfg_xml = CE_NC_DELETE_VNI_PEER_ADDRESS_IP_HEAD % ( + nve_name, vni_id) + for peer_ip in peer_ip_list: + cfg_xml += CE_NC_DELETE_VNI_PEER_ADDRESS_IP_DELETE % peer_ip + cfg_xml += CE_NC_DELETE_VNI_PEER_ADDRESS_IP_END + else: + cfg_xml = CE_NC_DELETE_PEER_ADDRESS_IP_HEAD % ( + nve_name, vni_id) + for peer_ip in peer_ip_list: + cfg_xml += CE_NC_DELETE_VNI_PEER_ADDRESS_IP_DELETE % peer_ip + cfg_xml += CE_NC_DELETE_PEER_ADDRESS_IP_END + + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "DELETE_VNI_PEER_IP") + self.updates_cmd.append("interface %s" % nve_name) + + for peer_ip in peer_ip_list: + cmd_output = "undo vni %s head-end peer-list %s" % (vni_id, peer_ip) + self.updates_cmd.append(cmd_output) + + self.changed = True + + def config_delete_vni_protocol_type(self, nve_name, vni_id, protocol_type): + """remove vni protocol type""" + + if not self.is_vni_protocol_exist(nve_name, vni_id, protocol_type): + return + + cfg_xml = CE_NC_DELETE_VNI_PROTOCOL % (nve_name, vni_id, protocol_type) + recv_xml = set_nc_config(self.module, cfg_xml) + self.check_response(recv_xml, "DELETE_VNI_PEER_PROTOCOL") + self.updates_cmd.append("interface %s" % nve_name) + self.updates_cmd.append( + "undo vni %s head-end peer-list protocol bgp " % vni_id) + self.changed = True + + def check_params(self): + """Check all input params""" + + # bridge_domain_id check + if self.bridge_domain_id: + if not self.bridge_domain_id.isdigit(): + self.module.fail_json( + msg='Error: The parameter of bridge domain id is invalid.') + if int(self.bridge_domain_id) > 16777215 or int(self.bridge_domain_id) < 1: + self.module.fail_json( + msg='Error: The bridge domain id must be an integer between 1 and 16777215.') + # vni_id check + if self.vni_id: + if not self.vni_id.isdigit(): + self.module.fail_json( + msg='Error: The parameter of vni id is invalid.') + if int(self.vni_id) > 16000000 or int(self.vni_id) < 1: + self.module.fail_json( + msg='Error: The vni id must be an integer between 1 and 16000000.') + + # nve_name check + if self.nve_name: + if not self.check_nve_name(): + self.module.fail_json( + msg='Error: Error: NVE interface %s is invalid.' % self.nve_name) + + # peer_list_ip check + if self.peer_list_ip: + for peer_ip in self.peer_list_ip: + if not is_valid_address(peer_ip): + self.module.fail_json( + msg='Error: The ip address %s is invalid.' % self.peer_list_ip) + # source_ip check + if self.source_ip: + if not is_valid_address(self.source_ip): + self.module.fail_json( + msg='Error: The ip address %s is invalid.' % self.source_ip) + + def get_proposed(self): + """get proposed info""" + + if self.bridge_domain_id: + self.proposed["bridge_domain_id"] = self.bridge_domain_id + if self.vni_id: + self.proposed["vni_id"] = self.vni_id + if self.nve_name: + self.proposed["nve_name"] = self.nve_name + if self.nve_mode: + self.proposed["nve_mode"] = self.nve_mode + if self.peer_list_ip: + self.proposed["peer_list_ip"] = self.peer_list_ip + if self.source_ip: + self.proposed["source_ip"] = self.source_ip + if self.state: + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if self.vni2bd_info: + self.existing["vni_to_bridge_domain"] = self.vni2bd_info[ + "vni2BdInfos"] + + if self.nve_info: + self.existing["nve_interface_name"] = self.nve_info["ifName"] + self.existing["source_ip"] = self.nve_info["srcAddr"] + self.existing["nve_mode"] = self.nve_info["nveType"] + self.existing["vni_peer_list_ip"] = self.nve_info[ + "vni_peer_ips"] + self.existing["vni_peer_list_protocol"] = self.nve_info[ + "vni_peer_protocols"] + + def get_end_state(self): + """get end state info""" + + vni2bd_info = self.get_vni2bd_dict() + if vni2bd_info: + self.end_state["vni_to_bridge_domain"] = vni2bd_info["vni2BdInfos"] + + nve_info = self.get_nve_dict(self.nve_name) + if nve_info: + self.end_state["nve_interface_name"] = nve_info["ifName"] + self.end_state["source_ip"] = nve_info["srcAddr"] + self.end_state["nve_mode"] = nve_info["nveType"] + self.end_state["vni_peer_list_ip"] = nve_info[ + "vni_peer_ips"] + self.end_state["vni_peer_list_protocol"] = nve_info[ + "vni_peer_protocols"] + + def work(self): + """worker""" + + self.check_params() + self.vni2bd_info = self.get_vni2bd_dict() + if self.nve_name: + self.nve_info = self.get_nve_dict(self.nve_name) + self.get_existing() + self.get_proposed() + # deal present or absent + if self.state == "present": + if self.bridge_domain_id and self.vni_id: + self.config_merge_vni2bd(self.bridge_domain_id, self.vni_id) + if self.nve_name: + if self.check_nve_interface(self.nve_name): + if self.nve_mode: + self.config_merge_mode(self.nve_name, self.nve_mode) + if self.source_ip: + self.config_merge_source_ip( + self.nve_name, self.source_ip) + if self.vni_id and self.peer_list_ip: + self.config_merge_vni_peer_ip( + self.nve_name, self.vni_id, self.peer_list_ip) + if self.vni_id and self.protocol_type: + self.config_merge_vni_protocol_type( + self.nve_name, self.vni_id, self.protocol_type) + else: + self.module.fail_json( + msg='Error: Nve interface %s does not exist.' % self.nve_name) + + else: + if self.bridge_domain_id and self.vni_id: + self.config_delete_vni2bd(self.bridge_domain_id, self.vni_id) + if self.nve_name: + if self.check_nve_interface(self.nve_name): + if self.nve_mode: + self.config_delete_mode(self.nve_name, self.nve_mode) + if self.source_ip: + self.config_delete_source_ip( + self.nve_name, self.source_ip) + if self.vni_id and self.peer_list_ip: + self.config_delete_vni_peer_ip( + self.nve_name, self.vni_id, self.peer_list_ip) + if self.vni_id and self.protocol_type: + self.config_delete_vni_protocol_type( + self.nve_name, self.vni_id, self.protocol_type) + else: + self.module.fail_json( + msg='Error: Nve interface %s does not exist.' % self.nve_name) + + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + bridge_domain_id=dict(required=False), + vni_id=dict(required=False, type='str'), + nve_name=dict(required=False, type='str'), + nve_mode=dict(required=False, choices=['mode-l2', 'mode-l3']), + peer_list_ip=dict(required=False, type='list'), + protocol_type=dict(required=False, type='str', choices=[ + 'bgp', 'null']), + + source_ip=dict(required=False), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = VxlanTunnel(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ce_vxlan_vap.py b/plugins/modules/ce_vxlan_vap.py new file mode 100644 index 0000000..58ae83b --- /dev/null +++ b/plugins/modules/ce_vxlan_vap.py @@ -0,0 +1,934 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ce_vxlan_vap +short_description: Manages VXLAN virtual access point on HUAWEI CloudEngine Devices. +description: + - Manages VXLAN Virtual access point on HUAWEI CloudEngine Devices. +author: QijunPan (@QijunPan) +notes: + - This module requires the netconf system service be enabled on the remote device being managed. + - Recommended connection is C(netconf). + - This module also works with C(local) connections for legacy playbooks. +options: + bridge_domain_id: + description: + - Specifies a bridge domain ID. + The value is an integer ranging from 1 to 16777215. + bind_vlan_id: + description: + - Specifies the VLAN binding to a BD(Bridge Domain). + The value is an integer ranging ranging from 1 to 4094. + l2_sub_interface: + description: + - Specifies an Sub-Interface full name, i.e. "10GE1/0/41.1". + The value is a string of 1 to 63 case-insensitive characters, spaces supported. + encapsulation: + description: + - Specifies an encapsulation type of packets allowed to pass through a Layer 2 sub-interface. + choices: ['dot1q', 'default', 'untag', 'qinq', 'none'] + ce_vid: + description: + - When I(encapsulation) is 'dot1q', specifies a VLAN ID in the outer VLAN tag. + When I(encapsulation) is 'qinq', specifies an outer VLAN ID for + double-tagged packets to be received by a Layer 2 sub-interface. + The value is an integer ranging from 1 to 4094. + pe_vid: + description: + - When I(encapsulation) is 'qinq', specifies an inner VLAN ID for + double-tagged packets to be received by a Layer 2 sub-interface. + The value is an integer ranging from 1 to 4094. + state: + description: + - Determines whether the config should be present or not + on the device. + default: present + choices: ['present', 'absent'] +''' + +EXAMPLES = ''' +- name: vxlan vap module test + hosts: ce128 + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + + - name: Create a mapping between a VLAN and a BD + ce_vxlan_vap: + bridge_domain_id: 100 + bind_vlan_id: 99 + provider: "{{ cli }}" + + - name: Bind a Layer 2 sub-interface to a BD + ce_vxlan_vap: + bridge_domain_id: 100 + l2_sub_interface: 10GE2/0/20.1 + provider: "{{ cli }}" + + - name: Configure an encapsulation type on a Layer 2 sub-interface + ce_vxlan_vap: + l2_sub_interface: 10GE2/0/20.1 + encapsulation: dot1q + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: verbose mode + type: dict + sample: {"bridge_domain_id": "100", "bind_vlan_id": "99", state="present"} +existing: + description: k/v pairs of existing configuration + returned: verbose mode + type: dict + sample: {"bridge_domain_id": "100", "bind_intf_list": ["10GE2/0/20.1", "10GE2/0/20.2"], + "bind_vlan_list": []} +end_state: + description: k/v pairs of configuration after module execution + returned: verbose mode + type: dict + sample: {"bridge_domain_id": "100", "bind_intf_list": ["110GE2/0/20.1", "10GE2/0/20.2"], + "bind_vlan_list": ["99"]} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["bridge-domain 100", + "l2 binding vlan 99"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +''' + +from xml.etree import ElementTree +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.huawei.cloudengine.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec + +CE_NC_GET_BD_VAP = """ + + + + + %s + + + + + + + + + + + + +""" + +CE_NC_MERGE_BD_VLAN = """ + + + + + %s + + %s:%s + + + + + +""" + +CE_NC_MERGE_BD_INTF = """ + + + + + %s + + + %s + + + + + + +""" + +CE_NC_DELETE_BD_INTF = """ + + + + + %s + + + %s + + + + + + +""" + +CE_NC_GET_ENCAP = """ + + + + + %s + + + + + + + + + + + + + + +""" + +CE_NC_SET_ENCAP = """ + + + + + %s + %s + + + + +""" + +CE_NC_UNSET_ENCAP = """ + + + + + %s + none + + + + +""" + +CE_NC_SET_ENCAP_DOT1Q = """ + + + + + %s + dot1q + + %s:%s + + + + + +""" + +CE_NC_SET_ENCAP_QINQ = """ + + + + + %s + qinq + + + %s + %s:%s + + + + + + +""" + + +def vlan_vid_to_bitmap(vid): + """convert VLAN list to VLAN bitmap""" + + vlan_bit = ['0'] * 1024 + int_vid = int(vid) + j = int_vid // 4 + bit_int = 0x8 >> (int_vid % 4) + vlan_bit[j] = str(hex(bit_int))[2] + + return ''.join(vlan_bit) + + +def bitmap_to_vlan_list(bitmap): + """convert VLAN bitmap to VLAN list""" + + tmp = list() + if not bitmap: + return tmp + + bit_len = len(bitmap) + for i in range(bit_len): + if bitmap[i] == "0": + continue + bit = int(bitmap[i]) + if bit & 0x8: + tmp.append(str(i * 4)) + if bit & 0x4: + tmp.append(str(i * 4 + 1)) + if bit & 0x2: + tmp.append(str(i * 4 + 2)) + if bit & 0x1: + tmp.append(str(i * 4 + 3)) + + return tmp + + +def is_vlan_bitmap_empty(bitmap): + """check VLAN bitmap empty""" + + if not bitmap or len(bitmap) == 0: + return True + + for bit in bitmap: + if bit != '0': + return False + + return True + + +def is_vlan_in_bitmap(vid, bitmap): + """check is VLAN id in bitmap""" + + if is_vlan_bitmap_empty(bitmap): + return False + + i = int(vid) // 4 + if i > len(bitmap): + return False + + if int(bitmap[i]) & (0x8 >> (int(vid) % 4)): + return True + + return False + + +def get_interface_type(interface): + """Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF...""" + + if interface is None: + return None + + iftype = None + + if interface.upper().startswith('GE'): + iftype = 'ge' + elif interface.upper().startswith('10GE'): + iftype = '10ge' + elif interface.upper().startswith('25GE'): + iftype = '25ge' + elif interface.upper().startswith('4X10GE'): + iftype = '4x10ge' + elif interface.upper().startswith('40GE'): + iftype = '40ge' + elif interface.upper().startswith('100GE'): + iftype = '100ge' + elif interface.upper().startswith('VLANIF'): + iftype = 'vlanif' + elif interface.upper().startswith('LOOPBACK'): + iftype = 'loopback' + elif interface.upper().startswith('METH'): + iftype = 'meth' + elif interface.upper().startswith('ETH-TRUNK'): + iftype = 'eth-trunk' + elif interface.upper().startswith('VBDIF'): + iftype = 'vbdif' + elif interface.upper().startswith('NVE'): + iftype = 'nve' + elif interface.upper().startswith('TUNNEL'): + iftype = 'tunnel' + elif interface.upper().startswith('ETHERNET'): + iftype = 'ethernet' + elif interface.upper().startswith('FCOE-PORT'): + iftype = 'fcoe-port' + elif interface.upper().startswith('FABRIC-PORT'): + iftype = 'fabric-port' + elif interface.upper().startswith('STACK-PORT'): + iftype = 'stack-Port' + elif interface.upper().startswith('NULL'): + iftype = 'null' + else: + return None + + return iftype.lower() + + +class VxlanVap(object): + """ + Manages VXLAN virtual access point. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.__init_module__() + + # module input info + self.bridge_domain_id = self.module.params['bridge_domain_id'] + self.bind_vlan_id = self.module.params['bind_vlan_id'] + self.l2_sub_interface = self.module.params['l2_sub_interface'] + self.ce_vid = self.module.params['ce_vid'] + self.pe_vid = self.module.params['pe_vid'] + self.encapsulation = self.module.params['encapsulation'] + self.state = self.module.params['state'] + + # state + self.vap_info = dict() + self.l2sub_info = dict() + self.changed = False + self.updates_cmd = list() + self.commands = list() + self.results = dict() + self.proposed = dict() + self.existing = dict() + self.end_state = dict() + + def __init_module__(self): + """init module""" + + required_together = [()] + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_bd_vap_dict(self): + """get virtual access point info""" + + vap_info = dict() + conf_str = CE_NC_GET_BD_VAP % self.bridge_domain_id + xml_str = get_nc_config(self.module, conf_str) + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get vap: VLAN + vap_info["bdId"] = self.bridge_domain_id + root = ElementTree.fromstring(xml_str) + vap_info["vlanList"] = "" + vap_vlan = root.find("evc/bds/bd/bdBindVlan") + if vap_vlan: + for ele in vap_vlan: + if ele.tag == "vlanList": + vap_info["vlanList"] = ele.text + + # get vap: l2 su-interface + vap_ifs = root.findall( + "evc/bds/bd/servicePoints/servicePoint/ifName") + if_list = list() + if vap_ifs: + for vap_if in vap_ifs: + if vap_if.tag == "ifName": + if_list.append(vap_if.text) + vap_info["intfList"] = if_list + + return vap_info + + def get_l2_sub_intf_dict(self, ifname): + """get l2 sub-interface info""" + + intf_info = dict() + if not ifname: + return intf_info + + conf_str = CE_NC_GET_ENCAP % ifname + xml_str = get_nc_config(self.module, conf_str) + + if "" in xml_str: + return intf_info + + xml_str = xml_str.replace('\r', '').replace('\n', '').\ + replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ + replace('xmlns="http://www.huawei.com/netconf/vrp"', "") + + # get l2 sub interface encapsulation info + root = ElementTree.fromstring(xml_str) + bds = root.find("ethernet/servicePoints/servicePoint") + if not bds: + return intf_info + + for ele in bds: + if ele.tag in ["ifName", "flowType"]: + intf_info[ele.tag] = ele.text.lower() + + if intf_info.get("flowType") == "dot1q": + ce_vid = root.find( + "ethernet/servicePoints/servicePoint/flowDot1qs") + intf_info["dot1qVids"] = "" + if ce_vid: + for ele in ce_vid: + if ele.tag == "dot1qVids": + intf_info["dot1qVids"] = ele.text + elif intf_info.get("flowType") == "qinq": + vids = root.find( + "ethernet/servicePoints/servicePoint/flowQinqs/flowQinq") + if vids: + for ele in vids: + if ele.tag in ["peVlanId", "ceVids"]: + intf_info[ele.tag] = ele.text + + return intf_info + + def config_traffic_encap_dot1q(self): + """configure traffic encapsulation type dot1q""" + + xml_str = "" + self.updates_cmd.append("interface %s" % self.l2_sub_interface) + if self.state == "present": + if self.encapsulation != self.l2sub_info.get("flowType"): + if self.ce_vid: + vlan_bitmap = vlan_vid_to_bitmap(self.ce_vid) + xml_str = CE_NC_SET_ENCAP_DOT1Q % ( + self.l2_sub_interface, vlan_bitmap, vlan_bitmap) + self.updates_cmd.append("encapsulation %s vid %s" % ( + self.encapsulation, self.ce_vid)) + else: + xml_str = CE_NC_SET_ENCAP % ( + self.l2_sub_interface, self.encapsulation) + self.updates_cmd.append( + "encapsulation %s" % self.encapsulation) + else: + if self.ce_vid and not is_vlan_in_bitmap( + self.ce_vid, self.l2sub_info.get("dot1qVids")): + vlan_bitmap = vlan_vid_to_bitmap(self.ce_vid) + xml_str = CE_NC_SET_ENCAP_DOT1Q % ( + self.l2_sub_interface, vlan_bitmap, vlan_bitmap) + self.updates_cmd.append("encapsulation %s vid %s" % ( + self.encapsulation, self.ce_vid)) + else: + if self.encapsulation == self.l2sub_info.get("flowType"): + if self.ce_vid: + if is_vlan_in_bitmap(self.ce_vid, self.l2sub_info.get("dot1qVids")): + xml_str = CE_NC_UNSET_ENCAP % self.l2_sub_interface + self.updates_cmd.append("undo encapsulation %s vid %s" % ( + self.encapsulation, self.ce_vid)) + else: + xml_str = CE_NC_UNSET_ENCAP % self.l2_sub_interface + self.updates_cmd.append( + "undo encapsulation %s" % self.encapsulation) + + if not xml_str: + self.updates_cmd.pop() + return + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "CONFIG_INTF_ENCAP_DOT1Q") + self.changed = True + + def config_traffic_encap_qinq(self): + """configure traffic encapsulation type qinq""" + + xml_str = "" + self.updates_cmd.append("interface %s" % self.l2_sub_interface) + if self.state == "present": + if self.encapsulation != self.l2sub_info.get("flowType"): + if self.ce_vid: + vlan_bitmap = vlan_vid_to_bitmap(self.ce_vid) + xml_str = CE_NC_SET_ENCAP_QINQ % (self.l2_sub_interface, + self.pe_vid, + vlan_bitmap, + vlan_bitmap) + self.updates_cmd.append( + "encapsulation %s vid %s ce-vid %s" % (self.encapsulation, + self.pe_vid, + self.ce_vid)) + else: + xml_str = CE_NC_SET_ENCAP % ( + self.l2_sub_interface, self.encapsulation) + self.updates_cmd.append( + "encapsulation %s" % self.encapsulation) + else: + if self.ce_vid: + if not is_vlan_in_bitmap(self.ce_vid, self.l2sub_info.get("ceVids")) \ + or self.pe_vid != self.l2sub_info.get("peVlanId"): + vlan_bitmap = vlan_vid_to_bitmap(self.ce_vid) + xml_str = CE_NC_SET_ENCAP_QINQ % (self.l2_sub_interface, + self.pe_vid, + vlan_bitmap, + vlan_bitmap) + self.updates_cmd.append( + "encapsulation %s vid %s ce-vid %s" % (self.encapsulation, + self.pe_vid, + self.ce_vid)) + else: + if self.encapsulation == self.l2sub_info.get("flowType"): + if self.ce_vid: + if is_vlan_in_bitmap(self.ce_vid, self.l2sub_info.get("ceVids")) \ + and self.pe_vid == self.l2sub_info.get("peVlanId"): + xml_str = CE_NC_UNSET_ENCAP % self.l2_sub_interface + self.updates_cmd.append( + "undo encapsulation %s vid %s ce-vid %s" % (self.encapsulation, + self.pe_vid, + self.ce_vid)) + else: + xml_str = CE_NC_UNSET_ENCAP % self.l2_sub_interface + self.updates_cmd.append( + "undo encapsulation %s" % self.encapsulation) + + if not xml_str: + self.updates_cmd.pop() + return + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "CONFIG_INTF_ENCAP_QINQ") + self.changed = True + + def config_traffic_encap(self): + """configure traffic encapsulation types""" + + if not self.l2sub_info: + self.module.fail_json(msg="Error: Interface %s does not exist." % self.l2_sub_interface) + + if not self.encapsulation: + return + + xml_str = "" + if self.encapsulation in ["default", "untag"]: + if self.state == "present": + if self.encapsulation != self.l2sub_info.get("flowType"): + xml_str = CE_NC_SET_ENCAP % ( + self.l2_sub_interface, self.encapsulation) + self.updates_cmd.append( + "interface %s" % self.l2_sub_interface) + self.updates_cmd.append( + "encapsulation %s" % self.encapsulation) + else: + if self.encapsulation == self.l2sub_info.get("flowType"): + xml_str = CE_NC_UNSET_ENCAP % self.l2_sub_interface + self.updates_cmd.append( + "interface %s" % self.l2_sub_interface) + self.updates_cmd.append( + "undo encapsulation %s" % self.encapsulation) + elif self.encapsulation == "none": + if self.state == "present": + if self.encapsulation != self.l2sub_info.get("flowType"): + xml_str = CE_NC_UNSET_ENCAP % self.l2_sub_interface + self.updates_cmd.append( + "interface %s" % self.l2_sub_interface) + self.updates_cmd.append( + "undo encapsulation %s" % self.l2sub_info.get("flowType")) + elif self.encapsulation == "dot1q": + self.config_traffic_encap_dot1q() + return + elif self.encapsulation == "qinq": + self.config_traffic_encap_qinq() + return + else: + pass + + if not xml_str: + return + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "CONFIG_INTF_ENCAP") + self.changed = True + + def config_vap_sub_intf(self): + """configure a Layer 2 sub-interface as a service access point""" + + if not self.vap_info: + self.module.fail_json(msg="Error: Bridge domain %s does not exist." % self.bridge_domain_id) + + xml_str = "" + if self.state == "present": + if self.l2_sub_interface not in self.vap_info["intfList"]: + self.updates_cmd.append("interface %s" % self.l2_sub_interface) + self.updates_cmd.append("bridge-domain %s" % + self.bridge_domain_id) + xml_str = CE_NC_MERGE_BD_INTF % ( + self.bridge_domain_id, self.l2_sub_interface) + else: + if self.l2_sub_interface in self.vap_info["intfList"]: + self.updates_cmd.append("interface %s" % self.l2_sub_interface) + self.updates_cmd.append( + "undo bridge-domain %s" % self.bridge_domain_id) + xml_str = CE_NC_DELETE_BD_INTF % ( + self.bridge_domain_id, self.l2_sub_interface) + + if not xml_str: + return + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "CONFIG_VAP_SUB_INTERFACE") + self.changed = True + + def config_vap_vlan(self): + """configure a VLAN as a service access point""" + + xml_str = "" + if self.state == "present": + if not is_vlan_in_bitmap(self.bind_vlan_id, self.vap_info["vlanList"]): + self.updates_cmd.append("bridge-domain %s" % + self.bridge_domain_id) + self.updates_cmd.append( + "l2 binding vlan %s" % self.bind_vlan_id) + vlan_bitmap = vlan_vid_to_bitmap(self.bind_vlan_id) + xml_str = CE_NC_MERGE_BD_VLAN % ( + self.bridge_domain_id, vlan_bitmap, vlan_bitmap) + else: + if is_vlan_in_bitmap(self.bind_vlan_id, self.vap_info["vlanList"]): + self.updates_cmd.append("bridge-domain %s" % + self.bridge_domain_id) + self.updates_cmd.append( + "undo l2 binding vlan %s" % self.bind_vlan_id) + vlan_bitmap = vlan_vid_to_bitmap(self.bind_vlan_id) + xml_str = CE_NC_MERGE_BD_VLAN % ( + self.bridge_domain_id, "0" * 1024, vlan_bitmap) + + if not xml_str: + return + recv_xml = set_nc_config(self.module, xml_str) + self.check_response(recv_xml, "CONFIG_VAP_VLAN") + self.changed = True + + def is_vlan_valid(self, vid, name): + """check VLAN id""" + + if not vid: + return + + if not vid.isdigit(): + self.module.fail_json(msg="Error: %s is not digit." % name) + return + + if int(vid) < 1 or int(vid) > 4094: + self.module.fail_json( + msg="Error: %s is not in the range from 1 to 4094." % name) + + def is_l2_sub_intf_valid(self, ifname): + """check l2 sub interface valid""" + + if ifname.count('.') != 1: + return False + + if_num = ifname.split('.')[1] + if not if_num.isdigit(): + return False + + if int(if_num) < 1 or int(if_num) > 4096: + self.module.fail_json( + msg="Error: Sub-interface number is not in the range from 1 to 4096.") + return False + + if not get_interface_type(ifname): + return False + + return True + + def check_params(self): + """Check all input params""" + + # bridge domain id check + if self.bridge_domain_id: + if not self.bridge_domain_id.isdigit(): + self.module.fail_json( + msg="Error: Bridge domain id is not digit.") + if int(self.bridge_domain_id) < 1 or int(self.bridge_domain_id) > 16777215: + self.module.fail_json( + msg="Error: Bridge domain id is not in the range from 1 to 16777215.") + + # check bind_vlan_id + if self.bind_vlan_id: + self.is_vlan_valid(self.bind_vlan_id, "bind_vlan_id") + + # check l2_sub_interface + if self.l2_sub_interface and not self.is_l2_sub_intf_valid(self.l2_sub_interface): + self.module.fail_json(msg="Error: l2_sub_interface is invalid.") + + # check ce_vid + if self.ce_vid: + self.is_vlan_valid(self.ce_vid, "ce_vid") + if not self.encapsulation or self.encapsulation not in ["dot1q", "qinq"]: + self.module.fail_json(msg="Error: ce_vid can not be set " + "when encapsulation is '%s'." % self.encapsulation) + if self.encapsulation == "qinq" and not self.pe_vid: + self.module.fail_json(msg="Error: ce_vid and pe_vid must be set at the same time " + "when encapsulation is '%s'." % self.encapsulation) + # check pe_vid + if self.pe_vid: + self.is_vlan_valid(self.pe_vid, "pe_vid") + if not self.encapsulation or self.encapsulation != "qinq": + self.module.fail_json(msg="Error: pe_vid can not be set " + "when encapsulation is '%s'." % self.encapsulation) + if not self.ce_vid: + self.module.fail_json(msg="Error: ce_vid and pe_vid must be set at the same time " + "when encapsulation is '%s'." % self.encapsulation) + + def get_proposed(self): + """get proposed info""" + + if self.bridge_domain_id: + self.proposed["bridge_domain_id"] = self.bridge_domain_id + if self.bind_vlan_id: + self.proposed["bind_vlan_id"] = self.bind_vlan_id + if self.l2_sub_interface: + self.proposed["l2_sub_interface"] = self.l2_sub_interface + if self.encapsulation: + self.proposed["encapsulation"] = self.encapsulation + if self.ce_vid: + self.proposed["ce_vid"] = self.ce_vid + if self.pe_vid: + self.proposed["pe_vid"] = self.pe_vid + self.proposed["state"] = self.state + + def get_existing(self): + """get existing info""" + + if self.bridge_domain_id: + if self.bind_vlan_id or self.l2_sub_interface: + self.existing["bridge_domain_id"] = self.bridge_domain_id + self.existing["bind_vlan_list"] = bitmap_to_vlan_list( + self.vap_info.get("vlanList")) + self.existing["bind_intf_list"] = self.vap_info.get("intfList") + + if self.encapsulation and self.l2_sub_interface: + self.existing["l2_sub_interface"] = self.l2_sub_interface + self.existing["encapsulation"] = self.l2sub_info.get("flowType") + if self.existing["encapsulation"] == "dot1q": + self.existing["ce_vid"] = bitmap_to_vlan_list( + self.l2sub_info.get("dot1qVids")) + if self.existing["encapsulation"] == "qinq": + self.existing["ce_vid"] = bitmap_to_vlan_list( + self.l2sub_info.get("ceVids")) + self.existing["pe_vid"] = self.l2sub_info.get("peVlanId") + + def get_end_state(self): + """get end state info""" + + if self.bridge_domain_id: + if self.bind_vlan_id or self.l2_sub_interface: + vap_info = self.get_bd_vap_dict() + self.end_state["bridge_domain_id"] = self.bridge_domain_id + self.end_state["bind_vlan_list"] = bitmap_to_vlan_list( + vap_info.get("vlanList")) + self.end_state["bind_intf_list"] = vap_info.get("intfList") + + if self.encapsulation and self.l2_sub_interface: + l2sub_info = self.get_l2_sub_intf_dict(self.l2_sub_interface) + self.end_state["l2_sub_interface"] = self.l2_sub_interface + self.end_state["encapsulation"] = l2sub_info.get("flowType") + if self.end_state["encapsulation"] == "dot1q": + self.end_state["ce_vid"] = bitmap_to_vlan_list( + l2sub_info.get("dot1qVids")) + if self.end_state["encapsulation"] == "qinq": + self.end_state["ce_vid"] = bitmap_to_vlan_list( + l2sub_info.get("ceVids")) + self.end_state["pe_vid"] = l2sub_info.get("peVlanId") + + def data_init(self): + """data init""" + if self.l2_sub_interface: + self.l2_sub_interface = self.l2_sub_interface.replace( + " ", "").upper() + if self.encapsulation and self.l2_sub_interface: + self.l2sub_info = self.get_l2_sub_intf_dict(self.l2_sub_interface) + if self.bridge_domain_id: + if self.bind_vlan_id or self.l2_sub_interface: + self.vap_info = self.get_bd_vap_dict() + + def work(self): + """worker""" + + self.check_params() + self.data_init() + self.get_existing() + self.get_proposed() + + # Traffic encapsulation types + if self.encapsulation and self.l2_sub_interface: + self.config_traffic_encap() + + # A VXLAN service access point can be a Layer 2 sub-interface or VLAN + if self.bridge_domain_id: + if self.l2_sub_interface: + # configure a Layer 2 sub-interface as a service access point + self.config_vap_sub_intf() + + if self.bind_vlan_id: + # configure a VLAN as a service access point + self.config_vap_vlan() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + bridge_domain_id=dict(required=False, type='str'), + bind_vlan_id=dict(required=False, type='str'), + l2_sub_interface=dict(required=False, type='str'), + encapsulation=dict(required=False, type='str', + choices=['dot1q', 'default', 'untag', 'qinq', 'none']), + ce_vid=dict(required=False, type='str'), + pe_vid=dict(required=False, type='str'), + state=dict(required=False, default='present', + choices=['present', 'absent']) + ) + argument_spec.update(ce_argument_spec) + module = VxlanVap(argument_spec) + module.work() + + +if __name__ == '__main__': + main() diff --git a/plugins/netconf/__init__.py b/plugins/netconf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/netconf/ce.py b/plugins/netconf/ce.py new file mode 100644 index 0000000..1431768 --- /dev/null +++ b/plugins/netconf/ce.py @@ -0,0 +1,247 @@ +# +# (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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' +--- +netconf: ce +short_description: Use ce netconf plugin to run netconf commands on Huawei Cloudengine platform +description: + - This ce plugin provides low level abstraction apis for + sending and receiving netconf commands from Huawei Cloudengine network devices. +options: + ncclient_device_handler: + type: str + default: huawei + description: + - Specifies the ncclient device handler name for Huawei Cloudengine. + To identify the ncclient device handler name refer ncclient library documentation. +''' + +import json +import re + +from ansible.module_utils._text import to_text, to_bytes +from ansible.errors import AnsibleConnectionFailure +from ansible.plugins.netconf import NetconfBase, ensure_ncclient + +try: + from ncclient import manager + from ncclient.operations import RPCError + from ncclient.transport.errors import SSHUnknownHostError + from ncclient.xml_ import to_ele, to_xml, new_ele + HAS_NCCLIENT = True +except (ImportError, AttributeError): # paramiko and gssapi are incompatible and raise AttributeError not ImportError + HAS_NCCLIENT = False + +try: + from lxml.etree import fromstring +except ImportError: + from xml.etree.ElementTree import fromstring + + +class Netconf(NetconfBase): + + @ensure_ncclient + def get_text(self, ele, tag): + try: + return to_text(ele.find(tag).text, errors='surrogate_then_replace').strip() + except AttributeError: + pass + + @ensure_ncclient + def get_device_info(self): + device_info = dict() + device_info['network_os'] = 'ce' + filter_xml = ''' + + + + + + + + + + ''' + data = self.get(filter_xml) + data = re.sub(r'xmlns=".+?"', r'', data) + reply = fromstring(to_bytes(data, errors='surrogate_or_strict')) + sw_info = reply.find('.//systemInfo') + + device_info['network_os_version'] = self.get_text(sw_info, 'productVer') + device_info['network_os_hostname'] = self.get_text(sw_info, 'sysName') + device_info['network_os_platform_version'] = self.get_text(sw_info, 'platformVer') + device_info['network_os_platform'] = self.get_text(sw_info, 'productName') + + return device_info + + def execute_rpc(self, name): + """RPC to be execute on remote device + :name: Name of rpc in string format""" + return self.rpc(name) + + @ensure_ncclient + def load_configuration(self, *args, **kwargs): + """Loads given configuration on device + :format: Format of configuration (xml, text, set) + :action: Action to be performed (merge, replace, override, update) + :target: is the name of the configuration datastore being edited + :config: is the configuration in string format.""" + if kwargs.get('config'): + kwargs['config'] = to_bytes(kwargs['config'], errors='surrogate_or_strict') + if kwargs.get('format', 'xml') == 'xml': + kwargs['config'] = to_ele(kwargs['config']) + + try: + return self.m.load_configuration(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + def get_capabilities(self): + result = dict() + result['rpc'] = self.get_base_rpc() + ['execute_rpc', 'load_configuration', 'get_configuration', 'compare_configuration', + 'execute_action', 'halt', 'reboot', 'execute_nc_cli', 'dispatch_rpc'] + result['network_api'] = 'netconf' + result['device_info'] = self.get_device_info() + result['server_capabilities'] = [c for c in self.m.server_capabilities] + result['client_capabilities'] = [c for c in self.m.client_capabilities] + result['session_id'] = self.m.session_id + return json.dumps(result) + + @staticmethod + @ensure_ncclient + def guess_network_os(obj): + try: + m = manager.connect( + host=obj._play_context.remote_addr, + port=obj._play_context.port or 830, + username=obj._play_context.remote_user, + password=obj._play_context.password, + key_filename=obj.key_filename, + hostkey_verify=obj.get_option('host_key_checking'), + look_for_keys=obj.get_option('look_for_keys'), + allow_agent=obj._play_context.allow_agent, + timeout=obj.get_option('persistent_connect_timeout'), + # We need to pass in the path to the ssh_config file when guessing + # the network_os so that a jumphost is correctly used if defined + ssh_config=obj._ssh_config + ) + except SSHUnknownHostError as exc: + raise AnsibleConnectionFailure(to_text(exc)) + + guessed_os = None + for c in m.server_capabilities: + if re.search('huawei', c): + guessed_os = 'ce' + break + + m.close_session() + return guessed_os + + def get_configuration(self, *args, **kwargs): + """Retrieve all or part of a specified configuration. + :format: format in configuration should be retrieved + :filter: specifies the portion of the configuration to retrieve + (by default entire configuration is retrieved)""" + return self.m.get_configuration(*args, **kwargs).data_xml + + def compare_configuration(self, *args, **kwargs): + """Compare configuration + :rollback: rollback id""" + return self.m.compare_configuration(*args, **kwargs).data_xml + + @ensure_ncclient + def execute_action(self, xml_str): + """huawei execute-action""" + con_obj = None + try: + con_obj = self.m.action(action=xml_str) + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + return con_obj.xml + + def halt(self): + """reboot the device""" + return self.m.halt().data_xml + + def reboot(self): + """reboot the device""" + return self.m.reboot().data_xml + + @ensure_ncclient + def get(self, *args, **kwargs): + try: + if_rpc_reply = kwargs.pop('if_rpc_reply', False) + if if_rpc_reply: + return self.m.get(*args, **kwargs).xml + return self.m.get(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_ncclient + def get_config(self, *args, **kwargs): + try: + return self.m.get_config(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_ncclient + def edit_config(self, *args, **kwargs): + try: + return self.m.edit_config(*args, **kwargs).xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_ncclient + def execute_nc_cli(self, *args, **kwargs): + try: + return self.m.cli(*args, **kwargs).xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + @ensure_ncclient + def commit(self, *args, **kwargs): + try: + return self.m.commit(*args, **kwargs).data_xml + except RPCError as exc: + raise Exception(to_xml(exc.xml)) + + def validate(self, *args, **kwargs): + return self.m.validate(*args, **kwargs).data_xml + + def discard_changes(self, *args, **kwargs): + return self.m.discard_changes(*args, **kwargs).data_xml + + @ensure_ncclient + def dispatch_rpc(self, rpc_command=None, source=None, filter=None): + """ + Execute rpc on the remote device eg. dispatch('get-next') + :param rpc_command: specifies rpc command to be dispatched either in plain text or in xml element format (depending on command) + :param source: name of the configuration datastore being queried + :param filter: specifies the portion of the configuration to retrieve (by default entire configuration is retrieved) + :return: Returns xml string containing the rpc-reply response received from remote host + """ + if rpc_command is None: + raise ValueError('rpc_command value must be provided') + resp = self.m.dispatch(fromstring(rpc_command), source=source, filter=filter) + # just return rpc-reply xml + return resp.xml diff --git a/plugins/terminal/__init__.py b/plugins/terminal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/terminal/ce.py b/plugins/terminal/ce.py new file mode 100644 index 0000000..67936e8 --- /dev/null +++ b/plugins/terminal/ce.py @@ -0,0 +1,60 @@ +# +# (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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +from ansible.plugins.terminal import TerminalBase +from ansible.errors import AnsibleConnectionFailure + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + re.compile(br'[\r\n]?<.+>(?:\s*)$'), + re.compile(br'[\r\n]?\[.+\](?:\s*)$'), + ] + #: terminal initial prompt + #: The password needs to be changed. Change now? [Y/N]: + terminal_initial_prompt = br'Change\s*now\s*\?\s*\[Y\/N\]\s*:' + + #: terminal initial answer + #: do not change password when it is asked to change with initial connection. + terminal_initial_answer = b'N' + terminal_stderr_re = [ + re.compile(br"% ?Error: "), + re.compile(br"^% \w+", re.M), + re.compile(br"% ?Bad secret"), + re.compile(br"invalid input", re.I), + re.compile(br"(?:incomplete|ambiguous) command", re.I), + re.compile(br"connection timed out", re.I), + re.compile(br"[^\r\n]+ not found", re.I), + re.compile(br"'[^']' +returned error code: ?\d+"), + re.compile(br"syntax error"), + re.compile(br"unknown command"), + re.compile(br"Error\[\d+\]: ", re.I), + re.compile(br"Error:", re.I) + ] + + def on_open_shell(self): + try: + self._exec_cli_command('screen-length 0 temporary') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to set terminal parameters') diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..ea1472e --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/tests/integration/targets/ce_is_is_instance/defaults/main.yaml b/tests/integration/targets/ce_is_is_instance/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_is_is_instance/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_is_is_instance/meta/main.yml b/tests/integration/targets/ce_is_is_instance/meta/main.yml new file mode 100644 index 0000000..6d800ee --- /dev/null +++ b/tests/integration/targets/ce_is_is_instance/meta/main.yml @@ -0,0 +1,2 @@ +null +... diff --git a/tests/integration/targets/ce_is_is_instance/tasks/main.yaml b/tests/integration/targets/ce_is_is_instance/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_is_is_instance/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_is_is_instance/tasks/netconf.yaml b/tests/integration/targets/ce_is_is_instance/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_is_is_instance/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_is_is_instance/tests/netconf/ce_is_is_instance.yaml b/tests/integration/targets/ce_is_is_instance/tests/netconf/ce_is_is_instance.yaml new file mode 100644 index 0000000..dd65b54 --- /dev/null +++ b/tests/integration/targets/ce_is_is_instance/tests/netconf/ce_is_is_instance.yaml @@ -0,0 +1,61 @@ +- debug: + msg: START ce_is_is_instance merged integration tests on connection={{ ansible_connection + }} +- block: + - name: berfore merged, there should be no isis 100. + ce_is_is_instance: &id002 + instance_id: 100 + state: absent + - name: Merge the provided configuration with the exisiting running configuration + ce_is_is_instance: &id001 + instance_id: 100 + vpn_name: __public__ + register: result + - name: change ansible_connection to network_cli + ce_netconf: + rpc: get + cfg_xml: + + register: result_xml + - name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true + - '''100'' in result_xml.end_state.result' + - '''__public__'' in result_xml.end_state.result' + - name: Merge the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_instance: *id001 + register: result + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - name: delete the provided configuration with the exisiting running configuration + ce_is_is_instance: *id002 + register: result + - name: change ansible_connection to network_cli + ce_netconf: + rpc: get + cfg_xml: + + register: result_xml + - name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true + - '''100'' not in result_xml.end_state.result' + - '''__public__'' not in result_xml.end_state.result' + - name: delete the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_instance: *id002 + register: result + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false +- debug: + msg: END ce_is_is_instance merged integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_is_is_interface/defaults/main.yaml b/tests/integration/targets/ce_is_is_interface/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_is_is_interface/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_is_is_interface/meta/main.yml b/tests/integration/targets/ce_is_is_interface/meta/main.yml new file mode 100644 index 0000000..6d800ee --- /dev/null +++ b/tests/integration/targets/ce_is_is_interface/meta/main.yml @@ -0,0 +1,2 @@ +null +... diff --git a/tests/integration/targets/ce_is_is_interface/tasks/main.yaml b/tests/integration/targets/ce_is_is_interface/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_is_is_interface/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_is_is_interface/tasks/netconf.yaml b/tests/integration/targets/ce_is_is_interface/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_is_is_interface/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_is_is_interface/tests/netconf/ce_is_is_interface.yaml b/tests/integration/targets/ce_is_is_interface/tests/netconf/ce_is_is_interface.yaml new file mode 100644 index 0000000..6e9786b --- /dev/null +++ b/tests/integration/targets/ce_is_is_interface/tests/netconf/ce_is_is_interface.yaml @@ -0,0 +1,106 @@ +- debug: + msg: START ce_is_is_interface merged integration tests on connection={{ ansible_connection + }} +- block: + - name: Merge the provided configuration with the exisiting running configuration + ce_is_is_interface: &id002 + instance_id: 100 + ifname: 10GE1/0/1 + leveltype: level_1 + level1dispriority: 10 + silentenable: true + silentcost: true + typep2penable: true + snpacheck: true + p2pnegotiationmode: 2_way + p2ppeeripignore: true + ppposicpcheckenable: true + level2cost: 10 + state: absent + register: result + - name: Merge the provided configuration with the exisiting running configuration + ce_is_is_interface: &id001 + instance_id: 100 + ifname: 10GE1/0/1 + leveltype: level_1 + level1dispriority: 10 + silentenable: true + silentcost: true + typep2penable: true + snpacheck: true + p2pnegotiationmode: 2_way + p2ppeeripignore: true + ppposicpcheckenable: true + level2cost: 10 + register: result + - name: use ce_netconf to get configuration + ce_netconf: + rpc: get + cfg_xml: 100 + + + register: result_xml + - name: Assert the configuration is reflected on host + assert: + that: + - '''10GE1/0/1'' in result_xml.end_state.result' + - '''level_1'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''2_way'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - name: Merge the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_interface: *id001 + register: result + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - name: delete the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_interface: *id002 + register: result + - name: use ce_netconf to get configuration + ce_netconf: + rpc: get + cfg_xml: 100 + + + register: result_xml + - name: Assert the configuration is reflected on host + assert: + that: + - '''10GE1/0/1'' not in result_xml.end_state.result' + - '''level_1'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''2_way'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - name: delete the provided configuration with the existing running configuration + (REPEAT) + ce_is_is_interface: *id002 + register: result + - name: Assert that the previous task was REPEAT + assert: + that: + - result['changed'] == false +- debug: + msg: END ce_is_is_interface merged integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_is_is_view/defaults/main.yaml b/tests/integration/targets/ce_is_is_view/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_is_is_view/tasks/main.yaml b/tests/integration/targets/ce_is_is_view/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_is_is_view/tasks/netconf.yaml b/tests/integration/targets/ce_is_is_view/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/cleanup.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/cleanup.yaml new file mode 100644 index 0000000..3049578 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/cleanup.yaml @@ -0,0 +1,13 @@ +- debug: + msg: CLEANUP ce_is_is_view, deleted integration tests on connection={{ ansible_connection + }} +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: 100 + _public_ ISIS + +- debug: + msg: END CLEANUP ce_is_is_view, deleted integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/setup.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/setup.yaml new file mode 100644 index 0000000..3d5a540 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/setup.yaml @@ -0,0 +1,13 @@ +- debug: + msg: SETUP ce_is_is_view integration tests on connection={{ ansible_connection + }} +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: 100 + _public_ ISIS + +- debug: + msg: END SETUP ce_is_is_view integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_is_is_view_entity .yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_is_is_view_entity .yaml new file mode 100644 index 0000000..645a261 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_is_is_view_entity .yaml @@ -0,0 +1,58 @@ +- debug: + msg: START ce_is_is_view entity presented integration tests on connection={{ ansible_connection + }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + netentity: isis_net_entity + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''isis_net_entity'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + netentity: isis_net_entity + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' not in result_xml.end_state.result' + - '''''isis_net_entity'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view pentity resentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_bfd.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_bfd.yaml new file mode 100644 index 0000000..5b4003e --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_bfd.yaml @@ -0,0 +1,69 @@ +- debug: + msg: START ce_is_is_view EXPORTROUTE route policy presented integration tests + on connection={{ ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + bfd_min_rx: 100 + bfd_min_tx: 100 + bfd_multiplier_num: 10 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''100'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + defaultmode: always + cost: 10 + mode_tag: 10 + level_type: level_1 + avoid_learning: true + mode_routepolicyname: routepolicy_name + tag: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' not in result_xml.end_state.result' + - '''100'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view EXPORTROUTE route policy resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_export.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_export.yaml new file mode 100644 index 0000000..9bbde05 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_export.yaml @@ -0,0 +1,70 @@ +- debug: + msg: START ce_is_is_view EXPORTROUTE route policy presented integration tests + on connection={{ ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + export_protocol: ospf + export_policytype: aclNumOrName + export_processid: 100 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''ospf'' in result_xml.end_state.result' + - '''100'' in result_xml.end_state.result' + - '''level_1'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + defaultmode: always + cost: 10 + mode_tag: 10 + level_type: level_1 + avoid_learning: true + mode_routepolicyname: routepolicy_name + tag: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''ospf'' not in result_xml.end_state.result' + - '''100'' not in result_xml.end_state.result' + - '''level_1'' in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view EXPORTROUTE route policy resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_import.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_import.yaml new file mode 100644 index 0000000..84105f9 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_import.yaml @@ -0,0 +1,93 @@ +- debug: + msg: START ce_is_is_view import route policy presented integration tests on connection={{ + ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + protocol: ospf + processid: 100 + cost_type: external + import_cost: 10 + import_tag: 10 + import_route_policy: routepolicy_name + impotr_leveltype: level_1 + inheritcost: true + permitibgp: true + tag: 100 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''ospf'' in result_xml.end_state.result' + - '''100'' in result_xml.end_state.result' + - '''external'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - '''level_1'' in result_xml.end_state.result' + - '''routepolicy_name'' in result_xml.end_state.result' + - '''level_1'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + defaultmode: always + cost: 10 + mode_tag: 10 + level_type: level_1 + avoid_learning: true + mode_routepolicyname: routepolicy_name + tag: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''ospf'' not in result_xml.end_state.result' + - '''100'' not in result_xml.end_state.result' + - '''external'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - '''level_1'' not in result_xml.end_state.result' + - '''routepolicy_name'' not in result_xml.end_state.result' + - '''level_1'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view import route policy resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_l1tol2.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_l1tol2.yaml new file mode 100644 index 0000000..a35c276 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_l1tol2.yaml @@ -0,0 +1,81 @@ +- debug: + msg: START ce_is_is_view import route policy presented integration tests on connection={{ + ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + allow_filter: true + allow_up_down: true + ip_prefix_name: prefix_name + aclnum_or_name: 3001 + penetration_direct: level1-level2 + import_routepolicy_name: routepolicy_name + tag: 100 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''routepolicy_name'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''3001'' in result_xml.end_state.result' + - '''prefix_name'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + allow_filter: true + allow_up_down: true + ip_prefix_name: prefix_name + aclnum_or_name: 3001 + penetration_direct: level1-level2 + import_routepolicy_name: routepolicy_name + tag: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''routepolicy_name'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''3001'' not in result_xml.end_state.result' + - '''prefix_name'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view import route policy resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_l2tol1.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_l2tol1.yaml new file mode 100644 index 0000000..8a572b4 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_l2tol1.yaml @@ -0,0 +1,81 @@ +- debug: + msg: START ce_is_is_view import route policy presented integration tests on connection={{ + ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + penetration_direct: level2-level1 + allow_filter: true + allow_up_down: true + ip_prefix_name: prefix_name + aclnum_or_name: 3001 + import_routepolicy_name: routepolicy_name + tag: 100 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''routepolicy_name'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''3001'' in result_xml.end_state.result' + - '''prefix_name'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + penetration_direct: level2-level1 + allow_filter: true + allow_up_down: true + ip_prefix_name: prefix_name + aclnum_or_name: 3001 + import_routepolicy_name: routepolicy_name + tag: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''routepolicy_name'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''3001'' not in result_xml.end_state.result' + - '''prefix_name'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view import route policy resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_max_load.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_max_load.yaml new file mode 100644 index 0000000..6f058e5 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_max_load.yaml @@ -0,0 +1,59 @@ +- debug: + msg: START ce_is_is_view maxLoadBalancing presented integration tests on connection={{ + ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + max_load: 30 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''30'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + max_load: 30 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' not in result_xml.end_state.result' + - '''30'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view maxLoadBalancing resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_preferences.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_preferences.yaml new file mode 100644 index 0000000..7edc6be --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_preferences.yaml @@ -0,0 +1,63 @@ +- debug: + msg: START ce_is_is_view preferences presented integration tests on connection={{ + ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + preference_value: 100 + route_policy_name: route + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''100'' in result_xml.end_state.result' + - '''route'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + preference_value: 100 + route_policy_name: route + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' not in result_xml.end_state.result' + - '''''100'' not in result_xml.end_state.result' + - '''''route'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view Preference resentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_view_basic.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_view_basic.yaml new file mode 100644 index 0000000..9c159d0 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_isis_view_basic.yaml @@ -0,0 +1,95 @@ +- debug: + msg: START ce_is_is_view presented integration tests on connection={{ ansible_connection + }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + description: ISIS + islevel: level_1 + coststyle: narrow + relaxSpfLimit: true + stdlevel1cost: 60 + stdlevel2cost: 60 + stdbandwidth: 100 + autocostenable: true + autocostenablecompatible: true + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''_public_'' in result_xml.end_state.result' + - '''ISIS'' in result_xml.end_state.result' + - '''level_1'' in result_xml.end_state.result' + - '''narrow'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''60'' in result_xml.end_state.result' + - '''60'' in result_xml.end_state.result' + - '''100'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + description: ISIS + islevel: level_1 + coststyle: narrow + relaxSpfLimit: true + stdlevel1cost: 60 + stdlevel2cost: 60 + stdbandwidth: 100 + autocostenable: true + autocostenablecompatible: true + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' not in result_xml.end_state.result' + - '''_public_'' not in result_xml.end_state.result' + - '''ISIS'' not in result_xml.end_state.result' + - '''level_1'' not in result_xml.end_state.result' + - '''narrow'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''60'' not in result_xml.end_state.result' + - '''60'' not in result_xml.end_state.result' + - '''100'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''true'' not in + result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view presentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_issi_default.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_issi_default.yaml new file mode 100644 index 0000000..2e7e3e5 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_ce_issi_default.yaml @@ -0,0 +1,83 @@ +- debug: + msg: START ce_is_is_view import route policy presented integration tests on connection={{ + ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + defaultmode: always + cost: 10 + mode_tag: 10 + level_type: level_1 + avoid_learning: true + mode_routepolicyname: routepolicy_name + tag: 100 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' in result_xml.end_state.result' + - '''always'' in result_xml.end_state.result' + - '''routepolicy_name'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - '''10'' in result_xml.end_state.result' + - '''level_1'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + defaultmode: always + cost: 10 + mode_tag: 10 + level_type: level_1 + avoid_learning: true + mode_routepolicyname: routepolicy_name + tag: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''100'' not in result_xml.end_state.result' + - '''always'' not in result_xml.end_state.result' + - '''routepolicy_name'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - '''10'' not in result_xml.end_state.result' + - '''level_1'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view import route policy resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_isis_filter_import.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_isis_filter_import.yaml new file mode 100644 index 0000000..3a9913e --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_isis_filter_import.yaml @@ -0,0 +1,70 @@ +- debug: + msg: START ce_is_is_view EXPORTROUTE route policy presented integration tests + on connection={{ ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + import_aclnumorname: 301 + import_ipprefix: ipprefix + import_routepolicyname: routepolicyname + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''3001'' in result_xml.end_state.result' + - '''ipprefix'' in result_xml.end_state.result' + - '''routepolicyname'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + defaultmode: always + cost: 10 + mode_tag: 10 + level_type: level_1 + avoid_learning: true + mode_routepolicyname: routepolicy_name + tag: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''3001'' not in result_xml.end_state.result' + - '''ipprefix'' not in result_xml.end_state.result' + - '''routepolicyname'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view EXPORTROUTE route policy resentd integration tests on connection={{ + ansible_connection }} diff --git a/tests/integration/targets/ce_is_is_view/tests/netconf/test_isis_next_hop.yaml b/tests/integration/targets/ce_is_is_view/tests/netconf/test_isis_next_hop.yaml new file mode 100644 index 0000000..61b6aa5 --- /dev/null +++ b/tests/integration/targets/ce_is_is_view/tests/netconf/test_isis_next_hop.yaml @@ -0,0 +1,62 @@ +- debug: + msg: START ce_is_is_view next hop presented integration tests on connection={{ + ansible_connection }} +- include_tasks: setup.yaml +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id001 + instance_id: 100 + ip_address: 1.1.1.1 + weight: 100 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 100 + afIpv4 0 + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''1.1.1.1'' in result_xml.end_state.result' + - '''100'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_is_is_view: &id002 + instance_id: 100 + ip_address: 1.1.1.1 + weight: 100 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_is_is_view: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''1.1.1.1'' not in result_xml.end_state.result' + - '''100'' not in result_xml.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_is_is_view next hop resentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lacp/defaults/main.yaml b/tests/integration/targets/ce_lacp/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_lacp/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_lacp/tasks/main.yaml b/tests/integration/targets/ce_lacp/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_lacp/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_lacp/tasks/netconf.yaml b/tests/integration/targets/ce_lacp/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_lacp/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_lacp/tests/netconf/absent.yaml b/tests/integration/targets/ce_lacp/tests/netconf/absent.yaml new file mode 100644 index 0000000..84fe95e --- /dev/null +++ b/tests/integration/targets/ce_lacp/tests/netconf/absent.yaml @@ -0,0 +1,69 @@ +- debug: + msg: START ce_lacp merged integration tests on connection={{ ansible_connection + }} +- include_tasks: merge.yaml +- name: Merge the provided configuration with the exisiting running configuration + ce_lacp: &id001 + mode: Dynamic + trunk_id: 10 + preempt_enable: true + state_flapping: true + port_id_extension_enable: true + unexpected_mac_disable: true + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: true + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: Eth-Trunk10 + + + + + + + register: result_ifs_merged +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: + + register: result_global_merged +- name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + ce_lacp: *id001 + register: result_re_merged +- name: Assert that the previous task was idempotent, some become ot default values, + others depend on devices. + assert: + that: + - result_re_merged.changed == false + - '''false'' == result_ifs_merged.end_state.result' + - '''Slow'' == result_ifs_merged.end_state.result' + - '''90'' == result_ifs_merged.end_state.result' + - '''Prority'' == result_ifs_merged.end_state.result' + - '''30'' == result_ifs_merged.end_state.result' + - '''0'' in result_ifs_merged.end_state.result' + - '''false'' in result_ifs_merged.end_state.result' + - '''false'' in result_ifs_merged.end_state.result' + - '''false'' in result_ifs_merged.end_state.result' + - '''false'' in result_ifs_merged.end_state.result' + - '''32768'' in result_global_merged.end_state.result' +- debug: + msg: END ce_lacp merged integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lacp/tests/netconf/delete.yaml b/tests/integration/targets/ce_lacp/tests/netconf/delete.yaml new file mode 100644 index 0000000..2f6235d --- /dev/null +++ b/tests/integration/targets/ce_lacp/tests/netconf/delete.yaml @@ -0,0 +1,30 @@ +- debug: + msg: START ce_lacp deleted integration tests on connection={{ ansible_connection + }} +- name: Merge the provided configuration with the exisiting running configuration + ce_lacp: + mode: Dynamic + trunk_id: 10 + preempt_enable: true + state_flapping: true + port_id_extension_enable: true + unexpected_mac_disable: true + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: true + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- debug: + msg: END ce_lacp deleted integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lacp/tests/netconf/merge.yaml b/tests/integration/targets/ce_lacp/tests/netconf/merge.yaml new file mode 100644 index 0000000..56529df --- /dev/null +++ b/tests/integration/targets/ce_lacp/tests/netconf/merge.yaml @@ -0,0 +1,29 @@ +- debug: + msg: START ce_lacp merged integration tests on connection={{ ansible_connection + }} +- name: Merge the provided configuration with the exisiting running configuration + ce_lacp: + mode: Dynamic + trunk_id: 10 + preempt_enable: true + state_flapping: true + port_id_extension_enable: true + unexpected_mac_disable: true + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: true + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- debug: + msg: END ce_lacp merged integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lacp/tests/netconf/present.yaml b/tests/integration/targets/ce_lacp/tests/netconf/present.yaml new file mode 100644 index 0000000..0abf087 --- /dev/null +++ b/tests/integration/targets/ce_lacp/tests/netconf/present.yaml @@ -0,0 +1,73 @@ +- debug: + msg: START ce_lacp presented integration tests on connection={{ ansible_connection + }} +- name: present the provided configuration with the exisiting running configuration + ce_lacp: &id001 + mode: Dynamic + trunk_id: 10 + preempt_enable: true + state_flapping: true + port_id_extension_enable: true + unexpected_mac_disable: true + system_id: 1111-2222-3333 + timeout_type: Fast + fast_timeout: 12 + mixed_rate_link_enable: true + preempt_delay: 12 + collector_delay: 12 + max_active_linknumber: 2 + select: Prority + priority: 23 + global_priority: 123 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: Eth-Trunk10 + + + + + + + register: result_ifs_presentd +- name: Get global lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: + + + register: result_global_presentd +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_lacp: *id001 + register: result_re_presentd +- name: Assert that the previous task was idempotent + assert: + that: + - result_re_presentd.changed == false + - '''Dynamic'' == result_ifs_presentd.end_state.result' + - '''true'' == result_ifs_presentd.end_state.result' + - '''Fast'' == result_ifs_presentd.end_state.result' + - '''12'' == result_ifs_presentd.end_state.result' + - '''Prority'' == result_ifs_presentd.end_state.result' + - '''12'' == result_ifs_presentd.end_state.result' + - '''2'' == result_ifs_presentd.end_state.result' + - '''12'' in result_ifs_presentd.end_state.result' + - '''true'' in result_ifs_presentd.end_state.result' + - '''true'' in result_ifs_presentd.end_state.result' + - '''true'' in result_ifs_presentd.end_state.result' + - '''true'' in result_ifs_presentd.end_state.result' + - '''true'' in result_ifs_presentd.end_state.result' + - '''1111-2222-3333'' in result_global_presentd.end_state.result' + - '''123'' in result_global_presentd.end_state.result' +- include_tasks: delete.yaml +- debug: + msg: END ce_lacp presentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lldp/defaults/main.yaml b/tests/integration/targets/ce_lldp/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_lldp/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_lldp/meta/main.yml b/tests/integration/targets/ce_lldp/meta/main.yml new file mode 100644 index 0000000..6d800ee --- /dev/null +++ b/tests/integration/targets/ce_lldp/meta/main.yml @@ -0,0 +1,2 @@ +null +... diff --git a/tests/integration/targets/ce_lldp/tasks/main.yaml b/tests/integration/targets/ce_lldp/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_lldp/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_lldp/tasks/netconf.yaml b/tests/integration/targets/ce_lldp/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_lldp/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_lldp/tests/netconf/absent.yaml b/tests/integration/targets/ce_lldp/tests/netconf/absent.yaml new file mode 100644 index 0000000..5a8b2ba --- /dev/null +++ b/tests/integration/targets/ce_lldp/tests/netconf/absent.yaml @@ -0,0 +1,95 @@ +- debug: + msg: START ce_lldp absent integration tests on connection={{ ansible_connection + }} +- block: + - name: present the provided configuration befor absent + ce_lldp: + lldpenable: enabled + mdnstatus: rxOnly + interval: 35 + hold_multiplier: 5 + restart_delay: 3 + transmit_delay: 5 + notification_interval: 6 + fast_count: 5 + mdn_notification_interval: 10.1.1.1 + management_address: 10.10.10.1 + bind_name: vlanif100 + register: result + - name: change ansible_connection to network_cli + set_fact: + ansible_connection: network_cli + - name: display lldp + ce_command: + commands: + - display current-configuration | include lldp + register: result_display + - name: change ansible_connection to netconf + set_fact: + ansible_connection: netconf + - name: Assert the configuration is reflected on host + assert: + that: + - '''lldp enable'' in result_display.stdout[0]' + - '''undo lldp mdn disable'' in result_display.stdout[0]' + - '''lldp transmit interval 35'' in result_display.stdout[0]' + - '''lldp transmit multiplier 5'' in result_display.stdout[0]' + - '''lldp restart 3'' in result_display.stdout[0]' + - '''lldp transmit delay 5'' in result_display.stdout[0]' + - '''lldp fast-count 5'' in result_display.stdout[0]' + - '''lldp management-address 10.10.10.1'' in result_display.stdout[0]' + - '''lldp mdn trap-interval 6'' in result_display.stdout[0]' + - '''lldp trap-interval 6'' in result_display.stdout[0]' + - '''lldp management-address bind interface vlanif100'' in result_display.stdout[0]' + - name: absent the provided configuration with the exisiting running configuration + ce_lldp: &id001 + lldpenable: enabled + mdnstatus: rxOnly + interval: 35 + hold_multiplier: 5 + restart_delay: 3 + transmit_delay: 5 + notification_interval: 6 + fast_count: 5 + mdn_notification_interval: 10.1.1.1 + management_address: 10.10.10.1 + bind_name: vlanif100 + state: absent + register: result + - name: change ansible_connection to network_cli + set_fact: + ansible_connection: network_cli + - name: display lldp + ce_command: + commands: + - display current-configuration | include lldp + register: result_display + - name: change ansible_connection to netconf + set_fact: + ansible_connection: netconf + - name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true + - '''lldp enable'' not in result_display.stdout[0]' + - '''undo lldp mdn disable'' not in result_display.stdout[0]' + - '''lldp transmit interval 35'' not in result_display.stdout[0]' + - '''lldp transmit multiplier 5'' not in result_display.stdout[0]' + - '''lldp restart 3'' not in result_display.stdout[0]' + - '''lldp transmit delay 5'' not in result_display.stdout[0]' + - '''lldp fast-count 5'' not in result_display.stdout[0]' + - '''lldp management-address 10.10.10.1'' not in result_display.stdout[0]' + - '''lldp mdn trap-interval 6'' not in result_display.stdout[0]' + - '''lldp trap-interval 6'' not in result_display.stdout[0]' + - '''lldp management-address bind interface vlanif100'' not in result_display.stdout[0]' + - name: Merge the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_lldp: *id001 + register: result + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false +- debug: + msg: END ce_lldp absent integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lldp/tests/netconf/clean.yaml b/tests/integration/targets/ce_lldp/tests/netconf/clean.yaml new file mode 100644 index 0000000..b3a48f6 --- /dev/null +++ b/tests/integration/targets/ce_lldp/tests/netconf/clean.yaml @@ -0,0 +1,17 @@ +- debug: + msg: Start ce_lldp deleted remove interface config ansible_connection={{ ansible_connection + }} +- name: change ansible_connection to network_cli + set_fact: + ansible_connection: network_cli +- name: display lldp + ce_command: + commands: + - undo lldp enable + - lldp mdn disable +- name: change ansible_connection to netconf + set_fact: + ansible_connection: netconf +- debug: + msg: End ce_lldp deleted remove interface config ansible_connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lldp/tests/netconf/present.yaml b/tests/integration/targets/ce_lldp/tests/netconf/present.yaml new file mode 100644 index 0000000..e6f5ac7 --- /dev/null +++ b/tests/integration/targets/ce_lldp/tests/netconf/present.yaml @@ -0,0 +1,57 @@ +- debug: + msg: START ce_lldp merged integration tests on connection={{ ansible_connection + }} +- block: + - include_tasks: cleanup.yaml + - name: Merge the provided configuration with the exisiting running configuration + ce_lldp: &id001 + lldpenable: enabled + mdnstatus: rxOnly + interval: 35 + hold_multiplier: 5 + restart_delay: 3 + transmit_delay: 5 + notification_interval: 6 + fast_count: 5 + mdn_notification_interval: 10.1.1.1 + management_address: 10.10.10.1 + bind_name: vlanif100 + register: result + - name: change ansible_connection to network_cli + set_fact: + ansible_connection: network_cli + - name: display lldp + ce_command: + commands: + - display current-configuration | include lldp + register: result_display + - name: change ansible_connection to netconf + set_fact: + ansible_connection: netconf + - name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true + - '''lldp enable'' in result_display.stdout[0]' + - '''undo lldp mdn disable'' in result_display.stdout[0]' + - '''lldp transmit interval 35'' in result_display.stdout[0]' + - '''lldp transmit multiplier 5'' in result_display.stdout[0]' + - '''lldp restart 3'' in result_display.stdout[0]' + - '''lldp transmit delay 5'' in result_display.stdout[0]' + - '''lldp fast-count 5'' in result_display.stdout[0]' + - '''lldp management-address 10.10.10.1'' in result_display.stdout[0]' + - '''lldp mdn trap-interval 6'' in result_display.stdout[0]' + - '''lldp trap-interval 6'' in result_display.stdout[0]' + - '''lldp management-address bind interface vlanif100'' in result_display.stdout[0]' + - name: Merge the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_lldp: *id001 + register: result + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - include_tasks: cleanup.yaml +- debug: + msg: END ce_lldp merged integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_lldp_interface/defaults/main.yaml b/tests/integration/targets/ce_lldp_interface/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_lldp_interface/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_lldp_interface/meta/main.yml b/tests/integration/targets/ce_lldp_interface/meta/main.yml new file mode 100644 index 0000000..6d800ee --- /dev/null +++ b/tests/integration/targets/ce_lldp_interface/meta/main.yml @@ -0,0 +1,2 @@ +null +... diff --git a/tests/integration/targets/ce_lldp_interface/tasks/main.yaml b/tests/integration/targets/ce_lldp_interface/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_lldp_interface/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_lldp_interface/tasks/netconf.yaml b/tests/integration/targets/ce_lldp_interface/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_lldp_interface/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_lldp_interface/tests/netconf/lldp_interface.yaml b/tests/integration/targets/ce_lldp_interface/tests/netconf/lldp_interface.yaml new file mode 100644 index 0000000..82f560c --- /dev/null +++ b/tests/integration/targets/ce_lldp_interface/tests/netconf/lldp_interface.yaml @@ -0,0 +1,119 @@ +- debug: + msg: START ce_lldp_interface merged integration tests on connection={{ ansible_connection + }} +- block: + - name: Merge the provided configuration with the exisiting running configuration + - basic-tlv + ce_lldp_interface: &id001 + config: + msg_interval: 8 + ifname: 10GE 1/0/1 + admin_status: txandrx + basic_tlv: + management_addr: true + port_desc: true + system_capability: true + system_description: true + system_name: true + register: result1 + - name: Merge the provided configuration with the existing running configuration + (REPEAT) + ce_lldp_interface: *id001 + register: result2 + - name: Netconf get operation + ce_netconf: + rpc: get + cfg_xml: + 10GE1/0/1 + + + + register: result3 + - name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false + - '''8'' in result3.endstate.result' + - '''txAndRx'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - name: Merge the provided configuration with the exisiting running configuration + - dot1-tlv + ce_lldp_interface: &id002 + config: + msg_interval: 8 + ifname: 10GE 1/0/1 + dot1_tlv: + port_vlan_enable: true + port_desc: true + prot_vlan_enable: true + prot_vlan_id: 123 + vlan_name: 234 + vlan_name_enable: true + register: result1 + - name: Merge the provided configuration with the existing running configuration + (REPEAT) + ce_lldp_interface: *id002 + register: result2 + - name: Netconf get operation + ce_netconf: + rpc: get + cfg_xml: + 10GE1/0/1 + + + + + register: result3 + - name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false + - '''true'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - '''123'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - name: Merge the provided configuration with the exisiting running configuration + - dot3-tlv + ce_lldp_interface: &id003 + config: + msg_interval: 8 + ifname: 10GE 1/0/1 + dot3_tlv: + eee: true + link_aggregation: true + mac_physic: true + max_frame_size: true + register: result1 + - name: Merge the provided configuration with the existing running configuration + (REPEAT) + ce_lldp_interface: *id003 + register: result2 + - name: Netconf get operation + ce_netconf: + rpc: get + cfg_xml: + 10GE1/0/1 + + + + register: result3 + - name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false + - '''true'' in result3.endstate.result' + - '''true'' in result3.endstate.result' + - '''123'' in result3.endstate.result' +- debug: + msg: END ce_lldp_interface merged integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_mdn_interface/defaults/main.yaml b/tests/integration/targets/ce_mdn_interface/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_mdn_interface/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_mdn_interface/tasks/main.yaml b/tests/integration/targets/ce_mdn_interface/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_mdn_interface/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_mdn_interface/tasks/netconf.yaml b/tests/integration/targets/ce_mdn_interface/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_mdn_interface/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_mdn_interface/tests/netconf/ce_mdn_interface.yaml b/tests/integration/targets/ce_mdn_interface/tests/netconf/ce_mdn_interface.yaml new file mode 100644 index 0000000..794ecca --- /dev/null +++ b/tests/integration/targets/ce_mdn_interface/tests/netconf/ce_mdn_interface.yaml @@ -0,0 +1,72 @@ +- debug: + msg: START ce_mdn_interface presented integration tests on connection={{ ansible_connection + }} +- name: clean up default configuration with the exisiting running configuration + ce_mdn_interface: + lldpenable: disabled + mdnstatus: disabled + ifname: 10GE1/0/1 +- name: present the provided configuration with the exisiting running configuration + ce_mdn_interface: &id001 + lldpenable: enabled + mdnstatus: rxOnly + ifname: 10GE1/0/1 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get mdnInterface config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: 10GE1/0/1 + + register: result_xml +- name: Get lldp enabled config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: + /filter> + register: result_xml_lldp +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_mdn_interface: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''rxOnly'' in result_xml.end_state.result' + - '''enabled'' in result_xml_lldp.end_state.result' +- name: absent the provided configuration with the exisiting running configuration + ce_mdn_interface: &id002 + lldpenable: disabled + mdnstatus: disabled + ifname: 10GE1/0/1 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: absent the provided configuration with the existing running configuration + (REPEAT) + ce_mdn_interface: *id002 + register: repeat +- name: Get mdnInterface config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Get lldp enabled config by ce_netconf. + ce_netconf: *id003 + register: result_xml_lldp +- name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - '''disabled'' not in result_xml.end_state.result' + - '''disabled'' in result_xml_lldp.end_state.result' +- debug: + msg: END ce_mdn_interface resentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_multicast_global/defaults/main.yaml b/tests/integration/targets/ce_multicast_global/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_multicast_global/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_multicast_global/tasks/main.yaml b/tests/integration/targets/ce_multicast_global/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_multicast_global/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_multicast_global/tasks/netconf.yaml b/tests/integration/targets/ce_multicast_global/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_multicast_global/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_multicast_global/tests/netconf/test_ce_multicast_global.yaml b/tests/integration/targets/ce_multicast_global/tests/netconf/test_ce_multicast_global.yaml new file mode 100644 index 0000000..6cbaa92 --- /dev/null +++ b/tests/integration/targets/ce_multicast_global/tests/netconf/test_ce_multicast_global.yaml @@ -0,0 +1,56 @@ +- debug: + msg: START ce_multicast_global presented integration tests on connection={{ ansible_connection + }} +- name: present the provided configuration with the exisiting running configuration + ce_multicast_global: &id001 + aftype: v4 + vrf: vpna + weight: 100 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_multicast_global: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''vpna'' in result_xml.end_state.result' + - '''vpna'' in result_xml.end_state.result' +- name: present the provided configuration with the exisiting running configuration + ce_multicast_global: &id002 + aftype: v4 + vrf: vpna + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_multicast_global: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''vpna'' not in result_xml.end_state.result' + - '''vpna'' not in result_xml.end_state.result' +- debug: + msg: END ce_multicast_global resentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_multicast_igmp_enable/defaults/main.yaml b/tests/integration/targets/ce_multicast_igmp_enable/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_multicast_igmp_enable/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_multicast_igmp_enable/tasks/main.yaml b/tests/integration/targets/ce_multicast_igmp_enable/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_multicast_igmp_enable/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_multicast_igmp_enable/tasks/netconf.yaml b/tests/integration/targets/ce_multicast_igmp_enable/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_multicast_igmp_enable/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_multicast_igmp_enable/tests/netconf/ce_multicast_igmp_enable.yaml b/tests/integration/targets/ce_multicast_igmp_enable/tests/netconf/ce_multicast_igmp_enable.yaml new file mode 100644 index 0000000..c98dc2d --- /dev/null +++ b/tests/integration/targets/ce_multicast_igmp_enable/tests/netconf/ce_multicast_igmp_enable.yaml @@ -0,0 +1,72 @@ +- debug: + msg: START ce_multicast_igmp_enable presented integration tests on connection={{ + ansible_connection }} +- name: clean up configuration with the exisiting running configuration + ce_multicast_igmp_enable: &id002 + aftype: v4 + features: vlan + vlan_id: 100 + igmp: true + version: 2 + proxy: true +- name: present the provided configuration with the exisiting running configuration + ce_multicast_igmp_enable: &id001 + aftype: v4 + features: vlan + vlan_id: 100 + igmp: true + version: 2 + proxy: true + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: Get basic config by ce_netconf. + ce_netconf: &id003 + rpc: get + cfg_xml: + + + + register: result_xml +- name: present the provided configuration with the existing running configuration + (IDEMPOTENT) + ce_multicast_igmp_enable: *id001 + register: repeat +- name: Assert that the previous task was idempotent + assert: + that: + - repeat.changed == false + - '''ipv4unicast'' in result_xml.end_state.result' + - '''100'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' + - '''2'' in result_xml.end_state.result' + - '''true'' in result_xml.end_state.result' +- name: absent the provided configuration with the exisiting running configuration + ce_multicast_igmp_enable: *id002 + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: absent the provided configuration with the existing running configuration + (REPEAT) + ce_multicast_igmp_enable: *id002 + register: repeat +- name: Get basic config by ce_netconf. + ce_netconf: *id003 + register: result_xml +- name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - '''ipv4unicast'' not in result_xml.end_state.result' + - '''100'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' + - '''2'' not in result_xml.end_state.result' + - '''true'' not in result_xml.end_state.result' +- debug: + msg: END ce_multicast_igmp_enable resentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_static_route_bfd/defaults/main.yaml b/tests/integration/targets/ce_static_route_bfd/defaults/main.yaml new file mode 100644 index 0000000..2fd3a4d --- /dev/null +++ b/tests/integration/targets/ce_static_route_bfd/defaults/main.yaml @@ -0,0 +1,2 @@ +testcase: '[^_].*' +test_items: [] diff --git a/tests/integration/targets/ce_static_route_bfd/tasks/main.yaml b/tests/integration/targets/ce_static_route_bfd/tasks/main.yaml new file mode 100644 index 0000000..e85504c --- /dev/null +++ b/tests/integration/targets/ce_static_route_bfd/tasks/main.yaml @@ -0,0 +1,3 @@ +- include: netconf.yaml + tags: + - netconf diff --git a/tests/integration/targets/ce_static_route_bfd/tasks/netconf.yaml b/tests/integration/targets/ce_static_route_bfd/tasks/netconf.yaml new file mode 100644 index 0000000..604dfb3 --- /dev/null +++ b/tests/integration/targets/ce_static_route_bfd/tasks/netconf.yaml @@ -0,0 +1,14 @@ +- name: collect all netconf test cases + find: + paths: '{{ role_path }}/tests/netconf' + patterns: '{{ testcase }}.yaml' + use_regex: true + connection: local + register: test_cases +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +- name: run test case (connection=netconf) + include: '{{ test_case_to_run }} ansible_connection=netconf' + with_items: '{{ test_items }}' + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ce_static_route_bfd/tests/netconf/ce_static_route_bfd.yaml b/tests/integration/targets/ce_static_route_bfd/tests/netconf/ce_static_route_bfd.yaml new file mode 100644 index 0000000..b1bc115 --- /dev/null +++ b/tests/integration/targets/ce_static_route_bfd/tests/netconf/ce_static_route_bfd.yaml @@ -0,0 +1,160 @@ +- debug: + msg: START ce_static_route_bfd presented integration tests on connection={{ ansible_connection + }} +- include_tasks: cleanup.yaml +- name: Config an ip route-static bfd 10GE1/0/1 3.3.3.3 min-rx-interval 50 min-tx-interval + 50 detect-multiplier 5 + ce_static_route_bfd: + function_flag: singleBFD + nhp_interface: 10GE1/0/1 + next_hop: 3.3.3.3 + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 5 + aftype: v4 + state: present + register: result1 +- name: (repeat)Config an ip route-static bfd 10GE1/0/1 3.3.3.3 min-rx-interval 50 + min-tx-interval 50 detect-multiplier 5 + ce_static_route_bfd: + function_flag: singleBFD + nhp_interface: 10GE1/0/1 + next_hop: 3.3.3.3 + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 5 + aftype: v4 + state: present + register: result2 +- name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false +- name: ip route-static bfd 10GE1/0/1 3.3.3.4 + ce_static_route_bfd: + function_flag: singleBFD + nhp_interface: 10GE1/0/1 + next_hop: 3.3.3.4 + aftype: v4 + register: result1 +- name: (repeat)ip route-static bfd 10GE1/0/1 3.3.3.4 + ce_static_route_bfd: + function_flag: singleBFD + nhp_interface: 10GE1/0/1 + next_hop: 3.3.3.4 + aftype: v4 + register: result2 +- name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false +- name: Config an ip route-static default-bfd min-rx-interval 50 min-tx-interval 50 + detect-multiplier 6 + ce_static_route_bfd: + function_flag: globalBFD + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 6 + aftype: v4 + state: present + register: result1 +- name: (repeat)Config an ip route-static default-bfd min-rx-interval 50 min-tx-interval + 50 detect-multiplier 6 + ce_static_route_bfd: + function_flag: globalBFD + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 6 + aftype: v4 + state: present + register: result2 +- name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false +- name: undo ip route-static default-bfd + ce_static_route_bfd: + function_flag: globalBFD + aftype: v4 + state: absent + commands: sys,undo ip route-static default-bfd,commit + register: result1 +- name: (repeat)undo ip route-static default-bfd + ce_static_route_bfd: + function_flag: globalBFD + aftype: v4 + state: absent + commands: sys,undo ip route-static default-bfd,commit + register: result2 +- name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false +- name: Config an ipv4 static route 2.2.2.0/24 2.2.2.1 preference 1 tag 2 description + test for staticBFD + ce_static_route_bfd: + function_flag: staticBFD + prefix: 2.2.2.2 + mask: 24 + next_hop: 2.2.2.1 + tag: 2 + description: test + pref: 1 + aftype: v4 + bfd_session_name: btoa + state: present + register: result1 +- name: (repeat) Config an ipv4 static route 2.2.2.0/24 2.2.2.1 preference 1 tag 2 + description test for staticBFD + ce_static_route_bfd: + function_flag: staticBFD + prefix: 2.2.2.2 + mask: 24 + next_hop: 2.2.2.1 + tag: 2 + description: test + pref: 1 + aftype: v4 + bfd_session_name: btoa + state: present + register: result2 +- name: Assert the configuration is reflected on host + assert: + that: + - result1['changed'] == true + - result2['changed'] == false +- name: Get lacp config by ce_netconf. + ce_netconf: + rpc: get + cfg_xml: + + + + register: result_present +- name: Assert that the previous task was idempotent + assert: + that: + - '''v4'' == result_present.end_state.result' + - '''10GE1/0/1'' == result_present.end_state.result' + - '''Fast'' == result_present.end_state.result' + - '''__publiv__'' == result_present.end_state.result' + - '''Prority'' == result_present.end_state.result' + - '''2.2.2.1'' == result_present.end_state.result' + - '''2.2.2.2'' == result_present.end_state.result' + - '''12'' in result_present.end_state.result' + - '''true'' in result_present.end_state.result' + - '''true'' in result_present.end_state.result' + - '''true'' in result_present.end_state.result' + - '''true'' in result_present.end_state.result' + - '''true'' in result_present.end_state.result' + - '''1111-2222-3333'' in result_present.end_state.result' + - '''123'' in result_present.end_state.result' +- include_tasks: cleanup.yaml +- debug: + msg: END ce_static_route_bfd presentd integration tests on connection={{ ansible_connection + }} diff --git a/tests/integration/targets/ce_static_route_bfd/tests/netconf/cleanup.yaml b/tests/integration/targets/ce_static_route_bfd/tests/netconf/cleanup.yaml new file mode 100644 index 0000000..b544c89 --- /dev/null +++ b/tests/integration/targets/ce_static_route_bfd/tests/netconf/cleanup.yaml @@ -0,0 +1,28 @@ +- name: Merge the provided configuration with the exisiting running configuration + ce_static_route_bfd: + function_flag: singleBFD + nhp_interface: 10GE1/0/1 + next_hop: 3.3.3.3 + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 5 + aftype: v4 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true +- name: ip route-static bfd 10GE1/0/1 3.3.3.4 + ce_static_route_bfd: + function_flag: globalBFD + min_tx_interval: 50 + min_rx_interval: 50 + detect_multiplier: 6 + aftype: v4 + state: absent + register: result +- name: Assert the configuration is reflected on host + assert: + that: + - result['changed'] == true diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt new file mode 100644 index 0000000..d834016 --- /dev/null +++ b/tests/sanity/ignore-2.10.txt @@ -0,0 +1,556 @@ +plugins/module_utils/network/cloudengine/ce.py future-import-boilerplate +plugins/module_utils/network/cloudengine/ce.py metaclass-boilerplate +plugins/modules/ce_aaa_server.py future-import-boilerplate +plugins/modules/ce_aaa_server.py metaclass-boilerplate +plugins/modules/ce_aaa_server.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_aaa_server.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_aaa_server.py validate-modules:doc-missing-type +plugins/modules/ce_aaa_server.py validate-modules:missing-suboption-docs +plugins/modules/ce_aaa_server.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_aaa_server.py validate-modules:undocumented-parameter +plugins/modules/ce_aaa_server_host.py future-import-boilerplate +plugins/modules/ce_aaa_server_host.py metaclass-boilerplate +plugins/modules/ce_aaa_server_host.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_aaa_server_host.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_aaa_server_host.py validate-modules:doc-missing-type +plugins/modules/ce_aaa_server_host.py validate-modules:missing-suboption-docs +plugins/modules/ce_aaa_server_host.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_aaa_server_host.py validate-modules:undocumented-parameter +plugins/modules/ce_acl.py future-import-boilerplate +plugins/modules/ce_acl.py metaclass-boilerplate +plugins/modules/ce_acl.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_acl.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_acl.py validate-modules:doc-missing-type +plugins/modules/ce_acl.py validate-modules:missing-suboption-docs +plugins/modules/ce_acl.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_acl.py validate-modules:undocumented-parameter +plugins/modules/ce_acl_advance.py future-import-boilerplate +plugins/modules/ce_acl_advance.py metaclass-boilerplate +plugins/modules/ce_acl_advance.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_acl_advance.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_acl_advance.py validate-modules:doc-missing-type +plugins/modules/ce_acl_advance.py validate-modules:missing-suboption-docs +plugins/modules/ce_acl_advance.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_acl_advance.py validate-modules:undocumented-parameter +plugins/modules/ce_acl_interface.py future-import-boilerplate +plugins/modules/ce_acl_interface.py metaclass-boilerplate +plugins/modules/ce_acl_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_acl_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_acl_interface.py validate-modules:doc-missing-type +plugins/modules/ce_acl_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_acl_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_acl_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_bfd_global.py future-import-boilerplate +plugins/modules/ce_bfd_global.py metaclass-boilerplate +plugins/modules/ce_bfd_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bfd_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bfd_global.py validate-modules:doc-missing-type +plugins/modules/ce_bfd_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_bfd_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bfd_global.py validate-modules:undocumented-parameter +plugins/modules/ce_bfd_session.py future-import-boilerplate +plugins/modules/ce_bfd_session.py metaclass-boilerplate +plugins/modules/ce_bfd_session.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bfd_session.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bfd_session.py validate-modules:doc-missing-type +plugins/modules/ce_bfd_session.py validate-modules:missing-suboption-docs +plugins/modules/ce_bfd_session.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bfd_session.py validate-modules:undocumented-parameter +plugins/modules/ce_bfd_view.py future-import-boilerplate +plugins/modules/ce_bfd_view.py metaclass-boilerplate +plugins/modules/ce_bfd_view.py validate-modules:doc-default-incompatible-type +plugins/modules/ce_bfd_view.py validate-modules:doc-missing-type +plugins/modules/ce_bfd_view.py validate-modules:doc-required-mismatch +plugins/modules/ce_bfd_view.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bfd_view.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp.py future-import-boilerplate +plugins/modules/ce_bgp.py metaclass-boilerplate +plugins/modules/ce_bgp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp.py validate-modules:doc-missing-type +plugins/modules/ce_bgp.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp_af.py future-import-boilerplate +plugins/modules/ce_bgp_af.py metaclass-boilerplate +plugins/modules/ce_bgp_af.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp_af.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp_af.py validate-modules:doc-missing-type +plugins/modules/ce_bgp_af.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp_af.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp_af.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp_neighbor.py future-import-boilerplate +plugins/modules/ce_bgp_neighbor.py metaclass-boilerplate +plugins/modules/ce_bgp_neighbor.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp_neighbor.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp_neighbor.py validate-modules:doc-missing-type +plugins/modules/ce_bgp_neighbor.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp_neighbor.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp_neighbor.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp_neighbor_af.py future-import-boilerplate +plugins/modules/ce_bgp_neighbor_af.py metaclass-boilerplate +plugins/modules/ce_bgp_neighbor_af.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp_neighbor_af.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp_neighbor_af.py validate-modules:doc-missing-type +plugins/modules/ce_bgp_neighbor_af.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp_neighbor_af.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp_neighbor_af.py validate-modules:undocumented-parameter +plugins/modules/ce_command.py future-import-boilerplate +plugins/modules/ce_command.py metaclass-boilerplate +plugins/modules/ce_command.py pylint:blacklisted-name +plugins/modules/ce_command.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_command.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_command.py validate-modules:doc-missing-type +plugins/modules/ce_command.py validate-modules:missing-suboption-docs +plugins/modules/ce_command.py validate-modules:parameter-list-no-elements +plugins/modules/ce_command.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_command.py validate-modules:undocumented-parameter +plugins/modules/ce_config.py future-import-boilerplate +plugins/modules/ce_config.py metaclass-boilerplate +plugins/modules/ce_config.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_config.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_config.py validate-modules:doc-missing-type +plugins/modules/ce_config.py validate-modules:missing-suboption-docs +plugins/modules/ce_config.py validate-modules:parameter-list-no-elements +plugins/modules/ce_config.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_config.py validate-modules:undocumented-parameter +plugins/modules/ce_dldp.py future-import-boilerplate +plugins/modules/ce_dldp.py metaclass-boilerplate +plugins/modules/ce_dldp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_dldp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_dldp.py validate-modules:doc-missing-type +plugins/modules/ce_dldp.py validate-modules:missing-suboption-docs +plugins/modules/ce_dldp.py validate-modules:nonexistent-parameter-documented +plugins/modules/ce_dldp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_dldp.py validate-modules:undocumented-parameter +plugins/modules/ce_dldp_interface.py future-import-boilerplate +plugins/modules/ce_dldp_interface.py metaclass-boilerplate +plugins/modules/ce_dldp_interface.py pylint:blacklisted-name +plugins/modules/ce_dldp_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_dldp_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_dldp_interface.py validate-modules:doc-missing-type +plugins/modules/ce_dldp_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_dldp_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_dldp_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_eth_trunk.py future-import-boilerplate +plugins/modules/ce_eth_trunk.py metaclass-boilerplate +plugins/modules/ce_eth_trunk.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_eth_trunk.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_eth_trunk.py validate-modules:doc-missing-type +plugins/modules/ce_eth_trunk.py validate-modules:missing-suboption-docs +plugins/modules/ce_eth_trunk.py validate-modules:parameter-list-no-elements +plugins/modules/ce_eth_trunk.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_eth_trunk.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_bd_vni.py future-import-boilerplate +plugins/modules/ce_evpn_bd_vni.py metaclass-boilerplate +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-required-mismatch +plugins/modules/ce_evpn_bd_vni.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_bd_vni.py validate-modules:parameter-list-no-elements +plugins/modules/ce_evpn_bd_vni.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_bd_vni.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_bgp.py future-import-boilerplate +plugins/modules/ce_evpn_bgp.py metaclass-boilerplate +plugins/modules/ce_evpn_bgp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_bgp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_bgp.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_bgp.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_bgp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_bgp.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_bgp_rr.py future-import-boilerplate +plugins/modules/ce_evpn_bgp_rr.py metaclass-boilerplate +plugins/modules/ce_evpn_bgp_rr.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_bgp_rr.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_bgp_rr.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_bgp_rr.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_bgp_rr.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_bgp_rr.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_global.py future-import-boilerplate +plugins/modules/ce_evpn_global.py metaclass-boilerplate +plugins/modules/ce_evpn_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_global.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_global.py validate-modules:undocumented-parameter +plugins/modules/ce_facts.py future-import-boilerplate +plugins/modules/ce_facts.py metaclass-boilerplate +plugins/modules/ce_facts.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_facts.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_facts.py validate-modules:doc-missing-type +plugins/modules/ce_facts.py validate-modules:missing-suboption-docs +plugins/modules/ce_facts.py validate-modules:parameter-list-no-elements +plugins/modules/ce_facts.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_facts.py validate-modules:undocumented-parameter +plugins/modules/ce_file_copy.py future-import-boilerplate +plugins/modules/ce_file_copy.py metaclass-boilerplate +plugins/modules/ce_file_copy.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_file_copy.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_file_copy.py validate-modules:doc-missing-type +plugins/modules/ce_file_copy.py validate-modules:missing-suboption-docs +plugins/modules/ce_file_copy.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_file_copy.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_debug.py future-import-boilerplate +plugins/modules/ce_info_center_debug.py metaclass-boilerplate +plugins/modules/ce_info_center_debug.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_debug.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_debug.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_debug.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_debug.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_debug.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_global.py future-import-boilerplate +plugins/modules/ce_info_center_global.py metaclass-boilerplate +plugins/modules/ce_info_center_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_global.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_global.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_log.py future-import-boilerplate +plugins/modules/ce_info_center_log.py metaclass-boilerplate +plugins/modules/ce_info_center_log.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_log.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_log.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_log.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_log.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_log.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_trap.py future-import-boilerplate +plugins/modules/ce_info_center_trap.py metaclass-boilerplate +plugins/modules/ce_info_center_trap.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_trap.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_trap.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_trap.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_trap.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_trap.py validate-modules:undocumented-parameter +plugins/modules/ce_interface.py future-import-boilerplate +plugins/modules/ce_interface.py metaclass-boilerplate +plugins/modules/ce_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_interface.py validate-modules:doc-missing-type +plugins/modules/ce_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_interface_ospf.py future-import-boilerplate +plugins/modules/ce_interface_ospf.py metaclass-boilerplate +plugins/modules/ce_interface_ospf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_interface_ospf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_interface_ospf.py validate-modules:doc-missing-type +plugins/modules/ce_interface_ospf.py validate-modules:missing-suboption-docs +plugins/modules/ce_interface_ospf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_interface_ospf.py validate-modules:undocumented-parameter +plugins/modules/ce_ip_interface.py future-import-boilerplate +plugins/modules/ce_ip_interface.py metaclass-boilerplate +plugins/modules/ce_ip_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ip_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ip_interface.py validate-modules:doc-missing-type +plugins/modules/ce_ip_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_ip_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ip_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_is_is_view.py validate-modules:doc-required-mismatch +plugins/modules/ce_link_status.py future-import-boilerplate +plugins/modules/ce_link_status.py metaclass-boilerplate +plugins/modules/ce_link_status.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_link_status.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_link_status.py validate-modules:doc-missing-type +plugins/modules/ce_link_status.py validate-modules:missing-suboption-docs +plugins/modules/ce_link_status.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_link_status.py validate-modules:undocumented-parameter +plugins/modules/ce_mlag_config.py future-import-boilerplate +plugins/modules/ce_mlag_config.py metaclass-boilerplate +plugins/modules/ce_mlag_config.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_mlag_config.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_mlag_config.py validate-modules:doc-missing-type +plugins/modules/ce_mlag_config.py validate-modules:missing-suboption-docs +plugins/modules/ce_mlag_config.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_mlag_config.py validate-modules:undocumented-parameter +plugins/modules/ce_mlag_interface.py future-import-boilerplate +plugins/modules/ce_mlag_interface.py metaclass-boilerplate +plugins/modules/ce_mlag_interface.py pylint:blacklisted-name +plugins/modules/ce_mlag_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_mlag_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_mlag_interface.py validate-modules:doc-missing-type +plugins/modules/ce_mlag_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_mlag_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_mlag_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_mtu.py future-import-boilerplate +plugins/modules/ce_mtu.py metaclass-boilerplate +plugins/modules/ce_mtu.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_mtu.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_mtu.py validate-modules:doc-missing-type +plugins/modules/ce_mtu.py validate-modules:doc-required-mismatch +plugins/modules/ce_mtu.py validate-modules:missing-suboption-docs +plugins/modules/ce_mtu.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_mtu.py validate-modules:undocumented-parameter +plugins/modules/ce_netconf.py future-import-boilerplate +plugins/modules/ce_netconf.py metaclass-boilerplate +plugins/modules/ce_netconf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netconf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netconf.py validate-modules:doc-missing-type +plugins/modules/ce_netconf.py validate-modules:missing-suboption-docs +plugins/modules/ce_netconf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netconf.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_aging.py future-import-boilerplate +plugins/modules/ce_netstream_aging.py metaclass-boilerplate +plugins/modules/ce_netstream_aging.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_aging.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_aging.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_aging.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_aging.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_aging.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_export.py future-import-boilerplate +plugins/modules/ce_netstream_export.py metaclass-boilerplate +plugins/modules/ce_netstream_export.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_export.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_export.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_export.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_export.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_export.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_global.py future-import-boilerplate +plugins/modules/ce_netstream_global.py metaclass-boilerplate +plugins/modules/ce_netstream_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_global.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_global.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_template.py future-import-boilerplate +plugins/modules/ce_netstream_template.py metaclass-boilerplate +plugins/modules/ce_netstream_template.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_template.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_template.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_template.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_template.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_template.py validate-modules:undocumented-parameter +plugins/modules/ce_ntp.py future-import-boilerplate +plugins/modules/ce_ntp.py metaclass-boilerplate +plugins/modules/ce_ntp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ntp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ntp.py validate-modules:doc-missing-type +plugins/modules/ce_ntp.py validate-modules:missing-suboption-docs +plugins/modules/ce_ntp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ntp.py validate-modules:undocumented-parameter +plugins/modules/ce_ntp_auth.py future-import-boilerplate +plugins/modules/ce_ntp_auth.py metaclass-boilerplate +plugins/modules/ce_ntp_auth.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ntp_auth.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ntp_auth.py validate-modules:doc-missing-type +plugins/modules/ce_ntp_auth.py validate-modules:missing-suboption-docs +plugins/modules/ce_ntp_auth.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ntp_auth.py validate-modules:undocumented-parameter +plugins/modules/ce_ospf.py future-import-boilerplate +plugins/modules/ce_ospf.py metaclass-boilerplate +plugins/modules/ce_ospf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ospf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ospf.py validate-modules:doc-missing-type +plugins/modules/ce_ospf.py validate-modules:missing-suboption-docs +plugins/modules/ce_ospf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ospf.py validate-modules:undocumented-parameter +plugins/modules/ce_ospf_vrf.py future-import-boilerplate +plugins/modules/ce_ospf_vrf.py metaclass-boilerplate +plugins/modules/ce_ospf_vrf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ospf_vrf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ospf_vrf.py validate-modules:doc-missing-type +plugins/modules/ce_ospf_vrf.py validate-modules:missing-suboption-docs +plugins/modules/ce_ospf_vrf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ospf_vrf.py validate-modules:undocumented-parameter +plugins/modules/ce_reboot.py future-import-boilerplate +plugins/modules/ce_reboot.py metaclass-boilerplate +plugins/modules/ce_reboot.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_reboot.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_reboot.py validate-modules:doc-missing-type +plugins/modules/ce_reboot.py validate-modules:missing-suboption-docs +plugins/modules/ce_reboot.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_reboot.py validate-modules:undocumented-parameter +plugins/modules/ce_rollback.py future-import-boilerplate +plugins/modules/ce_rollback.py metaclass-boilerplate +plugins/modules/ce_rollback.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_rollback.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_rollback.py validate-modules:doc-missing-type +plugins/modules/ce_rollback.py validate-modules:doc-required-mismatch +plugins/modules/ce_rollback.py validate-modules:missing-suboption-docs +plugins/modules/ce_rollback.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_rollback.py validate-modules:undocumented-parameter +plugins/modules/ce_sflow.py future-import-boilerplate +plugins/modules/ce_sflow.py metaclass-boilerplate +plugins/modules/ce_sflow.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_sflow.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_sflow.py validate-modules:doc-missing-type +plugins/modules/ce_sflow.py validate-modules:missing-suboption-docs +plugins/modules/ce_sflow.py validate-modules:parameter-list-no-elements +plugins/modules/ce_sflow.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_sflow.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_community.py future-import-boilerplate +plugins/modules/ce_snmp_community.py metaclass-boilerplate +plugins/modules/ce_snmp_community.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_community.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_community.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_community.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_community.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_community.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_contact.py future-import-boilerplate +plugins/modules/ce_snmp_contact.py metaclass-boilerplate +plugins/modules/ce_snmp_contact.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_contact.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_contact.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_contact.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_contact.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_contact.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_location.py future-import-boilerplate +plugins/modules/ce_snmp_location.py metaclass-boilerplate +plugins/modules/ce_snmp_location.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_location.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_location.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_location.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_location.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_location.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_target_host.py future-import-boilerplate +plugins/modules/ce_snmp_target_host.py metaclass-boilerplate +plugins/modules/ce_snmp_target_host.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_target_host.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_target_host.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_target_host.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_target_host.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_target_host.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_traps.py future-import-boilerplate +plugins/modules/ce_snmp_traps.py metaclass-boilerplate +plugins/modules/ce_snmp_traps.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_traps.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_traps.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_traps.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_traps.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_traps.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_user.py future-import-boilerplate +plugins/modules/ce_snmp_user.py metaclass-boilerplate +plugins/modules/ce_snmp_user.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_user.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_user.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_user.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_user.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_user.py validate-modules:undocumented-parameter +plugins/modules/ce_startup.py future-import-boilerplate +plugins/modules/ce_startup.py metaclass-boilerplate +plugins/modules/ce_startup.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_startup.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_startup.py validate-modules:doc-missing-type +plugins/modules/ce_startup.py validate-modules:missing-suboption-docs +plugins/modules/ce_startup.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_startup.py validate-modules:undocumented-parameter +plugins/modules/ce_static_route.py future-import-boilerplate +plugins/modules/ce_static_route.py metaclass-boilerplate +plugins/modules/ce_static_route.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_static_route.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_static_route.py validate-modules:doc-missing-type +plugins/modules/ce_static_route.py validate-modules:missing-suboption-docs +plugins/modules/ce_static_route.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_static_route.py validate-modules:undocumented-parameter +plugins/modules/ce_static_route_bfd.py validate-modules:doc-required-mismatch +plugins/modules/ce_static_route_bfd.py validate-modules:parameter-list-no-elements +plugins/modules/ce_stp.py future-import-boilerplate +plugins/modules/ce_stp.py metaclass-boilerplate +plugins/modules/ce_stp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_stp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_stp.py validate-modules:doc-missing-type +plugins/modules/ce_stp.py validate-modules:missing-suboption-docs +plugins/modules/ce_stp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_stp.py validate-modules:undocumented-parameter +plugins/modules/ce_switchport.py future-import-boilerplate +plugins/modules/ce_switchport.py metaclass-boilerplate +plugins/modules/ce_switchport.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_switchport.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_switchport.py validate-modules:doc-missing-type +plugins/modules/ce_switchport.py validate-modules:missing-suboption-docs +plugins/modules/ce_switchport.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_switchport.py validate-modules:undocumented-parameter +plugins/modules/ce_vlan.py future-import-boilerplate +plugins/modules/ce_vlan.py metaclass-boilerplate +plugins/modules/ce_vlan.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vlan.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vlan.py validate-modules:doc-missing-type +plugins/modules/ce_vlan.py validate-modules:missing-suboption-docs +plugins/modules/ce_vlan.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vlan.py validate-modules:undocumented-parameter +plugins/modules/ce_vrf.py future-import-boilerplate +plugins/modules/ce_vrf.py metaclass-boilerplate +plugins/modules/ce_vrf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrf.py validate-modules:doc-missing-type +plugins/modules/ce_vrf.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrf.py validate-modules:undocumented-parameter +plugins/modules/ce_vrf_af.py future-import-boilerplate +plugins/modules/ce_vrf_af.py metaclass-boilerplate +plugins/modules/ce_vrf_af.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrf_af.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrf_af.py validate-modules:doc-missing-type +plugins/modules/ce_vrf_af.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrf_af.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrf_af.py validate-modules:undocumented-parameter +plugins/modules/ce_vrf_interface.py future-import-boilerplate +plugins/modules/ce_vrf_interface.py metaclass-boilerplate +plugins/modules/ce_vrf_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrf_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrf_interface.py validate-modules:doc-missing-type +plugins/modules/ce_vrf_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrf_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrf_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_vrrp.py future-import-boilerplate +plugins/modules/ce_vrrp.py metaclass-boilerplate +plugins/modules/ce_vrrp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrrp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrrp.py validate-modules:doc-missing-type +plugins/modules/ce_vrrp.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrrp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrrp.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_arp.py future-import-boilerplate +plugins/modules/ce_vxlan_arp.py metaclass-boilerplate +plugins/modules/ce_vxlan_arp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_arp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_arp.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_arp.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_arp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_arp.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_gateway.py future-import-boilerplate +plugins/modules/ce_vxlan_gateway.py metaclass-boilerplate +plugins/modules/ce_vxlan_gateway.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_gateway.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_gateway.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_gateway.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_gateway.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_gateway.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_global.py future-import-boilerplate +plugins/modules/ce_vxlan_global.py metaclass-boilerplate +plugins/modules/ce_vxlan_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_global.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_global.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_tunnel.py future-import-boilerplate +plugins/modules/ce_vxlan_tunnel.py metaclass-boilerplate +plugins/modules/ce_vxlan_tunnel.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_tunnel.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_tunnel.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_tunnel.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_tunnel.py validate-modules:parameter-list-no-elements +plugins/modules/ce_vxlan_tunnel.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_tunnel.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_vap.py future-import-boilerplate +plugins/modules/ce_vxlan_vap.py metaclass-boilerplate +plugins/modules/ce_vxlan_vap.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_vap.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_vap.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_vap.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_vap.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_vap.py validate-modules:undocumented-parameter +plugins/action/ce.py action-plugin-docs # base class for deprecated network platform modules using `connection: local` +plugins/action/ce_template.py action-plugin-docs # undocumented action plugin to fix, existed before sanity test was added +plugins/doc_fragments/ce.py future-import-boilerplate +plugins/doc_fragments/ce.py metaclass-boilerplate +tests/unit/mock/path.py future-import-boilerplate +tests/unit/mock/path.py metaclass-boilerplate +tests/unit/mock/yaml_helper.py future-import-boilerplate +tests/unit/mock/yaml_helper.py metaclass-boilerplate +tests/unit/modules/utils.py future-import-boilerplate +tests/unit/modules/utils.py metaclass-boilerplate \ No newline at end of file diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt new file mode 100644 index 0000000..d834016 --- /dev/null +++ b/tests/sanity/ignore-2.9.txt @@ -0,0 +1,556 @@ +plugins/module_utils/network/cloudengine/ce.py future-import-boilerplate +plugins/module_utils/network/cloudengine/ce.py metaclass-boilerplate +plugins/modules/ce_aaa_server.py future-import-boilerplate +plugins/modules/ce_aaa_server.py metaclass-boilerplate +plugins/modules/ce_aaa_server.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_aaa_server.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_aaa_server.py validate-modules:doc-missing-type +plugins/modules/ce_aaa_server.py validate-modules:missing-suboption-docs +plugins/modules/ce_aaa_server.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_aaa_server.py validate-modules:undocumented-parameter +plugins/modules/ce_aaa_server_host.py future-import-boilerplate +plugins/modules/ce_aaa_server_host.py metaclass-boilerplate +plugins/modules/ce_aaa_server_host.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_aaa_server_host.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_aaa_server_host.py validate-modules:doc-missing-type +plugins/modules/ce_aaa_server_host.py validate-modules:missing-suboption-docs +plugins/modules/ce_aaa_server_host.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_aaa_server_host.py validate-modules:undocumented-parameter +plugins/modules/ce_acl.py future-import-boilerplate +plugins/modules/ce_acl.py metaclass-boilerplate +plugins/modules/ce_acl.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_acl.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_acl.py validate-modules:doc-missing-type +plugins/modules/ce_acl.py validate-modules:missing-suboption-docs +plugins/modules/ce_acl.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_acl.py validate-modules:undocumented-parameter +plugins/modules/ce_acl_advance.py future-import-boilerplate +plugins/modules/ce_acl_advance.py metaclass-boilerplate +plugins/modules/ce_acl_advance.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_acl_advance.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_acl_advance.py validate-modules:doc-missing-type +plugins/modules/ce_acl_advance.py validate-modules:missing-suboption-docs +plugins/modules/ce_acl_advance.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_acl_advance.py validate-modules:undocumented-parameter +plugins/modules/ce_acl_interface.py future-import-boilerplate +plugins/modules/ce_acl_interface.py metaclass-boilerplate +plugins/modules/ce_acl_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_acl_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_acl_interface.py validate-modules:doc-missing-type +plugins/modules/ce_acl_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_acl_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_acl_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_bfd_global.py future-import-boilerplate +plugins/modules/ce_bfd_global.py metaclass-boilerplate +plugins/modules/ce_bfd_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bfd_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bfd_global.py validate-modules:doc-missing-type +plugins/modules/ce_bfd_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_bfd_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bfd_global.py validate-modules:undocumented-parameter +plugins/modules/ce_bfd_session.py future-import-boilerplate +plugins/modules/ce_bfd_session.py metaclass-boilerplate +plugins/modules/ce_bfd_session.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bfd_session.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bfd_session.py validate-modules:doc-missing-type +plugins/modules/ce_bfd_session.py validate-modules:missing-suboption-docs +plugins/modules/ce_bfd_session.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bfd_session.py validate-modules:undocumented-parameter +plugins/modules/ce_bfd_view.py future-import-boilerplate +plugins/modules/ce_bfd_view.py metaclass-boilerplate +plugins/modules/ce_bfd_view.py validate-modules:doc-default-incompatible-type +plugins/modules/ce_bfd_view.py validate-modules:doc-missing-type +plugins/modules/ce_bfd_view.py validate-modules:doc-required-mismatch +plugins/modules/ce_bfd_view.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bfd_view.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp.py future-import-boilerplate +plugins/modules/ce_bgp.py metaclass-boilerplate +plugins/modules/ce_bgp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp.py validate-modules:doc-missing-type +plugins/modules/ce_bgp.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp_af.py future-import-boilerplate +plugins/modules/ce_bgp_af.py metaclass-boilerplate +plugins/modules/ce_bgp_af.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp_af.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp_af.py validate-modules:doc-missing-type +plugins/modules/ce_bgp_af.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp_af.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp_af.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp_neighbor.py future-import-boilerplate +plugins/modules/ce_bgp_neighbor.py metaclass-boilerplate +plugins/modules/ce_bgp_neighbor.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp_neighbor.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp_neighbor.py validate-modules:doc-missing-type +plugins/modules/ce_bgp_neighbor.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp_neighbor.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp_neighbor.py validate-modules:undocumented-parameter +plugins/modules/ce_bgp_neighbor_af.py future-import-boilerplate +plugins/modules/ce_bgp_neighbor_af.py metaclass-boilerplate +plugins/modules/ce_bgp_neighbor_af.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_bgp_neighbor_af.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_bgp_neighbor_af.py validate-modules:doc-missing-type +plugins/modules/ce_bgp_neighbor_af.py validate-modules:missing-suboption-docs +plugins/modules/ce_bgp_neighbor_af.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_bgp_neighbor_af.py validate-modules:undocumented-parameter +plugins/modules/ce_command.py future-import-boilerplate +plugins/modules/ce_command.py metaclass-boilerplate +plugins/modules/ce_command.py pylint:blacklisted-name +plugins/modules/ce_command.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_command.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_command.py validate-modules:doc-missing-type +plugins/modules/ce_command.py validate-modules:missing-suboption-docs +plugins/modules/ce_command.py validate-modules:parameter-list-no-elements +plugins/modules/ce_command.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_command.py validate-modules:undocumented-parameter +plugins/modules/ce_config.py future-import-boilerplate +plugins/modules/ce_config.py metaclass-boilerplate +plugins/modules/ce_config.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_config.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_config.py validate-modules:doc-missing-type +plugins/modules/ce_config.py validate-modules:missing-suboption-docs +plugins/modules/ce_config.py validate-modules:parameter-list-no-elements +plugins/modules/ce_config.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_config.py validate-modules:undocumented-parameter +plugins/modules/ce_dldp.py future-import-boilerplate +plugins/modules/ce_dldp.py metaclass-boilerplate +plugins/modules/ce_dldp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_dldp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_dldp.py validate-modules:doc-missing-type +plugins/modules/ce_dldp.py validate-modules:missing-suboption-docs +plugins/modules/ce_dldp.py validate-modules:nonexistent-parameter-documented +plugins/modules/ce_dldp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_dldp.py validate-modules:undocumented-parameter +plugins/modules/ce_dldp_interface.py future-import-boilerplate +plugins/modules/ce_dldp_interface.py metaclass-boilerplate +plugins/modules/ce_dldp_interface.py pylint:blacklisted-name +plugins/modules/ce_dldp_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_dldp_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_dldp_interface.py validate-modules:doc-missing-type +plugins/modules/ce_dldp_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_dldp_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_dldp_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_eth_trunk.py future-import-boilerplate +plugins/modules/ce_eth_trunk.py metaclass-boilerplate +plugins/modules/ce_eth_trunk.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_eth_trunk.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_eth_trunk.py validate-modules:doc-missing-type +plugins/modules/ce_eth_trunk.py validate-modules:missing-suboption-docs +plugins/modules/ce_eth_trunk.py validate-modules:parameter-list-no-elements +plugins/modules/ce_eth_trunk.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_eth_trunk.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_bd_vni.py future-import-boilerplate +plugins/modules/ce_evpn_bd_vni.py metaclass-boilerplate +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_bd_vni.py validate-modules:doc-required-mismatch +plugins/modules/ce_evpn_bd_vni.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_bd_vni.py validate-modules:parameter-list-no-elements +plugins/modules/ce_evpn_bd_vni.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_bd_vni.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_bgp.py future-import-boilerplate +plugins/modules/ce_evpn_bgp.py metaclass-boilerplate +plugins/modules/ce_evpn_bgp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_bgp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_bgp.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_bgp.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_bgp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_bgp.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_bgp_rr.py future-import-boilerplate +plugins/modules/ce_evpn_bgp_rr.py metaclass-boilerplate +plugins/modules/ce_evpn_bgp_rr.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_bgp_rr.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_bgp_rr.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_bgp_rr.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_bgp_rr.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_bgp_rr.py validate-modules:undocumented-parameter +plugins/modules/ce_evpn_global.py future-import-boilerplate +plugins/modules/ce_evpn_global.py metaclass-boilerplate +plugins/modules/ce_evpn_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_evpn_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_evpn_global.py validate-modules:doc-missing-type +plugins/modules/ce_evpn_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_evpn_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_evpn_global.py validate-modules:undocumented-parameter +plugins/modules/ce_facts.py future-import-boilerplate +plugins/modules/ce_facts.py metaclass-boilerplate +plugins/modules/ce_facts.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_facts.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_facts.py validate-modules:doc-missing-type +plugins/modules/ce_facts.py validate-modules:missing-suboption-docs +plugins/modules/ce_facts.py validate-modules:parameter-list-no-elements +plugins/modules/ce_facts.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_facts.py validate-modules:undocumented-parameter +plugins/modules/ce_file_copy.py future-import-boilerplate +plugins/modules/ce_file_copy.py metaclass-boilerplate +plugins/modules/ce_file_copy.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_file_copy.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_file_copy.py validate-modules:doc-missing-type +plugins/modules/ce_file_copy.py validate-modules:missing-suboption-docs +plugins/modules/ce_file_copy.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_file_copy.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_debug.py future-import-boilerplate +plugins/modules/ce_info_center_debug.py metaclass-boilerplate +plugins/modules/ce_info_center_debug.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_debug.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_debug.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_debug.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_debug.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_debug.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_global.py future-import-boilerplate +plugins/modules/ce_info_center_global.py metaclass-boilerplate +plugins/modules/ce_info_center_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_global.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_global.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_log.py future-import-boilerplate +plugins/modules/ce_info_center_log.py metaclass-boilerplate +plugins/modules/ce_info_center_log.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_log.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_log.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_log.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_log.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_log.py validate-modules:undocumented-parameter +plugins/modules/ce_info_center_trap.py future-import-boilerplate +plugins/modules/ce_info_center_trap.py metaclass-boilerplate +plugins/modules/ce_info_center_trap.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_info_center_trap.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_info_center_trap.py validate-modules:doc-missing-type +plugins/modules/ce_info_center_trap.py validate-modules:missing-suboption-docs +plugins/modules/ce_info_center_trap.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_info_center_trap.py validate-modules:undocumented-parameter +plugins/modules/ce_interface.py future-import-boilerplate +plugins/modules/ce_interface.py metaclass-boilerplate +plugins/modules/ce_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_interface.py validate-modules:doc-missing-type +plugins/modules/ce_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_interface_ospf.py future-import-boilerplate +plugins/modules/ce_interface_ospf.py metaclass-boilerplate +plugins/modules/ce_interface_ospf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_interface_ospf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_interface_ospf.py validate-modules:doc-missing-type +plugins/modules/ce_interface_ospf.py validate-modules:missing-suboption-docs +plugins/modules/ce_interface_ospf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_interface_ospf.py validate-modules:undocumented-parameter +plugins/modules/ce_ip_interface.py future-import-boilerplate +plugins/modules/ce_ip_interface.py metaclass-boilerplate +plugins/modules/ce_ip_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ip_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ip_interface.py validate-modules:doc-missing-type +plugins/modules/ce_ip_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_ip_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ip_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_is_is_view.py validate-modules:doc-required-mismatch +plugins/modules/ce_link_status.py future-import-boilerplate +plugins/modules/ce_link_status.py metaclass-boilerplate +plugins/modules/ce_link_status.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_link_status.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_link_status.py validate-modules:doc-missing-type +plugins/modules/ce_link_status.py validate-modules:missing-suboption-docs +plugins/modules/ce_link_status.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_link_status.py validate-modules:undocumented-parameter +plugins/modules/ce_mlag_config.py future-import-boilerplate +plugins/modules/ce_mlag_config.py metaclass-boilerplate +plugins/modules/ce_mlag_config.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_mlag_config.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_mlag_config.py validate-modules:doc-missing-type +plugins/modules/ce_mlag_config.py validate-modules:missing-suboption-docs +plugins/modules/ce_mlag_config.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_mlag_config.py validate-modules:undocumented-parameter +plugins/modules/ce_mlag_interface.py future-import-boilerplate +plugins/modules/ce_mlag_interface.py metaclass-boilerplate +plugins/modules/ce_mlag_interface.py pylint:blacklisted-name +plugins/modules/ce_mlag_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_mlag_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_mlag_interface.py validate-modules:doc-missing-type +plugins/modules/ce_mlag_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_mlag_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_mlag_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_mtu.py future-import-boilerplate +plugins/modules/ce_mtu.py metaclass-boilerplate +plugins/modules/ce_mtu.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_mtu.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_mtu.py validate-modules:doc-missing-type +plugins/modules/ce_mtu.py validate-modules:doc-required-mismatch +plugins/modules/ce_mtu.py validate-modules:missing-suboption-docs +plugins/modules/ce_mtu.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_mtu.py validate-modules:undocumented-parameter +plugins/modules/ce_netconf.py future-import-boilerplate +plugins/modules/ce_netconf.py metaclass-boilerplate +plugins/modules/ce_netconf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netconf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netconf.py validate-modules:doc-missing-type +plugins/modules/ce_netconf.py validate-modules:missing-suboption-docs +plugins/modules/ce_netconf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netconf.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_aging.py future-import-boilerplate +plugins/modules/ce_netstream_aging.py metaclass-boilerplate +plugins/modules/ce_netstream_aging.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_aging.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_aging.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_aging.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_aging.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_aging.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_export.py future-import-boilerplate +plugins/modules/ce_netstream_export.py metaclass-boilerplate +plugins/modules/ce_netstream_export.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_export.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_export.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_export.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_export.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_export.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_global.py future-import-boilerplate +plugins/modules/ce_netstream_global.py metaclass-boilerplate +plugins/modules/ce_netstream_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_global.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_global.py validate-modules:undocumented-parameter +plugins/modules/ce_netstream_template.py future-import-boilerplate +plugins/modules/ce_netstream_template.py metaclass-boilerplate +plugins/modules/ce_netstream_template.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_netstream_template.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_netstream_template.py validate-modules:doc-missing-type +plugins/modules/ce_netstream_template.py validate-modules:missing-suboption-docs +plugins/modules/ce_netstream_template.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_netstream_template.py validate-modules:undocumented-parameter +plugins/modules/ce_ntp.py future-import-boilerplate +plugins/modules/ce_ntp.py metaclass-boilerplate +plugins/modules/ce_ntp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ntp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ntp.py validate-modules:doc-missing-type +plugins/modules/ce_ntp.py validate-modules:missing-suboption-docs +plugins/modules/ce_ntp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ntp.py validate-modules:undocumented-parameter +plugins/modules/ce_ntp_auth.py future-import-boilerplate +plugins/modules/ce_ntp_auth.py metaclass-boilerplate +plugins/modules/ce_ntp_auth.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ntp_auth.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ntp_auth.py validate-modules:doc-missing-type +plugins/modules/ce_ntp_auth.py validate-modules:missing-suboption-docs +plugins/modules/ce_ntp_auth.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ntp_auth.py validate-modules:undocumented-parameter +plugins/modules/ce_ospf.py future-import-boilerplate +plugins/modules/ce_ospf.py metaclass-boilerplate +plugins/modules/ce_ospf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ospf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ospf.py validate-modules:doc-missing-type +plugins/modules/ce_ospf.py validate-modules:missing-suboption-docs +plugins/modules/ce_ospf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ospf.py validate-modules:undocumented-parameter +plugins/modules/ce_ospf_vrf.py future-import-boilerplate +plugins/modules/ce_ospf_vrf.py metaclass-boilerplate +plugins/modules/ce_ospf_vrf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_ospf_vrf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_ospf_vrf.py validate-modules:doc-missing-type +plugins/modules/ce_ospf_vrf.py validate-modules:missing-suboption-docs +plugins/modules/ce_ospf_vrf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_ospf_vrf.py validate-modules:undocumented-parameter +plugins/modules/ce_reboot.py future-import-boilerplate +plugins/modules/ce_reboot.py metaclass-boilerplate +plugins/modules/ce_reboot.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_reboot.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_reboot.py validate-modules:doc-missing-type +plugins/modules/ce_reboot.py validate-modules:missing-suboption-docs +plugins/modules/ce_reboot.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_reboot.py validate-modules:undocumented-parameter +plugins/modules/ce_rollback.py future-import-boilerplate +plugins/modules/ce_rollback.py metaclass-boilerplate +plugins/modules/ce_rollback.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_rollback.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_rollback.py validate-modules:doc-missing-type +plugins/modules/ce_rollback.py validate-modules:doc-required-mismatch +plugins/modules/ce_rollback.py validate-modules:missing-suboption-docs +plugins/modules/ce_rollback.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_rollback.py validate-modules:undocumented-parameter +plugins/modules/ce_sflow.py future-import-boilerplate +plugins/modules/ce_sflow.py metaclass-boilerplate +plugins/modules/ce_sflow.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_sflow.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_sflow.py validate-modules:doc-missing-type +plugins/modules/ce_sflow.py validate-modules:missing-suboption-docs +plugins/modules/ce_sflow.py validate-modules:parameter-list-no-elements +plugins/modules/ce_sflow.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_sflow.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_community.py future-import-boilerplate +plugins/modules/ce_snmp_community.py metaclass-boilerplate +plugins/modules/ce_snmp_community.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_community.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_community.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_community.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_community.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_community.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_contact.py future-import-boilerplate +plugins/modules/ce_snmp_contact.py metaclass-boilerplate +plugins/modules/ce_snmp_contact.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_contact.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_contact.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_contact.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_contact.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_contact.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_location.py future-import-boilerplate +plugins/modules/ce_snmp_location.py metaclass-boilerplate +plugins/modules/ce_snmp_location.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_location.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_location.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_location.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_location.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_location.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_target_host.py future-import-boilerplate +plugins/modules/ce_snmp_target_host.py metaclass-boilerplate +plugins/modules/ce_snmp_target_host.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_target_host.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_target_host.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_target_host.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_target_host.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_target_host.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_traps.py future-import-boilerplate +plugins/modules/ce_snmp_traps.py metaclass-boilerplate +plugins/modules/ce_snmp_traps.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_traps.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_traps.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_traps.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_traps.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_traps.py validate-modules:undocumented-parameter +plugins/modules/ce_snmp_user.py future-import-boilerplate +plugins/modules/ce_snmp_user.py metaclass-boilerplate +plugins/modules/ce_snmp_user.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_snmp_user.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_snmp_user.py validate-modules:doc-missing-type +plugins/modules/ce_snmp_user.py validate-modules:missing-suboption-docs +plugins/modules/ce_snmp_user.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_snmp_user.py validate-modules:undocumented-parameter +plugins/modules/ce_startup.py future-import-boilerplate +plugins/modules/ce_startup.py metaclass-boilerplate +plugins/modules/ce_startup.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_startup.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_startup.py validate-modules:doc-missing-type +plugins/modules/ce_startup.py validate-modules:missing-suboption-docs +plugins/modules/ce_startup.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_startup.py validate-modules:undocumented-parameter +plugins/modules/ce_static_route.py future-import-boilerplate +plugins/modules/ce_static_route.py metaclass-boilerplate +plugins/modules/ce_static_route.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_static_route.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_static_route.py validate-modules:doc-missing-type +plugins/modules/ce_static_route.py validate-modules:missing-suboption-docs +plugins/modules/ce_static_route.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_static_route.py validate-modules:undocumented-parameter +plugins/modules/ce_static_route_bfd.py validate-modules:doc-required-mismatch +plugins/modules/ce_static_route_bfd.py validate-modules:parameter-list-no-elements +plugins/modules/ce_stp.py future-import-boilerplate +plugins/modules/ce_stp.py metaclass-boilerplate +plugins/modules/ce_stp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_stp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_stp.py validate-modules:doc-missing-type +plugins/modules/ce_stp.py validate-modules:missing-suboption-docs +plugins/modules/ce_stp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_stp.py validate-modules:undocumented-parameter +plugins/modules/ce_switchport.py future-import-boilerplate +plugins/modules/ce_switchport.py metaclass-boilerplate +plugins/modules/ce_switchport.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_switchport.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_switchport.py validate-modules:doc-missing-type +plugins/modules/ce_switchport.py validate-modules:missing-suboption-docs +plugins/modules/ce_switchport.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_switchport.py validate-modules:undocumented-parameter +plugins/modules/ce_vlan.py future-import-boilerplate +plugins/modules/ce_vlan.py metaclass-boilerplate +plugins/modules/ce_vlan.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vlan.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vlan.py validate-modules:doc-missing-type +plugins/modules/ce_vlan.py validate-modules:missing-suboption-docs +plugins/modules/ce_vlan.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vlan.py validate-modules:undocumented-parameter +plugins/modules/ce_vrf.py future-import-boilerplate +plugins/modules/ce_vrf.py metaclass-boilerplate +plugins/modules/ce_vrf.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrf.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrf.py validate-modules:doc-missing-type +plugins/modules/ce_vrf.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrf.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrf.py validate-modules:undocumented-parameter +plugins/modules/ce_vrf_af.py future-import-boilerplate +plugins/modules/ce_vrf_af.py metaclass-boilerplate +plugins/modules/ce_vrf_af.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrf_af.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrf_af.py validate-modules:doc-missing-type +plugins/modules/ce_vrf_af.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrf_af.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrf_af.py validate-modules:undocumented-parameter +plugins/modules/ce_vrf_interface.py future-import-boilerplate +plugins/modules/ce_vrf_interface.py metaclass-boilerplate +plugins/modules/ce_vrf_interface.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrf_interface.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrf_interface.py validate-modules:doc-missing-type +plugins/modules/ce_vrf_interface.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrf_interface.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrf_interface.py validate-modules:undocumented-parameter +plugins/modules/ce_vrrp.py future-import-boilerplate +plugins/modules/ce_vrrp.py metaclass-boilerplate +plugins/modules/ce_vrrp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vrrp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vrrp.py validate-modules:doc-missing-type +plugins/modules/ce_vrrp.py validate-modules:missing-suboption-docs +plugins/modules/ce_vrrp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vrrp.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_arp.py future-import-boilerplate +plugins/modules/ce_vxlan_arp.py metaclass-boilerplate +plugins/modules/ce_vxlan_arp.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_arp.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_arp.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_arp.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_arp.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_arp.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_gateway.py future-import-boilerplate +plugins/modules/ce_vxlan_gateway.py metaclass-boilerplate +plugins/modules/ce_vxlan_gateway.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_gateway.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_gateway.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_gateway.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_gateway.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_gateway.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_global.py future-import-boilerplate +plugins/modules/ce_vxlan_global.py metaclass-boilerplate +plugins/modules/ce_vxlan_global.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_global.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_global.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_global.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_global.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_global.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_tunnel.py future-import-boilerplate +plugins/modules/ce_vxlan_tunnel.py metaclass-boilerplate +plugins/modules/ce_vxlan_tunnel.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_tunnel.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_tunnel.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_tunnel.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_tunnel.py validate-modules:parameter-list-no-elements +plugins/modules/ce_vxlan_tunnel.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_tunnel.py validate-modules:undocumented-parameter +plugins/modules/ce_vxlan_vap.py future-import-boilerplate +plugins/modules/ce_vxlan_vap.py metaclass-boilerplate +plugins/modules/ce_vxlan_vap.py validate-modules:doc-choices-do-not-match-spec +plugins/modules/ce_vxlan_vap.py validate-modules:doc-default-does-not-match-spec +plugins/modules/ce_vxlan_vap.py validate-modules:doc-missing-type +plugins/modules/ce_vxlan_vap.py validate-modules:missing-suboption-docs +plugins/modules/ce_vxlan_vap.py validate-modules:parameter-type-not-in-doc +plugins/modules/ce_vxlan_vap.py validate-modules:undocumented-parameter +plugins/action/ce.py action-plugin-docs # base class for deprecated network platform modules using `connection: local` +plugins/action/ce_template.py action-plugin-docs # undocumented action plugin to fix, existed before sanity test was added +plugins/doc_fragments/ce.py future-import-boilerplate +plugins/doc_fragments/ce.py metaclass-boilerplate +tests/unit/mock/path.py future-import-boilerplate +tests/unit/mock/path.py metaclass-boilerplate +tests/unit/mock/yaml_helper.py future-import-boilerplate +tests/unit/mock/yaml_helper.py metaclass-boilerplate +tests/unit/modules/utils.py future-import-boilerplate +tests/unit/modules/utils.py metaclass-boilerplate \ No newline at end of file diff --git a/tests/sanity/requirements.txt b/tests/sanity/requirements.txt new file mode 100644 index 0000000..3e3a966 --- /dev/null +++ b/tests/sanity/requirements.txt @@ -0,0 +1,4 @@ +packaging # needed for update-bundled and changelog +sphinx ; python_version >= '3.5' # docs build requires python 3+ +sphinx-notfound-page ; python_version >= '3.5' # docs build requires python 3+ +straight.plugin ; python_version >= '3.5' # needed for hacking/build-ansible.py which will host changelog generation and requires python 3+ diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/compat/__init__.py b/tests/unit/compat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/compat/builtins.py b/tests/unit/compat/builtins.py new file mode 100644 index 0000000..f60ee67 --- /dev/null +++ b/tests/unit/compat/builtins.py @@ -0,0 +1,33 @@ +# (c) 2014, Toshio Kuratomi +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# +# Compat for python2.7 +# + +# One unittest needs to import builtins via __import__() so we need to have +# the string that represents it +try: + import __builtin__ +except ImportError: + BUILTINS = 'builtins' +else: + BUILTINS = '__builtin__' diff --git a/tests/unit/compat/mock.py b/tests/unit/compat/mock.py new file mode 100644 index 0000000..0972cd2 --- /dev/null +++ b/tests/unit/compat/mock.py @@ -0,0 +1,122 @@ +# (c) 2014, Toshio Kuratomi +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python3.x's unittest.mock module +''' +import sys + +# Python 2.7 + +# Note: Could use the pypi mock library on python3.x as well as python2.x. It +# is the same as the python3 stdlib mock library + +try: + # Allow wildcard import because we really do want to import all of mock's + # symbols into this compat shim + # pylint: disable=wildcard-import,unused-wildcard-import + from unittest.mock import * +except ImportError: + # Python 2 + # pylint: disable=wildcard-import,unused-wildcard-import + try: + from mock import * + except ImportError: + print('You need the mock library installed on python2.x to run tests') + + +# Prior to 3.4.4, mock_open cannot handle binary read_data +if sys.version_info >= (3,) and sys.version_info < (3, 4, 4): + file_spec = None + + def _iterate_read_data(read_data): + # Helper for mock_open: + # Retrieve lines from read_data via a generator so that separate calls to + # readline, read, and readlines are properly interleaved + sep = b'\n' if isinstance(read_data, bytes) else '\n' + data_as_list = [l + sep for l in read_data.split(sep)] + + if data_as_list[-1] == sep: + # If the last line ended in a newline, the list comprehension will have an + # extra entry that's just a newline. Remove this. + data_as_list = data_as_list[:-1] + else: + # If there wasn't an extra newline by itself, then the file being + # emulated doesn't have a newline to end the last line remove the + # newline that our naive format() added + data_as_list[-1] = data_as_list[-1][:-1] + + for line in data_as_list: + yield line + + def mock_open(mock=None, read_data=''): + """ + A helper function to create a mock to replace the use of `open`. It works + for `open` called directly or used as a context manager. + + The `mock` argument is the mock object to configure. If `None` (the + default) then a `MagicMock` will be created for you, with the API limited + to methods or attributes available on standard file handles. + + `read_data` is a string for the `read` methoddline`, and `readlines` of the + file handle to return. This is an empty string by default. + """ + def _readlines_side_effect(*args, **kwargs): + if handle.readlines.return_value is not None: + return handle.readlines.return_value + return list(_data) + + def _read_side_effect(*args, **kwargs): + if handle.read.return_value is not None: + return handle.read.return_value + return type(read_data)().join(_data) + + def _readline_side_effect(): + if handle.readline.return_value is not None: + while True: + yield handle.readline.return_value + for line in _data: + yield line + + global file_spec + if file_spec is None: + import _io + file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) + + if mock is None: + mock = MagicMock(name='open', spec=open) + + handle = MagicMock(spec=file_spec) + handle.__enter__.return_value = handle + + _data = _iterate_read_data(read_data) + + handle.write.return_value = None + handle.read.return_value = None + handle.readline.return_value = None + handle.readlines.return_value = None + + handle.read.side_effect = _read_side_effect + handle.readline.side_effect = _readline_side_effect() + handle.readlines.side_effect = _readlines_side_effect + + mock.return_value = handle + return mock diff --git a/tests/unit/compat/unittest.py b/tests/unit/compat/unittest.py new file mode 100644 index 0000000..98f08ad --- /dev/null +++ b/tests/unit/compat/unittest.py @@ -0,0 +1,38 @@ +# (c) 2014, Toshio Kuratomi +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python2.7's unittest module +''' + +import sys + +# Allow wildcard import because we really do want to import all of +# unittests's symbols into this compat shim +# pylint: disable=wildcard-import,unused-wildcard-import +if sys.version_info < (2, 7): + try: + # Need unittest2 on python2.6 + from unittest2 import * + except ImportError: + print('You need unittest2 installed on python2.6.x to run tests') +else: + from unittest import * diff --git a/tests/unit/mock/__init__.py b/tests/unit/mock/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/mock/loader.py b/tests/unit/mock/loader.py new file mode 100644 index 0000000..0ee47fb --- /dev/null +++ b/tests/unit/mock/loader.py @@ -0,0 +1,116 @@ +# (c) 2012-2014, Michael DeHaan +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +from ansible.errors import AnsibleParserError +from ansible.parsing.dataloader import DataLoader +from ansible.module_utils._text import to_bytes, to_text + + +class DictDataLoader(DataLoader): + + def __init__(self, file_mapping=None): + file_mapping = {} if file_mapping is None else file_mapping + assert type(file_mapping) == dict + + super(DictDataLoader, self).__init__() + + self._file_mapping = file_mapping + self._build_known_directories() + self._vault_secrets = None + + def load_from_file(self, path, cache=True, unsafe=False): + path = to_text(path) + if path in self._file_mapping: + return self.load(self._file_mapping[path], path) + return None + + # TODO: the real _get_file_contents returns a bytestring, so we actually convert the + # unicode/text it's created with to utf-8 + def _get_file_contents(self, path): + path = to_text(path) + if path in self._file_mapping: + return (to_bytes(self._file_mapping[path]), False) + else: + raise AnsibleParserError("file not found: %s" % path) + + def path_exists(self, path): + path = to_text(path) + return path in self._file_mapping or path in self._known_directories + + def is_file(self, path): + path = to_text(path) + return path in self._file_mapping + + def is_directory(self, path): + path = to_text(path) + return path in self._known_directories + + def list_directory(self, path): + ret = [] + path = to_text(path) + for x in (list(self._file_mapping.keys()) + self._known_directories): + if x.startswith(path): + if os.path.dirname(x) == path: + ret.append(os.path.basename(x)) + return ret + + def is_executable(self, path): + # FIXME: figure out a way to make paths return true for this + return False + + def _add_known_directory(self, directory): + if directory not in self._known_directories: + self._known_directories.append(directory) + + def _build_known_directories(self): + self._known_directories = [] + for path in self._file_mapping: + dirname = os.path.dirname(path) + while dirname not in ('/', ''): + self._add_known_directory(dirname) + dirname = os.path.dirname(dirname) + + def push(self, path, content): + rebuild_dirs = False + if path not in self._file_mapping: + rebuild_dirs = True + + self._file_mapping[path] = content + + if rebuild_dirs: + self._build_known_directories() + + def pop(self, path): + if path in self._file_mapping: + del self._file_mapping[path] + self._build_known_directories() + + def clear(self): + self._file_mapping = dict() + self._known_directories = [] + + def get_basedir(self): + return os.getcwd() + + def set_vault_secrets(self, vault_secrets): + self._vault_secrets = vault_secrets diff --git a/tests/unit/mock/path.py b/tests/unit/mock/path.py new file mode 100644 index 0000000..7ccf058 --- /dev/null +++ b/tests/unit/mock/path.py @@ -0,0 +1,5 @@ +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import MagicMock +from ansible.utils.path import unfrackpath + + +mock_unfrackpath_noop = MagicMock(spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x) diff --git a/tests/unit/mock/procenv.py b/tests/unit/mock/procenv.py new file mode 100644 index 0000000..f64cd6d --- /dev/null +++ b/tests/unit/mock/procenv.py @@ -0,0 +1,90 @@ +# (c) 2016, Matt Davis +# (c) 2016, Toshio Kuratomi +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import json + +from contextlib import contextmanager +from io import BytesIO, StringIO +from ansible_collections.huawei.cloudengine.tests.unit.compat import unittest +from ansible.module_utils.six import PY3 +from ansible.module_utils._text import to_bytes + + +@contextmanager +def swap_stdin_and_argv(stdin_data='', argv_data=tuple()): + """ + context manager that temporarily masks the test runner's values for stdin and argv + """ + real_stdin = sys.stdin + real_argv = sys.argv + + if PY3: + fake_stream = StringIO(stdin_data) + fake_stream.buffer = BytesIO(to_bytes(stdin_data)) + else: + fake_stream = BytesIO(to_bytes(stdin_data)) + + try: + sys.stdin = fake_stream + sys.argv = argv_data + + yield + finally: + sys.stdin = real_stdin + sys.argv = real_argv + + +@contextmanager +def swap_stdout(): + """ + context manager that temporarily replaces stdout for tests that need to verify output + """ + old_stdout = sys.stdout + + if PY3: + fake_stream = StringIO() + else: + fake_stream = BytesIO() + + try: + sys.stdout = fake_stream + + yield fake_stream + finally: + sys.stdout = old_stdout + + +class ModuleTestCase(unittest.TestCase): + def setUp(self, module_args=None): + if module_args is None: + module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False} + + args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args)) + + # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually + self.stdin_swap = swap_stdin_and_argv(stdin_data=args) + self.stdin_swap.__enter__() + + def tearDown(self): + # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually + self.stdin_swap.__exit__(None, None, None) diff --git a/tests/unit/mock/vault_helper.py b/tests/unit/mock/vault_helper.py new file mode 100644 index 0000000..dcce9c7 --- /dev/null +++ b/tests/unit/mock/vault_helper.py @@ -0,0 +1,39 @@ +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils._text import to_bytes + +from ansible.parsing.vault import VaultSecret + + +class TextVaultSecret(VaultSecret): + '''A secret piece of text. ie, a password. Tracks text encoding. + + The text encoding of the text may not be the default text encoding so + we keep track of the encoding so we encode it to the same bytes.''' + + def __init__(self, text, encoding=None, errors=None, _bytes=None): + super(TextVaultSecret, self).__init__() + self.text = text + self.encoding = encoding or 'utf-8' + self._bytes = _bytes + self.errors = errors or 'strict' + + @property + def bytes(self): + '''The text encoded with encoding, unless we specifically set _bytes.''' + return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors) diff --git a/tests/unit/mock/yaml_helper.py b/tests/unit/mock/yaml_helper.py new file mode 100644 index 0000000..cc095fe --- /dev/null +++ b/tests/unit/mock/yaml_helper.py @@ -0,0 +1,121 @@ +import io +import yaml + +from ansible.module_utils.six import PY3 +from ansible.parsing.yaml.loader import AnsibleLoader +from ansible.parsing.yaml.dumper import AnsibleDumper + + +class YamlTestUtils(object): + """Mixin class to combine with a unittest.TestCase subclass.""" + def _loader(self, stream): + """Vault related tests will want to override this. + + Vault cases should setup a AnsibleLoader that has the vault password.""" + return AnsibleLoader(stream) + + def _dump_stream(self, obj, stream, dumper=None): + """Dump to a py2-unicode or py3-string stream.""" + if PY3: + return yaml.dump(obj, stream, Dumper=dumper) + else: + return yaml.dump(obj, stream, Dumper=dumper, encoding=None) + + def _dump_string(self, obj, dumper=None): + """Dump to a py2-unicode or py3-string""" + if PY3: + return yaml.dump(obj, Dumper=dumper) + else: + return yaml.dump(obj, Dumper=dumper, encoding=None) + + def _dump_load_cycle(self, obj): + # Each pass though a dump or load revs the 'generation' + # obj to yaml string + string_from_object_dump = self._dump_string(obj, dumper=AnsibleDumper) + + # wrap a stream/file like StringIO around that yaml + stream_from_object_dump = io.StringIO(string_from_object_dump) + loader = self._loader(stream_from_object_dump) + # load the yaml stream to create a new instance of the object (gen 2) + obj_2 = loader.get_data() + + # dump the gen 2 objects directory to strings + string_from_object_dump_2 = self._dump_string(obj_2, + dumper=AnsibleDumper) + + # The gen 1 and gen 2 yaml strings + self.assertEqual(string_from_object_dump, string_from_object_dump_2) + # the gen 1 (orig) and gen 2 py object + self.assertEqual(obj, obj_2) + + # again! gen 3... load strings into py objects + stream_3 = io.StringIO(string_from_object_dump_2) + loader_3 = self._loader(stream_3) + obj_3 = loader_3.get_data() + + string_from_object_dump_3 = self._dump_string(obj_3, dumper=AnsibleDumper) + + self.assertEqual(obj, obj_3) + # should be transitive, but... + self.assertEqual(obj_2, obj_3) + self.assertEqual(string_from_object_dump, string_from_object_dump_3) + + def _old_dump_load_cycle(self, obj): + '''Dump the passed in object to yaml, load it back up, dump again, compare.''' + stream = io.StringIO() + + yaml_string = self._dump_string(obj, dumper=AnsibleDumper) + self._dump_stream(obj, stream, dumper=AnsibleDumper) + + yaml_string_from_stream = stream.getvalue() + + # reset stream + stream.seek(0) + + loader = self._loader(stream) + # loader = AnsibleLoader(stream, vault_password=self.vault_password) + obj_from_stream = loader.get_data() + + stream_from_string = io.StringIO(yaml_string) + loader2 = self._loader(stream_from_string) + # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password) + obj_from_string = loader2.get_data() + + stream_obj_from_stream = io.StringIO() + stream_obj_from_string = io.StringIO() + + if PY3: + yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) + yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) + else: + yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None) + yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None) + + yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() + yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() + + stream_obj_from_stream.seek(0) + stream_obj_from_string.seek(0) + + if PY3: + yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) + yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) + else: + yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None) + yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None) + + assert yaml_string == yaml_string_obj_from_stream + assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string + assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream == + yaml_string_stream_obj_from_string) + assert obj == obj_from_stream + assert obj == obj_from_string + assert obj == yaml_string_obj_from_stream + assert obj == yaml_string_obj_from_string + assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string + return {'obj': obj, + 'yaml_string': yaml_string, + 'yaml_string_from_stream': yaml_string_from_stream, + 'obj_from_stream': obj_from_stream, + 'obj_from_string': obj_from_string, + 'yaml_string_obj_from_string': yaml_string_obj_from_string} diff --git a/tests/unit/modules/__init__.py b/tests/unit/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/modules/network/__init__.py b/tests/unit/modules/network/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/modules/network/cloudengine/__init__.py b/tests/unit/modules/network/cloudengine/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/modules/network/cloudengine/ce_module.py b/tests/unit/modules/network/cloudengine/ce_module.py new file mode 100644 index 0000000..0bf36a2 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/ce_module.py @@ -0,0 +1,90 @@ +# Copyright (c) 2019 Red Hat +# +# This file is a 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 . +# + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(module_name, name, device=''): + path = os.path.join(fixture_path, module_name, device, name) + if not os.path.exists(path): + path = os.path.join(fixture_path, module_name, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestCloudEngineModule(ModuleTestCase): + + def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False): + + self.load_fixtures(commands) + + if failed: + result = self.failed() + self.assertTrue(result['failed'], result) + else: + result = self.changed(changed) + self.assertEqual(result['changed'], changed, result) + + if commands is not None: + if sort: + self.assertEqual(sorted(commands), sorted(result['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands'], result['commands']) + + return result + + def failed(self): + with self.assertRaises(AnsibleFailJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertTrue(result['failed'], result) + return result + + def changed(self, changed=False): + with self.assertRaises(AnsibleExitJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], changed, result) + return result + + def load_fixtures(self, commands=None): + pass diff --git a/tests/unit/modules/network/cloudengine/fixtures/__init__.py b/tests/unit/modules/network/cloudengine/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_instance/after.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_instance/after.txt new file mode 100644 index 0000000..b8ff89b --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_instance/after.txt @@ -0,0 +1,11 @@ + + + + + 100 + _public_ + ISIS + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_instance/before.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_instance/before.txt new file mode 100644 index 0000000..618bbf0 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_instance/before.txt @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_interface/after_interface.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_interface/after_interface.txt new file mode 100644 index 0000000..f7aee10 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_interface/after_interface.txt @@ -0,0 +1,26 @@ + + + + + 100 + + + + level_1 + 10 + 10 + true + true + true + true + 2_way + true + true + 10 + 10 + + + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_interface/before_interface.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_interface/before_interface.txt new file mode 100644 index 0000000..0a07d5e --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_interface/before_interface.txt @@ -0,0 +1,26 @@ + + + + + 100 + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_view/after.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_view/after.txt new file mode 100644 index 0000000..8da6a62 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_view/after.txt @@ -0,0 +1,104 @@ + + + + + 100 + _public_ + ISIS + level_1 + narrow + true + 60 + 60 + 100 + true + true + + + netentity + + + + + afIpv4 + 0 + 100 + 100 + 10 + 32 + + + 100 + route + + + + + 1.1.1.1 + 100 + + + + + 3001 + ip + route + level_1 + + + + + ospf + 100 + level_1 + + + + + always + mode + 100 + 100 + level_1 + true + + + + + import + 100 + level_1 + 100 + 100 + level_1 + import + level_1 + 100 + true + + + + + 100 + route + 3001 + ip + true + true + + + + + 100 + route + 3001 + ip + true + + + + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_view/before.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_view/before.txt new file mode 100644 index 0000000..abdb8e7 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_is_is_view/before.txt @@ -0,0 +1,10 @@ + + + + + 100 + _public_ + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_00.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_00.txt new file mode 100644 index 0000000..974c52c --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_00.txt @@ -0,0 +1,26 @@ + + + + + + + Eth-Trunk10 + + false + Fast + 3 + Speed + 30 + 1 + 0 + false + false + false + 11-22-33 + false + + + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_01.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_01.txt new file mode 100644 index 0000000..03b3f31 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_01.txt @@ -0,0 +1,26 @@ + + + + + + + Eth-Trunk10 + + true + Fast + 10 + Speed + 130 + 13 + 12 + true + true + true + 0000-1111-2222 + true + + + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_10.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_10.txt new file mode 100644 index 0000000..6abbbbf --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_10.txt @@ -0,0 +1,10 @@ + + + + + + 32768 + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_11.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_11.txt new file mode 100644 index 0000000..22260aa --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lacp/ce_lacp_11.txt @@ -0,0 +1,10 @@ + + + + + + 32769 + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldpSysParameter_00.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldpSysParameter_00.txt new file mode 100644 index 0000000..051d71e --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldpSysParameter_00.txt @@ -0,0 +1,21 @@ + + + + + + + 30 + 4 + 2 + 2 + 5 + 4 + 5 + disabled + + + + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldpSysParameter_01.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldpSysParameter_01.txt new file mode 100644 index 0000000..4fde2b5 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldpSysParameter_01.txt @@ -0,0 +1,21 @@ + + + + + + + 8 + 8 + 8 + 8 + 8 + 8 + 8 + enabled + 1.1.1.1 + bind-name + + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldp_global_00.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldp_global_00.txt new file mode 100644 index 0000000..b540a21 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldp_global_00.txt @@ -0,0 +1,11 @@ + + + + + + disabled + disabled + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldp_global_01.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldp_global_01.txt new file mode 100644 index 0000000..62d1228 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/ce_lldp_global_01.txt @@ -0,0 +1,11 @@ + + + + + + enabled + rxOnly + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/result_ok.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/result_ok.txt new file mode 100644 index 0000000..5e245cf --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp/result_ok.txt @@ -0,0 +1,3 @@ + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/lldp_interface_changed.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/lldp_interface_changed.txt new file mode 100644 index 0000000..6105280 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/lldp_interface_changed.txt @@ -0,0 +1,29 @@ + + + + + 10GE1/0/1 + txAndRx + + 8 + + + true + true + true + true + true + true + true + 112 + true + 32 + true + true + true + true + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/lldp_interface_existing.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/lldp_interface_existing.txt new file mode 100644 index 0000000..3b6155f --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/lldp_interface_existing.txt @@ -0,0 +1,29 @@ + + + + + 10GE1/0/1 + txOnly + + 1 + + + false + false + false + false + false + false + false + + false + + false + false + false + + + + + + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/result_ok.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/result_ok.txt new file mode 100644 index 0000000..5e245cf --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_lldp_interface/result_ok.txt @@ -0,0 +1,3 @@ + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_mdn_interface/after.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_mdn_interface/after.txt new file mode 100644 index 0000000..7cbc500 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_mdn_interface/after.txt @@ -0,0 +1,14 @@ + + + + enabled + enabled + + + + 10GE1/0/1 + rxOnly + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_mdn_interface/before.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_mdn_interface/before.txt new file mode 100644 index 0000000..885dc90 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_mdn_interface/before.txt @@ -0,0 +1,14 @@ + + + + disabled + disabled + + + + 10GE1/0/1 + disabled + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_global/after.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_global/after.txt new file mode 100644 index 0000000..b196031 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_global/after.txt @@ -0,0 +1,10 @@ + + + + + vpna + ipv4unicast + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_global/before.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_global/before.txt new file mode 100644 index 0000000..fe6c839 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_global/before.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_igmp_enable/after.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_igmp_enable/after.txt new file mode 100644 index 0000000..3af30de --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_igmp_enable/after.txt @@ -0,0 +1,22 @@ + + + + + ipv4unicast + false + 192.168.0.1 + + + + + + ipv4unicast + 1 + 2 + true + true + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_igmp_enable/before.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_igmp_enable/before.txt new file mode 100644 index 0000000..fe6c839 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_multicast_igmp_enable/before.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/result_ok.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/result_ok.txt new file mode 100644 index 0000000..5e245cf --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/result_ok.txt @@ -0,0 +1,3 @@ + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/srBfdPara_1.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/srBfdPara_1.txt new file mode 100644 index 0000000..6e5e930 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/srBfdPara_1.txt @@ -0,0 +1,18 @@ + + + + + + ipv4unicast + Ethernet3/0/0 + _public_ + 192.168.2.2 + 192.168.2.1 + 50 + 50 + 3 + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/srBfdPara_2.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/srBfdPara_2.txt new file mode 100644 index 0000000..6e5e930 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/srBfdPara_2.txt @@ -0,0 +1,18 @@ + + + + + + ipv4unicast + Ethernet3/0/0 + _public_ + 192.168.2.2 + 192.168.2.1 + 50 + 50 + 3 + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/staticrtbase_1.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/staticrtbase_1.txt new file mode 100644 index 0000000..62800c8 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/staticrtbase_1.txt @@ -0,0 +1,18 @@ + + + + + + _public_ + ipv4unicast + base + 192.168.20.0 + 24 + + _public_ + 189.88.252.1 + + + + + diff --git a/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/staticrtbase_2.txt b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/staticrtbase_2.txt new file mode 100644 index 0000000..62800c8 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/fixtures/ce_static_route_bfd/staticrtbase_2.txt @@ -0,0 +1,18 @@ + + + + + + _public_ + ipv4unicast + base + 192.168.20.0 + 24 + + _public_ + 189.88.252.1 + + + + + diff --git a/tests/unit/modules/network/cloudengine/test_ce_is_is_instance.py b/tests/unit/modules/network/cloudengine/test_ce_is_is_instance.py new file mode 100644 index 0000000..220ecc8 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_is_is_instance.py @@ -0,0 +1,71 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_is_is_instance +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_is_is_instance + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_is_is_instance.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_is_is_instance.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = None + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_isis_instance_present(self): + xml_existing = load_fixture('ce_is_is_instance', 'before.txt') + xml_end_state = load_fixture('ce_is_is_instance', 'after.txt') + update = ['isis 100', 'vpn-instance __public__'] + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + config = dict( + instance_id=100, + vpn_name='__public__', + state='present') + set_module_args(config) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_isis_instance_present(self): + xml_existing = load_fixture('ce_is_is_instance', 'after.txt') + xml_end_state = load_fixture('ce_is_is_instance', 'before.txt') + update = ['undo isis 100'] + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + config = dict( + instance_id=100, + vpn_name='__public__', + state='absent') + set_module_args(config) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) diff --git a/tests/unit/modules/network/cloudengine/test_ce_is_is_interface.py b/tests/unit/modules/network/cloudengine/test_ce_is_is_interface.py new file mode 100644 index 0000000..1de056d --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_is_is_interface.py @@ -0,0 +1,100 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_is_is_interface +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_is_is_interface + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_is_is_interface.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_is_is_interface.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = None + + self.before = load_fixture('ce_is_is_interface', 'before_interface.txt') + self.after = load_fixture('ce_is_is_interface', 'after_interface.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_isis_interface_present(self): + update = ['interface 10GE1/0/1', + 'isis enable 100', + 'isis circuit-level level-1', + 'isis dis-priority 10 level-1', + 'isis ppp-negotiation 2-way', + 'isis cost 10 level-2'] + self.get_nc_config.side_effect = (self.before, self.after) + config = dict( + instance_id=100, + ifname='10GE1/0/1', + leveltype='level_1', + level1dispriority=10, + silentenable=True, + silentcost=True, + typep2penable=True, + snpacheck=True, + p2pnegotiationmode='2_way', + p2ppeeripignore=True, + ppposicpcheckenable=True, + level2cost=10 + ) + set_module_args(config) + result = self.execute_module(changed=True) + print(result['updates']) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_isis_interface_absent(self): + update = ['interface 10GE1/0/1', + 'undo isis enable', + 'undo isis circuit-level', + 'undo isis ppp-negotiation'] + self.get_nc_config.side_effect = (self.after, self.before) + config = dict( + instance_id=100, + ifname='10GE1/0/1', + leveltype='level_1', + level1dispriority=10, + silentenable=True, + silentcost=True, + typep2penable=True, + snpacheck=True, + p2pnegotiationmode='2_way', + p2ppeeripignore=True, + ppposicpcheckenable=True, + level2cost=10, + state='absent' + ) + set_module_args(config) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) diff --git a/tests/unit/modules/network/cloudengine/test_ce_is_is_view.py b/tests/unit/modules/network/cloudengine/test_ce_is_is_view.py new file mode 100644 index 0000000..9bd8424 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_is_is_view.py @@ -0,0 +1,248 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_is_is_view +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_is_is_view + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_is_is_view.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_is_is_view.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = None + + self.before = load_fixture('ce_is_is_view', 'before.txt') + self.after = load_fixture('ce_is_is_view', 'after.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_ce_is_is_view_absent(self): + self.get_nc_config.side_effect = (self.after, self.before) + config = dict( + instance_id=100, + description='ISIS', + islevel='level_1', + coststyle='narrow', + stdlevel2cost=60, + stdbandwidth=100, + autocostenable=True, + autocostenablecompatible=True, + netentity='netentity', + preference_value=100, + route_policy_name='route', + max_load=32, + ip_address='1.1.1.1', + weight=100, + penetration_direct='level2-level1', + import_routepolicy_name='import', + tag=100, + allow_filter=True, + allow_up_down=True, + enablelevel1tolevel2=True, + defaultmode='always', + mode_routepolicyname='mode', + cost=100, + mode_tag=100, + level_type='level_1', + avoid_learning=True, + protocol='ospf', + processid=100, + cost_type='external', + import_cost=100, + import_tag=100, + import_route_policy='import', + impotr_leveltype='level_1', + inheritcost=True, + permitibgp=True, + export_protocol='ospf', + export_policytype='aclNumOrName', + export_processid=100, + export_ipprefix='export', + export_routepolicyname='export', + import_aclnumorname='acl', + import_routepolicyname='import', + bfd_min_rx=100, + bfd_min_tx=100, + bfd_multiplier_num=10, + state='absent' + ) + set_module_args(config) + self.execute_module(changed=True) + + def test_ce_is_is_view_present(self): + self.get_nc_config.side_effect = (self.before, self.after) + update = ['isis 100', + 'description ISIS', + 'is-level level_1', + 'cost-style narrow', + 'circuit-cost 60 level-2', + 'bandwidth-reference 100', + 'network-entity netentity', + 'preference 100 route-policy route', + 'maximum load-balancing 32', + 'nexthop 1.1.1.1 weight 100', + 'import-route isis level-2 into level-1 filter-policy route-policy import tag 100 direct allow-filter-policy allow-up-down-bit', + 'preference 100 route-policy route', + 'undo import-route isis level-1 into level-2 disable', + 'default-route-advertise always cost 100 tag 100 level-1 avoid-learning', + 'import-route isis level-2 into level-1 filter-policy route-policy import tag 100 direct allow-filter-policy allow-up-down-bit', + 'preference 100 route-policy route', + 'import-route ospf 100 inherit-cost cost-type external cost 100 tag 100 route-policy import level-1', + 'default-route-advertise always cost 100 tag 100 level-1 avoid-learning', + 'import-route isis level-2 into level-1 filter-policy route-policy import tag 100 direct allow-filter-policy allow-up-down-bit', + 'preference 100 route-policy route', + 'bfd all-interfaces enable', + 'bfd all-interfaces min-rx-interval 100 min-tx-interval 100 detect-multiplier 10', + 'import-route ospf 100 inherit-cost cost-type external cost 100 tag 100 route-policy import level-1', + 'default-route-advertise always cost 100 tag 100 level-1 avoid-learning', + 'import-route isis level-2 into level-1 filter-policy route-policy import tag 100 direct allow-filter-policy allow-up-down-bit', + 'preference 100 route-policy route', + 'filter-policy ip-prefix export route-policy export export ospf 100', + 'bfd all-interfaces min-rx-interval 100 min-tx-interval 100 detect-multiplier 10', + 'import-route ospf 100 inherit-cost cost-type external cost 100 tag 100 route-policy import level-1', + 'default-route-advertise always cost 100 tag 100 level-1 avoid-learning', + 'import-route isis level-2 into level-1 filter-policy route-policy import tag 100 direct allow-filter-policy allow-up-down-bit', + 'preference 100 route-policy route', + 'filter-policy acl-name acl route-policy importimport', + 'filter-policy ip-prefix export route-policy export export ospf 100', + 'bfd all-interfaces min-rx-interval 100 min-tx-interval 100 detect-multiplier 10', + 'import-route ospf 100 inherit-cost cost-type external cost 100 tag 100 route-policy import level-1', + 'default-route-advertise always cost 100 tag 100 level-1 avoid-learning', + 'import-route isis level-2 into level-1 filter-policy route-policy import tag 100 direct allow-filter-policy allow-up-down-bit', + 'preference 100 route-policy route', + 'auto-cost enable', + 'auto-cost enable compatible'] + + config = dict( + instance_id=100, + description='ISIS', + islevel='level_1', + coststyle='narrow', + stdlevel2cost=60, + stdbandwidth=100, + autocostenable=True, + autocostenablecompatible=True, + netentity='netentity', + preference_value=100, + route_policy_name='route', + max_load=32, + ip_address='1.1.1.1', + weight=100, + penetration_direct='level2-level1', + import_routepolicy_name='import', + tag=100, + allow_filter=True, + allow_up_down=True, + enablelevel1tolevel2=True, + defaultmode='always', + mode_routepolicyname='mode', + cost=100, + mode_tag=100, + level_type='level_1', + avoid_learning=True, + protocol='ospf', + processid=100, + cost_type='external', + import_cost=100, + import_tag=100, + import_route_policy='import', + impotr_leveltype='level_1', + inheritcost=True, + permitibgp=True, + export_protocol='ospf', + export_policytype='aclNumOrName', + export_processid=100, + export_ipprefix='export', + export_routepolicyname='export', + import_aclnumorname='acl', + import_routepolicyname='import', + bfd_min_rx=100, + bfd_min_tx=100, + bfd_multiplier_num=10 + ) + set_module_args(config) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_ce_is_is_view_no_changed(self): + self.get_nc_config.side_effect = (self.after, self.after) + config = dict( + instance_id=100, + description='ISIS', + islevel='level_1', + coststyle='narrow', + stdlevel2cost=60, + stdbandwidth=100, + autocostenable=True, + autocostenablecompatible=True, + netentity='netentity', + preference_value=100, + route_policy_name='route', + max_load=32, + ip_address='1.1.1.1', + weight=100, + penetration_direct='level2-level1', + import_routepolicy_name='import', + tag=100, + allow_filter=True, + allow_up_down=True, + enablelevel1tolevel2=True, + defaultmode='always', + mode_routepolicyname='mode', + cost=100, + mode_tag=100, + level_type='level_1', + avoid_learning=True, + protocol='ospf', + processid=100, + cost_type='external', + import_cost=100, + import_tag=100, + import_route_policy='import', + impotr_leveltype='level_1', + inheritcost=True, + permitibgp=True, + export_protocol='ospf', + export_policytype='aclNumOrName', + export_processid=100, + export_ipprefix='export', + export_routepolicyname='export', + import_aclnumorname='acl', + import_routepolicyname='import', + bfd_min_rx=100, + bfd_min_tx=100, + bfd_multiplier_num=10 + ) + set_module_args(config) + self.execute_module(changed=False) diff --git a/tests/unit/modules/network/cloudengine/test_ce_lacp.py b/tests/unit/modules/network/cloudengine/test_ce_lacp.py new file mode 100644 index 0000000..64120d4 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_lacp.py @@ -0,0 +1,134 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_lacp +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args +from ..ce_module import TestCloudEngineModule, load_fixture + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_lacp + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_lacp.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_lacp.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = None + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_lacp_eturnk_present(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_00.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_01.txt') + update = ['lacp max active-linknumber 13', + 'lacp dampening state-flapping', + 'lacp port-id-extension enable', + 'lacp collector delay 12', + 'lacp preempt enable', + 'lacp system-id 0000-1111-2222', + 'lacp mixed-rate link enable', + 'lacp preempt delay 130', + 'lacp timeout user-defined 10', + 'lacp dampening unexpected-mac disable'] + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict( + mode='Dynamic', + trunk_id='10', + preempt_enable='true', + state_flapping='true', + port_id_extension_enable='true', + unexpected_mac_disable='true', + system_id='0000-1111-2222', + timeout_type='Fast', + fast_timeout='10', + mixed_rate_link_enable='true', + preempt_delay=11, + collector_delay=12, + max_active_linknumber=13, + select='Speed', + state='present')) + result = self.execute_module(changed=True) + self.assertEqual(sorted(result['updates']), sorted(update)) + + def test_lacp_eturnk_absent(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_10.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_00.txt') + default_values = ['undo lacp priority', + 'lacp timeout Fast', + 'lacp max active-linknumber 1', + 'lacp collector delay 0', + 'lacp preempt enable false', + 'lacp dampening state-flapping false', + 'lacp dampening unexpected-mac disable false', + 'lacp mixed-rate link enable false', + 'lacp port-id-extension enable false', + 'lacp preempt delay 30', + 'lacp select Speed', + 'lacp system-id 11-22-33', + 'lacp timeout user-defined 3'] + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict( + mode='Dynamic', + trunk_id='10', + preempt_enable='true', + state_flapping='true', + port_id_extension_enable='true', + unexpected_mac_disable='true', + system_id='0000-1111-2222', + timeout_type='Fast', + fast_timeout='10', + mixed_rate_link_enable='true', + preempt_delay=11, + collector_delay=12, + max_active_linknumber=13, + select='Speed', + state='absent' + )) + result = self.execute_module(changed=True) + self.assertEqual(sorted(result['updates']), sorted(default_values)) + + def test_lacp_global_present(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_10.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_11.txt') + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict(global_priority=32769, + state='present')) + result = self.execute_module(changed=True) + self.assertEqual(result['updates'], ['lacp priority 32769']) + + def test_lacp_global_absent(self): + xml_existing = load_fixture('ce_lacp', 'ce_lacp_11.txt') + xml_end_state = load_fixture('ce_lacp', 'ce_lacp_10.txt') + self.get_nc_config.side_effect = (xml_existing, xml_end_state) + set_module_args(dict(global_priority=32769, + state='absent')) + result = self.execute_module(changed=True) + # excpect: lacp priority is set to default value(32768) + self.assertEqual(result['updates'], ['lacp priority 32768']) diff --git a/tests/unit/modules/network/cloudengine/test_ce_lldp.py b/tests/unit/modules/network/cloudengine/test_ce_lldp.py new file mode 100644 index 0000000..747a9d9 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_lldp.py @@ -0,0 +1,113 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_lldp +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_lldp + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_lldp.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_lldp.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = None + xml_existing_1 = load_fixture('ce_lldp', 'ce_lldp_global_00.txt') + xml_existing_2 = load_fixture('ce_lldp', 'ce_lldp_global_01.txt') + xml_end_state_1 = load_fixture('ce_lldp', 'ce_lldpSysParameter_00.txt') + xml_end_state_2 = load_fixture('ce_lldp', 'ce_lldpSysParameter_01.txt') + self.get_side_effect = (xml_existing_1, xml_existing_2, xml_end_state_1, xml_end_state_2) + self.result_ok = load_fixture('ce_lldp', 'result_ok.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_lldp_global_present(self): + update = ['lldp enable', + 'lldp mdn enable', + 'lldp mdn enable', + 'lldp transmit interval 8', + 'lldp transmit multiplier 8', + 'lldp restart 8', + 'lldp transmit delay 8', + 'lldp trap-interval 8', + 'lldp fast-count 8', + 'lldp mdn trap-interval 8', + 'lldp management-address 1.1.1.1', + 'lldp management-address bind interface bind-name'] + self.get_nc_config.side_effect = self.get_side_effect + self.set_nc_config.side_effect = [self.result_ok] * 11 + set_module_args(dict( + lldpenable='enabled', + mdnstatus='rxOnly', + interval=8, + hold_multiplier=8, + restart_delay=8, + transmit_delay=8, + notification_interval=8, + fast_count=8, + mdn_notification_interval=8, + management_address='1.1.1.1', + bind_name='bind-name') + ) + result = self.execute_module(changed=True) + self.assertEqual(sorted(result['updates']), sorted(update)) + + def test_lacp_sys_parameter_present(self): + update = ['lldp enable', + 'lldp mdn enable', + 'lldp mdn enable', + 'lldp transmit interval 8', + 'lldp transmit multiplier 8', + 'lldp restart 8', + 'lldp transmit delay 8', + 'lldp trap-interval 8', + 'lldp fast-count 8', + 'lldp mdn trap-interval 8', + 'lldp management-address 1.1.1.1', + 'lldp management-address bind interface bind-name'] + self.get_nc_config.side_effect = self.get_side_effect + self.set_nc_config.side_effect = [self.result_ok] * 11 + set_module_args(dict( + lldpenable='enabled', + mdnstatus='rxOnly', + interval=8, + hold_multiplier=8, + restart_delay=8, + transmit_delay=8, + notification_interval=8, + fast_count=8, + mdn_notification_interval=8, + management_address='1.1.1.1', + bind_name='bind-name') + ) + result = self.execute_module(changed=True) + self.assertEqual(sorted(result['updates']), sorted(update)) diff --git a/tests/unit/modules/network/cloudengine/test_ce_lldp_interface.py b/tests/unit/modules/network/cloudengine/test_ce_lldp_interface.py new file mode 100644 index 0000000..f13fced --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_lldp_interface.py @@ -0,0 +1,111 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_lldp_interface +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_lldp_interface + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + # self.mock_get_config = patch('ansible.modules.network.cloudengine.ce_lldp.get_nc_config') + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_lldp_interface.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_nc_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_lldp_interface.set_nc_config') + self.set_nc_config = self.mock_set_nc_config.start() + self.xml_absent = load_fixture('ce_lldp_interface', 'lldp_interface_existing.txt') + self.xml_present = load_fixture('ce_lldp_interface', 'lldp_interface_changed.txt') + self.result_ok = load_fixture('ce_lldp_interface', 'result_ok.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_nc_config.stop() + self.mock_get_config.stop() + + def test_lldp_present(self): + self.get_nc_config.side_effect = (self.xml_absent, self.xml_present) * 5 + self.set_nc_config.return_value = self.result_ok + config = dict( + lldpenable='enabled', + function_lldp_interface_flag='disableINTERFACE', + type_tlv_disable='basic_tlv', + type_tlv_enable='dot1_tlv', + ifname='10GE1/0/1', + lldpadminstatus='txOnly', + manaddrtxenable=True, + portdesctxenable=True, + syscaptxenable=True, + sysdesctxenable=True, + sysnametxenable=True, + portvlantxenable=True, + protovlantxenable=True, + txprotocolvlanid=True, + vlannametxenable=True, + txvlannameid=8, + txinterval=8, + protoidtxenable=True, + macphytxenable=True, + linkaggretxenable=True, + maxframetxenable=True, + eee=True, + dcbx=True + ) + set_module_args(config) + result = self.execute_module(changed=True) + + def test_lldp_absent(self): + self.get_nc_config.side_effect = (self.xml_present, self.xml_present, self.xml_absent, self.xml_absent) + self.set_nc_config.return_value = self.result_ok + config = dict( + lldpenable='enabled', + function_lldp_interface_flag='disableINTERFACE', + type_tlv_disable='basic_tlv', + type_tlv_enable='dot1_tlv', + ifname='10GE1/0/1', + lldpadminstatus='txOnly', + manaddrtxenable=False, + portdesctxenable=False, + syscaptxenable=False, + sysdesctxenable=False, + sysnametxenable=False, + portvlantxenable=False, + protovlantxenable=False, + txprotocolvlanid=False, + vlannametxenable=False, + txvlannameid=18, + txinterval=18, + protoidtxenable=False, + macphytxenable=False, + linkaggretxenable=False, + maxframetxenable=False, + eee=False, + dcbx=False, + state='absent' + ) + set_module_args(config) + result = self.execute_module(changed=False) diff --git a/tests/unit/modules/network/cloudengine/test_ce_mdn_interface.py b/tests/unit/modules/network/cloudengine/test_ce_mdn_interface.py new file mode 100644 index 0000000..58b0ed5 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_mdn_interface.py @@ -0,0 +1,67 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_mdn_interface +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_mdn_interface + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_mdn_interface.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_mdn_interface.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = "" + self.before = load_fixture('ce_mdn_interface', 'before.txt') + self.after = load_fixture('ce_mdn_interface', 'after.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_mdn_enable(self): + update = [['lldp enable', 'interface 10GE1/0/1', 'lldp mdn enable']] + self.get_nc_config.side_effect = (self.before, self.before, self.after, self.after) + set_module_args(dict( + lldpenable='enabled', + mdnstatus='rxOnly', + ifname='10GE1/0/1') + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_repeat_enable(self): + self.get_nc_config.side_effect = (self.after, self.after, self.after, self.after, ) + set_module_args(dict( + lldpenable='enabled', + mdnstatus='rxOnly', + ifname='10GE1/0/1') + ) + self.execute_module(changed=False) diff --git a/tests/unit/modules/network/cloudengine/test_ce_multicast_global.py b/tests/unit/modules/network/cloudengine/test_ce_multicast_global.py new file mode 100644 index 0000000..22c19e7 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_multicast_global.py @@ -0,0 +1,69 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_multicast_global +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_multicast_global + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_multicast_global.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_multicast_global.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = "" + self.before = load_fixture('ce_multicast_global', 'before.txt') + self.after = load_fixture('ce_multicast_global', 'after.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_multicast_enable(self): + update = ['multicast routing-enable'] + self.get_nc_config.side_effect = (self.before, self.after) + set_module_args(dict( + aftype='v4', + vrf='vpna', + state='present') + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_multicast_undo_enable(self): + update = ['undo multicast routing-enable'] + self.get_nc_config.side_effect = (self.after, self.before) + set_module_args(dict( + aftype='v4', + vrf='vpna', + state='absent') + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) diff --git a/tests/unit/modules/network/cloudengine/test_ce_multicast_igmp_enable.py b/tests/unit/modules/network/cloudengine/test_ce_multicast_igmp_enable.py new file mode 100644 index 0000000..9736d64 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_multicast_igmp_enable.py @@ -0,0 +1,80 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_multicast_igmp_enable +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_multicast_igmp_enable + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_multicast_igmp_enable.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_multicast_igmp_enable.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = "" + self.before = load_fixture('ce_multicast_igmp_enable', 'before.txt') + self.after = load_fixture('ce_multicast_igmp_enable', 'after.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_igmp_enable(self): + update = ['igmp snooping enable', + 'igmp snooping version 2', + 'igmp snooping proxy'] + self.get_nc_config.side_effect = (self.before, self.after) + set_module_args(dict( + aftype='v4', + features='vlan', + vlan_id=1, + igmp=True, + version=2, + proxy=True) + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) + + def test_igmp_undo_enable(self): + update = ['undo igmp snooping enable', + 'undo igmp snooping version', + 'undo igmp snooping proxy'] + self.get_nc_config.side_effect = (self.after, self.before) + set_module_args(dict( + aftype='v4', + features='vlan', + vlan_id=1, + igmp=True, + version=2, + proxy=True, + state='absent') + ) + result = self.execute_module(changed=True) + self.assertEquals(sorted(result['updates']), sorted(update)) diff --git a/tests/unit/modules/network/cloudengine/test_ce_static_route_bfd.py b/tests/unit/modules/network/cloudengine/test_ce_static_route_bfd.py new file mode 100644 index 0000000..f10d0e1 --- /dev/null +++ b/tests/unit/modules/network/cloudengine/test_ce_static_route_bfd.py @@ -0,0 +1,102 @@ +# (c) 2019 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible_collections.huawei.cloudengine.plugins.modules import ce_static_route_bfd +from ansible_collections.huawei.cloudengine.tests.unit.modules.network.cloudengine.ce_module import TestCloudEngineModule, load_fixture +from ansible_collections.huawei.cloudengine.tests.unit.modules.utils import set_module_args + + +class TestCloudEngineLacpModule(TestCloudEngineModule): + module = ce_static_route_bfd + + def setUp(self): + super(TestCloudEngineLacpModule, self).setUp() + + self.mock_get_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_static_route_bfd.get_nc_config') + self.get_nc_config = self.mock_get_config.start() + + self.mock_set_config = patch('ansible_collections.huawei.cloudengine.plugins.modules.ce_static_route_bfd.set_nc_config') + self.set_nc_config = self.mock_set_config.start() + self.set_nc_config.return_value = load_fixture('ce_lldp', 'result_ok.txt') + + def tearDown(self): + super(TestCloudEngineLacpModule, self).tearDown() + self.mock_set_config.stop() + self.mock_get_config.stop() + + def test_ce_static_route_bfd_changed_false(self): + srBfdPara_1 = load_fixture('ce_static_route_bfd', 'srBfdPara_1.txt') + staticrtbase_1 = load_fixture('ce_static_route_bfd', 'staticrtbase_1.txt') + self.get_nc_config.side_effect = (srBfdPara_1, srBfdPara_1, staticrtbase_1, staticrtbase_1) + + config = dict( + prefix='255.255.0.0', + mask=22, + aftype='v4', + next_hop='10.10.1.1', + nhp_interface='10GE1/0/1', + vrf='mgnt', + destvrf='_public_', + tag=23, + description='for a test', + pref='22', + function_flag='dynamicBFD', + min_tx_interval='32', + min_rx_interval='23', + detect_multiplier='24', + bfd_session_name='43' + ) + set_module_args(config) + self.execute_module(changed=False) + + def test_ce_static_route_bfd_changed_true(self): + srBfdPara_1 = load_fixture('ce_static_route_bfd', 'srBfdPara_1.txt') + srBfdPara_2 = load_fixture('ce_static_route_bfd', 'srBfdPara_2.txt') + staticrtbase_1 = load_fixture('ce_static_route_bfd', 'staticrtbase_1.txt') + staticrtbase_2 = load_fixture('ce_static_route_bfd', 'staticrtbase_2.txt') + self.get_nc_config.side_effect = (srBfdPara_1, staticrtbase_1, srBfdPara_2, staticrtbase_2) + updates = ['ip route-static vpn-instance mgnt 255.255.0.0 255.255.252.0 10GE1/0/1 10.10.1.1', + ' preference 22', + ' tag 23', + ' track bfd-session 43', + ' description for a test'] + config = dict( + prefix='255.255.0.0', + mask=22, + aftype='v4', + next_hop='10.10.1.1', + nhp_interface='10GE1/0/1', + vrf='mgnt', + destvrf='_public_', + tag=23, + description='for a test', + pref='22', + function_flag='dynamicBFD', + min_tx_interval='32', + min_rx_interval='23', + detect_multiplier='24', + bfd_session_name='43' + ) + set_module_args(config) + result = self.execute_module(changed=True) + self.assertEqual(sorted(result['updates']), sorted(updates)) diff --git a/tests/unit/modules/utils.py b/tests/unit/modules/utils.py new file mode 100644 index 0000000..ae8d318 --- /dev/null +++ b/tests/unit/modules/utils.py @@ -0,0 +1,47 @@ +import json + +from ansible_collections.huawei.cloudengine.tests.unit.compat import unittest +from ansible_collections.huawei.cloudengine.tests.unit.compat.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + + +def set_module_args(args): + if '_ansible_remote_tmp' not in args: + args['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in args: + args['_ansible_keep_remote_files'] = False + + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +class ModuleTestCase(unittest.TestCase): + + def setUp(self): + self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) + self.mock_module.start() + self.mock_sleep = patch('time.sleep') + self.mock_sleep.start() + set_module_args({}) + self.addCleanup(self.mock_module.stop) + self.addCleanup(self.mock_sleep.stop) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt new file mode 100644 index 0000000..a9772be --- /dev/null +++ b/tests/unit/requirements.txt @@ -0,0 +1,42 @@ +boto3 +placebo +pycrypto +passlib +pypsrp +python-memcached +pytz +pyvmomi +redis +requests +setuptools > 0.6 # pytest-xdist installed via requirements does not work with very old setuptools (sanity_ok) +unittest2 ; python_version < '2.7' +importlib ; python_version < '2.7' +netaddr +ipaddress +netapp-lib +solidfire-sdk-python + +# requirements for F5 specific modules +f5-sdk ; python_version >= '2.7' +f5-icontrol-rest ; python_version >= '2.7' +deepdiff + +# requirement for Fortinet specific modules +pyFMG + +# requirement for aci_rest module +xmljson + +# requirement for winrm connection plugin tests +pexpect + +# requirement for the linode module +linode-python # APIv3 +linode_api4 ; python_version > '2.6' # APIv4 + +# requirement for the gitlab module +python-gitlab +httmock + +# requirment for kubevirt modules +openshift ; python_version >= '2.7'