diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml new file mode 100644 index 00000000..c37d08b6 --- /dev/null +++ b/.github/workflows/ansible-test.yml @@ -0,0 +1,198 @@ +# README FIRST +# 1. replace "NAMESPACE" and "COLLECTION_NAME" with the correct name in the env section (e.g. with 'community' and 'mycollection') +# 2. If you don't have unit tests remove that section +# 3. If your collection depends on other collections ensure they are installed, see "Install collection dependencies" +# If you need help please ask in #ansible-devel on Freenode IRC + +name: CI +on: + # Run CI against all pushes (direct commits, also merged PRs), Pull Requests + push: + pull_request: + # Run CI once per day (at 06:00 UTC) + # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-test for each ansible-base version + schedule: + - cron: '0 6 * * *' +env: + NAMESPACE: cisco + COLLECTION_NAME: ftdansible + +jobs: + +### +# Check Build +# + + + build: + name: Build collection + runs-on: ubuntu-latest + strategy: + matrix: + ansible: [2.9.17, 2.10.5] + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Install ansible-base (v${{ matrix.ansible }}) + run: pip install https://github.com/ansible/ansible/archive/v${{ matrix.ansible }}.tar.gz --disable-pip-version-check + + - name: Build a collection tarball + run: pwd && ls -al && find . && 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 + +### +# Check Importer +# + + importer: + name: Galaxy-importer check + needs: + - build + runs-on: ubuntu-latest + steps: + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Install ansible-base (v2.11.0) + run: pip install https://github.com/ansible/ansible/archive/v2.11.0.tar.gz --disable-pip-version-check + + - name: Download migrated collection artifacts + uses: actions/download-artifact@v1 + with: + name: collection + path: .cache/collection-tarballs + + - name: Install the collection tarball + run: ansible-galaxy collection install .cache/collection-tarballs/*.tar.gz + + - name: Install galaxy-importer + run: pip install galaxy-importer + + - name: Create galaxy-importer directory + run: sudo mkdir -p /etc/galaxy-importer + + - name: Create galaxy-importer.cfg + run: sudo cp /home/runner/.ansible/collections/ansible_collections/cisco/ftdansible/.github/workflows/galaxy-importer.cfg /etc/galaxy-importer/galaxy-importer.cfg + + - name: Run galaxy-importer check + run: python -m galaxy_importer.main .cache/collection-tarballs/cisco-*.tar.gz | tee .cache/collection-tarballs/log.txt && sudo cp ./importer_result.json .cache/collection-tarballs/importer_result.json + + - name: Check warnings and errors + run: if grep -E 'ERROR' .cache/collection-tarballs/log.txt; then exit 1; else exit 0; fi + + - name: Store galaxy_importer check log file + uses: actions/upload-artifact@v1 + with: + name: galaxy-importer-log + path: .cache/collection-tarballs/importer_result.json + +### +# Sanity tests (REQUIRED) +# +# https://docs.ansible.com/ansible/latest/dev_guide/testing_sanity.html + + sanity: + name: Sanity (Ⓐ${{ matrix.ansible }}) + strategy: + matrix: + ansible: + - stable-2.9 + - stable-2.10 + # It's important that Sanity is tested against all stable-X.Y branches + # Testing against `devel` may fail as new tests are added. + # - stable-2.9 # Only if your collection supports Ansible 2.9 + runs-on: ubuntu-latest + steps: + + # ansible-test requires the collection to be in a directory in the form + # .../ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}}/ + + - name: Check out code + uses: actions/checkout@v2 + with: + path: ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} + + - name: Set up Python + uses: actions/setup-python@v2 + with: + # it is just required to run that once as "ansible-test sanity" in the docker image + # will run on all python versions it supports. + python-version: 3.8 + + # Install the head of the given branch (devel, stable-2.10) + - name: Install ansible-base (${{ matrix.ansible }}) + run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check + + - name: Run sanity tests + run: ansible-test sanity --docker -v --color + working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} + + # ansible-test support producing code coverage date + - name: Generate coverage report + # run: ansible-test coverage xml -v --requirements --group-by command --group-by version + run: ansible-test coverage xml -v --requirements --group-by command --group-by version + working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} + + # See the reports at https://codecov.io/gh/GITHUBORG/REPONAME + - uses: codecov/codecov-action@v1 + with: + fail_ci_if_error: false + +### +# Unit tests (REQUIRED) +# +# https://docs.ansible.com/ansible/latest/dev_guide/testing_sanity.html + + units: + name: Unit (Ⓐ${{ matrix.ansible }}) + strategy: + matrix: + ansible: + - stable-2.9 + - stable-2.10 + # It's important that Sanity is tested against all stable-X.Y branches + # Testing against `devel` may fail as new tests are added. + # - stable-2.9 # Only if your collection supports Ansible 2.9 + runs-on: ubuntu-latest + steps: + + # ansible-test requires the collection to be in a directory in the form + # .../ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}}/ + + - name: Check out code + uses: actions/checkout@v2 + with: + path: ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} + + - name: Set up Python + uses: actions/setup-python@v2 + with: + # it is just required to run that once as "ansible-test sanity" in the docker image + # will run on all python versions it supports. + python-version: 3.8 + + # Install the head of the given branch (devel, stable-2.10) + - name: Install ansible-base (${{ matrix.ansible }}) + run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check + + # Install units + # - name: Install units + # run: pip install units + + # Run the unit tests on python 3.5 + - name: Run unit tests + run: ansible-test units --docker -v --color + working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} \ No newline at end of file diff --git a/.github/workflows/galaxy-importer.cfg b/.github/workflows/galaxy-importer.cfg new file mode 100644 index 00000000..631359cf --- /dev/null +++ b/.github/workflows/galaxy-importer.cfg @@ -0,0 +1,9 @@ +[galaxy-importer] +LOG_LEVEL_MAIN = INFO +RUN_FLAKE8 = True +RUN_ANSIBLE_DOC = True +RUN_ANSIBLE_LINT = True +RUN_ANSIBLE_TEST = False +ANSIBLE_TEST_LOCAL_IMAGE = False +LOCAL_IMAGE_DOCKER = False +INFRA_OSD = False \ No newline at end of file diff --git a/.gitignore b/.gitignore index dfddbc05..b63e8551 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ venv.bak/ # Doc distribution folder docs/dist +workspace/ +tests/output \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 168cc2e8..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,42 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -## [v0.3.1] - 2020-04-28 -### Fixed -- Minor bugs to support FTD 6.6 - -## [v0.3.0] - 2019-10-23 -### Added -- Update duplicate object lookup process according to API updated in newer versions of FDM -- Switch to Ansible 2.8.3 -- Add handling of No content response for update resource requests - -## [v0.2.2] - 2019-06-06 -### Fixed -- Usage of `register_as` parameter in `ftd_configuration` module. - -## [v0.2.1] - 2019-05-23 -### Added -- Ansible playbooks for configuring DHCP servers and Static Routes. -### Changed -- `firepower-kickstart` dependency used in `ftd_install` module being installed from official PyPI. - -## [v0.2.0] - 2019-04-12 -### Added -- Ansible module (`ftd_install`) for installing package images on hardware FTD device. -- Ansible playbooks for provisioning virtual FTDs on AWS, KVM, and VMware platforms. -### Changed -- Dynamic lookup of API version in FTD HTTP API plugin. -- More Ansible playbooks for various FTD configurations (advanced Access Rules, registering Smart License, creating a backup, etc). -- Automatic [removal of duplicates](https://github.com/CiscoDevNet/FTDAnsible/issues/79) from reference lists for better idempotency. - -## [v0.1.1] - 2019-01-16 -### Changed -- Update Ansible module (`ftd_configuration`) to support `upsert` operations for non-creatable objects (e.g., PhysicalInterfaces). - -## [v0.1.0] - 2018-11-01 -### Added -- Ansible HTTP API plugin that connects to FTD devices over REST API and communicates with them. -- Ansible module (`ftd_configuration`) for managing configuration on FTD devices. -- Ansible module (`ftd_file_download`) for downloading files from FTD devices. -- Ansible module (`ftd_file_upload`) for uploading filed to FTD devices. diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..e56b4674 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,9 @@ +============================== +Cisco.Ftdansible Release Notes +============================== + +.. contents:: Topics + + +v0.3.0 +====== diff --git a/Dockerfile b/Dockerfile index 535c7577..5d6d00cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,19 @@ ARG PYTHON_VERSION=3.6 FROM python:${PYTHON_VERSION} -ARG FTD_ANSIBLE_VERSION=v0.3.1 +ARG FTD_ANSIBLE_VERSION=v0.4.1236 ARG FTD_ANSIBLE_FOLDER=ftd-ansible RUN apt-get update && \ apt-get install -yq sshpass && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -RUN wget https://github.com/CiscoDevNet/FTDAnsible/archive/${FTD_ANSIBLE_VERSION}.tar.gz && \ - tar -xvf ${FTD_ANSIBLE_VERSION}.tar.gz +RUN wget https://github.com/meignw2021/FTDAnsible/archive/refs/tags/${FTD_ANSIBLE_VERSION}.tar.gz && \ + tar -xvf ${FTD_ANSIBLE_VERSION}.tar.gz -RUN mkdir /${FTD_ANSIBLE_FOLDER}/ && \ - export FTD_SOURCE_FOLDER=`find ./ -maxdepth 1 -type d -name '*FTDAnsible-*'` && \ - mv $FTD_SOURCE_FOLDER/httpapi_plugins /${FTD_ANSIBLE_FOLDER} && \ - mv $FTD_SOURCE_FOLDER/library /${FTD_ANSIBLE_FOLDER} && \ - mv $FTD_SOURCE_FOLDER/module_utils /${FTD_ANSIBLE_FOLDER} && \ - mv $FTD_SOURCE_FOLDER/requirements.txt /${FTD_ANSIBLE_FOLDER} && \ +RUN mkdir -p /${FTD_ANSIBLE_FOLDER}/ +RUN export FTD_SOURCE_FOLDER=`find ./ -maxdepth 1 -type d -name '*FTDAnsible-*'` && \ + mv $FTD_SOURCE_FOLDER/ansible_collections /${FTD_ANSIBLE_FOLDER}/ && \ + mv $FTD_SOURCE_FOLDER/requirements.txt /${FTD_ANSIBLE_FOLDER}/ && \ mv $FTD_SOURCE_FOLDER/ansible.cfg /${FTD_ANSIBLE_FOLDER} RUN pip install --no-cache-dir -r /${FTD_ANSIBLE_FOLDER}/requirements.txt diff --git a/Dockerfile.ansible29.tests b/Dockerfile.ansible29.tests new file mode 100644 index 00000000..7ed230e7 --- /dev/null +++ b/Dockerfile.ansible29.tests @@ -0,0 +1,28 @@ +ARG PYTHON_VERSION=3.6 +FROM python:${PYTHON_VERSION} + +COPY requirements.ansible29.txt /requirements.txt + +RUN pip download $(grep ^ansible ./requirements.txt) --no-cache-dir --no-deps -d /tmp/pip && \ + mkdir /tmp/ansible && \ + tar -C /tmp/ansible -xf /tmp/pip/ansible* && \ + mv /tmp/ansible/ansible* /ansible && \ + rm -rf /tmp/pip /tmp/ansible + +COPY test-requirements.txt /test-requirements.txt + +RUN pip install \ + --no-cache-dir \ + -c /ansible/test/lib/ansible_test/_data/requirements/constraints.txt \ + -r /test-requirements.txt \ + -r /requirements.txt + +ENV PYTHONPATH="$PYTHONPATH:/ansible/lib:/ansible/test" + +COPY . /ftd-ansible + +WORKDIR /ftd-ansible + +ENTRYPOINT ["pytest"] + +CMD ["test"] diff --git a/Dockerfile.tests b/Dockerfile.tests index 8c43aa7b..ba81d9c4 100644 --- a/Dockerfile.tests +++ b/Dockerfile.tests @@ -13,11 +13,11 @@ COPY test-requirements.txt /test-requirements.txt RUN pip install \ --no-cache-dir \ - -c /ansible/test/runner/requirements/constraints.txt \ + -c /ansible/ansible_collections/community/general/tests/utils/constraints.txt \ -r /test-requirements.txt \ -r /requirements.txt -ENV PYTHONPATH="$PYTHONPATH:/ansible/lib:/ansible/test" +ENV PYTHONPATH="$PYTHONPATH:/ftd-ansible/ansible/lib:/ftd-ansible/ansible/test" COPY . /ftd-ansible @@ -26,3 +26,4 @@ WORKDIR /ftd-ansible ENTRYPOINT ["pytest"] CMD ["test"] + diff --git a/Dockerfile_integration b/Dockerfile_integration new file mode 100644 index 00000000..d3ee94de --- /dev/null +++ b/Dockerfile_integration @@ -0,0 +1,28 @@ +ARG PYTHON_VERSION=3.6 +FROM python:${PYTHON_VERSION} +ARG FTD_ANSIBLE_VERSION=v0.4.1236 +ARG FTD_ANSIBLE_FOLDER=/root/ansible_collections/cisco/ftdansible + +RUN apt-get update && \ + apt-get install -yq sshpass && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN mkdir -p ${FTD_ANSIBLE_FOLDER}/ + +RUN ls -al /tmp + +COPY requirements.txt ${FTD_ANSIBLE_FOLDER}/ +RUN pip install --no-cache-dir -r /${FTD_ANSIBLE_FOLDER}/requirements.txt + +COPY test-requirements.txt ${FTD_ANSIBLE_FOLDER}/ +COPY galaxy.yml ${FTD_ANSIBLE_FOLDER}/ +COPY meta ${FTD_ANSIBLE_FOLDER}/meta +COPY plugins ${FTD_ANSIBLE_FOLDER}/plugins +COPY samples ${FTD_ANSIBLE_FOLDER}/samples +COPY tests ${FTD_ANSIBLE_FOLDER}/tests + + +ENV PYTHONPATH="$PYTHONPATH:/${FTD_ANSIBLE_FOLDER}/" +WORKDIR /${FTD_ANSIBLE_FOLDER} +ENTRYPOINT ["ansible-playbook"] +# CMD ["bash"] \ No newline at end of file diff --git a/README.md b/README.md index 1d67085f..0b8c8ab9 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,229 @@ # FTD Ansible Modules -A collection of Ansible modules that automate configuration management -and execution of operational tasks on Cisco Firepower Threat Defense (FTD) devices using FTD REST API. +IMPORTANT: When cloning this repository place it under ansible_collections/cisco (requirement to run some of the Ansible tools like ansible-test). + +An Ansible Collection that automates configuration management +and execution of operational tasks on Cisco Firepower Threat Defense (FTD) devices using FTD REST API. _This file describes the development and testing aspects. In case you are looking for the user documentation, please check [FTD Ansible docs on DevNet](https://developer.cisco.com/site/ftd-ansible/)._ ## Installation Guide -The project contains four Ansible modules: +The collection contains four Ansible modules: -* [`ftd_configuration.py`](./library/ftd_configuration.py) - manages device configuration via REST API. The module configures virtual and physical devices by sending HTTPS calls formatted according to the REST API specification; -* [`ftd_file_download.py`](./library/ftd_file_download.py) - downloads files from FTD devices via HTTPS protocol; -* [`ftd_file_upload.py`](./library/ftd_file_upload.py) - uploads files to FTD devices via HTTPS protocol; -* [`ftd_install.py`](./library/ftd_install.py) - installs FTD images on hardware devices. The module performs a complete reimage of the Firepower system by downloading the new software image and installing it. +* [`ftd_configuration.py`](./ansible_collections/plugins/modules/ftd_configuration.py) - manages device configuration via REST API. The module configures virtual and physical devices by sending HTTPS calls formatted according to the REST API specification; +* [`ftd_file_download.py`](./ansible_collections/plugins/modules//ftd_file_download.py) - downloads files from FTD devices via HTTPS protocol; +* [`ftd_file_upload.py`](./ansible_collections/plugins/modules//ftd_file_upload.py) - uploads files to FTD devices via HTTPS protocol; Sample playbooks are located in the [`samples`](./samples) folder. -### Running playbooks in Docker +## View Collection Documentation With ansible-docs -1. Build the default Docker image: - ``` - docker build -t ftd-ansible . - ``` - **NOTE** The default image is based on the release v0.1.0 of the [`FTD-Ansible`](https://github.com/CiscoDevNet/FTDAnsible) and Python 3.6. +The following commands will generate ansible-docs for each of the collection modules -2. You can build the custom Docker image: - ``` - docker build -t ftd-ansible --build-arg PYTHON_VERSION=<2.7|3.5|3.6|3.7> --build-arg FTD_ANSIBLE_VERSION= . - ``` +``` +ansible-doc -M ./plugins/modules/ ftd_configuration +ansible-doc -M ./plugins/modules/ ftd_file_download +ansible-doc -M ./plugins/modules/ ftd_file_upload +``` -3. Create an inventory file that tells Ansible what devices to run the tasks on. [`sample_hosts`](./inventory/sample_hosts) shows an example of inventory file. -4. Run the playbook in Docker mounting playbook folder to `/ftd-ansible/playbooks` and inventory file to `/etc/ansible/hosts`: - ``` - docker run -v $(pwd)/samples:/ftd-ansible/playbooks -v $(pwd)/inventory/sample_hosts:/etc/ansible/hosts ftd-ansible playbooks/network_object.yml - ``` +## Using the collection in Ansible + +1. Setup docker environment + +``` +docker run -it -v $(pwd)/samples:/ftd-ansible/playbooks \ +-v $(pwd)/ansible.cfg:/ftd-ansible/ansible.cfg \ +-v $(pwd)/requirements.txt:/ftd-ansible/requirements.txt \ +-v $(pwd)/inventory/sample_hosts:/etc/ansible/hosts \ +python:3.6 bash + +cd /ftd-ansible +pip install -r requirements.txt +``` + +2. Install the ansible collection + +``` +ansible-galaxy collection install git+https://github.com/meignw2021/FTDAnsible.git,ftd-7 + +Starting collection install process +Installing 'cisco.ftdansible:0.4.0' to '/root/.ansible/collections/ansible_collections/cisco/ftdansible' +Created collection for cisco.ftdansible at /root/.ansible/collections/ansible_collections/cisco/ftdansible +cisco.ftdansible (0.4.0) was installed successfully +``` + +3. List installed collections. +``` +ansible-galaxy collection list +``` -### Running playbooks locally +4. Validate your ansible.cfg file contains a path to ansible collections: -1. Create a virtual environment and activate it: ``` -python3 -m venv venv -. venv/bin/activate +cat ansible.cfg ``` -2. Install dependencies: -`pip install -r requirements.txt` +5. Reference the collection from your playbook + +**NOTE**: The tasks in the playbook reference the collection -3. Update Python path to include the project's directory: ``` -export PYTHONPATH=.:$PYTHONPATH +- hosts: all + connection: httpapi + tasks: + - name: Find a Google application + cisco.ftdansible.ftd_configuration: + operation: getApplicationList + filters: + name: Google + register_as: google_app_results +``` + +Run the sample playbook. + ``` - -4. Run the playbook: -``` -ansible-playbook samples/network_object.yml +ansible-playbook -i /etc/ansible/hosts playbooks/ftd_configuration/download_upload.yml ``` -## Unit Tests +## Tests The project contains unit tests for Ansible modules, HTTP API plugin and util files. They can be found in `test/unit` directory. Ansible has many utils for mocking and running tests, so unit tests in this project also rely on them and including Ansible test module to the Python path is required. -### Running unit tests in Docker +### Running Sanity Tests Using Docker + +When running sanity tests locally this project needs to be located at a path under ansible_collections/cisco (for example ansible_collections/cisco/ftdansible). -1. Build the Docker image: ``` -docker build -t ftd-ansible-test -f Dockerfile.tests . +rm -rf tests/output +ansible-test sanity --docker -v --color --python 3.6 +ansible-test sanity --docker -v --color --python 3.7 ``` -**NOTE**: Dockerfile uses Ansible version from `requirements.txt`. You can change it by replacing the version in `requirements.txt` and rebuilding the Docker image. -2. Run unit tests with: +### Running Units Tests Using Docker + +When running sanity tests locally this project needs to be located at a path under ansible_collections/cisco (for example ansible_collections/cisco/ftdansible) + + ``` -docker run ftd-ansible-test +rm -rf tests/output +ansible-test units --docker -v --python 3.6 +ansible-test units --docker -v --python 3.7 ``` + To run a single test, specify the filename at the end of command: ``` -docker run ftd-ansible-test test/unit/test_ftd_configuration.py +rm -rf tests/output +ansible-test units --docker -v tests/unit/httpapi_plugins/test_ftd.py --color --python 3.6 +ansible-test units --docker -v tests/unit/module_utils/test_upsert_functionality.py --color --python 3.6 ``` -**NOTE**: You need to rebuild the Docker image on every change of the code. +### Integration Tests via Docker Container -#### Troubleshooting +Integration tests are written in a form of playbooks. Thus, integration tests are written as sample playbooks with assertion and can be found in the `samples` folder. They start with `test_` prefix and can be run as usual playbooks. The integration tests use a local Docker container which copies the necessary code and folders from your local path into a docker container for testing. -``` -import file mismatch: -imported module 'test.unit.module_utils.test_common' has this __file__ attribute: ... -which is not the same as the test file we want to collect: - /ftd-ansible/test/unit/module_utils/test_common.py -HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules -``` +1. Build the default Python 3.6, Ansible 2.10 Docker image: + ``` + docker build -t ftd-ansible:integration -f Dockerfile_integration . + ``` + **NOTE**: The default image is based on the release v0.4.0 of the [`FTD-Ansible`](https://github.com/CiscoDevNet/FTDAnsible) and Python 3.6. -In case you experience the following error while running the tests in Docker, remove compiled bytecode files files with -`find . -name "*.pyc" -type f -delete` command and try again. +2. You can build the custom Docker image: + ``` + docker build -t ftd-ansible:integration \ + -f Dockerfile_integration \ + --build-arg PYTHON_VERSION=<2.7|3.5|3.6|3.7> \ + --build-arg FTD_ANSIBLE_VERSION= . + ``` -### Running unit tests locally +3. Create an inventory file that tells Ansible what devices to run the tasks on. [`sample_hosts`](./inventory/sample_hosts) shows an example of inventory file. -1. Clone [Ansible repository](https://github.com/ansible/ansible) from GitHub; +4. Run the playbook in Docker mounting playbook folder to `/ftd-ansible/playbooks` and inventory file to `/etc/ansible/hosts`: + + ``` + docker run -v $(pwd)/inventory/sample_hosts:/etc/ansible/hosts \ + -v $(pwd)/ansible.cfg:/root/ansible_collections/cisco/ftdansible/ansible.cfg \ + ftd-ansible:integration /root/ansible_collections/cisco/ftdansible/samples/ftd_configuration/download_upload.yml + + ``` + +5. To run all of the integration tests -2. Install Ansible and test dependencies: ``` -pip install $ANSIBLE_DIR/requirements.txt -pip install test-requirements.txt +bash ./all_sample_tests.txt ``` -3. Add Ansible modules to the Python path: + +## Developing Locally With Docker + +1. Setup docker environment + ``` -export PYTHONPATH=$PYTHONPATH:$ANSIBLE_DIR/lib:$ANSIBLE_DIR/test +docker run -it -v $(pwd):/root/ansible_collections/ansible/ftdansible \ +python:3.6 bash ``` -4. Run unit tests: +2. Change to working directory + ``` -pytest test/unit +cd /root/ansible_collections/ansible/ftdansible +apt update && apt upgrade -y ``` - -### Running tests with [TOX](https://tox.readthedocs.io/en/latest/) -**NOTE**: To be able to run tests with the specific version of Python using tox you need to have this version of Python installed locally -Install tox locally: +3. Clone [Ansible repository](https://github.com/ansible/ansible) from GitHub; ``` -pip install tox +cd /root/ansible_collections/ansible/ftdansible +rm -rf ./ansible +git clone https://github.com/ansible/ansible.git + +# check out the stable version +# if you want to test with 2.9 specify that in the version below +cd /root/ansible_collections/ansible/ftdansible/ansible +git checkout stable-2.10 ``` -Check the list of currently supported environments: + ``` -tox -l +cd /root/ansible_collections/ansible/ftdansible +pip download $(grep ^ansible ./requirements.txt) --no-cache-dir --no-deps -d /tmp/pip +mkdir /tmp/ansible +tar -C /tmp/ansible -xf /tmp/pip/ansible* +mv /tmp/ansible/ansible* /ansible +rm -rf /tmp/pip /tmp/ansible ``` -**NOTE**: environments with _-integration_ postfix preconfigured for integration tests: -Setup `PYTHONPATH` as described in the previous section -Run unit tests in virtualenvs using tox: +4. Install requirements and test dependencies: + ``` -tox -e py27,py35,py36,py37 +cd /root/ansible_collections/ansible/ftdansible +export ANSIBLE_DIR=`pwd`/ansible +pip install -r requirements.txt +pip install -r $ANSIBLE_DIR/requirements.txt +pip install -r test-requirements.txt + +# used when running sanity tests +ansible-galaxy collection install community.general +ansible-galaxy collection install community.network ``` -Run integration tests in virtualenvs using tox: + +5. Add Ansible modules to the Python path: + ``` -export REPORTS_DIR= -tox -e py27-integration,py35-integration,py36-integration,py37-integration -- samples/network_object.yml -i inventory/sample_hosts +cd /root/ansible_collections/ansible/ftdansible +export ANSIBLE_DIR=`pwd`/ansible +export PYTHONPATH=$PYTHONPATH:.:$ANSIBLE_DIR/lib:$ANSIBLE_DIR/test ``` -### Running style check locally -1. Install [Flake8](http://flake8.pycqa.org/en/latest/) locally: - ``` - pip install flake8 - ``` -2. Run Flake8 check: - ``` - flake8 - ``` +6. Run unit tests: -Flake8 configuration is defined in the [tox config file](./tox.ini) file. +See Secion Above -## Integration Tests +7. Create an inventory file that tells Ansible what devices to run the tasks on. [`sample_hosts`](./inventory/sample_hosts) shows an example of inventory file. -Integration tests are written in a form of playbooks and usually started with `ansible-test` command from Ansible repository. As this project is created outside Ansible, it does not have utils to run the tests. Thus, integration tests are written as sample playbooks with assertion and can be found in the `samples` folder. They start with `test_` prefix and can be run as usual playbooks. +8. Run an integration playbook. + +See section Above ## Debugging @@ -160,7 +231,22 @@ Integration tests are written in a form of playbooks and usually started with `a 2. Run `ansible-playbook` with `-vvvv` ``` - $ ansible-playbook samples/network_object.yml -vvvv + $ ansible-playbook -i inventory/sample_hosts samples/ftd_configuration/access_rule_with_applications.yml -vvvv ``` 3. The log file will contain additional information (REST, etc.) + + +## Troubleshooting + +``` +import file mismatch: +imported module 'test.unit.module_utils.test_common' has this __file__ attribute: ... +which is not the same as the test file we want to collect: + /ftd-ansible/test/unit/module_utils/test_common.py +HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules +``` + +In case you experience the following error while running the tests in Docker, remove compiled bytecode files files with +`find . -name "*.pyc" -type f -delete` command and try again. + diff --git a/httpapi_plugins/__init__.py b/__init__.py similarity index 100% rename from httpapi_plugins/__init__.py rename to __init__.py diff --git a/all_sample_tests.txt b/all_sample_tests.txt new file mode 100644 index 00000000..a53b6338 --- /dev/null +++ b/all_sample_tests.txt @@ -0,0 +1,40 @@ +samples=( +playbooks/ftd_configuration/access_policy.yml +playbooks/ftd_configuration/access_rule_with_applications.yml +playbooks/ftd_configuration/access_rule_with_intrusion_and_file_policies.yml +playbooks/ftd_configuration/access_rule_with_logging.yml +playbooks/ftd_configuration/access_rule_with_networks.yml +playbooks/ftd_configuration/access_rule_with_urls.yml +playbooks/ftd_configuration/access_rule_with_users.yml +playbooks/ftd_configuration/anyconnect_package_file.yml +playbooks/ftd_configuration/backup.yml +playbooks/ftd_configuration/data_dns_settings.yml +playbooks/ftd_configuration/deployment.yml +playbooks/ftd_configuration/dhcp_container.yml +playbooks/ftd_configuration/download_upload.yml +playbooks/ftd_configuration/ha_join.yml +playbooks/ftd_configuration/identity_policy.yml +playbooks/ftd_configuration/initial_provisioning.yml +playbooks/ftd_configuration/nat.yml +playbooks/ftd_configuration/network_object_with_host_vars.yml +playbooks/ftd_configuration/network_object.yml +playbooks/ftd_configuration/physical_interface.yml +playbooks/ftd_configuration/port_object.yml +playbooks/ftd_configuration/ra_vpn_license.yml +playbooks/ftd_configuration/ra_vpn.yml +playbooks/ftd_configuration/security_intelligence_url_policy.yml +playbooks/ftd_configuration/smart_license.yml +playbooks/ftd_configuration/ssl_policy.yml +playbooks/ftd_configuration/static_route_entry.yml +playbooks/ftd_configuration/sub_interface.yml +) + +for f in "${samples[@]}" +do + echo "Running playbook for $f" + docker run -v $(pwd)/samples:/ftd-ansible/playbooks \ + -v $(pwd)/inventory/sample_hosts:/etc/ansible/hosts \ + ftd-ansible $f + +done + diff --git a/ansible.cfg b/ansible.cfg index cee0610c..2f325a16 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,4 +1,11 @@ [defaults] -library = ./library -module_utils = ./module_utils -httpapi_plugins = ./httpapi_plugins + +log_path = ./logs + +# +collections_paths = /root:~/.ansible/collections:/usr/share/ansible/collections + +# this is necessary when running this from docker and should point to the bin for your python executable +# if this is not specified you may see errors where ansible is not able to find the firepower kickstart +# libary even though the library is already installed on your system +interpreter_python = /usr/local/bin/python \ No newline at end of file diff --git a/ansible_collections/cisco/ftdansible/tests/sanity/requirements.txt b/ansible_collections/cisco/ftdansible/tests/sanity/requirements.txt new file mode 100644 index 00000000..de32b18c --- /dev/null +++ b/ansible_collections/cisco/ftdansible/tests/sanity/requirements.txt @@ -0,0 +1,15 @@ +coverage==4.5.4 +mock +pycodestyle +pylint +pytest +pytest-cov==2.10.1 +pytest-xdist +pytest-mock +six +units +urllib3==1.26.5 +voluptuous +yamllint +enum34 +ordereddict diff --git a/changelogs/.plugin-cache.yaml b/changelogs/.plugin-cache.yaml new file mode 100644 index 00000000..9a0747e4 --- /dev/null +++ b/changelogs/.plugin-cache.yaml @@ -0,0 +1,40 @@ +objects: {} +plugins: + become: {} + cache: {} + callback: {} + cliconf: {} + connection: {} + httpapi: + ftd: + description: HttpApi Plugin for Cisco ASA Firepower device + name: ftd + version_added: 2.7.0 + inventory: {} + lookup: {} + module: + ftd_configuration: + description: Manages configuration on Cisco FTD devices over REST API + name: ftd_configuration + namespace: '' + version_added: 2.7.0 + ftd_file_download: + description: Downloads files from Cisco FTD devices over HTTP(S) + name: ftd_file_download + namespace: '' + version_added: 2.7.0 + ftd_file_upload: + description: Uploads files to Cisco FTD devices over HTTP(S) + name: ftd_file_upload + namespace: '' + version_added: 2.7.0 + ftd_install: + description: Installs FTD pkg image on the firewall + name: ftd_install + namespace: '' + version_added: 2.8.0 + netconf: {} + shell: {} + strategy: {} + vars: {} +version: 0.3.0 diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml new file mode 100644 index 00000000..9c1fa358 --- /dev/null +++ b/changelogs/changelog.yaml @@ -0,0 +1,57 @@ +ancestor: null +releases: + 0.4.0: + release_date: '2021-09-21' + changes: + major_changes: + release_summary: This is the first release of the ``cisco.ftdansible`` collection. + The modules in this collection were migrated from Ansible Core with no changes + to their functionality. + fragments: + - 0.4.0.yml + 0.3.1: + release_date: '2021-09-21' + changes: + bugfixes: + - Minor bugs to support FTD 6.6 + 0.3.0: + release_date: '2019-10-23' + changes: + minor_changes: + - Update duplicate object lookup process according to API updated in newer versions of FDM + - Add handling of No content response for update resource requests + - Switch to Ansible 2.8.3 + + 0.2.2: + release_date: '2019-05-23' + changes: + bugfixes: + - Usage of ``register_as`` parameter in ``ftd_configuration`` module. + 0.2.1: + release_date: '2019-06-06' + changes: + minor_changes: + - Ansible playbooks for configuring DHCP servers and Static Routes. + - firepower-kickstart dependency used in ``ftd_install`` module being installed from official PyPI + 0.2.0: + release_date: '2019-04-12' + changes: + minor_changes: + - Ansible module (``ftd_install``) for installing package images on hardware FTD device. + - Ansible playbooks for provisioning virtual FTDs on AWS, KVM, and VMware platforms. + - Dynamic lookup of API version in FTD HTTP API plugin. + - More Ansible playbooks for various FTD configurations (advanced Access Rules, registering Smart License, creating a backup, etc). + - Automatic [removal of duplicates](https://github.com/CiscoDevNet/FTDAnsible/issues/79) from reference lists for better idempotency. + 0.1.1: + release_date: '2019-01-16' + changes: + minor_changes: + - Update Ansible module (``ftd_configuration``) to support ``upsert`` operations for non-creatable objects (e.g., PhysicalInterfaces). + 0.1.0: + release_date: '2018-11-01' + changes: + minor_changes: + - Ansible HTTP API plugin that connects to FTD devices over REST API and communicates with them. + - Ansible module (``ftd_configuration``) for managing configuration on FTD devices. + - Ansible module (``ftd_file_download``) for downloading files from FTD devices. + - Ansible module (``ftd_file_upload``) for uploading filed to FTD devices. diff --git a/changelogs/config.yaml b/changelogs/config.yaml new file mode 100644 index 00000000..1fb5b45f --- /dev/null +++ b/changelogs/config.yaml @@ -0,0 +1,32 @@ +changelog_filename_template: ../CHANGELOG.rst +changelog_filename_version_depth: 0 +changes_file: changelog.yaml +changes_format: combined +ignore_other_fragment_extensions: true +keep_fragments: false +mention_ancestor: true +new_plugins_after_name: removed_features +notesdir: fragments +prelude_section_name: release_summary +prelude_section_title: Release Summary +sanitize_changelog: true +sections: +- - major_changes + - Major Changes +- - minor_changes + - Minor Changes +- - breaking_changes + - Breaking Changes / Porting Guide +- - deprecated_features + - Deprecated Features +- - removed_features + - Removed Features (previously deprecated) +- - security_fixes + - Security Fixes +- - bugfixes + - Bugfixes +- - known_issues + - Known Issues +title: Cisco FTDAnsible Collection +trivial_section_name: trivial +use_fqcn: true diff --git a/changelogs/fragments/0.4.0.yml b/changelogs/fragments/0.4.0.yml new file mode 100644 index 00000000..736a50d6 --- /dev/null +++ b/changelogs/fragments/0.4.0.yml @@ -0,0 +1,4 @@ +major_changes: + - Migrated modules to Ansible collection `cisco.ftdansible` + - fixed linting issues and other issues raised by ansible-test + - Deprecate Ansible module (``ftd_install``) for installing package images on hardware FTD device. diff --git a/docs/build.py b/docs/build.py index 49b64fc4..d7015a66 100644 --- a/docs/build.py +++ b/docs/build.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import argparse import json import logging @@ -12,9 +16,9 @@ from docs import generator from docs.enricher import ApiSpecAutocomplete -from httpapi_plugins.ftd import BASE_HEADERS -from module_utils.common import HTTPMethod -from module_utils.fdm_swagger_client import FdmSwaggerParser, SpecProp, OperationField +from ansible_collections.cisco.ftdansible.plugins.httpapi.ftd import BASE_HEADERS +from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod +from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import FdmSwaggerParser, SpecProp, OperationField BASE_DIR_PATH = os.path.dirname(os.path.realpath(__file__)) DEFAULT_TEMPLATE_DIR = os.path.join(BASE_DIR_PATH, 'templates') @@ -22,7 +26,7 @@ FTD_API_STATIC_TEMPLATE_DIR = os.path.join(DEFAULT_TEMPLATE_DIR, 'static', 'ftd_api') DEFAULT_SAMPLES_DIR = os.path.join(os.path.dirname(BASE_DIR_PATH), 'samples') DEFAULT_DIST_DIR = os.path.join(BASE_DIR_PATH, 'dist') -DEFAULT_MODULE_DIR = os.path.join(os.path.dirname(BASE_DIR_PATH), 'library') +DEFAULT_MODULE_DIR = os.path.join(os.path.dirname(BASE_DIR_PATH), 'ansible_collections/cisco/ftdansible/plugins/modules') logger = logging.getLogger() logger.setLevel(logging.DEBUG) diff --git a/docs/enricher.py b/docs/enricher.py index 5f32aa09..02f60ec6 100644 --- a/docs/enricher.py +++ b/docs/enricher.py @@ -1,7 +1,11 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + from copy import deepcopy -from module_utils.configuration import OperationChecker, QueryParams, OperationNamePrefix -from module_utils.fdm_swagger_client import SpecProp, OperationField, OperationParams, PropName, PathParams +from ansible_collections.cisco.ftdansible.plugins.module_utils.configuration import OperationChecker, QueryParams, OperationNamePrefix +from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import SpecProp, OperationField, OperationParams, PropName, PathParams class ApiSpecAutocomplete(object): diff --git a/docs/extension.py b/docs/extension.py index 7d68463e..36f648c4 100644 --- a/docs/extension.py +++ b/docs/extension.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import collections import yaml diff --git a/docs/generator.py b/docs/generator.py index c75786e2..27f4e729 100644 --- a/docs/generator.py +++ b/docs/generator.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import importlib import os import re @@ -10,8 +14,8 @@ import yaml from jinja2 import Environment, FileSystemLoader -from module_utils.common import HTTPMethod -from module_utils.fdm_swagger_client import SpecProp, OperationField, PropName, OperationParams, FILE_MODEL_NAME +from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod +from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import SpecProp, OperationField, PropName, OperationParams, FILE_MODEL_NAME from docs.snippets_generation import swagger_ui_bravado, swagger_ui_curlify from docs import utils from docs import jinja_filters @@ -249,7 +253,7 @@ def generate_doc_files(self, dest_dir): module_name = os.path.splitext(module_filename)[0] module = importlib.import_module(module_name) - module_docs = yaml.load(module.DOCUMENTATION) + module_docs = yaml.safe_load(module.DOCUMENTATION) module_spec = ModuleSpec( name=module_name, short_description=self._doc_to_text(module_docs.get('short_description')), @@ -259,6 +263,7 @@ def generate_doc_files(self, dest_dir): examples=module.EXAMPLES ) module_content = module_template.render(module=module_spec, **self._template_ctx) + print("Generating documentation for %s" % module_name) self._write_generated_file(module_dir, module_name + self.MD_SUFFIX, module_content) module_index.append(module_name) @@ -270,7 +275,7 @@ def _doc_to_text(text): @staticmethod def _get_module_params(module): - docs = yaml.load(module.DOCUMENTATION) + docs = yaml.safe_load(module.DOCUMENTATION) return {k: { 'description': ModuleDocGenerator._doc_to_text(v.get('description')), 'required': v.get('required', False), @@ -279,7 +284,7 @@ def _get_module_params(module): @staticmethod def _get_module_return_values(module): - return_params = yaml.load(module.RETURN) + return_params = yaml.safe_load(module.RETURN) return {k: { 'description': ModuleDocGenerator._doc_to_text(v.get('description')), 'returned': v.get('returned', ''), diff --git a/docs/jinja_filters.py b/docs/jinja_filters.py index 375f3cee..de4c6c23 100644 --- a/docs/jinja_filters.py +++ b/docs/jinja_filters.py @@ -1,6 +1,12 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import re -from module_utils.fdm_swagger_client import SpecProp, PropName, PropType +from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import SpecProp, PropName, PropType def camel_to_snake(text): @@ -22,11 +28,11 @@ def get_link_to_model_page_by_name(model_name, display_name="object"): :param display_name: string value to be displayed as hyperlink text. default value is "object" :return: string value which represents Markdown hyperlink statement """ - return "[{}]{}".format(display_name, _get_link_path(model_name)) + return "[%s]%s" % (display_name, _get_link_path(model_name)) def _get_link_path(model_name): - return "(/models/{}.md)".format(camel_to_snake(model_name)) + return "(/models/%s.md)" % (camel_to_snake(model_name)) def show_type_or_reference(data_param_spec, api_spec=None): @@ -50,9 +56,9 @@ def process_array(): ref_name = data_param_spec[PropName.ITEMS].get(PropName.REF) if ref_name: model = ref_to_model_name(ref_name) - return "[{}]".format(get_link_to_model_page_by_name(model)) + return "[%s]" % (get_link_to_model_page_by_name(model)) - return "[{}]".format(data_param_spec[PropName.ITEMS][PropName.TYPE]) + return "[%s]" % (data_param_spec[PropName.ITEMS][PropName.TYPE]) def process_object(): if PropName.REF not in data_param_spec: diff --git a/docs/scripts/generate_error_codes_page.py b/docs/scripts/generate_error_codes_page.py index f6724ccb..38fe50bf 100644 --- a/docs/scripts/generate_error_codes_page.py +++ b/docs/scripts/generate_error_codes_page.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import argparse import json diff --git a/docs/snippets_generation/body_generator.py b/docs/snippets_generation/body_generator.py index 1c1a2a7f..5cd0417e 100644 --- a/docs/snippets_generation/body_generator.py +++ b/docs/snippets_generation/body_generator.py @@ -1,3 +1,8 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + def _get_default_value(value, *args): if value.get("default"): return value["default"] diff --git a/docs/snippets_generation/swagger_ui_bravado.py b/docs/snippets_generation/swagger_ui_bravado.py index 667ccf5d..976372c6 100644 --- a/docs/snippets_generation/swagger_ui_bravado.py +++ b/docs/snippets_generation/swagger_ui_bravado.py @@ -1,6 +1,10 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + from pprint import PrettyPrinter -from module_utils.fdm_swagger_client import OperationField +from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import OperationField from docs.snippets_generation import body_generator from docs import utils @@ -9,7 +13,7 @@ def generate_sample(op_name, op_spec, data_params_are_present, model_name, full_spec, jinja_env): template_name = "snippet_bravado.j2" operation_arguments = { - k: '"{}"'.format(v['type']) + k: '"%s"' % (v['type']) for k, v in op_spec.get("parameters", {}).get('path', {}).items() } diff --git a/docs/snippets_generation/swagger_ui_curlify.py b/docs/snippets_generation/swagger_ui_curlify.py index e9d5913e..bfaacc82 100644 --- a/docs/snippets_generation/swagger_ui_curlify.py +++ b/docs/snippets_generation/swagger_ui_curlify.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + """ Content of the file is just Python implementation of the curlify functionality implemented in SwaggerUI Original implementation: https://github.com/swagger-api/swagger-ui/blob/master/src/core/curlify.js diff --git a/docs/templates/auth.md.j2 b/docs/templates/auth.md.j2 index a9906061..734a2bef 100644 --- a/docs/templates/auth.md.j2 +++ b/docs/templates/auth.md.j2 @@ -187,7 +187,7 @@ curl -k -X GET \ ## Refreshing an Access Token -After an access token expires, you need to refresh it using the refresh token that was supplied in the original grant. A refreshed access token is actually different than the original access token. “Refreshing” actually supplies a new pair of access token and refresh token, it does not simply extend the life of the old access token. +After an access token expires, you need to refresh it using the refresh token that was supplied in the original grant. A refreshed access token is actually different than the original access token. "Refreshing" actually supplies a new pair of access token and refresh token, it does not simply extend the life of the old access token. ### Procedure diff --git a/docs/templates/includes/api_compatibility_disclaimer.j2 b/docs/templates/includes/api_compatibility_disclaimer.j2 index fae2c4ba..a7336d54 100644 --- a/docs/templates/includes/api_compatibility_disclaimer.j2 +++ b/docs/templates/includes/api_compatibility_disclaimer.j2 @@ -1 +1 @@ -__NOTE__: _Cisco makes no guarantee that the API version included on this Firepower Threat Device (the “API”) will be compatible with future releases. Cisco, at any time in its sole discretion, may modify, enhance or otherwise improve the API based on user feedback._ +__NOTE__: _Cisco makes no guarantee that the API version included on this Firepower Threat Device (the "API") will be compatible with future releases. Cisco, at any time in its sole discretion, may modify, enhance or otherwise improve the API based on user feedback._ diff --git a/docs/templates/intro.md.j2 b/docs/templates/intro.md.j2 index 553184fc..53943df5 100644 --- a/docs/templates/intro.md.j2 +++ b/docs/templates/intro.md.j2 @@ -45,7 +45,7 @@ For example, you can do a GET /object/networks, and see something similar to the ```url https://ftd.example.com{{ base_path }}/object/networks ``` -The server name part of the URL is the hostname or IP address of the Firepower Threat Defense device, and will be different for your device in place of “ftd.example.com.” In this example, you delete /object/networks from the path to get the base URL: +The server name part of the URL is the hostname or IP address of the Firepower Threat Defense device, and will be different for your device in place of "ftd.example.com." In this example, you delete /object/networks from the path to get the base URL: ```url https://ftd.example.com{{ base_path }}/ ``` diff --git a/docs/templates/static/ftd_ansible/installation_guide.md b/docs/templates/static/ftd_ansible/installation_guide.md index 108ad949..f046e829 100644 --- a/docs/templates/static/ftd_ansible/installation_guide.md +++ b/docs/templates/static/ftd_ansible/installation_guide.md @@ -4,14 +4,14 @@ Welcome to the FTD Ansible Installation Guide! ## Dependency Requirements -FTD modules can be run from any machine with Ansible 2.7 or higher installed. +FTD modules can be run from any machine with Ansible 2.9 or higher installed. Ansible itself requires Python 2 (version 2.7) or Python 3 (versions 3.5 and higher) installed. ## Installing Ansible with FTD modules ### Using Ansible -Ansible (version 2.7 and higher) automatically [contains](https://docs.ansible.com/ansible/latest/modules/list_of_all_modules.html?highlight=ftd) +Ansible (version 2.9 and higher) automatically [contains](https://docs.ansible.com/ansible/latest/modules/list_of_all_modules.html?highlight=ftd) FTD modules inside, and in most cases installing the latest Ansible should be sufficient to run the playbooks. We periodically update FTD modules and they get released in Ansible according to the [roadmap](https://docs.ansible.com/ansible/latest/roadmap/index.html). diff --git a/docs/templates/static/ftd_ansible/intro.md.j2 b/docs/templates/static/ftd_ansible/intro.md.j2 index 444e6053..a65e129c 100644 --- a/docs/templates/static/ftd_ansible/intro.md.j2 +++ b/docs/templates/static/ftd_ansible/intro.md.j2 @@ -6,7 +6,6 @@ Cisco Firepower Threat Defense (FTD) devices. Currently, four Ansible modules ar * [`ftd_configuration`](modules/ftd_configuration.md) - manages device configuration via REST API. The module configures virtual and physical devices by sending HTTPS calls formatted according to the REST API specification; * [`ftd_file_download`](modules/ftd_file_download.md) - downloads files from FTD devices via HTTPS protocol; * [`ftd_file_upload`](modules/ftd_file_upload.md) - uploads files to FTD devices via HTTPS protocol; -* [`ftd_install`](modules/ftd_install.md) - installs FTD images on hardware devices. The module performs a complete reimage of the Firepower system by downloading the new software image and installing it. {% include 'includes/api_compatibility_disclaimer.j2' %} diff --git a/docs/utils.py b/docs/utils.py index 95e43c02..2135b739 100644 --- a/docs/utils.py +++ b/docs/utils.py @@ -1,4 +1,8 @@ -from module_utils.common import HTTPMethod, IDENTITY_PROPERTIES +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod, IDENTITY_PROPERTIES def filter_data_params(op_name, op_method, data_params): diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 00000000..97cb3db4 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,70 @@ +### REQUIRED +# The namespace of the collection. This can be a company/brand/organization or product namespace under which all +# content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with +# underscores or numbers and cannot contain consecutive underscores +namespace: cisco + +# The name of the collection. Has the same character restrictions as 'namespace' +name: ftdansible + +# The version of the collection. Must be compatible with semantic versioning +version: 0.4.0 + +# The path to the Markdown (.md) readme file. This path is relative to the root of the collection +readme: README.md + +# A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) +# @nicks:irc/im.site#channel' +authors: +- Cisco Systems + + +### OPTIONAL but strongly recommended +# A short summary description of the collection +description: An Ansible collection for managing Cisco FTD + +# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only +# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' +license: +- GPL-2.0-or-later + +# The path to the license file for the collection. This path is relative to the root of the collection. This key is +# mutually exclusive with 'license' +license_file: '' + +# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character +# requirements as 'namespace' and 'name' +tags: + - "cisco" + - "ftd" + - "cloud" + - "collection" + - "networking" + - "sdn" + +# Collections that this collection requires to be installed for it to be usable. The key of the dict is the +# collection label 'namespace.name'. The value is a version range +# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version +# range specifiers can be set and are separated by ',' +dependencies: + community.general: 3.8.0 + community.network: 3.0.0 + + +# The URL of the originating SCM repository +repository: https://github.com/CiscoDevNet/FTDAnsible + +# The URL to any online docs +documentation: https://developer.cisco.com/site/ftd-ansible/ + +# The URL to the homepage of the collection/project +homepage: https://github.com/CiscoDevNet/FTDAnsible + +# The URL to the collection issue tracker +issues: https://github.com/CiscoDevNet/FTDAnsible/issue/tracker + +# A list of file glob-like patterns used to filter any files or directories that should not be included in the build +# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This +# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', +# and '.git' are always filtered +build_ignore: [] \ No newline at end of file diff --git a/inventory/sample_hosts b/inventory/sample_hosts index 199bce02..c9dacc70 100644 --- a/inventory/sample_hosts +++ b/inventory/sample_hosts @@ -1,9 +1,9 @@ [all:vars] -ansible_network_os=ftd +ansible_network_os=cisco.ftdansible.ftd [vftd] localhost ansible_user=admin ansible_password=123qwe ansible_httpapi_port=8585 ansible_httpapi_use_ssl=False network_value=130.51.181.228 -10.89.21.213 ansible_user=admin ansible_password=123qwe ansible_httpapi_port=4343 ansible_httpapi_use_ssl=True ansible_httpapi_validate_certs=True network_value=130.51.181.225 +#10.89.21.213 ansible_user=admin ansible_password=123qwe ansible_httpapi_port=4343 ansible_httpapi_use_ssl=True ansible_httpapi_validate_certs=True network_value=130.51.181.225 [vftd:vars] network_type=HOST diff --git a/library/ftd_install.py b/library/ftd_install.py deleted file mode 100644 index e5d8cc03..00000000 --- a/library/ftd_install.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 2019 Cisco and/or its affiliates. -# -# 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': 'network'} - -DOCUMENTATION = """ ---- -module: ftd_install -short_description: Installs FTD pkg image on the firewall -description: - - Provisioning module for FTD devices that installs ROMMON image (if needed) and - FTD pkg image on the firewall. - - Can be used with `httpapi` and `local` connection types. The `httpapi` is preferred, - the `local` connection should be used only when the device cannot be accessed via - REST API. -version_added: "2.8" -requirements: [ "python >= 3.5", "firepower-kickstart" ] -author: "Cisco Systems, Inc." -options: - device_hostname: - description: - - Hostname of the device as appears in the prompt (e.g., 'firepower-5516'). - required: true - type: string - device_username: - description: - - Username to login on the device. - - Defaulted to 'admin' if not specified. - required: false - type: string - default: admin - device_password: - description: - - Password to login on the device. - required: true - type: string - device_sudo_password: - description: - - Root password for the device. If not specified, `device_password` is used. - required: false - type: string - device_new_password: - description: - - New device password to set after image installation. - - If not specified, current password from `device_password` property is reused. - - Not applicable for ASA5500-X series devices. - required: false - type: string - device_ip: - description: - - Device IP address of management interface. - - If not specified and connection is 'httpapi`, the module tries to fetch the existing value via REST API. - - For 'local' connection type, this parameter is mandatory. - required: false - type: string - device_gateway: - description: - - Device gateway of management interface. - - If not specified and connection is 'httpapi`, the module tries to fetch the existing value via REST API. - - For 'local' connection type, this parameter is mandatory. - required: false - type: string - device_netmask: - description: - - Device netmask of management interface. - - If not specified and connection is 'httpapi`, the module tries to fetch the existing value via REST API. - - For 'local' connection type, this parameter is mandatory. - required: false - type: string - device_model: - description: - - Platform model of the device (e.g., 'Cisco ASA5506-X Threat Defense'). - - If not specified and connection is 'httpapi`, the module tries to fetch the device model via REST API. - - For 'local' connection type, this parameter is mandatory. - required: false - type: string - dns_server: - description: - - DNS IP address of management interface. - - If not specified and connection is 'httpapi`, the module tries to fetch the existing value via REST API. - - For 'local' connection type, this parameter is mandatory. - required: false - type: string - console_ip: - description: - - IP address of a terminal server. - - Used to set up an SSH connection with device's console port through the terminal server. - required: true - type: string - console_port: - description: - - Device's port on a terminal server. - required: true - type: string - console_username: - description: - - Username to login on a terminal server. - required: true - type: string - console_password: - description: - - Password to login on a terminal server. - required: true - type: string - rommon_file_location: - description: - - Path to the boot (ROMMON) image on TFTP server. - - Only TFTP is supported. - required: true - type: string - image_file_location: - description: - - Path to the FTD pkg image on the server to be downloaded. - - FTP, SCP, SFTP, TFTP, or HTTP protocols are usually supported, but may depend on the device model. - required: true - type: string - image_version: - description: - - Version of FTD image to be installed. - - Helps to compare target and current FTD versions to prevent unnecessary reinstalls. - required: true - type: string - force_install: - description: - - Forces the FTD image to be installed even when the same version is already installed on the firewall. - - By default, the module stops execution when the target version is installed in the device. - required: false - type: boolean - default: false - search_domains: - description: - - Search domains delimited by comma. - - Defaulted to 'cisco.com' if not specified. - required: false - type: string - default: cisco.com -""" - -EXAMPLES = """ - - name: Install image v6.3.0 on FTD 5516 - ftd_install: - device_hostname: firepower - device_password: pass - device_ip: 192.168.0.1 - device_netmask: 255.255.255.0 - device_gateway: 192.168.0.254 - dns_server: 8.8.8.8 - - console_ip: 10.89.0.0 - console_port: 2004 - console_username: console_user - console_password: console_pass - - rommon_file_location: 'tftp://10.89.0.11/installers/ftd-boot-9.10.1.3.lfbff' - image_file_location: 'https://10.89.0.11/installers/ftd-6.3.0-83.pkg' - image_version: 6.3.0-83 -""" - -RETURN = """ -msg: - description: The message saying whether the image was installed or explaining why the installation failed. - returned: always - type: string -""" -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.connection import Connection -from enum import Enum -from six import iteritems - -try: - from ansible.module_utils.configuration import BaseConfigurationResource, ParamName, PATH_PARAMS_FOR_DEFAULT_OBJ - from ansible.module_utils.device import HAS_KICK, FtdPlatformFactory, FtdModel -except ImportError: - from module_utils.configuration import BaseConfigurationResource, ParamName, PATH_PARAMS_FOR_DEFAULT_OBJ - from module_utils.device import HAS_KICK, FtdPlatformFactory, FtdModel - -REQUIRED_PARAMS_FOR_LOCAL_CONNECTION = ['device_ip', 'device_netmask', 'device_gateway', 'device_model', 'dns_server'] - - -class FtdOperations(Enum): - GET_SYSTEM_INFO = 'getSystemInformation' - GET_MANAGEMENT_IP_LIST = 'getManagementIPList' - GET_DNS_SETTING_LIST = 'getDeviceDNSSettingsList' - GET_DNS_SERVER_GROUP = 'getDNSServerGroup' - - -def main(): - fields = dict( - device_hostname=dict(type='str', required=True), - device_username=dict(type='str', required=False, default='admin'), - device_password=dict(type='str', required=True, no_log=True), - device_sudo_password=dict(type='str', required=False, no_log=True), - device_new_password=dict(type='str', required=False, no_log=True), - device_ip=dict(type='str', required=False), - device_netmask=dict(type='str', required=False), - device_gateway=dict(type='str', required=False), - device_model=dict(type='str', required=False, choices=[e.value for e in FtdModel]), - dns_server=dict(type='str', required=False), - search_domains=dict(type='str', required=False, default='cisco.com'), - - console_ip=dict(type='str', required=True), - console_port=dict(type='str', required=True), - console_username=dict(type='str', required=True), - console_password=dict(type='str', required=True, no_log=True), - - rommon_file_location=dict(type='str', required=True), - image_file_location=dict(type='str', required=True), - image_version=dict(type='str', required=True), - force_reinstall=dict(type='bool', required=False, default=False) - ) - module = AnsibleModule(argument_spec=fields) - if not HAS_KICK: - module.fail_json(msg='Kick Python module is required to run this module. ' - 'Please, install it with `pip install firepower-kickstart` ' - 'command and run the playbook again.') - - use_local_connection = module._socket_path is None - if use_local_connection: - check_required_params_for_local_connection(module, module.params) - platform_model = module.params['device_model'] - check_that_model_is_supported(module, platform_model) - else: - connection = Connection(module._socket_path) - resource = BaseConfigurationResource(connection, module.check_mode) - system_info = get_system_info(resource) - - platform_model = module.params['device_model'] or system_info['platformModel'] - check_that_model_is_supported(module, platform_model) - check_that_update_is_needed(module, system_info) - check_management_and_dns_params(resource, module.params) - - ftd_platform = FtdPlatformFactory.create(platform_model, module.params) - ftd_platform.install_ftd_image(module.params) - - module.exit_json(changed=True, - msg='Successfully installed FTD image %s on the firewall device.' % module.params["image_version"]) - - -def check_required_params_for_local_connection(module, params): - missing_params = [k for k, v in iteritems(params) if k in REQUIRED_PARAMS_FOR_LOCAL_CONNECTION and v is None] - if missing_params: - message = "The following parameters are mandatory when the module is used with 'local' connection: %s." %\ - ', '.join(sorted(missing_params)) - module.fail_json(msg=message) - - -def get_system_info(resource): - path_params = {ParamName.PATH_PARAMS: PATH_PARAMS_FOR_DEFAULT_OBJ} - system_info = resource.execute_operation(FtdOperations.GET_SYSTEM_INFO.value, path_params) - return system_info - - -def check_that_model_is_supported(module, platform_model): - if not FtdModel.has_value(platform_model): - module.fail_json(msg="Platform model '%s' is not supported by this module." % platform_model) - - -def check_that_update_is_needed(module, system_info): - target_ftd_version = module.params["image_version"] - if not module.params["force_reinstall"] and target_ftd_version == system_info['softwareVersion']: - module.exit_json(changed=False, msg="FTD already has %s version of software installed." % target_ftd_version) - - -def check_management_and_dns_params(resource, params): - if not all([params['device_ip'], params['device_netmask'], params['device_gateway']]): - management_ip = resource.execute_operation(FtdOperations.GET_MANAGEMENT_IP_LIST.value, {})['items'][0] - params['device_ip'] = params['device_ip'] or management_ip['ipv4Address'] - params['device_netmask'] = params['device_netmask'] or management_ip['ipv4NetMask'] - params['device_gateway'] = params['device_gateway'] or management_ip['ipv4Gateway'] - if not params['dns_server']: - dns_setting = resource.execute_operation(FtdOperations.GET_DNS_SETTING_LIST.value, {})['items'][0] - dns_server_group_id = dns_setting['dnsServerGroup']['id'] - dns_server_group = resource.execute_operation(FtdOperations.GET_DNS_SERVER_GROUP.value, - {ParamName.PATH_PARAMS: {'objId': dns_server_group_id}}) - params['dns_server'] = dns_server_group['dnsServers'][0]['ipAddress'] - - -if __name__ == '__main__': - main() diff --git a/meta/runtime.yml b/meta/runtime.yml new file mode 100644 index 00000000..30a544b7 --- /dev/null +++ b/meta/runtime.yml @@ -0,0 +1 @@ +requires_ansible: '>=2.9.10' \ No newline at end of file diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 00000000..4ef43224 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,31 @@ +# Collections Plugins Directory. + +This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that +is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that +would contain module utils and modules respectively. + +Here is an example directory of the majority of plugins currently supported by Ansible: + +``` +└── plugins + ├── action + ├── become + ├── cache + ├── callback + ├── cliconf + ├── connection + ├── filter + ├── httpapi + ├── inventory + ├── lookup + ├── module_utils + ├── modules + ├── netconf + ├── shell + ├── strategy + ├── terminal + ├── test + └── vars +``` + +A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/2.11/plugins/plugins.html). diff --git a/library/__init__.py b/plugins/httpapi/__init__.py similarity index 100% rename from library/__init__.py rename to plugins/httpapi/__init__.py diff --git a/httpapi_plugins/ftd.py b/plugins/httpapi/ftd.py similarity index 93% rename from httpapi_plugins/ftd.py rename to plugins/httpapi/ftd.py index 7b43f09a..6fc3b544 100644 --- a/httpapi_plugins/ftd.py +++ b/plugins/httpapi/ftd.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -16,8 +18,7 @@ # along with Ansible. If not, see . # -from __future__ import (absolute_import, division, print_function) - +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -29,7 +30,7 @@ description: - This HttpApi plugin provides methods to connect to Cisco ASA firepower devices over a HTTP(S)-based api. -version_added: "2.7" +version_added: "2.7.0" options: token_path: type: str @@ -61,13 +62,13 @@ from urllib3.fields import RequestField from ansible.module_utils.connection import ConnectionError -from module_utils.fdm_swagger_client import FdmSwaggerParser, SpecProp, FdmSwaggerValidator -from module_utils.common import HTTPMethod, ResponseParams +from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import FdmSwaggerParser, SpecProp, FdmSwaggerValidator +from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod, ResponseParams BASE_HEADERS = { 'Content-Type': 'application/json', 'Accept': 'application/json', - 'User-Agent': 'FTD Ansible/%s' % ansible_version + 'User-Agent': 'FTD Ansible/{ansible_version}' } TOKEN_EXPIRATION_STATUS_CODE = 408 @@ -83,7 +84,7 @@ 'specify the `ansible_httpapi_ftd_token_path` variable in the inventory file.') try: - from __main__ import display + import display except ImportError: from ansible.utils.display import Display @@ -126,8 +127,8 @@ def refresh_token_payload(refresh_token): try: self.refresh_token = response['refresh_token'] self.access_token = response['access_token'] - self.connection._auth = {'Authorization': 'Bearer %s' % self.access_token} - except KeyError: + self.connection._auth = {'Authorization': 'Bearer %s' % (self.access_token)} + except KeyError as key_error: raise ConnectionError( 'Server returned response without token info during connection authentication: %s' % response) @@ -149,7 +150,7 @@ def _lookup_login_url(self, payload): response = self._send_login_request(payload, url) except ConnectionError as e: - display.vvvv('REST:request to {0} failed because of connection error: {1}'.format(url, e)) + display.vvvv('REST:request to %s failed because of connection error: %s' % (url, e)) # In the case of ConnectionError caused by HTTPError we should check response code. # Response code 400 returned in case of invalid credentials so we should stop attempts to log in and # inform the user. @@ -196,12 +197,13 @@ def _send_service_request(self, path, error_msg_prefix, data=None, **kwargs): except HTTPError as e: # HttpApi connection does not read the error response from HTTPError, so we do it here and wrap it up in # ConnectionError, so the actual error message is displayed to the user. - error_msg = json.loads(to_text(e.read())) + # error_msg = json.loads(to_text(e.read())) + error_msg = to_text(e.read()) raise ConnectionError('%s: %s' % (error_msg_prefix, error_msg), http_code=e.code) finally: self._ignore_http_errors = False - def update_auth(self, response, response_data): + def update_auth(self, response, response_text): # With tokens, authentication should not be checked and updated on each request return None @@ -274,7 +276,7 @@ def handle_httperror(self, exc): return False def _display(self, http_method, title, msg=''): - display.vvvv('REST:{0}:{1}:{2}\n{3}'.format(http_method, self.connection._url, title, msg)) + display.vvvv('REST:%s:%s:%s\n%s' % (http_method, self.connection._url, title, msg)) @staticmethod def _get_response_value(response_data): @@ -330,7 +332,7 @@ def _response_to_json(response_text): try: return json.loads(response_text) if response_text else {} # JSONDecodeError only available on Python 3.5+ - except getattr(json.decoder, 'JSONDecodeError', ValueError): + except getattr(json.decoder, 'JSONDecodeError', ValueError) as get_attr_error: raise ConnectionError('Invalid JSON response: %s' % response_text) def get_operation_spec(self, operation_name): @@ -362,8 +364,10 @@ def api_spec(self): if response[ResponseParams.SUCCESS]: self._api_spec = FdmSwaggerParser().parse_spec(response[ResponseParams.RESPONSE]) else: - raise ConnectionError('Failed to download API specification. Status code: %s. Response: %s' % ( - response[ResponseParams.STATUS_CODE], response[ResponseParams.RESPONSE])) + s = response[ResponseParams.STATUS_CODE] + r = response[ResponseParams.RESPONSE] + + raise ConnectionError('Failed to download API specification. Status code: %s. Response: %s' % (s, r)) return self._api_spec @property diff --git a/module_utils/__init__.py b/plugins/module_utils/__init__.py similarity index 100% rename from module_utils/__init__.py rename to plugins/module_utils/__init__.py diff --git a/module_utils/common.py b/plugins/module_utils/common.py similarity index 97% rename from module_utils/common.py rename to plugins/module_utils/common.py index c678ffed..b7461f99 100644 --- a/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -15,6 +17,11 @@ # 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 + try: from collections import OrderedDict except ImportError: @@ -63,14 +70,14 @@ class FtdUnexpectedResponse(Exception): def construct_ansible_facts(response, params): - facts = dict() + facts = {} if response: response_body = response['items'] if 'items' in response else response if params.get('register_as'): facts[params['register_as']] = response_body elif type(response_body) is dict and response_body.get('name') and response_body.get('type'): object_name = re.sub(INVALID_IDENTIFIER_SYMBOLS, '_', response_body['name'].lower()) - fact_name = '%s_%s' % (response_body['type'], object_name) + fact_name = "%s_%s" % (response_body['type'], object_name) facts[fact_name] = response_body return facts diff --git a/module_utils/configuration.py b/plugins/module_utils/configuration.py similarity index 96% rename from module_utils/configuration.py rename to plugins/module_utils/configuration.py index 8d8dc03a..9708eecd 100644 --- a/module_utils/configuration.py +++ b/plugins/module_utils/configuration.py @@ -15,6 +15,11 @@ # 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 copy from functools import partial @@ -25,9 +30,9 @@ FtdServerError, ResponseParams, copy_identity_properties, FtdUnexpectedResponse from ansible.module_utils.fdm_swagger_client import OperationField, ValidationError except ImportError: - from module_utils.common import HTTPMethod, equal_objects, FtdConfigurationError, \ + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod, equal_objects, FtdConfigurationError, \ FtdServerError, ResponseParams, copy_identity_properties, FtdUnexpectedResponse - from module_utils.fdm_swagger_client import OperationField, ValidationError + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import OperationField, ValidationError DEFAULT_PAGE_SIZE = 10 DEFAULT_OFFSET = 0 @@ -283,7 +288,7 @@ def match_filters(filter_params, obj): return False return True - _, query_params, path_params = _get_user_params(params) + __, query_params, path_params = _get_user_params(params) # copy required params to avoid mutation of passed `params` dict url_params = {ParamName.QUERY_PARAMS: dict(query_params), ParamName.PATH_PARAMS: dict(path_params)} @@ -300,8 +305,8 @@ def match_filters(filter_params, obj): def _stringify_name_filter(self, filters): build_version = self.get_build_version() if build_version >= '6.4.0': - return "fts~%s" % filters['name'] - return "name:%s" % filters['name'] + return "fts~%s" % (filters['name']) + return "name:%s" % (filters['name']) def _fetch_system_info(self): if not self._system_info: @@ -360,10 +365,10 @@ def _find_object_matching_params(self, model_name, params): obj = None filtered_objs = self.get_objects_by_filter(get_list_operation, params) - for i, obj in enumerate(filtered_objs): + for i, o in enumerate(filtered_objs): if i > 0: raise FtdConfigurationError(MULTIPLE_DUPLICATES_FOUND_ERROR) - obj = obj + obj = o return obj @@ -392,7 +397,7 @@ def is_invalid_uuid_error(err): raise e def edit_object(self, operation_name, params): - existing_object, _, _ = data, _, path_params = _get_user_params(params) + existing_object, __, __ = data, __, path_params = _get_user_params(params) model_name = self.get_operation_spec(operation_name)[OperationField.MODEL_NAME] get_operation = self._find_get_operation(model_name) @@ -442,7 +447,7 @@ def validate_params(self, operation_name, params): data, query_params, path_params = _get_user_params(params) def validate(validation_method, field_name, user_params): - key = 'Invalid %s provided' % field_name + key = 'Invalid %s provided' % (field_name) try: is_valid, validation_report = validation_method(operation_name, user_params) if not is_valid: @@ -557,8 +562,7 @@ def received_less_items_than_requested(items_in_response, items_expected): raise FtdUnexpectedResponse( "Get List of Objects Response from the server contains more objects than requested. " - "There are {0} item(s) in the response while {1} was(ere) requested".format( - items_in_response, items_expected) + "There are %s item(s) in the response while %s was(ere) requested" % (items_in_response, items_expected) ) while True: diff --git a/module_utils/device.py b/plugins/module_utils/device.py similarity index 97% rename from module_utils/device.py rename to plugins/module_utils/device.py index 537b6c01..2417c021 100644 --- a/module_utils/device.py +++ b/plugins/module_utils/device.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + from enum import Enum from ansible.module_utils.six.moves.urllib.parse import urlparse @@ -32,7 +36,7 @@ def create(model, module_params): for cls in AbstractFtdPlatform.__subclasses__(): if cls.supports_ftd_model(model): return cls(module_params) - raise ValueError("FTD model '%s' is not supported by this module." % model) + raise ValueError("FTD model '%s' is not supported by this module." % (model)) class AbstractFtdPlatform(object): diff --git a/module_utils/fdm_swagger_client.py b/plugins/module_utils/fdm_swagger_client.py similarity index 98% rename from module_utils/fdm_swagger_client.py rename to plugins/module_utils/fdm_swagger_client.py index 00e22574..a1a04326 100644 --- a/module_utils/fdm_swagger_client.py +++ b/plugins/module_utils/fdm_swagger_client.py @@ -16,7 +16,12 @@ # along with Ansible. If not, see . # -from ansible.module_utils.network.ftd.common import HTTPMethod +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod + from ansible.module_utils.six import integer_types, string_types, iteritems FILE_MODEL_NAME = '_File' @@ -393,7 +398,7 @@ def _check_validate_data_params(self, data, operation_name): if not isinstance(data, dict): raise IllegalArgumentException("The data parameter must be a dict") if operation_name not in self._operations: - raise IllegalArgumentException("{0} operation does not support".format(operation_name)) + raise IllegalArgumentException("%s operation does not support" % (operation_name)) def validate_query_params(self, operation_name, params): """ @@ -496,7 +501,7 @@ def _check_validate_url_params(self, operation, params): if not isinstance(params, dict): raise IllegalArgumentException("The params parameter must be a dict") if operation not in self._operations: - raise IllegalArgumentException("{0} operation does not support".format(operation)) + raise IllegalArgumentException("%s operation does not support" % (operation)) def _check_url_params(self, status, spec, params): for prop_name in spec.keys(): @@ -580,7 +585,7 @@ def _check_array(self, status, model, data, path): item_model = model[PropName.ITEMS] for i, item_data in enumerate(data): model_type = item_model.get(PropName.TYPE, PropType.OBJECT) - self._check_types(status, item_data, model_type, item_model, "{0}[{1}]".format(path, i), '') + self._check_types(status, item_data, model_type, item_model, "%s[%s]" % (path, i), '') @staticmethod def _is_correct_simple_types(expected_type, value, allow_null=True): @@ -631,7 +636,7 @@ def _create_path_to_field(path='', field=''): separator = '' if path and field: separator = '.' - return "{0}{1}{2}".format(path, separator, field) + return "%s%s%s" % (path, separator, field) @staticmethod def _is_object(model): diff --git a/test/__init__.py b/plugins/modules/__init__.py similarity index 100% rename from test/__init__.py rename to plugins/modules/__init__.py diff --git a/library/ftd_configuration.py b/plugins/modules/ftd_configuration.py similarity index 90% rename from library/ftd_configuration.py rename to plugins/modules/ftd_configuration.py index cb440d61..9e552d66 100644 --- a/library/ftd_configuration.py +++ b/plugins/modules/ftd_configuration.py @@ -1,5 +1,5 @@ #!/usr/bin/python - +# -*- coding: utf-8 -*- # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -33,15 +33,15 @@ description: - Manages configuration on Cisco FTD devices including creating, updating, removing configuration objects, scheduling and staring jobs, deploying pending changes, etc. All operation are performed over REST API. -version_added: "2.7" -author: "Cisco Systems, Inc." +version_added: "2.7.0" +author: "Cisco Systems (@cisco)" options: operation: description: - The name of the operation to execute. Commonly, the operation starts with 'add', 'edit', 'get', 'upsert' or 'delete' verbs, but can have an arbitrary name too. required: true - type: string + type: str data: description: - Key-value pairs that should be sent as body parameters in a REST API call @@ -57,7 +57,7 @@ register_as: description: - Specifies Ansible fact name that is used to register received response from the FTD device. - type: string + type: str filters: description: - Key-value dict that represents equality filters. Every key is a property name and value is its desired value. @@ -102,9 +102,10 @@ from ansible.module_utils.common import construct_ansible_facts, FtdConfigurationError, \ FtdServerError, FtdUnexpectedResponse except ImportError: - from module_utils.configuration import BaseConfigurationResource, CheckModeException, FtdInvalidOperationNameError - from module_utils.fdm_swagger_client import ValidationError - from module_utils.common import construct_ansible_facts, FtdConfigurationError, \ + from ansible_collections.cisco.ftdansible.plugins.module_utils.configuration import BaseConfigurationResource, \ + CheckModeException, FtdInvalidOperationNameError + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import ValidationError + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import construct_ansible_facts, FtdConfigurationError, \ FtdServerError, FtdUnexpectedResponse @@ -129,7 +130,7 @@ def main(): module.exit_json(changed=resource.config_changed, response=resp, ansible_facts=construct_ansible_facts(resp, module.params)) except FtdInvalidOperationNameError as e: - module.fail_json(msg='Invalid operation name provided: %s' % e.operation_name) + module.fail_json(msg='Invalid operation name provided: %s' % (e.operation_name)) except FtdConfigurationError as e: module.fail_json(msg='Failed to execute %s operation because of the configuration error: %s' % (op_name, e.msg)) except FtdServerError as e: diff --git a/library/ftd_file_download.py b/plugins/modules/ftd_file_download.py similarity index 89% rename from library/ftd_file_download.py rename to plugins/modules/ftd_file_download.py index 5d26b213..125c6f10 100644 --- a/library/ftd_file_download.py +++ b/plugins/modules/ftd_file_download.py @@ -1,5 +1,5 @@ #!/usr/bin/python - +# -*- coding: utf-8 -*- # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -19,6 +19,7 @@ # from __future__ import absolute_import, division, print_function + __metaclass__ = type @@ -33,15 +34,15 @@ description: - Downloads files from Cisco FTD devices including pending changes, disk files, certificates, troubleshoot reports, and backups. -version_added: "2.7" -author: "Cisco Systems, Inc." +version_added: "2.7.0" +author: "Cisco Systems (@cisco)" options: operation: description: - The name of the operation to execute. - Only operations that return a file can be used in this module. required: true - type: string + type: str path_params: description: - Key-value pairs that should be sent as path parameters in a REST API call. @@ -68,7 +69,7 @@ msg: description: The error message describing why the module failed. returned: error - type: string + type: str """ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import Connection @@ -77,8 +78,8 @@ from ansible.module_utils.fdm_swagger_client import OperationField, ValidationError, FILE_MODEL_NAME from ansible.module_utils.common import FtdServerError, HTTPMethod except ImportError: - from module_utils.fdm_swagger_client import OperationField, ValidationError, FILE_MODEL_NAME - from module_utils.common import FtdServerError, HTTPMethod + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import OperationField, ValidationError, FILE_MODEL_NAME + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import FtdServerError, HTTPMethod def is_download_operation(op_spec): @@ -94,9 +95,7 @@ def validate_params(connection, op_name, path_params): field_name: validation_report }) except Exception as e: - raise ValidationError({ - field_name: str(e) - }) + raise ValidationError({field_name: str(e)}) def main(): @@ -113,11 +112,10 @@ def main(): op_name = params['operation'] op_spec = connection.get_operation_spec(op_name) if op_spec is None: - module.fail_json(msg='Operation with specified name is not found: %s' % op_name) + module.fail_json(msg='Operation with specified name is not found: %s' % (op_name)) if not is_download_operation(op_spec): module.fail_json( - msg='Invalid download operation: %s. The operation must make GET request and return a file.' % - op_name) + msg='Invalid download operation: %s. The operation must make GET request and return a file.' % (op_name)) try: path_params = params['path_params'] diff --git a/library/ftd_file_upload.py b/plugins/modules/ftd_file_upload.py similarity index 80% rename from library/ftd_file_upload.py rename to plugins/modules/ftd_file_upload.py index ce074676..67a42289 100644 --- a/library/ftd_file_upload.py +++ b/plugins/modules/ftd_file_upload.py @@ -1,5 +1,5 @@ #!/usr/bin/python - +# -*- coding: utf-8 -*- # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -19,6 +19,7 @@ # from __future__ import absolute_import, division, print_function + __metaclass__ = type @@ -32,15 +33,15 @@ short_description: Uploads files to Cisco FTD devices over HTTP(S) description: - Uploads files to Cisco FTD devices including disk files, backups, and upgrades. -version_added: "2.7" -author: "Cisco Systems, Inc." +version_added: "2.7.0" +author: "Cisco Systems (@cisco)" options: operation: description: - The name of the operation to execute. - Only operations that upload file can be used in this module. required: true - type: string + type: str file_to_upload: description: - Absolute path to the file that should be uploaded. @@ -49,7 +50,7 @@ register_as: description: - Specifies Ansible fact name that is used to register received response from the FTD device. - type: string + type: str """ EXAMPLES = """ @@ -63,7 +64,7 @@ msg: description: The error message describing why the module failed. returned: error - type: string + type: str """ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import Connection @@ -72,8 +73,8 @@ from ansible.module_utils.fdm_swagger_client import OperationField from ansible.module_utils.common import construct_ansible_facts, FtdServerError, HTTPMethod except ImportError: - from module_utils.fdm_swagger_client import OperationField - from module_utils.common import construct_ansible_facts, FtdServerError, HTTPMethod + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import OperationField + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import construct_ansible_facts, FtdServerError, HTTPMethod def is_upload_operation(op_spec): @@ -93,11 +94,10 @@ def main(): op_spec = connection.get_operation_spec(params['operation']) if op_spec is None: - module.fail_json(msg='Operation with specified name is not found: %s' % params['operation']) + module.fail_json(msg="Operation with specified name is not found: %s" % (params['operation'])) if not is_upload_operation(op_spec): module.fail_json( - msg='Invalid upload operation: %s. The operation must make POST request and return UploadStatus model.' % - params['operation']) + msg="Invalid upload operation: %s. The operation must make POST request and return UploadStatus model." % (params['operation'])) try: if module.check_mode: @@ -105,8 +105,8 @@ def main(): resp = connection.upload_file(params['file_to_upload'], op_spec[OperationField.URL]) module.exit_json(changed=True, response=resp, ansible_facts=construct_ansible_facts(resp, module.params)) except FtdServerError as e: - module.fail_json(msg='Upload request for %s operation failed. Status code: %s. ' - 'Server response: %s' % (params['operation'], e.code, e.response)) + module.fail_json(msg="Upload request for %s operation failed. Status code: %s. " + "Server response: %s" % (params['operation'], e.code, e.response)) if __name__ == '__main__': diff --git a/requirements.ansible29.txt b/requirements.ansible29.txt new file mode 100644 index 00000000..e60bb238 --- /dev/null +++ b/requirements.ansible29.txt @@ -0,0 +1,7 @@ +ansible==2.9.26 +urllib3==1.26.5 +lxml==4.6.3 + +# kick library depends on unicon that is available for specific Pythons: https://pypi.org/project/unicon/#files +firepower-kickstart ; python_version >= '3.4' and python_version <= '3.7' and platform_python_implementation == 'CPython' +ordereddict ; python_version == '2.6' diff --git a/requirements.txt b/requirements.txt index 3e758751..c1c72a78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -ansible==2.8.8 -urllib3==1.24.2 -lxml==4.2.6 +ansible==2.10.7 +urllib3==1.26.5 +lxml==4.6.3 + # kick library depends on unicon that is available for specific Pythons: https://pypi.org/project/unicon/#files -firepower-kickstart ; python_version >= '3.4' and python_version <= '3.7' and platform_python_implementation == 'CPython' -ordereddict ; python_version == '2.6' +firepower-kickstart ; python_version >= '3.4' and python_version <= '3.8' and platform_python_implementation == 'CPython' + diff --git a/samples/README.md b/samples/README.md index 5cb40a17..d710bd80 100644 --- a/samples/README.md +++ b/samples/README.md @@ -9,41 +9,41 @@ table. ## Samples in `ftd_configuration` -| Name | FTD 6.2.3 | FTD 6.3.0 | FTD 6.4.0 | FTD 6.5.0 | +| Name | FTD 6.2.3 | FTD 6.3.0 | FTD 6.4.0 | FTD 6.5.0 | FTD 7.0.0 | | ----------- | :-------: | :-----: | :-----: | :-----: | -| `access_policy.yaml` | ✔ | ✔ | ✔ | ✔ | -| `access_rule_with_applications.yml` | ✔ | ✔ | ✔ | ✔ | -| `access_rule_with_intrusion_and_file_policies.yml` | ✔ | ✔ | ✔ | ✔ | -| `access_rule_with_logging.yml` | ✖ | ✔ | ✔ | ✔ | -| `access_rule_with_networks.yml` | ✖ | ✔ | ✔ | ✔ | -| `access_rule_with_urls.yml` | ✔ | ✔ | ✔ | ✔ | -| `access_rule_with_users.yml` | ✖ | ✖ | ✔ | ✔ | -| `anyconnect_package_file.yml` | ✔ | ✔ | ✔ | ✔ | -| `backup.yml` | ✔ | ✔ | ✔ | ✔ | -| `data_dns_settings.yml` | ✖ | ✔ | ✔ | ✔ | -| `deployment.yml` | ✖ | ✔ | ✔ | ✔ | -| `dhcp_container.yml` | ✖ | ✔ | ✔ | ✔ | -| `download_upload.yml` | ✔ | ✔ | ✔ | ✔ | -| `ha_join.yml` | ✖ | ✔ | ✔ | ✔ | -| `identity_policy.yml` | ✖ | ✖ | ✔ | ✔ | -| `initial_provisioning.yml` | ✖ | ✔ | ✔ | ✔ | -| `nat.yml` | ✔ | ✔ | ✔ | ✔ | -| `network_object.yml` | ✔ | ✔ | ✔ | ✔ | -| `network_object_with_host_vars.yml` | ✔ | ✔ | ✔ | ✔ | -| `physical_interface.yml` | ✖ | ✔ | ✔ | ✔ | -| `port_object.yml` | ✔ | ✔ | ✔ | ✔ | -| `ra_vpn.yml` | ✖ | ✖ | ✔ | ✔ | -| `ra_vpn_license.yaml` | ✔ | ✔ | ✔ | ✔ | -| `security_intelligence_url_policy.yml` | ✖ | ✔ | ✔ | ✔ | -| `smart_license.yml` | ✔ | ✔ | ✔ | ✔ | -| `ssl_policy.yml` | ✔ | ✔ | ✔ | ✔ | -| `static_route_entry.yml` | ✔ | ✔ | ✔ | ✔ | -| `sub_interface.yml` | ✖ | ✔ | ✔ | ✖ | +| `access_policy.yaml` | ✔ | ✔ | ✔ | ✔ | ? | +| `access_rule_with_applications.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `access_rule_with_intrusion_and_file_policies.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `access_rule_with_logging.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `access_rule_with_networks.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `access_rule_with_urls.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `access_rule_with_users.yml` | ✖ | ✖ | ✔ | ✔ | ? | +| `anyconnect_package_file.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `backup.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `data_dns_settings.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `deployment.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `dhcp_container.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `download_upload.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `ha_join.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `identity_policy.yml` | ✖ | ✖ | ✔ | ✔ | ? | +| `initial_provisioning.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `nat.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `network_object.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `network_object_with_host_vars.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `physical_interface.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `port_object.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `ra_vpn.yml` | ✖ | ✖ | ✔ | ✔ | ? | +| `ra_vpn_license.yaml` | ✔ | ✔ | ✔ | ✔ | ? | +| `security_intelligence_url_policy.yml` | ✖ | ✔ | ✔ | ✔ | ? | +| `smart_license.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `ssl_policy.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `static_route_entry.yml` | ✔ | ✔ | ✔ | ✔ | ? | +| `sub_interface.yml` | ✖ | ✔ | ✔ | ✖ | ? | ## Samples in `deployment/vmware` -| Name | FTD 6.2.3 | FTD 6.3.0 | FTD 6.4.0 | FTD 6.5.0 | -| ----------- | :-------: | :-----: | :-----: | :-----: | -| deploy.yml | ✔ | ✔ | ✔ | ✔ | -| destroy.yml | ✔ | ✔ | ✔ | ✔ | +| Name | FTD 6.2.3 | FTD 6.3.0 | FTD 6.4.0 | FTD 6.5.0 | FTD 7.0.0 | +| ----------- | :-------: | :-----: | :-----: | :-----: | :-----: | +| deploy.yml | ✔ | ✔ | ✔ | ✔ | ✔ | +| destroy.yml | ✔ | ✔ | ✔ | ✔ | ✔ | | deploy_and_destroy.yml | ✔ | ✔ | ✔ | ✔ | \ No newline at end of file diff --git a/samples/deployment/aws/README.md b/samples/deployment/aws/README.md index 4690081b..8f20a763 100644 --- a/samples/deployment/aws/README.md +++ b/samples/deployment/aws/README.md @@ -3,7 +3,7 @@ ## Setup dependencies 1. Create Python virtual environment ```bash - python3 -m venv ./venv + python -m venv ./venv source venv/bin/activate pip install -r requirements.txt ``` diff --git a/samples/deployment/kvm/README.md b/samples/deployment/kvm/README.md index da1e209d..793024f5 100644 --- a/samples/deployment/kvm/README.md +++ b/samples/deployment/kvm/README.md @@ -3,7 +3,7 @@ ## Setup dependencies 1. Create Python virtual environment ```bash - python3 -m venv ./venv + python -m venv ./venv source venv/bin/activate pip install ansible ``` diff --git a/samples/deployment/vmware/README.md b/samples/deployment/vmware/README.md index 248a0af3..ab492e15 100644 --- a/samples/deployment/vmware/README.md +++ b/samples/deployment/vmware/README.md @@ -4,7 +4,7 @@ 1. Create Python virtual environment ```bash - python3 -m venv ./venv + python -m venv ./venv source venv/bin/activate pip install ansible pyvmomi ``` diff --git a/samples/ftd_configuration/access_policy.yml b/samples/ftd_configuration/access_policy.yml index c2c37f10..1b11bd02 100644 --- a/samples/ftd_configuration/access_policy.yml +++ b/samples/ftd_configuration/access_policy.yml @@ -2,12 +2,12 @@ connection: httpapi tasks: - name: Find an intrustion policy - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getIntrusionPolicyList register_as: policies - name: Allow traffic by default - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessPolicy data: name: NGFW-Access-Policy diff --git a/samples/ftd_configuration/access_rule_with_applications.yml b/samples/ftd_configuration/access_rule_with_applications.yml index 001869f3..13b6e87e 100644 --- a/samples/ftd_configuration/access_rule_with_applications.yml +++ b/samples/ftd_configuration/access_rule_with_applications.yml @@ -2,21 +2,21 @@ connection: httpapi tasks: - name: Find a Google application - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getApplicationList filters: name: Google register_as: google_app_results - name: Find a Dropbox application - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getApplicationList filters: name: Dropbox register_as: dropbox_app_results - name: Create an access rule allowing traffic from cloud file storages - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessRule data: name: Allow traffic from cloud file storages diff --git a/samples/ftd_configuration/access_rule_with_intrusion_and_file_policies.yml b/samples/ftd_configuration/access_rule_with_intrusion_and_file_policies.yml index 4b7f3ee2..a4b6c013 100644 --- a/samples/ftd_configuration/access_rule_with_intrusion_and_file_policies.yml +++ b/samples/ftd_configuration/access_rule_with_intrusion_and_file_policies.yml @@ -2,12 +2,12 @@ connection: httpapi tasks: - name: Check Licenses - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getLicenseList register_as: all_license_list - name: Enable Malware License - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addLicense data: licenseType: MALWARE @@ -16,7 +16,7 @@ when: 'all_license_list | selectattr("licenseType", "in", ["MALWARE"]) | list | length == 0' - name: Enable Threat License - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addLicense data: licenseType: THREAT @@ -25,17 +25,17 @@ when: 'all_license_list | selectattr("licenseType", "in", ["THREAT"]) | list | length == 0' - name: Find an intrustion policy - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getIntrusionPolicyList register_as: intrusion_policies - name: Find a file policy - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getFilePolicyList register_as: file_policies - name: Create an access rule with intrusion and file policies - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessRule data: name: Intrusion and file policy rule diff --git a/samples/ftd_configuration/access_rule_with_logging.yml b/samples/ftd_configuration/access_rule_with_logging.yml index 7c0299b0..61cc6802 100644 --- a/samples/ftd_configuration/access_rule_with_logging.yml +++ b/samples/ftd_configuration/access_rule_with_logging.yml @@ -2,12 +2,12 @@ connection: httpapi tasks: - name: Check Licenses - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getLicenseList register_as: all_license_list - name: Enable Malware License - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addLicense data: licenseType: MALWARE @@ -16,12 +16,12 @@ when: 'all_license_list | selectattr("licenseType", "in", ["MALWARE"]) | list | length == 0' - name: Find a file policy - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getFilePolicyList register_as: file_policies - name: Setup a syslog server - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertSyslogServer data: name: 10.0.4.5:514 @@ -35,7 +35,7 @@ register_as: syslog_server - name: Create an access rule logging malware files - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessRule data: name: Log traffic with malware files diff --git a/samples/ftd_configuration/access_rule_with_networks.yml b/samples/ftd_configuration/access_rule_with_networks.yml index 01759bd1..d5ba9b28 100644 --- a/samples/ftd_configuration/access_rule_with_networks.yml +++ b/samples/ftd_configuration/access_rule_with_networks.yml @@ -2,7 +2,7 @@ connection: httpapi tasks: - name: Create an FQDN network for Cisco DevNet - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertNetworkObject data: name: CiscoDevNetNetwork @@ -12,7 +12,7 @@ dnsResolution: IPV4_AND_IPV6 - name: Create an access rule allowing trafic from Cisco DevNet - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessRule data: name: AllowCiscoTraffic @@ -25,7 +25,7 @@ parentId: default - name: Update the access rule allowing trafic from/to Cisco DevNet - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: editAccessRule data: name: AllowCiscoTraffic @@ -44,14 +44,14 @@ parentId: default - name: Delete the access rule allowing trafic from/to Cisco DevNet - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: deleteAccessRule path_params: objId: '{{ accessrule_allowciscotraffic.id }}' parentId: default - name: Delete the FQDN network for Cisco DevNet - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: deleteNetworkObject path_params: objId: '{{ networkobject_ciscodevnetnetwork.id }}' diff --git a/samples/ftd_configuration/access_rule_with_urls.yml b/samples/ftd_configuration/access_rule_with_urls.yml index f078fac8..74e01d1b 100644 --- a/samples/ftd_configuration/access_rule_with_urls.yml +++ b/samples/ftd_configuration/access_rule_with_urls.yml @@ -2,12 +2,12 @@ connection: httpapi tasks: - name: Check Licenses - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getLicenseList register_as: all_license_list - name: Enable URL License - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addLicense data: licenseType: URLFILTERING @@ -16,19 +16,19 @@ when: 'all_license_list | selectattr("licenseType", "in", ["URLFILTERING"]) | list | length == 0' - name: Find an URL category - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getURLCategoryList register_as: category_results - name: Find a high risk URL reputation - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getURLReputationList filters: startPercentage: 1 register_as: reputation_results - name: Create an access rule blocking traffic - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessRule data: name: Block traffic from alcohol URLs with high risks diff --git a/samples/ftd_configuration/access_rule_with_users.yml b/samples/ftd_configuration/access_rule_with_users.yml index 557fb664..1882e8c0 100644 --- a/samples/ftd_configuration/access_rule_with_users.yml +++ b/samples/ftd_configuration/access_rule_with_users.yml @@ -2,7 +2,7 @@ connection: httpapi tasks: - name: Create an FQDN network for Cisco DevNet - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertNetworkObject data: name: CiscoDevNetNetwork @@ -12,14 +12,14 @@ dnsResolution: IPV4_AND_IPV6 - name: Find a guest realm - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getSpecialRealmList filters: name: Special-Identities-Realm register_as: special_realms - name: Create an access rule blocking access for guests to Cisco website - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessRule data: name: Block traffic for guests to Cisco diff --git a/samples/ftd_configuration/anyconnect_package_file.yml b/samples/ftd_configuration/anyconnect_package_file.yml index 1bc047fb..0ad24d92 100644 --- a/samples/ftd_configuration/anyconnect_package_file.yml +++ b/samples/ftd_configuration/anyconnect_package_file.yml @@ -5,7 +5,7 @@ - variables.yaml tasks: - name: Check AnyConnect package file presence - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getAnyConnectPackageFileList register_as: all_anyconnect_package_files @@ -27,14 +27,14 @@ run_once: true - name: Upload AnyConnect package from local filesystem to device - ftd_file_upload: + cisco.ftdansible.ftd_file_upload: operation: postuploaddiskfile file_to_upload: '{{ anyconnect_linux_image_file_path }}' register_as: anyconnect_linux_image_file_on_device when: 'all_anyconnect_package_files | length == 0' - name: Add AnyConnect package file - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addAnyConnectPackageFile data: name: anyconnect_package_file diff --git a/samples/ftd_configuration/backup.yml b/samples/ftd_configuration/backup.yml index 9061a081..cc250abc 100644 --- a/samples/ftd_configuration/backup.yml +++ b/samples/ftd_configuration/backup.yml @@ -2,7 +2,7 @@ connection: httpapi tasks: - name: Schedule an immediate backup - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addBackupImmediate data: scheduleType: IMMEDIATE @@ -12,7 +12,7 @@ register_as: backup - name: Wait till the backup job is completed - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getJobHistoryBackup path_params: objId: "{{ backup.jobHistoryUuid }}" @@ -27,7 +27,7 @@ when: backup_job.status != 'SUCCESS' - name: Download the backup file - ftd_file_download: + cisco.ftdansible.ftd_file_download: operation: getdownloadbackup path_params: objId: "{{ backup_job.archiveName }}" @@ -44,7 +44,7 @@ mode: put - name: Upload the backup file back to FTD - ftd_file_upload: + cisco.ftdansible.ftd_file_upload: operation: postuploadbackup file_to_upload: /tmp/ftd.backup register_as: uploadedBackupFile diff --git a/samples/ftd_configuration/data_dns_settings.yml b/samples/ftd_configuration/data_dns_settings.yml index 063c99cd..a8e05aad 100644 --- a/samples/ftd_configuration/data_dns_settings.yml +++ b/samples/ftd_configuration/data_dns_settings.yml @@ -2,7 +2,7 @@ connection: httpapi tasks: - name: Create custom DNS object - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertDNSServerGroup data: type: dnsservergroup @@ -17,7 +17,7 @@ type: dnsserver - name: Update Data-plane DNS server to customer server - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertDataDNSSettings data: name: DataDNSSettings diff --git a/samples/ftd_configuration/deployment.yml b/samples/ftd_configuration/deployment.yml index e46726b6..06574642 100644 --- a/samples/ftd_configuration/deployment.yml +++ b/samples/ftd_configuration/deployment.yml @@ -2,7 +2,7 @@ connection: httpapi tasks: - name: Fetch pending changes - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getBaseEntityDiffList register_as: pending_changes @@ -11,12 +11,12 @@ when: pending_changes | length == 0 - name: Start deployment - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addDeployment register_as: deployment_job - name: Poll deployment status until the job is finished - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getDeployment path_params: objId: '{{ deployment_job.id }}' diff --git a/samples/ftd_configuration/dhcp_container.yml b/samples/ftd_configuration/dhcp_container.yml index dd89f470..11c8f1ea 100644 --- a/samples/ftd_configuration/dhcp_container.yml +++ b/samples/ftd_configuration/dhcp_container.yml @@ -4,11 +4,11 @@ iface_name: "inside" tasks: - name: Get List of DHCP servers - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: "getDHCPServerContainerList" register_as: dhcp_containers_list - name: Remove default DHCP server for inside interface - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: "editDHCPServerContainer" data: name: "{{ dhcp_containers_list.0.name }}" diff --git a/samples/ftd_configuration/download_upload.yml b/samples/ftd_configuration/download_upload.yml index 3a5778b7..13624564 100644 --- a/samples/ftd_configuration/download_upload.yml +++ b/samples/ftd_configuration/download_upload.yml @@ -1,15 +1,19 @@ - hosts: all connection: httpapi tasks: + - name: Creating a file with content + copy: + dest: "/tmp/test_up.json" + content: '{"a":"b"}' - name: Upload disk file - ftd_file_upload: + cisco.ftdansible.ftd_file_upload: operation: postuploaddiskfile - file_to_upload: /tmp/test1.txt + file_to_upload: "/tmp/test_up.json" register_as: diskFile - name: Download disk file - ftd_file_download: + cisco.ftdansible.ftd_file_download: operation: getdownloaddiskfile path_params: objId: '{{ diskFile.id }}' - destination: /tmp/test2.txt + destination: "/tmp/test_down.json" diff --git a/samples/ftd_configuration/ha_join.yml b/samples/ftd_configuration/ha_join.yml index e57a8b93..c75eb26c 100644 --- a/samples/ftd_configuration/ha_join.yml +++ b/samples/ftd_configuration/ha_join.yml @@ -3,14 +3,14 @@ connection: httpapi tasks: - name: Get failover interface - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getPhysicalInterfaceList filters: hardwareName: GigabitEthernet0/6 register_as: failover_interfaces - name: Update HA configuration - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertHAConfiguration data: name: HA @@ -38,12 +38,12 @@ statefulFailoverInterface: "{{ failover_interfaces[0] }}" - name: Start HA join - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: addJoinHAStatus register_as: ha_job - name: Poll HA status until the job is finished - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getJoinHAStatus path_params: objId: '{{ ha_job.id }}' diff --git a/samples/ftd_configuration/identity_policy.yml b/samples/ftd_configuration/identity_policy.yml index d2bd20e4..bfee12dc 100644 --- a/samples/ftd_configuration/identity_policy.yml +++ b/samples/ftd_configuration/identity_policy.yml @@ -2,7 +2,7 @@ connection: httpapi tasks: - name: Setup an identity policy - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertIdentityPolicy data: name: NGFW-Default-Identity-Policy @@ -14,12 +14,12 @@ register_as: identity_policy - name: Find an intrustion policy with maximum detection - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getIntrusionPolicyList register_as: intrusion_policies - name: Enable the identity policy as a part of access policy - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessPolicy data: name: NGFW-Access-Policy @@ -35,7 +35,7 @@ type: accesspolicy - name: Disable the identity policy from access policy - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: upsertAccessPolicy data: name: NGFW-Access-Policy diff --git a/samples/ftd_configuration/initial_provisioning.yml b/samples/ftd_configuration/initial_provisioning.yml index 0060626f..be7b8288 100644 --- a/samples/ftd_configuration/initial_provisioning.yml +++ b/samples/ftd_configuration/initial_provisioning.yml @@ -4,7 +4,7 @@ connection: httpapi tasks: - name: Get provisioning info - ftd_configuration: + cisco.ftdansible.ftd_configuration: operation: getInitialProvision path_params: objId: default @@ -23,7 +23,7 @@ ansible_password_temp: wiaS=KwZA7iY72iRb+4ZsmQoLApUpRh9UE=vNnEK>LmYTD*eo +# +# 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 00000000..0972cd2e --- /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 00000000..98f08ad6 --- /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/httpapi_plugins/__init__.py b/tests/unit/httpapi_plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/httpapi_plugins/test_ftd.py b/tests/unit/httpapi_plugins/test_ftd.py similarity index 87% rename from test/unit/httpapi_plugins/test_ftd.py rename to tests/unit/httpapi_plugins/test_ftd.py index 6d8d6fc0..e7a762b9 100644 --- a/test/unit/httpapi_plugins/test_ftd.py +++ b/tests/unit/httpapi_plugins/test_ftd.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -15,19 +16,24 @@ # 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 json from ansible.errors import AnsibleConnectionFailure from ansible.module_utils.connection import ConnectionError from ansible.module_utils.six import BytesIO, PY3, StringIO from ansible.module_utils.six.moves.urllib.error import HTTPError -from units.compat import mock -from units.compat import unittest -from units.compat.mock import mock_open, patch +from ansible_collections.cisco.ftdansible.tests.unit.compat import mock +from ansible_collections.cisco.ftdansible.tests.unit.compat import unittest +from ansible_collections.cisco.ftdansible.tests.unit.compat.mock import mock_open, patch -from httpapi_plugins.ftd import HttpApi, BASE_HEADERS, TOKEN_PATH_TEMPLATE, DEFAULT_API_VERSIONS -from module_utils.common import HTTPMethod, ResponseParams -from module_utils.fdm_swagger_client import FdmSwaggerParser, SpecProp +from ansible_collections.cisco.ftdansible.plugins.httpapi.ftd import HttpApi, BASE_HEADERS, TOKEN_PATH_TEMPLATE, DEFAULT_API_VERSIONS +from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod, ResponseParams +from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import FdmSwaggerParser, SpecProp if PY3: BUILTINS_NAME = 'builtins' @@ -43,11 +49,11 @@ def __init__(self, conn): 'spec_path': '/testSpecUrl' } - def get_option(self, var): - return self.hostvars[var] + def get_option(self, option): + return self.hostvars[option] - def set_option(self, var, val): - self.hostvars[var] = val + def set_option(self, option, value): + self.hostvars[option] = value class TestFtdHttpApi(unittest.TestCase): @@ -199,7 +205,7 @@ def test_download_file(self): self.connection_mock.send.return_value = self._connection_response('File content') open_mock = mock_open() - with patch('%s.open' % BUILTINS_NAME, open_mock): + with patch('%s.open' % (BUILTINS_NAME), open_mock): self.ftd_plugin.download_file('/files/1', '/tmp/test.txt') open_mock.assert_called_once_with('/tmp/test.txt', 'wb') @@ -209,25 +215,25 @@ def test_download_file(self): def test_download_file_should_extract_filename_from_headers(self): filename = 'test_file.txt' response = mock.Mock() - response.info.return_value = {'Content-Disposition': 'attachment; filename="%s"' % filename} + response.info.return_value = {'Content-Disposition': 'attachment; filename="%s"' % (filename)} dummy, response_data = self._connection_response('File content') self.connection_mock.send.return_value = response, response_data open_mock = mock_open() - with patch('%s.open' % BUILTINS_NAME, open_mock): + with patch('%s.open' % (BUILTINS_NAME), open_mock): self.ftd_plugin.download_file('/files/1', '/tmp/') - open_mock.assert_called_once_with('/tmp/%s' % filename, 'wb') + open_mock.assert_called_once_with('/tmp/%s' % (filename), 'wb') open_mock().write.assert_called_once_with(b'File content') @patch('os.path.basename', mock.Mock(return_value='test.txt')) - @patch('httpapi_plugins.ftd.encode_multipart_formdata', + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.encode_multipart_formdata', mock.Mock(return_value=('--Encoded data--', 'multipart/form-data'))) def test_upload_file(self): self.connection_mock.send.return_value = self._connection_response({'id': '123'}) open_mock = mock_open() - with patch('%s.open' % BUILTINS_NAME, open_mock): + with patch('%s.open' % (BUILTINS_NAME), open_mock): resp = self.ftd_plugin.upload_file('/tmp/test.txt', '/files') assert {'id': '123'} == resp @@ -239,13 +245,13 @@ def test_upload_file(self): open_mock.assert_called_once_with('/tmp/test.txt', 'rb') @patch('os.path.basename', mock.Mock(return_value='test.txt')) - @patch('httpapi_plugins.ftd.encode_multipart_formdata', + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.encode_multipart_formdata', mock.Mock(return_value=('--Encoded data--', 'multipart/form-data'))) def test_upload_file_raises_exception_when_invalid_response(self): self.connection_mock.send.return_value = self._connection_response('invalidJsonResponse') open_mock = mock_open() - with patch('%s.open' % BUILTINS_NAME, open_mock): + with patch('%s.open' % (BUILTINS_NAME), open_mock): with self.assertRaises(ConnectionError) as res: self.ftd_plugin.upload_file('/tmp/test.txt', '/files') @@ -320,7 +326,7 @@ def _connection_response(response, status=200): def test_get_list_of_supported_api_versions_with_failed_http_request(self): error_msg = "Invalid Credentials" fp = mock.MagicMock() - fp.read.return_value = '{{"error-msg": "{0}"}}'.format(error_msg) + fp.read.return_value = '"error-msg": "%s"' % (error_msg) send_mock = mock.MagicMock(side_effect=HTTPError('url', 400, 'msg', 'hdrs', fp)) with mock.patch.object(self.ftd_plugin.connection, 'send', send_mock): with self.assertRaises(ConnectionError) as res: @@ -349,8 +355,8 @@ def test_get_list_of_supported_api_versions_with_positive_response(self): supported_versions = self.ftd_plugin._get_supported_api_versions() assert supported_versions == ['v1'] - @patch('httpapi_plugins.ftd.HttpApi._get_api_token_path', mock.MagicMock(return_value=None)) - @patch('httpapi_plugins.ftd.HttpApi._get_known_token_paths') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._get_api_token_path', mock.MagicMock(return_value=None)) + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._get_known_token_paths') def test_lookup_login_url_with_empty_response(self, get_known_token_paths_mock): payload = mock.MagicMock() get_known_token_paths_mock.return_value = [] @@ -360,9 +366,9 @@ def test_lookup_login_url_with_empty_response(self, get_known_token_paths_mock): payload ) - @patch('httpapi_plugins.ftd.HttpApi._get_known_token_paths') - @patch('httpapi_plugins.ftd.HttpApi._send_login_request') - @patch('httpapi_plugins.ftd.display') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._get_known_token_paths') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._send_login_request') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.display') def test_lookup_login_url_with_failed_request(self, display_mock, api_request_mock, get_known_token_paths_mock): payload = mock.MagicMock() url = mock.MagicMock() @@ -375,10 +381,10 @@ def test_lookup_login_url_with_failed_request(self, display_mock, api_request_mo ) assert display_mock.vvvv.called - @patch('httpapi_plugins.ftd.HttpApi._get_api_token_path', mock.MagicMock(return_value=None)) - @patch('httpapi_plugins.ftd.HttpApi._get_known_token_paths') - @patch('httpapi_plugins.ftd.HttpApi._send_login_request') - @patch('httpapi_plugins.ftd.HttpApi._set_api_token_path') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._get_api_token_path', mock.MagicMock(return_value=None)) + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._get_known_token_paths') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._send_login_request') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._set_api_token_path') def test_lookup_login_url_with_positive_result(self, set_api_token_mock, api_request_mock, get_known_token_paths_mock): payload = mock.MagicMock() @@ -392,14 +398,14 @@ def test_lookup_login_url_with_positive_result(self, set_api_token_mock, api_req set_api_token_mock.assert_called_once_with(url) assert resp == response_mock - @patch('httpapi_plugins.ftd.HttpApi._get_supported_api_versions') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._get_supported_api_versions') def test_get_known_token_paths_with_positive_response(self, get_list_of_supported_api_versions_mock): test_versions = ['v1', 'v2'] get_list_of_supported_api_versions_mock.return_value = test_versions result = self.ftd_plugin._get_known_token_paths() assert result == [TOKEN_PATH_TEMPLATE.format(version) for version in test_versions] - @patch('httpapi_plugins.ftd.HttpApi._get_supported_api_versions') + @patch('ansible_collections.cisco.ftdansible.plugins.httpapi.ftd.HttpApi._get_supported_api_versions') def test_get_known_token_paths_with_failed_api_call(self, get_list_of_supported_api_versions_mock): get_list_of_supported_api_versions_mock.side_effect = ConnectionError('test errro message') result = self.ftd_plugin._get_known_token_paths() diff --git a/tests/unit/module_utils/__init__.py b/tests/unit/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/module_utils/test_common.py b/tests/unit/module_utils/test_common.py similarity index 98% rename from test/unit/module_utils/test_common.py rename to tests/unit/module_utils/test_common.py index 991265a7..4eb9fb18 100644 --- a/test/unit/module_utils/test_common.py +++ b/tests/unit/module_utils/test_common.py @@ -16,7 +16,11 @@ # along with Ansible. If not, see . # -from module_utils.common import equal_objects, delete_ref_duplicates, construct_ansible_facts +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.cisco.ftdansible.plugins.module_utils.common import equal_objects, delete_ref_duplicates, construct_ansible_facts # simple objects diff --git a/test/unit/module_utils/test_configuration.py b/tests/unit/module_utils/test_configuration.py similarity index 96% rename from test/unit/module_utils/test_configuration.py rename to tests/unit/module_utils/test_configuration.py index aba41c06..e22cf039 100644 --- a/test/unit/module_utils/test_configuration.py +++ b/tests/unit/module_utils/test_configuration.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -15,29 +16,32 @@ # 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 json import unittest import pytest -from units.compat import mock -from units.compat.mock import call, patch +from ansible_collections.cisco.ftdansible.tests.unit.compat import mock +from ansible_collections.cisco.ftdansible.tests.unit.compat.mock import call, patch -from module_utils.configuration import iterate_over_pageable_resource, BaseConfigurationResource, \ +from ansible_collections.cisco.ftdansible.plugins.module_utils.configuration import iterate_over_pageable_resource, BaseConfigurationResource, \ OperationChecker, OperationNamePrefix, ParamName, QueryParams try: from ansible.module_utils.common import HTTPMethod, FtdUnexpectedResponse from ansible.module_utils.fdm_swagger_client import ValidationError, OperationField except ImportError: - from module_utils.common import HTTPMethod, FtdUnexpectedResponse - from module_utils.fdm_swagger_client import ValidationError, OperationField + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod, FtdUnexpectedResponse + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import ValidationError, OperationField class TestBaseConfigurationResource(object): @pytest.fixture def connection_mock(self, mocker): - connection_class_mock = mocker.patch('library.ftd_configuration.Connection') + connection_class_mock = mocker.patch('ansible_collections.cisco.ftdansible.plugins.modules.ftd_configuration.Connection') connection_instance = connection_class_mock.return_value connection_instance.validate_data.return_value = True, None connection_instance.validate_query_params.return_value = True, None @@ -334,8 +338,7 @@ def test_stringify_name_filter(self, test_api_version, expected_result, connecti } resource = BaseConfigurationResource(connection_mock, False) - assert resource._stringify_name_filter(filters) == expected_result, "Unexpected result for version %s" % ( - test_api_version) + assert resource._stringify_name_filter(filters) == expected_result, "Unexpected result for version %s" % (test_api_version) class TestIterateOverPageableResource(object): diff --git a/test/unit/module_utils/test_data/ngfw_with_ex.json b/tests/unit/module_utils/test_data/ngfw_with_ex.json similarity index 100% rename from test/unit/module_utils/test_data/ngfw_with_ex.json rename to tests/unit/module_utils/test_data/ngfw_with_ex.json diff --git a/test/unit/module_utils/test_device.py b/tests/unit/module_utils/test_device.py similarity index 86% rename from test/unit/module_utils/test_device.py rename to tests/unit/module_utils/test_device.py index 0ec8e90d..17d5af08 100644 --- a/test/unit/module_utils/test_device.py +++ b/tests/unit/module_utils/test_device.py @@ -1,9 +1,14 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import pytest pytest.importorskip("kick") -from module_utils.device import FtdPlatformFactory, FtdModel, FtdAsa5500xPlatform, Ftd2100Platform, AbstractFtdPlatform -from test.unit.test_ftd_install import DEFAULT_MODULE_PARAMS +from ansible_collections.cisco.ftdansible.plugins.module_utils.device import FtdPlatformFactory, FtdModel, \ + FtdAsa5500xPlatform, Ftd2100Platform, AbstractFtdPlatform +from ansible_collections.cisco.ftdansible.tests.unit.test_ftd_install import DEFAULT_MODULE_PARAMS class TestFtdModel(object): @@ -21,8 +26,8 @@ class TestFtdPlatformFactory(object): @pytest.fixture(autouse=True) def mock_devices(self, mocker): - mocker.patch('module_utils.device.Kp') - mocker.patch('module_utils.device.Ftd5500x') + mocker.patch('ansible_collections.cisco.ftdansible.plugins.module_utils.device.Kp') + mocker.patch('ansible_collections.cisco.ftdansible.plugins.module_utils.device.Ftd5500x') def test_factory_should_return_corresponding_platform(self): ftd_platform = FtdPlatformFactory.create(FtdModel.FTD_ASA5508_X.value, dict(DEFAULT_MODULE_PARAMS)) @@ -66,7 +71,7 @@ class TestFtd2100Platform(object): @pytest.fixture def kp_mock(self, mocker): - return mocker.patch('module_utils.device.Kp') + return mocker.patch('ansible_collections.cisco.ftdansible.plugins.module_utils.device.Kp') @pytest.fixture def module_params(self): @@ -98,7 +103,7 @@ class TestFtdAsa5500xPlatform(object): @pytest.fixture def asa5500x_mock(self, mocker): - return mocker.patch('module_utils.device.Ftd5500x') + return mocker.patch('ansible_collections.cisco.ftdansible.plugins.module_utils.device.Ftd5500x') @pytest.fixture def module_params(self): diff --git a/test/unit/module_utils/test_fdm_swagger_parser.py b/tests/unit/module_utils/test_fdm_swagger_parser.py similarity index 98% rename from test/unit/module_utils/test_fdm_swagger_parser.py rename to tests/unit/module_utils/test_fdm_swagger_parser.py index 5fe7a235..586094af 100644 --- a/test/unit/module_utils/test_fdm_swagger_parser.py +++ b/tests/unit/module_utils/test_fdm_swagger_parser.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import copy import os import unittest @@ -24,8 +28,8 @@ from ansible.module_utils.fdm_swagger_client import FdmSwaggerParser from ansible.module_utils.common import HTTPMethod except ImportError: - from module_utils.fdm_swagger_client import FdmSwaggerParser - from module_utils.common import HTTPMethod + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import FdmSwaggerParser + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod DIR_PATH = os.path.dirname(os.path.realpath(__file__)) TEST_DATA_FOLDER = os.path.join(DIR_PATH, 'test_data') diff --git a/test/unit/module_utils/test_fdm_swagger_validator.py b/tests/unit/module_utils/test_fdm_swagger_validator.py similarity index 99% rename from test/unit/module_utils/test_fdm_swagger_validator.py rename to tests/unit/module_utils/test_fdm_swagger_validator.py index 9955b678..81ef7aeb 100644 --- a/test/unit/module_utils/test_fdm_swagger_validator.py +++ b/tests/unit/module_utils/test_fdm_swagger_validator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2018 Cisco and/or its affiliates. # # This file is part of Ansible @@ -15,6 +16,11 @@ # 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 copy import os import unittest @@ -24,7 +30,7 @@ try: from ansible.module_utils.fdm_swagger_client import FdmSwaggerValidator, IllegalArgumentException except ImportError: - from module_utils.fdm_swagger_client import FdmSwaggerValidator, IllegalArgumentException + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import FdmSwaggerValidator, IllegalArgumentException DIR_PATH = os.path.dirname(os.path.realpath(__file__)) TEST_DATA_FOLDER = os.path.join(DIR_PATH, 'test_data') @@ -123,8 +129,7 @@ def sort_validator_rez(data): data['required'] = sorted(data['required']) if 'invalid_type' in data: data['invalid_type'] = sorted(data['invalid_type'], - key=lambda k: '{0}{1}{2}'.format(k['path'], ['expected_type'], - ['actually_value'])) + key=lambda k: "%s%s%s" % (k['path'], ['expected_type'], ['actually_value'])) return data diff --git a/test/unit/module_utils/test_fdm_swagger_with_real_data.py b/tests/unit/module_utils/test_fdm_swagger_with_real_data.py similarity index 92% rename from test/unit/module_utils/test_fdm_swagger_with_real_data.py rename to tests/unit/module_utils/test_fdm_swagger_with_real_data.py index 3fb89125..5cec1ad0 100644 --- a/test/unit/module_utils/test_fdm_swagger_with_real_data.py +++ b/tests/unit/module_utils/test_fdm_swagger_with_real_data.py @@ -1,3 +1,7 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + import json import os import unittest @@ -5,7 +9,7 @@ try: from ansible.module_utils.fdm_swagger_client import FdmSwaggerValidator, FdmSwaggerParser except ImportError: - from module_utils.fdm_swagger_client import FdmSwaggerValidator, FdmSwaggerParser + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import FdmSwaggerValidator, FdmSwaggerParser DIR_PATH = os.path.dirname(os.path.realpath(__file__)) TEST_DATA_FOLDER = os.path.join(DIR_PATH, 'test_data') @@ -61,7 +65,7 @@ def test_parse_all_data(self): expected_operations_counter = 0 for key in self.base_data['paths']: operation = self.base_data['paths'][key] - for _ in operation: + for __ in operation: expected_operations_counter += 1 for key in operations: diff --git a/test/unit/module_utils/test_upsert_functionality.py b/tests/unit/module_utils/test_upsert_functionality.py similarity index 93% rename from test/unit/module_utils/test_upsert_functionality.py rename to tests/unit/module_utils/test_upsert_functionality.py index fa63b1cd..9f4c3791 100644 --- a/test/unit/module_utils/test_upsert_functionality.py +++ b/tests/unit/module_utils/test_upsert_functionality.py @@ -1,11 +1,15 @@ -from __future__ import absolute_import +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type import copy import json import unittest import pytest -from units.compat import mock +from ansible_collections.cisco.ftdansible.tests.unit.compat import mock try: from ansible.module_utils.common import FtdServerError, HTTPMethod, ResponseParams, FtdConfigurationError @@ -14,11 +18,11 @@ ADD_OPERATION_NOT_SUPPORTED_ERROR, ParamName from ansible.module_utils.fdm_swagger_client import ValidationError except ImportError: - from module_utils.common import FtdServerError, HTTPMethod, ResponseParams, FtdConfigurationError - from module_utils.configuration import DUPLICATE_NAME_ERROR_MESSAGE, UNPROCESSABLE_ENTITY_STATUS, \ + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import FtdServerError, HTTPMethod, ResponseParams, FtdConfigurationError + from ansible_collections.cisco.ftdansible.plugins.module_utils.configuration import DUPLICATE_NAME_ERROR_MESSAGE, UNPROCESSABLE_ENTITY_STATUS, \ MULTIPLE_DUPLICATES_FOUND_ERROR, BaseConfigurationResource, FtdInvalidOperationNameError, QueryParams, \ ADD_OPERATION_NOT_SUPPORTED_ERROR, ParamName - from module_utils.fdm_swagger_client import ValidationError + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import ValidationError ADD_RESPONSE = {'status': 'Object added'} EDIT_RESPONSE = {'status': 'Object edited'} @@ -86,8 +90,8 @@ def test_add_upserted_object_with_no_add_operation(self, add_object_mock, get_op @mock.patch.object(BaseConfigurationResource, "_get_operation_name") @mock.patch.object(BaseConfigurationResource, "edit_object") - @mock.patch("module_utils.configuration.copy_identity_properties") - @mock.patch("module_utils.configuration._set_default") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.copy_identity_properties") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration._set_default") def test_edit_upserted_object(self, _set_default_mock, copy_properties_mock, edit_object_mock, get_operation_mock): model_operations = mock.MagicMock() existing_object = mock.MagicMock() @@ -117,7 +121,7 @@ def test_edit_upserted_object(self, _set_default_mock, copy_properties_mock, edi params ) - @mock.patch("module_utils.configuration.OperationChecker.is_upsert_operation_supported") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.OperationChecker.is_upsert_operation_supported") @mock.patch.object(BaseConfigurationResource, "get_operation_specs_by_model_name") @mock.patch.object(BaseConfigurationResource, "_find_object_matching_params") @mock.patch.object(BaseConfigurationResource, "_add_upserted_object") @@ -139,8 +143,8 @@ def test_upsert_object_successfully_added(self, edit_mock, add_mock, find_object add_mock.assert_called_once_with(get_operation_mock.return_value, params) edit_mock.assert_not_called() - @mock.patch("module_utils.configuration.equal_objects") - @mock.patch("module_utils.configuration.OperationChecker.is_upsert_operation_supported") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.equal_objects") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.OperationChecker.is_upsert_operation_supported") @mock.patch.object(BaseConfigurationResource, "get_operation_specs_by_model_name") @mock.patch.object(BaseConfigurationResource, "_find_object_matching_params") @mock.patch.object(BaseConfigurationResource, "_add_upserted_object") @@ -164,8 +168,8 @@ def test_upsert_object_successfully_edited(self, edit_mock, add_mock, find_objec equal_objects_mock.assert_called_once_with(existing_obj, params[ParamName.DATA]) edit_mock.assert_called_once_with(get_operation_mock.return_value, existing_obj, params) - @mock.patch("module_utils.configuration.equal_objects") - @mock.patch("module_utils.configuration.OperationChecker.is_upsert_operation_supported") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.equal_objects") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.OperationChecker.is_upsert_operation_supported") @mock.patch.object(BaseConfigurationResource, "get_operation_specs_by_model_name") @mock.patch.object(BaseConfigurationResource, "_find_object_matching_params") @mock.patch.object(BaseConfigurationResource, "_add_upserted_object") @@ -189,7 +193,7 @@ def test_upsert_object_returned_without_modifications(self, edit_mock, add_mock, equal_objects_mock.assert_called_once_with(existing_obj, params[ParamName.DATA]) edit_mock.assert_not_called() - @mock.patch("module_utils.configuration.OperationChecker.is_upsert_operation_supported") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.OperationChecker.is_upsert_operation_supported") @mock.patch.object(BaseConfigurationResource, "get_operation_specs_by_model_name") @mock.patch.object(BaseConfigurationResource, "_find_object_matching_params") @mock.patch.object(BaseConfigurationResource, "_add_upserted_object") @@ -212,7 +216,7 @@ def test_upsert_object_not_supported(self, edit_mock, add_mock, find_object, get add_mock.assert_not_called() edit_mock.assert_not_called() - @mock.patch("module_utils.configuration.OperationChecker.is_upsert_operation_supported") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.OperationChecker.is_upsert_operation_supported") @mock.patch.object(BaseConfigurationResource, "get_operation_specs_by_model_name") @mock.patch.object(BaseConfigurationResource, "_find_object_matching_params") @mock.patch.object(BaseConfigurationResource, "_add_upserted_object") @@ -234,8 +238,8 @@ def test_upsert_object_when_model_not_supported(self, edit_mock, add_mock, find_ add_mock.assert_not_called() edit_mock.assert_not_called() - @mock.patch("module_utils.configuration.equal_objects") - @mock.patch("module_utils.configuration.OperationChecker.is_upsert_operation_supported") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.equal_objects") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.OperationChecker.is_upsert_operation_supported") @mock.patch.object(BaseConfigurationResource, "get_operation_specs_by_model_name") @mock.patch.object(BaseConfigurationResource, "_find_object_matching_params") @mock.patch.object(BaseConfigurationResource, "_add_upserted_object") @@ -262,7 +266,7 @@ def test_upsert_object_with_fatal_error_during_edit(self, edit_mock, add_mock, f add_mock.assert_not_called() edit_mock.assert_called_once_with(get_operation_mock.return_value, existing_obj, params) - @mock.patch("module_utils.configuration.OperationChecker.is_upsert_operation_supported") + @mock.patch("ansible_collections.cisco.ftdansible.plugins.module_utils.configuration.OperationChecker.is_upsert_operation_supported") @mock.patch.object(BaseConfigurationResource, "get_operation_specs_by_model_name") @mock.patch.object(BaseConfigurationResource, "_find_object_matching_params") @mock.patch.object(BaseConfigurationResource, "_add_upserted_object") @@ -295,7 +299,7 @@ class TestUpsertOperationFunctionalTests(object): @pytest.fixture(autouse=True) def connection_mock(self, mocker): - connection_class_mock = mocker.patch('library.ftd_configuration.Connection') + connection_class_mock = mocker.patch('ansible_collections.cisco.ftdansible.plugins.modules.ftd_configuration.Connection') connection_instance = connection_class_mock.return_value connection_instance.validate_data.return_value = True, None connection_instance.validate_query_params.return_value = True, None @@ -424,7 +428,7 @@ def test_module_should_update_object_when_upsert_operation_and_object_exists(sel url = '/test' obj_id = '456' version = 'test_version' - url_with_id_templ = '{0}/{1}'.format(url, '{objId}') + url_with_id_templ = '%s/%s' % (url, obj_id) new_value = '0000' old_value = '1111' @@ -507,7 +511,8 @@ def get_operation_spec(name): def test_module_should_not_update_object_when_upsert_operation_and_object_exists_with_the_same_fields( self, connection_mock): url = '/test' - url_with_id_templ = '{0}/{1}'.format(url, '{objId}') + obj_id = '456' + url_with_id_templ = '%s/%s' % (url, obj_id) params = { 'operation': 'upsertObject', @@ -572,8 +577,9 @@ def get_operation_spec(name): def test_module_should_not_update_object_when_upsert_operation_and_server_returns_204( self, connection_mock): url = '/test' - url_object = '/test/{objId}' - url_with_id_templ = '{0}/{1}'.format(url, '{objId}') + obj_id = '456' + url_object = '/test/%s' % (obj_id) + url_with_id_templ = '%s/%s' % (url, obj_id) params = { 'operation': 'upsertObject', @@ -675,7 +681,8 @@ def test_module_should_fail_when_upsert_operation_is_not_supported(self, connect # when create operation raised FtdConfigurationError exception without id and version def test_module_should_fail_when_upsert_operation_and_failed_create_without_id_and_version(self, connection_mock): url = '/test' - url_with_id_templ = '{0}/{1}'.format(url, '{objId}') + obj_id = '456' + url_with_id_templ = '%s/%s' % (url, obj_id) params = { 'operation': 'upsertObject', @@ -739,7 +746,7 @@ def test_module_should_fail_when_upsert_operation_and_failed_update_operation(se url = '/test' obj_id = '456' version = 'test_version' - url_with_id_templ = '{0}/{1}'.format(url, '{objId}') + url_with_id_templ = '%s/%s' % (url, obj_id) error_code = 404 @@ -875,7 +882,8 @@ def get_operation_spec(name): def test_module_should_fail_when_upsert_operation_and_few_objects_found_by_filter(self, connection_mock): url = '/test' - url_with_id_templ = '{0}/{1}'.format(url, '{objId}') + obj_id = '456' + url_with_id_templ = '%s/%s' % (url, obj_id) sample_obj = {'name': 'testObject', 'value': '3333', 'type': 'object'} params = { diff --git a/tests/unit/modules/__init__.py b/tests/unit/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/modules/fixtures/complex_schema.json b/tests/unit/modules/fixtures/complex_schema.json new file mode 100644 index 00000000..49a11765 --- /dev/null +++ b/tests/unit/modules/fixtures/complex_schema.json @@ -0,0 +1,212 @@ +{ + "meta": { + "prefix": "ansible", + "namespace": "http://example.com/ansible", + "types": { + }, + "keypath": "/ansible:action/complex" + }, + "data": { + "kind": "action", + "mandatory": true, + "name": "complex", + "qname": "ansible:complex", + "access": { + "read": false, + "create": false, + "execute": true, + "update": false, + "delete": false + }, + "children": [ + { + "kind": "leaf", + "is_action_input": true, + "name": "number", + "qname": "ansible:number", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "uint8" + } + }, + { + "kind": "container", + "is_action_input": true, + "mandatory": true, + "name": "ansible", + "qname": "ansible:ansible", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "children": [ + { + "kind": "choice", + "cases": [ + { + "kind": "case", + "name": "version", + "children": [ + { + "kind": "leaf", + "is_action_input": true, + "name": "version", + "qname": "ansible:version", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + } + } + ] + }, + { + "kind": "case", + "name": "release", + "children": [ + { + "kind": "container", + "is_action_input": true, + "mandatory": true, + "name": "release", + "qname": "ansible:release", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "children": [ + { + "kind": "leaf", + "is_action_input": true, + "name": "major", + "qname": "ansible:major", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "uint8" + } + }, + { + "kind": "leaf", + "is_action_input": true, + "name": "minor", + "qname": "ansible:minor", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "uint8" + } + } + ] + } + ] + } + ], + "name": "version-releae-choice" + } + ] + }, + { + "kind": "choice", + "cases": [ + { + "kind": "case", + "name": "version", + "children": [ + { + "kind": "list", + "min_elements": 0, + "name": "version", + "max_elements": "unbounded", + "qname": "ansible:version", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "mandatory": true, + "children": [ + { + "kind": "leaf", + "name": "name", + "qname": "ansible:name", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ], + "is_action_output": true + } + ] + }, + { + "kind": "case", + "name": "release", + "children": [ + { + "kind": "leaf", + "name": "release", + "qname": "ansible:release", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + } + ], + "name": "version-release-choice" + } + ] + } +} diff --git a/tests/unit/modules/fixtures/config_config.json b/tests/unit/modules/fixtures/config_config.json new file mode 100644 index 00000000..b7318586 --- /dev/null +++ b/tests/unit/modules/fixtures/config_config.json @@ -0,0 +1,20 @@ +{ + "l3vpn:vpn": { + "l3vpn": [ + { + "name": "company", + "route-distinguisher": 999, + "endpoint": [ + { + "id": "branch-office1", + "ce-device": "ce6", + "ce-interface": "GigabitEthernet0/12", + "ip-network": "10.10.1.0/24", + "bandwidth": 12000000, + "as-number": 65101 + } + ] + } + ] + } +} diff --git a/tests/unit/modules/fixtures/config_config_changes.json b/tests/unit/modules/fixtures/config_config_changes.json new file mode 100644 index 00000000..3ef234b7 --- /dev/null +++ b/tests/unit/modules/fixtures/config_config_changes.json @@ -0,0 +1,46 @@ +{ + "changes": [ + { + "path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-device", + "old": "", + "value": "ce6", + "op": "value_set" + }, + { + "path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ip-network", + "old": "", + "value": "10.10.1.0/24", + "op": "value_set" + }, + { + "path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/as-number", + "old": "", + "value": "65101", + "op": "value_set" + }, + { + "path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-interface", + "old": "", + "value": "GigabitEthernet0/12", + "op": "value_set" + }, + { + "path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/bandwidth", + "old": "", + "value": "12000000", + "op": "value_set" + }, + { + "path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}", + "old": "", + "value": "", + "op": "created" + }, + { + "path": "/l3vpn:vpn/l3vpn{company}", + "old": "", + "value": "", + "op": "modified" + } + ] +} diff --git a/tests/unit/modules/fixtures/config_empty_data.json b/tests/unit/modules/fixtures/config_empty_data.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/unit/modules/fixtures/config_empty_data.json @@ -0,0 +1 @@ +{} diff --git a/tests/unit/modules/fixtures/description_schema.json b/tests/unit/modules/fixtures/description_schema.json new file mode 100644 index 00000000..2680a484 --- /dev/null +++ b/tests/unit/modules/fixtures/description_schema.json @@ -0,0 +1,28 @@ +{ + "meta": { + "prefix": "ncs", + "namespace": "http://tail-f.com/ns/ncs", + "types": { + }, + "keypath": "/ncs:devices/device{ce0}/description" + }, + "data": { + "info": { + "string": "Free form textual description" + }, + "kind": "leaf", + "name": "description", + "qname": "ncs:description", + "access": { + "read": true, + "create": true, + "execute": false, + "update": true, + "delete": true + }, + "type": { + "primitive": true, + "name": "string" + } + } +} diff --git a/tests/unit/modules/fixtures/device_schema.json b/tests/unit/modules/fixtures/device_schema.json new file mode 100644 index 00000000..d3bd2ac3 --- /dev/null +++ b/tests/unit/modules/fixtures/device_schema.json @@ -0,0 +1 @@ +{"meta": {"prefix": "ncs", "namespace": "http://tail-f.com/ns/ncs", "types": {"http://tail-f.com/ns/ncs:t85": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t85"}], "leaf_type": [{"name": "string"}]}], "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number": [{"range": {"value": [["0", "65535"]]}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number"}, {"name": "uint16"}], "http://tail-f.com/ns/ncs:t83": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t83"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:node-name": [{"name": "http://tail-f.com/ns/ncs:node-name"}, {"max-length": {"value": 253}, "min-length": {"value": 1}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:domain-name", "pattern": {"value": "((([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.)*([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.?)|\\."}}, {"name": "string"}], "http://tail-f.com/ns/ncs:t29": [{"range": {"value": [["1", "4294967"]]}, "name": "http://tail-f.com/ns/ncs:t29"}, {"name": "uint32"}], "http://tail-f.com/ns/ncs:t101": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t101"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t43": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t43"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t27": [{"range": {"value": [["1", "4294967"]]}, "name": "http://tail-f.com/ns/ncs:t27"}, {"name": "uint32"}], "http://tail-f.com/ns/ncs:t40": [{"name": "http://tail-f.com/ns/ncs:t40", "enumeration": [{"label": "reject"}, {"label": "accept"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:t47": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t47"}], "leaf_type": [{"name": "string"}]}], "urn:ietf:params:xml:ns:yang:ietf-inet-types:host": [{"union": [[{"name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:ip-address"}, {"name": "ip-address"}], [{"max-length": {"value": 253}, "min-length": {"value": 1}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:domain-name", "pattern": {"value": "((([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.)*([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.?)|\\."}}, {"name": "string"}]]}], "http://tail-f.com/ns/ncs:t45": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t45"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t49": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t49"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:trace-flag": [{"name": "http://tail-f.com/ns/ncs:trace-flag", "enumeration": [{"info": "Trace is disabled", "label": "false"}, {"info": "Raw, unformatted data", "label": "raw"}, {"info": "Pretty-printed data", "label": "pretty"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:t28": [{"range": {"value": [["1", "4294967"]]}, "name": "http://tail-f.com/ns/ncs:t28"}, {"name": "uint32"}]}, "keypath": "/ncs:devices/device"}, "data": {"info": {"string": "The list of managed devices"}, "kind": "list", "leafref_groups": [["remote-node"], ["authgroup"], ["device-profile"]], "mandatory": true, "name": "device", "max_elements": "unbounded", "contains_when_statement": true, "qname": "ncs:device", "children": [{"info": {"string": "A string uniquely identifying the managed device"}, "kind": "key", "mandatory": true, "name": "name", "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "IP address or host name for the management interface"}, "kind": "leaf", "name": "address", "qname": "ncs:address", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "host"}}, {"info": {"string": "Port for the management interface"}, "kind": "leaf", "name": "port", "qname": "ncs:port", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "Name of remote node which connects to device"}, "kind": "leaf", "name": "remote-node", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "node-name"}, "qname": "ncs:remote-node", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:cluster/remote-node/name", "is_leafref": true}, {"info": {"string": "SSH connection configuration"}, "kind": "container", "mandatory": true, "name": "ssh", "qname": "ncs:ssh", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Free form textual description"}, "kind": "leaf", "name": "description", "qname": "ncs:description", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "Physical location of devices in the group"}, "kind": "container", "mandatory": true, "name": "location", "qname": "ncs:location", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Authentication credentials for the device"}, "kind": "leaf", "name": "authgroup", "type": {"primitive": true, "name": "string"}, "qname": "ncs:authgroup", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/authgroups/group/name", "is_leafref": true}, {"info": {"string": "Management protocol for the device"}, "kind": "container", "mandatory": true, "name": "device-type", "qname": "ncs:device-type", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "leaf", "name": "device-profile", "type": {"primitive": true, "name": "string"}, "qname": "ncs:device-profile", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/profiles/profile/name", "is_leafref": true}, {"info": {"string": "Timeout in seconds for new connections"}, "kind": "leaf", "name": "connect-timeout", "qname": "ncs:connect-timeout", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "units": "seconds", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t27"}}, {"info": {"string": "Timeout in seconds used when reading data"}, "kind": "leaf", "name": "read-timeout", "qname": "ncs:read-timeout", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "units": "seconds", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t28"}}, {"info": {"string": "Timeout in seconds used when writing data"}, "kind": "leaf", "name": "write-timeout", "qname": "ncs:write-timeout", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "units": "seconds", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t29"}}, {"info": {"string": "Controls SSH keep alive settings"}, "kind": "container", "mandatory": true, "name": "ssh-keep-alive", "qname": "ncs:ssh-keep-alive", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Trace the southbound communication to devices"}, "kind": "leaf", "name": "trace", "qname": "ncs:trace", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "trace-flag"}}, {"info": {"string": "Control which device capabilities NCS uses"}, "kind": "container", "mandatory": true, "name": "ned-settings", "qname": "ncs:ned-settings", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Control settings for the commit-queue"}, "kind": "container", "mandatory": true, "name": "commit-queue", "qname": "ncs:commit-queue", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Control how sessions to related devices can be pooled."}, "kind": "container", "mandatory": true, "name": "session-pool", "qname": "ncs:session-pool", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Control settings for no-overwrite sync check"}, "kind": "container", "mandatory": true, "name": "no-overwrite", "qname": "ncs:no-overwrite", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Specifies the behaviour of a commit operation involving a\ndevice that is out of sync with NCS. Value accept assumes that\nthe device's sync state is unknown and it is cleared on commit.\nThe default behaviour is to reject such commits."}, "kind": "leaf", "name": "out-of-sync-commit-behaviour", "qname": "ncs:out-of-sync-commit-behaviour", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t40"}}, {"default": "use-lsa", "kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"info": {"string": "Handle the LSA nodes as such. This is the default"}, "kind": "leaf", "name": "use-lsa", "qname": "ncs:use-lsa", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"info": {"string": "Do not handle any of the LSA nodes as such. These nodes\nwill be handled as any other device. This has the same\nresult as adding the commit flag 'no-lsa' to every commit."}, "kind": "leaf", "name": "no-lsa", "qname": "ncs:no-lsa", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"info": {"string": "Show all active settings for the device"}, "kind": "container", "mandatory": true, "name": "active-settings", "qname": "ncs:active-settings", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Additional protocols for the live-tree (read-only)"}, "kind": "list", "leafref_groups": [["authgroup"]], "min_elements": 0, "name": "live-status-protocol", "max_elements": "unbounded", "qname": "ncs:live-status-protocol", "children": [{"kind": "key", "mandatory": true, "name": "name", "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "IP Address for the management interface"}, "kind": "leaf", "name": "address", "qname": "ncs:address", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "host"}}, {"info": {"string": "Port for the management interface"}, "kind": "leaf", "name": "port", "qname": "ncs:port", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "SSH host key configuration"}, "kind": "container", "name": "ssh", "presence": true, "qname": "ncs:ssh", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}}, {"info": {"string": "Authentication credentials for the device"}, "kind": "leaf", "name": "authgroup", "type": {"primitive": true, "name": "string"}, "qname": "ncs:authgroup", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/authgroups/group/name", "is_leafref": true}, {"info": {"string": "Management protocol for the device"}, "kind": "container", "mandatory": true, "name": "device-type", "qname": "ncs:device-type", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Operational State for the live protocol"}, "kind": "container", "mandatory": true, "name": "state", "qname": "ncs:state", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "List of capabillities supported by the device"}, "kind": "list", "min_elements": 0, "name": "capability", "max_elements": "unbounded", "qname": "ncs:capability", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["uri"], "mandatory": true, "config": false, "children": [{"info": {"string": "Capability URI"}, "kind": "key", "mandatory": true, "name": "uri", "type": {"primitive": true, "name": "string"}, "qname": "ncs:uri", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Capability revision"}, "kind": "leaf", "name": "revision", "type": {"primitive": true, "name": "string"}, "qname": "ncs:revision", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Capability module"}, "kind": "leaf", "name": "module", "type": {"primitive": true, "name": "string"}, "qname": "ncs:module", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Capability features"}, "kind": "leaf-list", "name": "feature", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t83"}, "qname": "ncs:feature", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Capability deviations"}, "kind": "leaf-list", "name": "deviation", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t85"}, "qname": "ncs:deviation", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}]}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["authgroup"]]}, {"info": {"string": "Show states for the device"}, "kind": "container", "mandatory": true, "name": "state", "qname": "ncs:state", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "How the device was added to NCS"}, "kind": "container", "mandatory": true, "name": "source", "qname": "ncs:source", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "A list of capabilities supported by the device"}, "kind": "list", "min_elements": 0, "name": "capability", "max_elements": "unbounded", "qname": "ncs:capability", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["uri"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "uri", "type": {"primitive": true, "name": "string"}, "qname": "ncs:uri", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "revision", "config": false, "qname": "ncs:revision", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "name": "module", "config": false, "qname": "ncs:module", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf-list", "name": "feature", "config": false, "qname": "ncs:feature", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t43"}}, {"kind": "leaf-list", "name": "deviation", "config": false, "qname": "ncs:deviation", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t45"}}, {"info": {"string": "This action removes a capability from the list of capabilities.\nIf leaf module is set then corresponding module is attempted to\nbe removed from the list of modules for this device. This action\nis only intended to be used for pre-provisioning: it is not\npossible to override capabilities and modules provided by the\nNED implementation using this action."}, "kind": "action", "mandatory": true, "name": "remove", "qname": "ncs:remove", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}]}, {"info": {"string": "This is a list of the YANG modules supported by the device.\n\nThis list is populated the first time NCS connects to the\ndevice."}, "kind": "list", "min_elements": 0, "name": "module", "max_elements": "unbounded", "qname": "ncs:module", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["name"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "name", "type": {"primitive": true, "name": "string"}, "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "revision", "config": false, "qname": "ncs:revision", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf-list", "name": "feature", "config": false, "qname": "ncs:feature", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t47"}}, {"kind": "leaf-list", "name": "deviation", "config": false, "qname": "ncs:deviation", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t49"}}]}, {"info": {"string": "Contains vendor-specific information for\nidentifying the system platform.\n\nNEDs MAY augment this container with more device-specific\nnodes."}, "kind": "container", "mandatory": true, "name": "platform", "qname": "ncs:platform", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "NCS copy of the device configuration"}, "kind": "container", "mandatory": true, "name": "config", "qname": "ncs:config", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Status data fetched from the device"}, "kind": "container", "mandatory": true, "name": "live-status", "qname": "ncs:live-status", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "RPCs from the device"}, "kind": "container", "mandatory": true, "name": "rpc", "qname": "ncs:rpc", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "NETCONF notifications from the device"}, "kind": "container", "mandatory": true, "name": "netconf-notifications", "qname": "ncs:netconf-notifications", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Show services that use this device"}, "kind": "leaf-list", "name": "service-list", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t101"}, "qname": "ncs:service-list", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Notification address if different from device address"}, "kind": "leaf", "name": "snmp-notification-address", "qname": "ncs:snmp-notification-address", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "host"}}, {"info": {"string": "Device specific information"}, "kind": "container", "name": "platform", "presence": true, "when_targets": ["/ncs:devices/device/device-type/cli/ned-id"], "qname": "alu-meta:platform", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}}, {"info": {"string": "A summary of all active alarms per device."}, "kind": "container", "mandatory": true, "name": "alarm-summary", "qname": "al:alarm-summary", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Note: this action overwrites existing list of capabilities.\n\nThis action copies the list of capabilities and the list of modules\nfrom another device or profile. When used on a device, this action\nis only intended to be used for pre-provisioning: it is not possible\nto override capabilities and modules provided by the\nNED implementation using this action."}, "kind": "action", "mandatory": true, "name": "copy-capabilities", "qname": "ncs:copy-capabilities", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Note: this action overwrites existing list of capabilities.\n\nThis action populates the list of capabilities based on the\nconfigured ned-id for this device, if possible. NCS will look up\nthe package corresponding to the ned-id and add all the modules\nfrom this packages to the list of this device's capabilities and\nlist of modules. It is the responsibility of the caller to verify\nthat the automatically populated list of capabilities matches actual\ndevice's capabilities. The list of capabilities can then be\nfine-tuned using add-capability and capability/remove actions.\nCurrently this approach will only work for CLI and generic devices.\nThis action is only intended to be used for pre-provisioning:\nit is not possible to override capabilities and modules provided\nby the NED implementation using this action."}, "kind": "action", "mandatory": true, "name": "find-capabilities", "qname": "ncs:find-capabilities", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "This action adds a capability to the list of capabilities.\nIf uri is specified, then it is parsed as YANG capability string\nand module, revision, feature and deviation parameters are derived\nfrom the string. If module is specified, then the namespace is\nlooked up in the list of loaded namespaces and capability string\nconstructed automatically. If the module is specified and the\nattempt to look it up failed, then the action does nothing.\nIf module is specified or can be derived from capability string,\nthen the module is also added/replaced in the list of modules. This\naction is only intended to be used for pre-provisioning: it is not\npossible to override capabilities and modules provided by the NED\nimplementation using this action."}, "kind": "action", "mandatory": true, "name": "add-capability", "qname": "ncs:add-capability", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Take a named template and copy it here"}, "kind": "action", "mandatory": true, "name": "apply-template", "leafrefGroups": [["template-name"]], "qname": "ncs:apply-template", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["template-name"]]}, {"info": {"string": "Instantiate the config for the device from existing device"}, "kind": "action", "mandatory": true, "name": "instantiate-from-other-device", "leafrefGroups": [["device-name"]], "qname": "ncs:instantiate-from-other-device", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device-name"]]}, {"info": {"string": "Compare the actual device config with the NCS copy"}, "kind": "action", "mandatory": true, "name": "compare-config", "qname": "ncs:compare-config", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Synchronize the config by pulling from the device"}, "kind": "action", "mandatory": true, "name": "sync-from", "qname": "ncs:sync-from", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Synchronize the config by pushing to the device"}, "kind": "action", "mandatory": true, "name": "sync-to", "qname": "ncs:sync-to", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if the NCS config is in sync with the device"}, "kind": "action", "mandatory": true, "name": "check-sync", "qname": "ncs:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if NCS and the device have compatible YANG modules"}, "kind": "action", "mandatory": true, "name": "check-yang-modules", "qname": "ncs:check-yang-modules", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Connect to the device"}, "kind": "action", "mandatory": true, "name": "connect", "qname": "ncs:connect", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Close all sessions to the device"}, "kind": "action", "mandatory": true, "name": "disconnect", "qname": "ncs:disconnect", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "ICMP ping the device"}, "kind": "action", "mandatory": true, "name": "ping", "qname": "ncs:ping", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Delete the config in NCS without deleting it in the device"}, "kind": "action", "mandatory": true, "name": "delete-config", "qname": "ncs:delete-config", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Secure copy file to the device"}, "kind": "action", "mandatory": true, "name": "scp-to", "qname": "ncs:scp-to", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Secure copy file to the device"}, "kind": "action", "mandatory": true, "name": "scp-from", "qname": "ncs:scp-from", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "min_elements": 0, "leafrefGroups": [["remote-node"], ["authgroup"], ["device-profile"]]}} diff --git a/tests/unit/modules/fixtures/devices_schema.json b/tests/unit/modules/fixtures/devices_schema.json new file mode 100644 index 00000000..541ba010 --- /dev/null +++ b/tests/unit/modules/fixtures/devices_schema.json @@ -0,0 +1 @@ +{"meta": {"prefix": "ncs", "namespace": "http://tail-f.com/ns/ncs", "types": {"http://tail-f.com/ns/ncs:t68": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t68"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t85": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t85"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t83": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t83"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t60": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t60"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t101": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t101"}], "leaf_type": [{"name": "string"}]}], "urn:ietf:params:xml:ns:yang:ietf-inet-types:host": [{"union": [[{"name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:ip-address"}, {"name": "ip-address"}], [{"max-length": {"value": 253}, "min-length": {"value": 1}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:domain-name", "pattern": {"value": "((([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.)*([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.?)|\\."}}, {"name": "string"}]]}], "http://tail-f.com/ns/ncs:trace-flag": [{"name": "http://tail-f.com/ns/ncs:trace-flag", "enumeration": [{"info": "Trace is disabled", "label": "false"}, {"info": "Raw, unformatted data", "label": "raw"}, {"info": "Pretty-printed data", "label": "pretty"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:t43": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t43"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t27": [{"range": {"value": [["1", "4294967"]]}, "name": "http://tail-f.com/ns/ncs:t27"}, {"name": "uint32"}], "http://tail-f.com/ns/ncs:t40": [{"name": "http://tail-f.com/ns/ncs:t40", "enumeration": [{"label": "reject"}, {"label": "accept"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:t47": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t47"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t45": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t45"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t49": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t49"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t29": [{"range": {"value": [["1", "4294967"]]}, "name": "http://tail-f.com/ns/ncs:t29"}, {"name": "uint32"}], "http://tail-f.com/ns/ncs:t28": [{"range": {"value": [["1", "4294967"]]}, "name": "http://tail-f.com/ns/ncs:t28"}, {"name": "uint32"}], "http://tail-f.com/ns/ncs:node-name": [{"name": "http://tail-f.com/ns/ncs:node-name"}, {"max-length": {"value": 253}, "min-length": {"value": 1}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:domain-name", "pattern": {"value": "((([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.)*([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.?)|\\."}}, {"name": "string"}], "http://tail-f.com/ns/ncs:t74": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t74"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t72": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t72"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t70": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t70"}], "leaf_type": [{"name": "string"}]}], "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number": [{"range": {"value": [["0", "65535"]]}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number"}, {"name": "uint16"}], "http://tail-f.com/ns/ncs:t56": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t56"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:t58": [{"list_type": [{"leaf-list": true, "name": "http://tail-f.com/ns/ncs:t58"}], "leaf_type": [{"name": "string"}]}]}, "keypath": "/ncs:devices"}, "data": {"info": {"string": "The managed devices and device communication settings"}, "kind": "container", "mandatory": true, "name": "devices", "contains_when_statement": true, "qname": "ncs:devices", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "children": [{"info": {"string": "Global settings for all managed devices."}, "kind": "container", "mandatory": true, "name": "global-settings", "qname": "ncs:global-settings", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Device profile parameters"}, "kind": "container", "mandatory": true, "name": "profiles", "qname": "ncs:profiles", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Authentication for managed devices"}, "kind": "container", "mandatory": true, "name": "authgroups", "qname": "ncs:authgroups", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Named configuration templates for devices"}, "kind": "list", "min_elements": 0, "name": "template", "max_elements": "unbounded", "qname": "ncs:template", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "children": [{"info": {"string": "The name of a specific template configuration."}, "kind": "key", "mandatory": true, "name": "name", "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "This container is augmented with data models from the devices."}, "kind": "container", "mandatory": true, "name": "config", "qname": "ncs:config", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}]}, {"info": {"string": "Groups of devices"}, "kind": "list", "leafref_groups": [["device-name"], ["device-group"], ["member"]], "min_elements": 0, "name": "device-group", "max_elements": "unbounded", "qname": "ncs:device-group", "children": [{"kind": "key", "mandatory": true, "name": "name", "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "Physical location of devices in the group"}, "kind": "container", "mandatory": true, "name": "location", "qname": "ncs:location", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Device within group"}, "kind": "leaf-list", "name": "device-name", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t56"}, "qname": "ncs:device-name", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/device/name", "is_leafref": true}, {"info": {"string": "Group within group"}, "kind": "leaf-list", "name": "device-group", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t58"}, "qname": "ncs:device-group", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/device-group/name", "is_leafref": true}, {"info": {"string": "Flattened list of all members"}, "kind": "leaf-list", "name": "member", "is_leafref": true, "qname": "ncs:member", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:devices/device/name", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t60"}, "config": false}, {"info": {"string": "RPCs from the device's"}, "kind": "container", "mandatory": true, "name": "rpc", "qname": "ncs:rpc", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "A summary of all active alarms per device group."}, "kind": "container", "mandatory": true, "name": "alarm-summary", "qname": "al:alarm-summary", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Set up sessions to all unlocked devices"}, "kind": "action", "mandatory": true, "name": "connect", "qname": "ncs:connect", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Synchronize the config by pushing to the devices"}, "kind": "action", "mandatory": true, "name": "sync-to", "qname": "ncs:sync-to", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Synchronize the config by pulling from the devices"}, "kind": "action", "mandatory": true, "name": "sync-from", "qname": "ncs:sync-from", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if the NCS config is in sync with the device"}, "kind": "action", "mandatory": true, "name": "check-sync", "qname": "ncs:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if NCS and the devices have compatible YANG modules"}, "kind": "action", "mandatory": true, "name": "check-yang-modules", "qname": "ncs:check-yang-modules", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Retrieve SSH host keys from all devices"}, "kind": "action", "mandatory": true, "name": "fetch-ssh-host-keys", "qname": "ncs:fetch-ssh-host-keys", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Take a named template and copy it here"}, "kind": "action", "mandatory": true, "name": "apply-template", "leafrefGroups": [["template-name"]], "qname": "ncs:apply-template", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["template-name"]]}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["device-name"], ["device-group"], ["member"]]}, {"info": {"string": "A list of named groups of MIBs"}, "kind": "list", "leafref_groups": [["mib-group"]], "min_elements": 0, "name": "mib-group", "max_elements": "unbounded", "qname": "ncs:mib-group", "children": [{"info": {"string": "An arbitrary name of the MIB group."}, "kind": "key", "mandatory": true, "name": "name", "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "MIB module names or name prefixes"}, "kind": "leaf-list", "name": "mib-module", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t68"}, "qname": "ncs:mib-module", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/device-module/mib-module", "is_leafref": true}, {"info": {"string": "A list of MIB groups contained in this MIB group"}, "kind": "leaf-list", "name": "mib-group", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t70"}, "qname": "ncs:mib-group", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/mib-group/name", "is_leafref": true}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["mib-group"]]}, {"info": {"string": "List the devices and supported modules"}, "kind": "list", "min_elements": 0, "name": "device-module", "max_elements": "unbounded", "qname": "ncs:device-module", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["name"], "mandatory": true, "config": false, "children": [{"info": {"string": "The module name"}, "kind": "key", "mandatory": true, "name": "name", "type": {"primitive": true, "name": "string"}, "qname": "ncs:name", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "The module revision"}, "kind": "leaf-list", "name": "revision", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t72"}, "qname": "ncs:revision", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "The XML namespace uri for the module"}, "kind": "leaf", "name": "uri", "type": {"primitive": true, "name": "string"}, "qname": "ncs:uri", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "The names of the devices that support this module"}, "kind": "leaf-list", "name": "devices", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t74"}, "qname": "ncs:devices", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}]}, {"info": {"string": "The list of managed devices"}, "kind": "list", "leafref_groups": [["remote-node"], ["authgroup"], ["device-profile"]], "min_elements": 0, "name": "device", "max_elements": "unbounded", "qname": "ncs:device", "children": [{"info": {"string": "A string uniquely identifying the managed device"}, "kind": "key", "mandatory": true, "name": "name", "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "IP address or host name for the management interface"}, "kind": "leaf", "name": "address", "qname": "ncs:address", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "host"}}, {"info": {"string": "Port for the management interface"}, "kind": "leaf", "name": "port", "qname": "ncs:port", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "Name of remote node which connects to device"}, "kind": "leaf", "name": "remote-node", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "node-name"}, "qname": "ncs:remote-node", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:cluster/remote-node/name", "is_leafref": true}, {"info": {"string": "SSH connection configuration"}, "kind": "container", "mandatory": true, "name": "ssh", "qname": "ncs:ssh", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Free form textual description"}, "kind": "leaf", "name": "description", "qname": "ncs:description", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "Physical location of devices in the group"}, "kind": "container", "mandatory": true, "name": "location", "qname": "ncs:location", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Authentication credentials for the device"}, "kind": "leaf", "name": "authgroup", "type": {"primitive": true, "name": "string"}, "qname": "ncs:authgroup", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/authgroups/group/name", "is_leafref": true}, {"info": {"string": "Management protocol for the device"}, "kind": "container", "mandatory": true, "name": "device-type", "qname": "ncs:device-type", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "leaf", "name": "device-profile", "type": {"primitive": true, "name": "string"}, "qname": "ncs:device-profile", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/profiles/profile/name", "is_leafref": true}, {"info": {"string": "Timeout in seconds for new connections"}, "kind": "leaf", "name": "connect-timeout", "qname": "ncs:connect-timeout", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "units": "seconds", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t27"}}, {"info": {"string": "Timeout in seconds used when reading data"}, "kind": "leaf", "name": "read-timeout", "qname": "ncs:read-timeout", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "units": "seconds", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t28"}}, {"info": {"string": "Timeout in seconds used when writing data"}, "kind": "leaf", "name": "write-timeout", "qname": "ncs:write-timeout", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "units": "seconds", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t29"}}, {"info": {"string": "Controls SSH keep alive settings"}, "kind": "container", "mandatory": true, "name": "ssh-keep-alive", "qname": "ncs:ssh-keep-alive", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Trace the southbound communication to devices"}, "kind": "leaf", "name": "trace", "qname": "ncs:trace", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "trace-flag"}}, {"info": {"string": "Control which device capabilities NCS uses"}, "kind": "container", "mandatory": true, "name": "ned-settings", "qname": "ncs:ned-settings", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Control settings for the commit-queue"}, "kind": "container", "mandatory": true, "name": "commit-queue", "qname": "ncs:commit-queue", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Control how sessions to related devices can be pooled."}, "kind": "container", "mandatory": true, "name": "session-pool", "qname": "ncs:session-pool", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Control settings for no-overwrite sync check"}, "kind": "container", "mandatory": true, "name": "no-overwrite", "qname": "ncs:no-overwrite", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Specifies the behaviour of a commit operation involving a\ndevice that is out of sync with NCS. Value accept assumes that\nthe device's sync state is unknown and it is cleared on commit.\nThe default behaviour is to reject such commits."}, "kind": "leaf", "name": "out-of-sync-commit-behaviour", "qname": "ncs:out-of-sync-commit-behaviour", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t40"}}, {"default": "use-lsa", "kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"info": {"string": "Handle the LSA nodes as such. This is the default"}, "kind": "leaf", "name": "use-lsa", "qname": "ncs:use-lsa", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"info": {"string": "Do not handle any of the LSA nodes as such. These nodes\nwill be handled as any other device. This has the same\nresult as adding the commit flag 'no-lsa' to every commit."}, "kind": "leaf", "name": "no-lsa", "qname": "ncs:no-lsa", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"info": {"string": "Show all active settings for the device"}, "kind": "container", "mandatory": true, "name": "active-settings", "qname": "ncs:active-settings", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Additional protocols for the live-tree (read-only)"}, "kind": "list", "leafref_groups": [["authgroup"]], "min_elements": 0, "name": "live-status-protocol", "max_elements": "unbounded", "qname": "ncs:live-status-protocol", "children": [{"kind": "key", "mandatory": true, "name": "name", "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "IP Address for the management interface"}, "kind": "leaf", "name": "address", "qname": "ncs:address", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "host"}}, {"info": {"string": "Port for the management interface"}, "kind": "leaf", "name": "port", "qname": "ncs:port", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "SSH host key configuration"}, "kind": "container", "name": "ssh", "presence": true, "qname": "ncs:ssh", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}}, {"info": {"string": "Authentication credentials for the device"}, "kind": "leaf", "name": "authgroup", "type": {"primitive": true, "name": "string"}, "qname": "ncs:authgroup", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/ncs:devices/authgroups/group/name", "is_leafref": true}, {"info": {"string": "Management protocol for the device"}, "kind": "container", "mandatory": true, "name": "device-type", "qname": "ncs:device-type", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Operational State for the live protocol"}, "kind": "container", "mandatory": true, "name": "state", "qname": "ncs:state", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "List of capabillities supported by the device"}, "kind": "list", "min_elements": 0, "name": "capability", "max_elements": "unbounded", "qname": "ncs:capability", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["uri"], "mandatory": true, "config": false, "children": [{"info": {"string": "Capability URI"}, "kind": "key", "mandatory": true, "name": "uri", "type": {"primitive": true, "name": "string"}, "qname": "ncs:uri", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Capability revision"}, "kind": "leaf", "name": "revision", "type": {"primitive": true, "name": "string"}, "qname": "ncs:revision", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Capability module"}, "kind": "leaf", "name": "module", "type": {"primitive": true, "name": "string"}, "qname": "ncs:module", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Capability features"}, "kind": "leaf-list", "name": "feature", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t83"}, "qname": "ncs:feature", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Capability deviations"}, "kind": "leaf-list", "name": "deviation", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t85"}, "qname": "ncs:deviation", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}]}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["authgroup"]]}, {"info": {"string": "Show states for the device"}, "kind": "container", "mandatory": true, "name": "state", "qname": "ncs:state", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "How the device was added to NCS"}, "kind": "container", "mandatory": true, "name": "source", "qname": "ncs:source", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "A list of capabilities supported by the device"}, "kind": "list", "min_elements": 0, "name": "capability", "max_elements": "unbounded", "qname": "ncs:capability", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["uri"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "uri", "type": {"primitive": true, "name": "string"}, "qname": "ncs:uri", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "revision", "config": false, "qname": "ncs:revision", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "name": "module", "config": false, "qname": "ncs:module", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf-list", "name": "feature", "config": false, "qname": "ncs:feature", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t43"}}, {"kind": "leaf-list", "name": "deviation", "config": false, "qname": "ncs:deviation", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t45"}}, {"info": {"string": "This action removes a capability from the list of capabilities.\nIf leaf module is set then corresponding module is attempted to\nbe removed from the list of modules for this device. This action\nis only intended to be used for pre-provisioning: it is not\npossible to override capabilities and modules provided by the\nNED implementation using this action."}, "kind": "action", "mandatory": true, "name": "remove", "qname": "ncs:remove", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}]}, {"info": {"string": "This is a list of the YANG modules supported by the device.\n\nThis list is populated the first time NCS connects to the\ndevice."}, "kind": "list", "min_elements": 0, "name": "module", "max_elements": "unbounded", "qname": "ncs:module", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["name"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "name", "type": {"primitive": true, "name": "string"}, "qname": "ncs:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "revision", "config": false, "qname": "ncs:revision", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf-list", "name": "feature", "config": false, "qname": "ncs:feature", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t47"}}, {"kind": "leaf-list", "name": "deviation", "config": false, "qname": "ncs:deviation", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t49"}}]}, {"info": {"string": "Contains vendor-specific information for\nidentifying the system platform.\n\nNEDs MAY augment this container with more device-specific\nnodes."}, "kind": "container", "mandatory": true, "name": "platform", "qname": "ncs:platform", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "NCS copy of the device configuration"}, "kind": "container", "mandatory": true, "name": "config", "qname": "ncs:config", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Status data fetched from the device"}, "kind": "container", "mandatory": true, "name": "live-status", "qname": "ncs:live-status", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "RPCs from the device"}, "kind": "container", "mandatory": true, "name": "rpc", "qname": "ncs:rpc", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "NETCONF notifications from the device"}, "kind": "container", "mandatory": true, "name": "netconf-notifications", "qname": "ncs:netconf-notifications", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}}, {"info": {"string": "Show services that use this device"}, "kind": "leaf-list", "name": "service-list", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "t101"}, "qname": "ncs:service-list", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Notification address if different from device address"}, "kind": "leaf", "name": "snmp-notification-address", "qname": "ncs:snmp-notification-address", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "host"}}, {"info": {"string": "Device specific information"}, "kind": "container", "name": "platform", "presence": true, "when_targets": ["/ncs:devices/device/device-type/cli/ned-id"], "qname": "alu-meta:platform", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}}, {"info": {"string": "A summary of all active alarms per device."}, "kind": "container", "mandatory": true, "name": "alarm-summary", "qname": "al:alarm-summary", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Note: this action overwrites existing list of capabilities.\n\nThis action copies the list of capabilities and the list of modules\nfrom another device or profile. When used on a device, this action\nis only intended to be used for pre-provisioning: it is not possible\nto override capabilities and modules provided by the\nNED implementation using this action."}, "kind": "action", "mandatory": true, "name": "copy-capabilities", "qname": "ncs:copy-capabilities", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Note: this action overwrites existing list of capabilities.\n\nThis action populates the list of capabilities based on the\nconfigured ned-id for this device, if possible. NCS will look up\nthe package corresponding to the ned-id and add all the modules\nfrom this packages to the list of this device's capabilities and\nlist of modules. It is the responsibility of the caller to verify\nthat the automatically populated list of capabilities matches actual\ndevice's capabilities. The list of capabilities can then be\nfine-tuned using add-capability and capability/remove actions.\nCurrently this approach will only work for CLI and generic devices.\nThis action is only intended to be used for pre-provisioning:\nit is not possible to override capabilities and modules provided\nby the NED implementation using this action."}, "kind": "action", "mandatory": true, "name": "find-capabilities", "qname": "ncs:find-capabilities", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "This action adds a capability to the list of capabilities.\nIf uri is specified, then it is parsed as YANG capability string\nand module, revision, feature and deviation parameters are derived\nfrom the string. If module is specified, then the namespace is\nlooked up in the list of loaded namespaces and capability string\nconstructed automatically. If the module is specified and the\nattempt to look it up failed, then the action does nothing.\nIf module is specified or can be derived from capability string,\nthen the module is also added/replaced in the list of modules. This\naction is only intended to be used for pre-provisioning: it is not\npossible to override capabilities and modules provided by the NED\nimplementation using this action."}, "kind": "action", "mandatory": true, "name": "add-capability", "qname": "ncs:add-capability", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Take a named template and copy it here"}, "kind": "action", "mandatory": true, "name": "apply-template", "leafrefGroups": [["template-name"]], "qname": "ncs:apply-template", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["template-name"]]}, {"info": {"string": "Instantiate the config for the device from existing device"}, "kind": "action", "mandatory": true, "name": "instantiate-from-other-device", "leafrefGroups": [["device-name"]], "qname": "ncs:instantiate-from-other-device", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device-name"]]}, {"info": {"string": "Compare the actual device config with the NCS copy"}, "kind": "action", "mandatory": true, "name": "compare-config", "qname": "ncs:compare-config", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Synchronize the config by pulling from the device"}, "kind": "action", "mandatory": true, "name": "sync-from", "qname": "ncs:sync-from", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Synchronize the config by pushing to the device"}, "kind": "action", "mandatory": true, "name": "sync-to", "qname": "ncs:sync-to", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if the NCS config is in sync with the device"}, "kind": "action", "mandatory": true, "name": "check-sync", "qname": "ncs:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if NCS and the device have compatible YANG modules"}, "kind": "action", "mandatory": true, "name": "check-yang-modules", "qname": "ncs:check-yang-modules", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Connect to the device"}, "kind": "action", "mandatory": true, "name": "connect", "qname": "ncs:connect", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Close all sessions to the device"}, "kind": "action", "mandatory": true, "name": "disconnect", "qname": "ncs:disconnect", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "ICMP ping the device"}, "kind": "action", "mandatory": true, "name": "ping", "qname": "ncs:ping", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Delete the config in NCS without deleting it in the device"}, "kind": "action", "mandatory": true, "name": "delete-config", "qname": "ncs:delete-config", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Secure copy file to the device"}, "kind": "action", "mandatory": true, "name": "scp-to", "qname": "ncs:scp-to", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Secure copy file to the device"}, "kind": "action", "mandatory": true, "name": "scp-from", "qname": "ncs:scp-from", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["remote-node"], ["authgroup"], ["device-profile"]]}, {"info": {"string": "List of queued and completed commits"}, "kind": "container", "mandatory": true, "name": "commit-queue", "qname": "ncs:commit-queue", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "List of pooled NED sessions"}, "kind": "container", "mandatory": true, "name": "session-pool", "qname": "ncs:session-pool", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"info": {"string": "Set up sessions to all unlocked devices"}, "kind": "action", "mandatory": true, "name": "connect", "leafrefGroups": [["device"]], "qname": "ncs:connect", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device"]]}, {"info": {"string": "Synchronize the config by pushing to the devices"}, "kind": "action", "mandatory": true, "name": "sync-to", "leafrefGroups": [["device"]], "qname": "ncs:sync-to", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device"]]}, {"info": {"string": "Synchronize the config by pulling from the devices"}, "kind": "action", "mandatory": true, "name": "sync-from", "leafrefGroups": [["device"]], "qname": "ncs:sync-from", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device"]]}, {"info": {"string": "Close all sessions to all devices"}, "kind": "action", "mandatory": true, "name": "disconnect", "qname": "ncs:disconnect", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if the NCS config is in sync with the device"}, "kind": "action", "mandatory": true, "name": "check-sync", "leafrefGroups": [["device"]], "qname": "ncs:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device"]]}, {"info": {"string": "Check if NCS and the devices have compatible YANG modules"}, "kind": "action", "mandatory": true, "name": "check-yang-modules", "leafrefGroups": [["device"]], "qname": "ncs:check-yang-modules", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device"]]}, {"info": {"string": "Retrieve SSH host keys from all devices"}, "kind": "action", "mandatory": true, "name": "fetch-ssh-host-keys", "leafrefGroups": [["device"]], "qname": "ncs:fetch-ssh-host-keys", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "leafref_groups": [["device"]]}, {"info": {"string": "Clear all trace files"}, "kind": "action", "mandatory": true, "name": "clear-trace", "qname": "ncs:clear-trace", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Synchronize parts of the devices' configuration by pulling from\nthe network."}, "kind": "action", "mandatory": true, "name": "partial-sync-from", "qname": "ncs:partial-sync-from", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}]}} diff --git a/tests/unit/modules/fixtures/l3vpn_l3vpn_endpoint_schema.json b/tests/unit/modules/fixtures/l3vpn_l3vpn_endpoint_schema.json new file mode 100644 index 00000000..0330aeb9 --- /dev/null +++ b/tests/unit/modules/fixtures/l3vpn_l3vpn_endpoint_schema.json @@ -0,0 +1 @@ +{"meta": {"prefix": "l3vpn", "namespace": "http://com/example/l3vpn", "types": {}, "keypath": "/l3vpn:vpn/l3vpn/endpoint"}, "data": {"kind": "list", "leafref_groups": [["ce-device"]], "min_elements": 0, "name": "endpoint", "max_elements": "unbounded", "qname": "l3vpn:endpoint", "children": [{"info": {"string": "Endpoint identifier"}, "kind": "key", "mandatory": true, "name": "id", "qname": "l3vpn:id", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ce-device", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:ce-device", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_target": "/ncs:devices/device/name", "is_leafref": true}, {"kind": "leaf", "mandatory": true, "name": "ce-interface", "qname": "l3vpn:ce-interface", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ip-network", "qname": "l3vpn:ip-network", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "ip-prefix"}}, {"info": {"string": "Bandwidth in bps"}, "kind": "leaf", "mandatory": true, "name": "bandwidth", "qname": "l3vpn:bandwidth", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"info": {"string": "CE Router as-number"}, "kind": "leaf", "name": "as-number", "qname": "l3vpn:as-number", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "uint32"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["id"], "mandatory": true, "leafrefGroups": [["ce-device"]]}} diff --git a/tests/unit/modules/fixtures/l3vpn_l3vpn_schema.json b/tests/unit/modules/fixtures/l3vpn_l3vpn_schema.json new file mode 100644 index 00000000..2737e7a5 --- /dev/null +++ b/tests/unit/modules/fixtures/l3vpn_l3vpn_schema.json @@ -0,0 +1 @@ +{"meta": {"prefix": "l3vpn", "namespace": "http://com/example/l3vpn", "types": {"http://com/example/l3vpn:t19": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t19"}], "leaf_type": [{"name": "instance-identifier"}]}], "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number": [{"range": {"value": [["0", "65535"]]}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number"}, {"name": "uint16"}], "http://tail-f.com/ns/ncs:outformat-deep-check-sync": [{"name": "http://tail-f.com/ns/ncs:outformat-deep-check-sync", "enumeration": [{"info": "The CLI config that would have to be applied\nto the device(s) in order for the service to\nbecome in sync with the network.", "label": "cli"}, {"info": "The XML (NETCONF format) that would have to be\napplied to the device(s) in order for the service to\nbecome in sync with the network.", "label": "xml"}, {"info": "Returns if the service is in sync or not.", "label": "boolean"}]}, {"name": "string"}], "http://com/example/l3vpn:t21": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t21"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:t15": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t15"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:protocol-type": [{"name": "http://com/example/l3vpn:protocol-type", "enumeration": [{"label": "icmp"}, {"label": "igmp"}, {"label": "ipip"}, {"label": "tcp"}, {"label": "egp"}, {"label": "udp"}, {"label": "rsvp"}, {"label": "gre"}, {"label": "esp"}, {"label": "ah"}, {"label": "icmp6"}, {"label": "ospf"}, {"label": "pim"}, {"label": "sctp"}]}, {"name": "string"}], "http://com/example/l3vpn:t17": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t17"}], "leaf_type": [{"name": "instance-identifier"}]}], "http://com/example/l3vpn:t23": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t23"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:t11": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t11"}], "leaf_type": [{"name": "instance-identifier"}]}], "http://com/example/l3vpn:t13": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t13"}], "leaf_type": [{"name": "instance-identifier"}]}], "http://com/example/l3vpn:t24": [{"name": "http://com/example/l3vpn:t24", "enumeration": [{"label": "waiting"}, {"label": "executing"}, {"label": "blocking"}, {"label": "blocked"}, {"label": "failed"}, {"label": "admin-cleared"}, {"label": "commit-queue-failed"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:log-entry-t": [{"info": "This leaf identifies the specific log entry.", "name": "http://tail-f.com/ns/ncs:log-entry-t", "enumeration": [{"label": "device-modified"}, {"label": "service-modified"}]}, {"name": "identityref"}], "http://com/example/l3vpn:t7": [{"name": "http://com/example/l3vpn:t7", "enumeration": [{"label": "async"}, {"label": "timeout"}, {"label": "deleted"}]}, {"name": "string"}], "http://com/example/l3vpn:qos-match-type": [{"union": [[{"name": "ipv4-address-and-prefix-length"}], [{"name": "http://com/example/l3vpn:t2", "enumeration": [{"label": "any"}]}, {"name": "string"}]]}], "http://com/example/l3vpn:t9": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t9"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:outformat4": [{"name": "http://tail-f.com/ns/ncs:outformat4", "enumeration": [{"info": "NCS CLI curly bracket format.", "label": "cli"}, {"info": "NETCONF XML edit-config format, i.e., the edit-config that\nwould be applied locally (at NCS) to get a config\nthat is equal to that of the managed device.", "label": "xml"}, {"info": "The actual data in native format that would be sent to\nthe device", "label": "native"}, {"label": "boolean"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:log-entry-level-t": [{"info": "Levels used for identifying the severity of an event.\nLevels are organized from least specific to most where\n'all' is least specific and 'error' is most specific.", "name": "http://tail-f.com/ns/ncs:log-entry-level-t", "enumeration": [{"label": "all"}, {"label": "trace"}, {"label": "debug"}, {"label": "info"}, {"label": "warn"}, {"label": "error"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:outformat2": [{"name": "http://tail-f.com/ns/ncs:outformat2", "enumeration": [{"info": "NCS CLI curly bracket format.", "label": "cli"}, {"info": "NETCONF XML edit-config format, i.e., the edit-config that\nwould be applied locally (at NCS) to get a config\nthat is equal to that of the managed device.", "label": "xml"}]}, {"name": "string"}]}, "keypath": "/l3vpn:vpn/l3vpn"}, "data": {"kind": "list", "leafref_groups": [["used-by-customer-service"]], "min_elements": 0, "name": "l3vpn", "max_elements": "unbounded", "qname": "l3vpn:l3vpn", "children": [{"info": {"string": "Unique service id"}, "kind": "key", "mandatory": true, "name": "name", "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "Devices and other services this service modified directly or\nindirectly."}, "kind": "container", "mandatory": true, "name": "modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false, "children": [{"info": {"string": "Devices this service modified directly or indirectly"}, "kind": "leaf-list", "name": "devices", "is_leafref": true, "qname": "l3vpn:devices", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:devices/device/name", "type": {"namespace": "http://com/example/l3vpn", "name": "t9"}, "config": false}, {"info": {"string": "Services this service modified directly or indirectly"}, "kind": "leaf-list", "name": "services", "type": {"namespace": "http://com/example/l3vpn", "name": "t11"}, "qname": "l3vpn:services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Services residing on remote LSA nodes this service\nhas modified directly or indirectly."}, "kind": "leaf-list", "name": "lsa-services", "type": {"namespace": "http://com/example/l3vpn", "name": "t13"}, "qname": "l3vpn:lsa-services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}]}, {"info": {"string": "Devices and other services this service has explicitly\nmodified."}, "kind": "container", "mandatory": true, "name": "directly-modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:directly-modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false, "children": [{"info": {"string": "Devices this service has explicitly modified."}, "kind": "leaf-list", "name": "devices", "is_leafref": true, "qname": "l3vpn:devices", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:devices/device/name", "type": {"namespace": "http://com/example/l3vpn", "name": "t15"}, "config": false}, {"info": {"string": "Services this service has explicitly modified."}, "kind": "leaf-list", "name": "services", "type": {"namespace": "http://com/example/l3vpn", "name": "t17"}, "qname": "l3vpn:services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Services residing on remote LSA nodes this service\nhas explicitly modified."}, "kind": "leaf-list", "name": "lsa-services", "type": {"namespace": "http://com/example/l3vpn", "name": "t19"}, "qname": "l3vpn:lsa-services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}]}, {"info": {"string": "A list of devices this service instance has manipulated"}, "kind": "leaf-list", "name": "device-list", "type": {"namespace": "http://com/example/l3vpn", "name": "t21"}, "qname": "l3vpn:device-list", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Customer facing services using this service"}, "kind": "leaf-list", "name": "used-by-customer-service", "is_leafref": true, "qname": "l3vpn:used-by-customer-service", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:services/customer-service/object-id", "type": {"namespace": "http://com/example/l3vpn", "name": "t23"}, "config": false}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false, "children": [{"kind": "list", "min_elements": 0, "name": "queue-item", "max_elements": "unbounded", "qname": "l3vpn:queue-item", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["id"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "status", "type": {"namespace": "http://com/example/l3vpn", "name": "t24"}, "qname": "l3vpn:status", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"kind": "list", "leafref_groups": [["name"]], "min_elements": 0, "name": "failed-device", "max_elements": "unbounded", "qname": "l3vpn:failed-device", "children": [{"kind": "key", "mandatory": true, "name": "name", "is_leafref": true, "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_target": "/ncs:devices/device/name", "type": {"primitive": true, "name": "string"}, "config": false}, {"kind": "leaf", "name": "time", "config": false, "qname": "l3vpn:time", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "date-and-time"}}, {"kind": "leaf", "name": "config-data", "is_cli_preformatted": true, "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:config-data", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"kind": "leaf", "name": "error", "config": false, "qname": "l3vpn:error", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["name"], "mandatory": true, "config": false, "leafrefGroups": [["name"]]}, {"access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "kind": "action", "mandatory": true, "name": "admin-clear", "qname": "l3vpn:admin-clear"}, {"access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "kind": "action", "mandatory": true, "name": "delete", "qname": "l3vpn:delete"}]}, {"access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "kind": "action", "mandatory": true, "name": "clear", "qname": "l3vpn:clear"}]}, {"kind": "container", "mandatory": true, "name": "log", "qname": "l3vpn:log", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false, "children": [{"kind": "list", "min_elements": 0, "name": "log-entry", "max_elements": "unbounded", "qname": "l3vpn:log-entry", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["when"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "when", "type": {"primitive": true, "name": "date-and-time"}, "qname": "l3vpn:when", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "mandatory": true, "name": "type", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "log-entry-t"}, "qname": "l3vpn:type", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "mandatory": true, "name": "level", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "log-entry-level-t"}, "qname": "l3vpn:level", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "message", "config": false, "qname": "l3vpn:message", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}]}, {"info": {"string": "Remove log entries"}, "kind": "action", "mandatory": true, "name": "purge", "qname": "l3vpn:purge", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}]}, {"kind": "leaf", "mandatory": true, "name": "route-distinguisher", "qname": "l3vpn:route-distinguisher", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"kind": "list", "leafref_groups": [["ce-device"]], "min_elements": 0, "name": "endpoint", "max_elements": "unbounded", "qname": "l3vpn:endpoint", "children": [{"info": {"string": "Endpoint identifier"}, "kind": "key", "mandatory": true, "name": "id", "qname": "l3vpn:id", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ce-device", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:ce-device", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_target": "/ncs:devices/device/name", "is_leafref": true}, {"kind": "leaf", "mandatory": true, "name": "ce-interface", "qname": "l3vpn:ce-interface", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ip-network", "qname": "l3vpn:ip-network", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "ip-prefix"}}, {"info": {"string": "Bandwidth in bps"}, "kind": "leaf", "mandatory": true, "name": "bandwidth", "qname": "l3vpn:bandwidth", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"info": {"string": "CE Router as-number"}, "kind": "leaf", "name": "as-number", "qname": "l3vpn:as-number", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "uint32"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["id"], "mandatory": true, "leafrefGroups": [["ce-device"]]}, {"kind": "container", "mandatory": true, "name": "qos", "leafrefGroups": [["qos-policy"]], "qname": "l3vpn:qos", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_groups": [["qos-policy"]], "children": [{"kind": "leaf", "name": "qos-policy", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:qos-policy", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/l3vpn:qos/qos-policy/name", "is_leafref": true}, {"kind": "list", "leafref_groups": [["qos-class"]], "min_elements": 0, "name": "custom-qos-match", "max_elements": "unbounded", "qname": "l3vpn:custom-qos-match", "children": [{"kind": "key", "mandatory": true, "name": "name", "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "qos-class", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:qos-class", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_target": "/l3vpn:qos/qos-class/name", "is_leafref": true}, {"access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "kind": "leaf", "type": {"namespace": "http://com/example/l3vpn", "name": "qos-match-type"}, "name": "source-ip", "qname": "l3vpn:source-ip"}, {"access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "kind": "leaf", "type": {"namespace": "http://com/example/l3vpn", "name": "qos-match-type"}, "name": "destination-ip", "qname": "l3vpn:destination-ip"}, {"info": {"string": "Destination IP port"}, "kind": "leaf", "name": "port-start", "qname": "l3vpn:port-start", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "Destination IP port"}, "kind": "leaf", "name": "port-end", "qname": "l3vpn:port-end", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "Source IP protocol"}, "kind": "leaf", "name": "protocol", "qname": "l3vpn:protocol", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "http://com/example/l3vpn", "name": "protocol-type"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["qos-class"]]}]}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "check-sync", "qname": "l3vpn:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "is_action_input": true, "name": "outformat", "default": "boolean", "qname": "l3vpn:outformat", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "outformat4"}}, {"default": "deep", "kind": "choice", "cases": [{"kind": "case", "name": "deep", "children": [{"kind": "leaf", "is_action_input": true, "name": "deep", "qname": "l3vpn:deep", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "shallow", "children": [{"kind": "leaf", "is_action_input": true, "name": "shallow", "qname": "l3vpn:shallow", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "depth"}, {"info": {"string": "Return list only contains negatives"}, "kind": "leaf", "is_action_input": true, "name": "suppress-positive-result", "qname": "l3vpn:suppress-positive-result", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "choice", "cases": [{"kind": "case", "name": "in-sync", "children": [{"kind": "leaf", "name": "in-sync", "qname": "l3vpn:in-sync", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "boolean"}, "is_action_output": true}]}, {"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-native", "children": [{"kind": "container", "mandatory": true, "name": "native", "qname": "l3vpn:native", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}]}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "deep-check-sync", "qname": "l3vpn:deep-check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "is_action_input": true, "name": "outformat", "default": "boolean", "qname": "l3vpn:outformat", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "outformat-deep-check-sync"}}, {"info": {"string": "Return list only contains negatives"}, "kind": "leaf", "is_action_input": true, "name": "suppress-positive-result", "qname": "l3vpn:suppress-positive-result", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-sync", "children": [{"kind": "container", "mandatory": true, "name": "sync-result", "qname": "l3vpn:sync-result", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}]}, {"info": {"string": "Run/Dryrun the service logic again"}, "kind": "action", "mandatory": true, "name": "re-deploy", "qname": "l3vpn:re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "container", "is_action_input": true, "name": "dry-run", "presence": true, "qname": "l3vpn:dry-run", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "leaf", "is_action_input": true, "name": "no-revision-drop", "qname": "l3vpn:no-revision-drop", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "leaf", "is_action_input": true, "name": "no-networking", "qname": "l3vpn:no-networking", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "no-overwrite", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-overwrite", "qname": "l3vpn:no-overwrite", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-out-of-sync-check", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-out-of-sync-check", "qname": "l3vpn:no-out-of-sync-check", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-sync-check"}, {"kind": "container", "is_action_input": true, "name": "commit-queue", "presence": true, "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"default": "deep", "kind": "choice", "cases": [{"kind": "case", "name": "deep", "children": [{"kind": "leaf", "is_action_input": true, "name": "deep", "qname": "l3vpn:deep", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "shallow", "children": [{"kind": "leaf", "is_action_input": true, "name": "shallow", "qname": "l3vpn:shallow", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "depth"}, {"kind": "container", "is_action_input": true, "name": "reconcile", "presence": true, "qname": "l3vpn:reconcile", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-native", "children": [{"kind": "container", "mandatory": true, "name": "native", "qname": "l3vpn:native", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}, {"kind": "leaf", "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "leafref_target": "/ncs:devices/commit-queue/queue-item/id", "is_leafref": true, "is_action_output": true}, {"kind": "leaf", "name": "tag", "qname": "l3vpn:tag", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "string"}, "is_action_output": true}, {"kind": "leaf", "name": "status", "qname": "l3vpn:status", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"namespace": "http://com/example/l3vpn", "name": "t7"}, "is_action_output": true}]}, {"info": {"string": "Reactive redeploy of service logic"}, "kind": "action", "mandatory": true, "name": "reactive-re-deploy", "qname": "l3vpn:reactive-re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "leafref_target": "/ncs:devices/commit-queue/queue-item/id", "is_leafref": true, "is_action_output": true}, {"kind": "leaf", "name": "tag", "qname": "l3vpn:tag", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "string"}, "is_action_output": true}, {"kind": "leaf", "name": "status", "qname": "l3vpn:status", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"namespace": "http://com/example/l3vpn", "name": "t7"}, "is_action_output": true}]}, {"info": {"string": "Touch a service"}, "kind": "action", "mandatory": true, "name": "touch", "qname": "l3vpn:touch", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": []}, {"info": {"string": "Get the data this service created"}, "kind": "action", "mandatory": true, "name": "get-modifications", "qname": "l3vpn:get-modifications", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "is_action_input": true, "name": "outformat", "qname": "l3vpn:outformat", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "outformat2"}}, {"kind": "leaf", "is_action_input": true, "name": "reverse", "qname": "l3vpn:reverse", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"default": "deep", "kind": "choice", "cases": [{"kind": "case", "name": "deep", "children": [{"kind": "leaf", "is_action_input": true, "name": "deep", "qname": "l3vpn:deep", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "shallow", "children": [{"kind": "leaf", "is_action_input": true, "name": "shallow", "qname": "l3vpn:shallow", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "depth"}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}]}, {"info": {"string": "Undo the effects of this service"}, "kind": "action", "mandatory": true, "name": "un-deploy", "qname": "l3vpn:un-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "container", "is_action_input": true, "name": "dry-run", "presence": true, "qname": "l3vpn:dry-run", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "leaf", "is_action_input": true, "name": "no-revision-drop", "qname": "l3vpn:no-revision-drop", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "leaf", "is_action_input": true, "name": "no-networking", "qname": "l3vpn:no-networking", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "no-overwrite", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-overwrite", "qname": "l3vpn:no-overwrite", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-out-of-sync-check", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-out-of-sync-check", "qname": "l3vpn:no-out-of-sync-check", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-sync-check"}, {"kind": "container", "is_action_input": true, "name": "commit-queue", "presence": true, "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "leaf", "is_action_input": true, "name": "ignore-refcount", "qname": "l3vpn:ignore-refcount", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-native", "children": [{"kind": "container", "mandatory": true, "name": "native", "qname": "l3vpn:native", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}, {"kind": "leaf", "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "leafref_target": "/ncs:devices/commit-queue/queue-item/id", "is_leafref": true, "is_action_output": true}, {"kind": "leaf", "name": "tag", "qname": "l3vpn:tag", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "string"}, "is_action_output": true}, {"kind": "leaf", "name": "status", "qname": "l3vpn:status", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"namespace": "http://com/example/l3vpn", "name": "t7"}, "is_action_output": true}]}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["used-by-customer-service"]]}} diff --git a/tests/unit/modules/fixtures/l3vpn_schema.json b/tests/unit/modules/fixtures/l3vpn_schema.json new file mode 100644 index 00000000..0e7e3703 --- /dev/null +++ b/tests/unit/modules/fixtures/l3vpn_schema.json @@ -0,0 +1 @@ +{"meta": {"prefix": "l3vpn", "namespace": "http://com/example/l3vpn", "types": {"http://com/example/l3vpn:t21": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t21"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:t23": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t23"}], "leaf_type": [{"name": "string"}]}]}, "keypath": "/l3vpn:vpn"}, "data": {"kind": "container", "mandatory": true, "name": "vpn", "qname": "l3vpn:vpn", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "children": [{"kind": "list", "leafref_groups": [["used-by-customer-service"]], "min_elements": 0, "name": "l3vpn", "max_elements": "unbounded", "qname": "l3vpn:l3vpn", "children": [{"info": {"string": "Unique service id"}, "kind": "key", "mandatory": true, "name": "name", "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "Devices and other services this service modified directly or\nindirectly."}, "kind": "container", "mandatory": true, "name": "modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false}, {"info": {"string": "Devices and other services this service has explicitly\nmodified."}, "kind": "container", "mandatory": true, "name": "directly-modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:directly-modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false}, {"info": {"string": "A list of devices this service instance has manipulated"}, "kind": "leaf-list", "name": "device-list", "type": {"namespace": "http://com/example/l3vpn", "name": "t21"}, "qname": "l3vpn:device-list", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Customer facing services using this service"}, "kind": "leaf-list", "name": "used-by-customer-service", "is_leafref": true, "qname": "l3vpn:used-by-customer-service", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:services/customer-service/object-id", "type": {"namespace": "http://com/example/l3vpn", "name": "t23"}, "config": false}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "container", "mandatory": true, "name": "log", "qname": "l3vpn:log", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "mandatory": true, "name": "route-distinguisher", "qname": "l3vpn:route-distinguisher", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"kind": "list", "leafref_groups": [["ce-device"]], "min_elements": 0, "name": "endpoint", "max_elements": "unbounded", "qname": "l3vpn:endpoint", "children": [{"info": {"string": "Endpoint identifier"}, "kind": "key", "mandatory": true, "name": "id", "qname": "l3vpn:id", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ce-device", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:ce-device", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_target": "/ncs:devices/device/name", "is_leafref": true}, {"kind": "leaf", "mandatory": true, "name": "ce-interface", "qname": "l3vpn:ce-interface", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ip-network", "qname": "l3vpn:ip-network", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "ip-prefix"}}, {"info": {"string": "Bandwidth in bps"}, "kind": "leaf", "mandatory": true, "name": "bandwidth", "qname": "l3vpn:bandwidth", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"info": {"string": "CE Router as-number"}, "kind": "leaf", "name": "as-number", "qname": "l3vpn:as-number", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "uint32"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["id"], "mandatory": true, "leafrefGroups": [["ce-device"]]}, {"kind": "container", "mandatory": true, "name": "qos", "leafrefGroups": [["qos-policy"]], "qname": "l3vpn:qos", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_groups": [["qos-policy"]]}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "check-sync", "qname": "l3vpn:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "deep-check-sync", "qname": "l3vpn:deep-check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Run/Dryrun the service logic again"}, "kind": "action", "mandatory": true, "name": "re-deploy", "qname": "l3vpn:re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Reactive redeploy of service logic"}, "kind": "action", "mandatory": true, "name": "reactive-re-deploy", "qname": "l3vpn:reactive-re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Touch a service"}, "kind": "action", "mandatory": true, "name": "touch", "qname": "l3vpn:touch", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Get the data this service created"}, "kind": "action", "mandatory": true, "name": "get-modifications", "qname": "l3vpn:get-modifications", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Undo the effects of this service"}, "kind": "action", "mandatory": true, "name": "un-deploy", "qname": "l3vpn:un-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["used-by-customer-service"]]}]}} diff --git a/tests/unit/modules/fixtures/sync_from_schema.json b/tests/unit/modules/fixtures/sync_from_schema.json new file mode 100644 index 00000000..dc2206d4 --- /dev/null +++ b/tests/unit/modules/fixtures/sync_from_schema.json @@ -0,0 +1,178 @@ +{ + "meta": { + "prefix": "ncs", + "namespace": "http://tail-f.com/ns/ncs", + "types": { + "http://tail-f.com/ns/ncs:outformat2": [ + { + "name": "http://tail-f.com/ns/ncs:outformat2", + "enumeration": [ + { + "info": "NCS CLI curly bracket format.", + "label": "cli" + }, + { + "info": "NETCONF XML edit-config format, i.e., the edit-config that\nwould be applied locally (at NCS) to get a config\nthat is equal to that of the managed device.", + "label": "xml" + } + ] + }, + { + "name": "string" + } + ] + }, + "keypath": "/ncs:devices/device{ce0}/sync-from" + }, + "data": { + "info": { + "string": "Synchronize the config by pulling from the device" + }, + "kind": "action", + "mandatory": true, + "name": "sync-from", + "qname": "ncs:sync-from", + "access": { + "read": false, + "create": false, + "execute": true, + "update": false, + "delete": false + }, + "children": [ + { + "kind": "container", + "is_action_input": true, + "name": "dry-run", + "presence": true, + "qname": "ncs:dry-run", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "children": [ + { + "info": { + "string": "Report what would be done towards CDB, without\nactually doing anything." + }, + "kind": "leaf", + "is_action_input": true, + "name": "outformat", + "qname": "ncs:outformat", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "namespace": "http://tail-f.com/ns/ncs", + "name": "outformat2" + } + } + ] + }, + { + "kind": "choice", + "cases": [ + { + "kind": "case", + "name": "result", + "children": [ + { + "kind": "leaf", + "name": "result", + "qname": "ncs:result", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "boolean" + }, + "is_action_output": true + } + ] + }, + { + "kind": "case", + "name": "result-xml", + "children": [ + { + "kind": "leaf", + "name": "result-xml", + "is_cli_preformatted": true, + "qname": "ncs:result-xml", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + }, + { + "kind": "case", + "name": "cli", + "children": [ + { + "kind": "leaf", + "name": "cli", + "is_cli_preformatted": true, + "qname": "ncs:cli", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + } + ], + "name": "outformat" + }, + { + "info": { + "string": "If present, contains additional information about the result." + }, + "kind": "leaf", + "name": "info", + "qname": "ncs:info", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + } +} diff --git a/tests/unit/modules/fixtures/verify_violation_data.json b/tests/unit/modules/fixtures/verify_violation_data.json new file mode 100644 index 00000000..05742c11 --- /dev/null +++ b/tests/unit/modules/fixtures/verify_violation_data.json @@ -0,0 +1,10 @@ +{ + "tailf-ncs:devices": { + "device": [ + { + "name": "ce0", + "description": "Example Device" + } + ] + } +} diff --git a/tests/unit/modules/utils.py b/tests/unit/modules/utils.py new file mode 100644 index 00000000..ea25b246 --- /dev/null +++ b/tests/unit/modules/utils.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import json + +from ansible_collections.cisco.ftdansible.tests.unit.compat import unittest +from ansible_collections.cisco.ftdansible.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/test/unit/test_ftd_configuration.py b/tests/unit/test_ftd_configuration.py similarity index 79% rename from test/unit/test_ftd_configuration.py rename to tests/unit/test_ftd_configuration.py index 6eb88a39..6ad44822 100644 --- a/test/unit/test_ftd_configuration.py +++ b/tests/unit/test_ftd_configuration.py @@ -1,19 +1,21 @@ -from __future__ import absolute_import +from __future__ import absolute_import, division, print_function + +__metaclass__ = type import pytest from ansible.module_utils import basic -from units.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson - -from library import ftd_configuration +from ansible_collections.cisco.ftdansible.tests.unit.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson try: from ansible.module_utils.common import FtdConfigurationError, FtdServerError, FtdUnexpectedResponse from ansible.module_utils.configuration import FtdInvalidOperationNameError, CheckModeException from ansible.module_utils.fdm_swagger_client import ValidationError + from library import ftd_configuration except ImportError: - from module_utils.common import FtdConfigurationError, FtdServerError, FtdUnexpectedResponse - from module_utils.configuration import FtdInvalidOperationNameError, CheckModeException - from module_utils.fdm_swagger_client import ValidationError + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import FtdConfigurationError, FtdServerError, FtdUnexpectedResponse + from ansible_collections.cisco.ftdansible.plugins.module_utils.configuration import FtdInvalidOperationNameError, CheckModeException + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import ValidationError + from ansible_collections.cisco.ftdansible.plugins.modules import ftd_configuration class TestFtdConfiguration(object): @@ -25,12 +27,12 @@ def module_mock(self, mocker): @pytest.fixture(autouse=True) def connection_mock(self, mocker): - connection_class_mock = mocker.patch('library.ftd_configuration.Connection') + connection_class_mock = mocker.patch('ansible_collections.cisco.ftdansible.plugins.modules.ftd_configuration.Connection') return connection_class_mock.return_value @pytest.fixture def resource_mock(self, mocker): - resource_class_mock = mocker.patch('library.ftd_configuration.BaseConfigurationResource') + resource_class_mock = mocker.patch('ansible_collections.cisco.ftdansible.plugins.modules.ftd_configuration.BaseConfigurationResource') resource_instance = resource_class_mock.return_value return resource_instance.execute_operation @@ -108,5 +110,6 @@ def _run_module_with_fail_json(self, module_args): set_module_args(module_args) with pytest.raises(AnsibleFailJson) as exc: self.module.main() + result = exc.value.args[0] return result diff --git a/test/unit/test_ftd_file_download.py b/tests/unit/test_ftd_file_download.py similarity index 74% rename from test/unit/test_ftd_file_download.py rename to tests/unit/test_ftd_file_download.py index c3990803..0fa1088a 100644 --- a/test/unit/test_ftd_file_download.py +++ b/tests/unit/test_ftd_file_download.py @@ -1,12 +1,20 @@ -from __future__ import absolute_import +from __future__ import absolute_import, division, print_function + +__metaclass__ = type import pytest from ansible.module_utils import basic -from units.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson -from library import ftd_file_download -from module_utils.fdm_swagger_client import FILE_MODEL_NAME, OperationField -from module_utils.common import HTTPMethod +try: + from library import ftd_file_download + from module_utils.fdm_swagger_client import FILE_MODEL_NAME, OperationField + from module_utils.common import HTTPMethod + from units.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson +except ImportError: + from ansible_collections.cisco.ftdansible.plugins.modules import ftd_file_download + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import FILE_MODEL_NAME, OperationField + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod + from ansible_collections.cisco.ftdansible.tests.unit.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson class TestFtdFileDownload(object): @@ -18,7 +26,7 @@ def module_mock(self, mocker): @pytest.fixture def connection_mock(self, mocker): - connection_class_mock = mocker.patch('library.ftd_file_download.Connection') + connection_class_mock = mocker.patch('ansible_collections.cisco.ftdansible.plugins.modules.ftd_file_download.Connection') return connection_class_mock.return_value @pytest.mark.parametrize("missing_arg", ['operation', 'destination']) diff --git a/test/unit/test_ftd_file_upload.py b/tests/unit/test_ftd_file_upload.py similarity index 75% rename from test/unit/test_ftd_file_upload.py rename to tests/unit/test_ftd_file_upload.py index f4ea1349..688ff8f8 100644 --- a/test/unit/test_ftd_file_upload.py +++ b/tests/unit/test_ftd_file_upload.py @@ -1,12 +1,20 @@ -from __future__ import absolute_import +from __future__ import absolute_import, division, print_function + +__metaclass__ = type import pytest from ansible.module_utils import basic -from units.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson -from library import ftd_file_upload -from module_utils.fdm_swagger_client import OperationField -from module_utils.common import HTTPMethod +try: + from library import ftd_file_upload + from module_utils.fdm_swagger_client import OperationField + from module_utils.common import HTTPMethod + from units.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson +except ImportError: + from ansible_collections.cisco.ftdansible.plugins.modules import ftd_file_upload + from ansible_collections.cisco.ftdansible.plugins.module_utils.fdm_swagger_client import OperationField + from ansible_collections.cisco.ftdansible.plugins.module_utils.common import HTTPMethod + from ansible_collections.cisco.ftdansible.tests.unit.modules.utils import set_module_args, exit_json, fail_json, AnsibleFailJson, AnsibleExitJson class TestFtdFileUpload(object): @@ -18,7 +26,7 @@ def module_mock(self, mocker): @pytest.fixture def connection_mock(self, mocker): - connection_class_mock = mocker.patch('library.ftd_file_upload.Connection') + connection_class_mock = mocker.patch('ansible_collections.cisco.ftdansible.plugins.modules.ftd_file_upload.Connection') return connection_class_mock.return_value @pytest.mark.parametrize("missing_arg", ['operation', 'file_to_upload']) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 361ba9fd..00000000 --- a/tox.ini +++ /dev/null @@ -1,43 +0,0 @@ -# tox (https://tox.readthedocs.io/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - -[tox] -envlist = py{27,35,36,37}{,-integration} -skipsdist = True - -[testenv] -deps = - -r{toxinidir}/requirements.txt - !integration: -r{toxinidir}/test-requirements.txt - integration: junit_xml - -# Following line allow tox inherit predefined PYTHONPATH env var. -# set PYTHONPATH= can be used instead as well -passenv = PYTHONPATH -setenv = - integration: ANSIBLE_STDOUT_CALLBACK = junit - integration: JUNIT_OUTPUT_DIR = {env:REPORTS_DIR} - -commands = - python --version -# Run unit tests by default - !integration: pytest --junitxml=.tox/junit-{envname}.xml test/unit -# Run integration test if in intgration env - integration: ansible-playbook {posargs} - -[flake8] -ignore = E402 -max-line-length = 120 -exclude = - .git, - .tox, - .pytest_cache, - venv, - docs/dist, - docs/static - docs/templates, - inventory, - samples, - __pycache__