diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8af1a17a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.travis.yml +ISSUE_TEMPLATE.md +docs/ +test/ +tools/ +COPYRIGHT +env-setup +.vscode diff --git a/.gitignore b/.gitignore index 5a1cee33..ef97d417 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,18 @@ nosetests.xml docs/build docs/*.rst docs/_build/ +docs/sphinx.log + +tests/.vagrant/* + +# Window File Explorer +desktop.ini + +# Mac OS X Finder +.DS_Store + +# PyCharm +.idea + +# VC Code +.vscode diff --git a/COPYRIGHT b/COPYRIGHT index 0a5bbb37..5274b3e4 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,5 +1,5 @@ - Copyright (c) 1999-2014, Juniper Networks Inc. + Copyright (c) 1999-2020, Juniper Networks Inc. 2014, Jeremy Schulman All rights reserved. diff --git a/DOCKER-EXAMPLES.md b/DOCKER-EXAMPLES.md new file mode 100644 index 00000000..ea927c87 --- /dev/null +++ b/DOCKER-EXAMPLES.md @@ -0,0 +1,78 @@ +# Examples of using the Docker image + +To run this as a Docker container, which includes JSNAPy and PyEZ, simply pull it from the Docker hub and run it. The following will pull the latest image and run it in an interactive ash shell. + + docker run -it --rm juniper/pyez-ansible + +Although, you'll probably want to bind mount a host directory (perhaps the directory containing your playbooks and associated files). The following will bind mount the current working directory and start the ash shell. + + docker run -it --rm -v $PWD:/project juniper/pyez-ansible + +You can also use the container as an executable to run your playbooks. Let's assume we have a typical playbook structure as below: + + example + |playbook.yml + |hosts + |-vars + |-templates + |-scripts + +We can move to the example directory and run the playbook with the following command: + + cd example/ + docker run -it --rm -v $PWD:/playbooks juniper/pyez-ansible ansible-playbook -i hosts playbook.yml + +You can pass any valid command string after the container name and it will be passed to Bash for execution. + +You may have noticed that the base command is almost always the same. We can also use an alias to save some keystrokes. + + alias pb-ansible="docker run -it --rm -v $PWD:/project juniper/pyez-ansible ansible-playbook" + pb-ansible -i hosts playbook.yml + +### Extending the container with additional packages + +It's possible to install additional OS (Alpine) packages, Python packages (via pip), and Ansible collections at container instantiation. This can be done by passing in environment variables or binding mount files. + +#### OS Packages + +Environment Variable: `$APK` +Bind Mount: `/extras/apk.txt` +File Format: list of valid Alpine packages, one per line +Examples: + +As an environment variable, where the file containing a list of packages is in the current directory. + + docker run -it --rm -v $PWD:/project -e APK="apk.txt" juniper/pyez-ansible + +As a bind mount. + + docker run -it --rm -v $PWD/apk.txt:/extras/apk.txt juniper/pyez-ansible + +#### Python Packages + +Environment Variable: `$REQ` +Bind Mount: `/extras/requirements.txt` +File Format: pip [requirements](https://pip.pypa.io/en/stable/reference/requirements-file-format/) file + +Examples: + + docker run -it --rm -v $PWD:/project -e REQ="requirements.txt" juniper/pyez-ansible + +As a bind mount. + + docker run -it --rm -v $PWD/requirements.txt:/extras/requirements.txt juniper/pyez-ansible + +#### Ansible Packages + +Environment Variable: `$COLLECTIONS` +Bind Mount: `/extras/requirements.yml` +File Format: Ansible [requirements](https://docs.ansible.com/ansible/devel/user_guide/collections_using.html#install-multiple-collections-with-a-requirements-file) file + + +Examples: + + docker run -it --rm -v $PWD:/project -e REQ="requirements.yml" juniper/pyez-ansible + +As a bind mount. + + docker run -it --rm -v $PWD/requirements.txt:/extras/requirements.yml juniper/pyez-ansible diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..cce6b48d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM python:3.12-alpine + +LABEL net.juniper.image.maintainer="Juniper Networks " \ + net.juniper.image.description="Lightweight image with Ansible and the Junos roles" + +WORKDIR /tmp + +## Copy project inside the containers +ADD requirements.txt . +ADD entrypoint.sh /usr/local/bin/. + +## Install dependencies and PyEZ +RUN apk add --no-cache build-base python3-dev py3-pip \ + libxslt-dev libxml2-dev libffi-dev openssl-dev curl \ + ca-certificates py3-pip bash openssh-client + +RUN pip install --upgrade pip \ + && python3 -m pip install -r requirements.txt + +# Also install the collections juniper.device +# Install Ansible modules in one layer +RUN ansible-galaxy collection install juniper.device + +## Clean up and start init +RUN apk del -r --purge gcc make g++ \ + && rm -rf /var/cache/apk/* \ + && rm -rf /tmp/* \ + && rm -rf /root/.cache \ + && chmod +x /usr/local/bin/entrypoint.sh + +WORKDIR /project + +VOLUME /project + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..35546af3 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,55 @@ + + +Issue Type +------ + + - Bug Report + - Feature Idea + - Documentation Report + +Module Name +------ + + +juniper.device collection and Python libraries version + +``` + +``` + +OS / Environment +------ + + +Summary +------ + + +Steps to reproduce +------ + + + +```yaml + +``` + + + +Expected results + +``` + +``` +Actual results +------ + + + +``` + +``` diff --git a/LICENSE b/LICENSE index e06d2081..5c304d1a 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,3 @@ Apache License WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/README.md b/README.md index 289f90b9..55c1479f 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,203 @@ -## ABOUT +[![Documentation Status](https://readthedocs.org/projects/junos-ansible-modules/badge/?version=stable)](https://junos-ansible-modules.readthedocs.io/en/2.3.0/) -Juniper Networks provides support for using Ansible to deploy devices running the Junos operating system (Junos OS). The Juniper Networks Ansible library, which is hosted on the Ansible Galaxy website under the role [junos](https://galaxy.ansible.com/list#/roles/1116), enables you to use Ansible to perform specific operational and configuration tasks on devices running Junos OS, including installing and upgrading Junos OS, deploying specific devices in the network, loading configuration changes, retrieving information, and resetting, rebooting, or shutting down managed devices. Please refer to [INSTALLATION](#installation) section for setup. +# Juniper Ansible collection for Junos -## OVERVIEW OF MODULES +## About -- junos_commit — Commit candidate configuration on device. -- junos_get_config — Retrieve configuration of device. -- junos_get_facts — Retrieve device-specific information from the host. -- junos_install_config — Modify the configuration of a device running Junos OS. -- junos_install_os — Install a Junos OS software package. -- junos_rollback — Rollback configuration of device. -- junos_shutdown — Shut down or reboot a device running Junos OS. -- junos_srx_cluster — Enable/Disable cluster mode for SRX devices -- junos_zeroize — Remove all configuration information on the Routing Engines and reset all key values on a device. +Juniper Networks supports Ansible for managing devices running the Junos operating system (Junos OS and Junos Evolved). +This collection is hosted on the Ansible Galaxy website under the collection +[juniper.device](https://galaxy.ansible.com/ui/repo/published/juniper/device/). -## DOCUMENTATION +The `juniper.device` collection includes a set of Ansible modules that perform specific operational and configuration tasks on devices running Junos OS. +These tasks include: installing and upgrading Junos OS, provisioning new Junos devices in the network, loading configuration changes, +retrieving information, and resetting, rebooting, or shutting down managed devices. Please refer to the +[INSTALLATION](#installation) section for instructions on installing this collection. + +## Two Sets of Ansible Modules for Junos devices + +Since Ansible version >= 2.1, Ansible also natively includes +[core modules for Junos](https://docs.ansible.com/ansible/latest/collections/junipernetworks/junos/index.html#plugins-in-junipernetworks-junos). The Junos modules included +in Ansible core have names which begin with the prefix `junos_`. The Junos modules included in this `Juniper.device` +collection have names starting with module types. These two sets of Junos modules can coexist on the same +Ansible control machine, and an Ansible playbook may invoke a module from either (or both) sets. Juniper Networks recommends +using the modules in `juniper.device` collection when writing new playbooks that manage Junos devices. + +## Overview of Modules + +This `juniper.device` collection includes the following modules: + +- **command** — Execute one or more CLI commands on a Junos device. +- **config** — Manipulate the configuration of a Junos device. +- **facts** — Retrieve facts from a Junos device. +- **file_copy** - Copy the files from and to a Junos device. +- **jsnapy** — Execute JSNAPy tests on a Junos device. +- **ping** — Execute ping from a Junos device. +- **pmtud** — Perform path MTU discovery from a Junos device to a destination. +- **rpc** — Execute one or more NETCONF RPCs on a Junos device. +- **software** — Install software on a Junos device. +- **srx_cluster** — Add or remove SRX chassis cluster configuration. +- **system** — Initiate operational actions on the Junos system. +- **table** — Retrieve data from a Junos device using a PyEZ table/view. + +### PyEZ Version Requirement + +For ansible collection `juniper.device` we will need to install [junos-eznc](https://github.com/Juniper/py-junos-eznc) version 2.6.0 or higher. + +### Overview of Plugins + +In addition to the modules listed above, a callback_plugin `jsnapy` is available for the module [jsnapy](https://github.com/Juniper/jsnapy). -[Official documentation](http://www.juniper.net/techpubs/en_US/release-independent/junos-ansible/information-products/pathway-pages/index.html) (detailed information, includes examples) +The callback_plugin `jsnapy` helps to print on the screen additional information regarding jsnapy failed tests. +For each failed test, a log will be printed after the RECAP of the playbook as shown in this example: -[Ansible style documentation](http://junos-ansible-modules.readthedocs.org) + PLAY RECAP ********************************************************************* + qfx10002-01 : ok=3 changed=0 unreachable=0 failed=1 + qfx10002-02 : ok=3 changed=0 unreachable=0 failed=1 + qfx5100-01 : ok=1 changed=0 unreachable=0 failed=1 + JSNAPy Results for: qfx10002-01 ************************************************ + Value of 'peer-state' not 'is-equal' at '//bgp-information/bgp-peer' with {"peer-as": "65200", "peer-state": "Active", "peer-address": "100.0.0.21"} + Value of 'peer-state' not 'is-equal' at '//bgp-information/bgp-peer' with {"peer-as": "60021", "peer-state": "Idle", "peer-address": "192.168.0.1"} + Value of 'oper-status' not 'is-equal' at '//interface-information/physical-interface[normalize-space(admin-status)='up' and logical-interface/address-family/address-family-name ]' with {"oper-status": "down", "name": "et-0/0/18"} + + JSNAPy Results for: qfx10002-02 ************************************************ + Value of 'peer-state' not 'is-equal' at '//bgp-information/bgp-peer' with {"peer-as": "65200", "peer-state": "Active", "peer-address": "100.0.0.21"} + +Callback plugins are not activated by default. They must be manually added to the Ansible +configuration file under the `[defaults]` section using the variable `callback_whitelist`. Specifically, these lines +should be added to the Ansible configuration file in order to allow the jsnapy callback plugin: + + [defaults] + callback_whitelist = jsnapy + +## DOCUMENTATION + +[Official Juniper documentation](http://www.juniper.net/techpubs/en_US/release-independent/junos-ansible/information-products/pathway-pages/index.html) (detailed information, including examples) + +[Ansible style documentation](https://ansible-juniper-collection.readthedocs.io) ## INSTALLATION -This repo assumes you have the [DEPENDENCIES](#dependencies) installed on your system. +You must have the [DEPENDENCIES](#dependencies) installed on your system. +Check requirements.txt for the dependencies. + +### NOTICES + +### MacOS Mojave and newer -### Ansible Galaxy Role -To download the junos role to the Ansible server, execute the ansible-galaxy install command, and specify **Juniper.junos**. +In MacOS Mojave and newer (>=10.14), ssh keys created with the system `ssh-keygen` are created using the newer 'OPENSSH' key format, even when specifying `-t rsa` during creation. This directly affects the usage of ssh keys, particularly when using the `ssh_private_key_file`. To create/convert/check keys, follow these steps: +- Create a new RSA key: `ssh-keygen -m PEM -t rsa -b 4096` +- Check existing keys: `head -n1 ~/.ssh/some_private_key` RSA keys will be `-----BEGIN RSA PRIVATE KEY-----` and OPENSSH keys will be `-----BEGIN OPENSSH PRIVATE KEY-----` +- Convert an OPENSSH key to an RSA key: `ssh-keygen -p -m PEM -f ~/.ssh/some_key` + +### Ansible Galaxy collection + +You can use the ansible-galaxy install command to install the latest +version of the `juniper.device` collection. + +```bash +sudo ansible-galaxy collection install juniper.device ``` -[root@ansible-cm]# ansible-galaxy install Juniper.junos -downloading role 'junos', owned by Juniper -no version specified, installing 1.0.0 -- downloading role from -https://github.com/Juniper/ansible-junos-stdlib/archive/1.0.0.tar.gz -- extracting Juniper.junos to /etc/ansible/roles/Juniper.junos -Juniper.junos was installed successfully + +You can also use the ansible-galaxy install command to install the latest development version of the junos collections directly from GitHub. + +```bash +sudo ansible-galaxy collection install git+https://github.com/Juniper/ansible-junos-stdlib.git#/ansible_collections/juniper/device ``` +For more information visit - https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#specifying-the-location-to-search-for-collections + + ### Git clone + For testing you can `git clone` this repo and run the `env-setup` script in the repo directory: - user@ansible-junos-stdlib> source env-setup - + user@ansible-junos-stdlib> source env-setup + This will set your `$ANSIBLE_LIBRARY` variable to the repo location and the installed Ansible library path. For example: -```` -[jeremy@ansible-junos-stdlib]$ echo $ANSIBLE_LIBRARY -/home/jeremy/Ansible/ansible-junos-stdlib/library:/usr/share/ansible -```` + $ echo $ANSIBLE_LIBRARY + /home/jeremy/Ansible/ansible-junos-stdlib/library:/usr/share/ansible ## Example Playbook + This example outlines how to use Ansible to install or upgrade the software image on a device running Junos OS. -``` +```yaml --- - name: Install Junos OS hosts: dc1 - roles: - - Juniper.junos connection: local - gather_facts: no + gather_facts: false vars: wait_time: 3600 pkg_dir: /var/tmp/junos-install - OS_version: 14.1R1.10 - OS_package: jinstall-14.1R1.10-domestic-signed.tgz + os_version: 14.1R1.10 + os_package: jinstall-14.1R1.10-domestic-signed.tgz log_dir: /var/log/ansible tasks: - name: Checking NETCONF connectivity - wait_for: host={{ inventory_hostname }} port=830 timeout=5 + ansible.builtin.wait_for: + host: "{{ inventory_hostname }}" + port: 830 + timeout: 5 - name: Install Junos OS package - junos_install_os: - host={{ inventory_hostname }} - reboot=yes - version={{ OS_version }} - package={{ pkg_dir }}/{{ OS_package }} - logfile={{ log_dir }}/software.log + juniper.device.software: + reboot: true + version: "{{ os_version }}" + package: "{{ pkg_dir }}/{{ os_package }}" + logfile: "{{ log_dir }}/software.log" register: sw notify: - - wait_reboot + - Wait_reboot handlers: - - name: wait_reboot - wait_for: host={{ inventory_hostname }} port=830 timeout={{ wait_time }} + - name: Wait_reboot + ansible.builtin.wait_for: + host: "{{ inventory_hostname }}" + port: 830 + timeout: "{{ wait_time }}" when: not sw.check_mode -``` +``` ## DEPENDENCIES -Thes modules require the following to be installed on the Ansible server: +This modules requires the following to be installed on the Ansible control machine: -* Python 2.6 or 2.7 -* [Ansible](http://www.ansible.com) 1.5 or later -* Junos [py-junos-eznc](https://github.com/Juniper/py-junos-eznc) 1.2.2 or later -* Junos [netconify](https://github.com/jeremyschulman/py-junos-netconify) 1.0.1 or later (if using console) +- Python >= 3.8 +- [Ansible](http://www.ansible.com) 2.9 or later +- Junos [py-junos-eznc](https://github.com/Juniper/py-junos-eznc) 2.6.0 or later +- [jxmlease](https://github.com/Juniper/jxmlease) 1.0.1 or later +- [xmltodict](https://pypi.org/project/xmltodict/) 0.13.0 or later +- [jsnapy](https://github.com/Juniper/jsnapy) 1.3.7 or later ## LICENSE Apache 2.0 - + +## SUPPORT + +Support for this `juniper.device` collection is provided by the community and Juniper Networks. If you have an +issue with a module in the `juniper.device` collection, you may: + +- Open a [GitHub issue](https://github.com/Juniper/ansible-junos-stdlib/issues). +- Post a question on our [Google Group](https://groups.google.com/forum/#!forum/junos-python-ez) +- Email [jnpr-community-netdev@juniper.net](jnpr-community-netdev@juniper.net) +- Open a [JTAC Case](https://www.juniper.net/casemanager/#/create) + +Support for the Junos modules included in Ansible core is provided by Ansible. If you have an issue with an Ansible +core module you should open a [Github issue against the Ansible project](https://github.com/ansible/ansible/issues). + ## CONTRIBUTORS -- Jeremy Schulman (@nwkautomaniac), Core developer -- Rick Sherman (@shermdog01) -- Nitin Kumar (@vnitinv) -- Patrik Bok -- Ashley Burston +Juniper Networks is actively contributing to and maintaining this repo. Please contact +[jnpr-community-netdev@juniper.net](jnpr-community-netdev@juniper.net) for any queries. + +*Contributors:* +[Stephen Steiner](https://github.com/ntwrkguru), [Dinesh Babu](https://github.com/dineshbaburam91), [Chidanand Pujar](https://github.com/chidanandpujar) + +*Former Contributors:* +[Stacy W Smith](https://github.com/stacywsmith), [Jeremy Schulman](https://github.com/jeremyschulman), [Rick Sherman](https://github.com/shermdog), [Damien Garros](https://github.com/dgarros), [David Gethings](https://github.com/dgjnpr), [Nitin Kumar](https://github.com/vnitinv), [Rahul Kumar](https://github.com/rahkumar651991) diff --git a/ansible-junos-awx/Makefile b/ansible-junos-awx/Makefile new file mode 100644 index 00000000..851b500a --- /dev/null +++ b/ansible-junos-awx/Makefile @@ -0,0 +1,85 @@ +PWD = $(shell pwd) +UNAME_S := $(shell uname -s) + +ifeq ($(UNAME_S),Darwin) + SED := sed -i '' +else + SED := sed -i +endif + +include Makefile.variable + +all:prequisite virtual-env ansible-awx docker-start docker-exec ## install ansible-junos-awx + + +.PHONY: prequisite +prequisite: + pip install virtualenv + rm -rf ./awx ./awx-env + +.PHONY: virtual-env +virtual-env: + virtualenv awx-env --no-site-packages + . awx-env/bin/activate && \ + pip install ansible docker-py + +.PHONY: ansible-awx +ansible-awx: + . awx-env/bin/activate && \ + git clone https://github.com/ansible/awx.git --single-branch --depth 1 + +.PHONY: docker-start +docker-start: +ifneq '$(PROJECT_DATA_DIR)' '' + mkdir -p $(PROJECT_DATA_DIR) + @${SED} '/project_data_dir/s/^#//g' $(PWD)/awx/installer/inventory + @${SED} 's|project_data_dir=.*|project_data_dir=$(PROJECT_DATA_DIR)|g' $(PWD)/awx/installer/inventory +endif +ifneq '$(AWX_TASK_TAG)' '' + @${SED} 's/dockerhub_version=.*/dockerhub_version=$(AWX_TASK_TAG)/g' $(PWD)/awx/installer/inventory +endif +ifneq '$(POSTGRES_DATA_DIR)' '' + @${SED} 's|postgres_data_dir=.*|postgres_data_dir=$(POSTGRES_DATA_DIR)|g' $(PWD)/awx/installer/inventory + @mkdir -p ${POSTGRES_DATA_DIR}/pg_snapshots && touch ${POSTGRES_DATA_DIR}/pg_snapshots/.keep + @mkdir -p ${POSTGRES_DATA_DIR}/pg_replslot && touch ${POSTGRES_DATA_DIR}/pg_replslot/.keep + @mkdir -p ${POSTGRES_DATA_DIR}/pg_stat_tmp && touch ${POSTGRES_DATA_DIR}/pg_stat_tmp/.keep + @mkdir -p ${POSTGRES_DATA_DIR}/pg_stat && touch ${POSTGRES_DATA_DIR}/pg_stat/.keep + @mkdir -p ${POSTGRES_DATA_DIR}/pg_twophase && touch ${POSTGRES_DATA_DIR}/pg_twophase/.keep + @mkdir -p ${POSTGRES_DATA_DIR}/pg_tblspc && touch ${POSTGRES_DATA_DIR}/pg_tblspc/.keep +endif +ifneq '$(HOST_FILE)' '' + cp $(HOST_FILE) $(strip $(PROJECT_DATA_DIR))/hosts +endif + + . awx-env/bin/activate && \ + ansible-playbook -i $(PWD)/awx/installer/inventory $(PWD)/awx/installer/install.yml + sleep 120 + +.PHONY: docker-exec +docker-exec: + docker exec -it awx_task pip install jsnapy jxmlease junos-eznc + docker exec -it awx_task ansible-galaxy install juniper.junos,$(ANSIBLE_JUNOS_VERSION) -p /etc/ansible/roles + docker exec -it awx_task /bin/bash -c 'sed -i '/roles_path/s/^#//g' /etc/ansible/ansible.cfg' +ifneq '$(HOST_FILE)' '' + curl -u admin:password --noproxy '*' http://localhost/api/v2/inventories/ --header "Content-Type: application/json" -x POST -d '{"name":"$(INVENTORY_NAME)" , "organization": 1}' + docker exec -it awx_task /bin/bash -c 'awx-manage inventory_import --source=/var/lib/awx/projects/hosts --inventory-name=$(INVENTORY_NAME) --overwrite' +endif + +.PHONY: docker-stop +docker-stop: ## stop the docker + docker stop awx_task + docker stop awx_web + docker stop memcached + docker stop rabbitmq + docker stop postgres + +.PHONY: docker-remove +docker-remove: docker-stop ##clean the docker + docker rm awx_task + docker rm awx_web + docker rm memcached + docker rm rabbitmq + docker rm postgres + +clean: prequisite ## clean the project + docker system prune -f diff --git a/ansible-junos-awx/Makefile.variable b/ansible-junos-awx/Makefile.variable new file mode 100644 index 00000000..dfbebb71 --- /dev/null +++ b/ansible-junos-awx/Makefile.variable @@ -0,0 +1,6 @@ +PROJECT_DATA_DIR = +AWX_TASK_TAG = +POSTGRES_DATA_DIR = +ANSIBLE_JUNOS_VERSION = +HOST_FILE = +INVENTORY_NAME = diff --git a/ansible-junos-awx/README.md b/ansible-junos-awx/README.md new file mode 100644 index 00000000..65515018 --- /dev/null +++ b/ansible-junos-awx/README.md @@ -0,0 +1,612 @@ +## About + + Ansible-junos-awx provides a web-based user interface and task engine built on top of [Ansible](https://github.com/ansible/ansible.git) which helps to perform specific +operational and configuration tasks on devices running Junos OS using [ansible-junos-stdlib](https://github.com/Juniper/ansible-junos-stdlib.git). + +## Requirements + Before you can run a deployment, you'll need the following installed in your local environment: + +- [Docker](https://www.docker.com) +- pip module +- [GNU Make](https://ftp.gnu.org/gnu/make/) +- [Git](https://git-scm.com/downloads) Requires Version 1.8.4+ + +## Installation + +Clone repo and run make inside Juniper-awx folder + +``` +$ git clone https://github.com/Juniper/ansible-junos-awx +$ cd ansible-junos-awx +$ make or make all +``` +This will do the following operations: +- Creates virtual environment Juniper-awx. +- Install python modules required for the project in the virtualenv: Ansible,docker-py. +- Clone AWX repository into the Juniper-awx/awx folder +- Change AWX inventory file to include user specifications.Refer [Makefile.variable](#makefilevariable). +- Launch AWX conatiners. +- Install juniper.junos role with user specified version.Refer [Makefile.variable](#makefilevariable). +- Install python modules required for juniper.junos role in awx_task container: jxmlease,junos-eznc,jsnappy. +- Change roles_path in ansible.cfg for awx_task container. +- If HOST_FILE is mentioned, an inventory with name INVENTORY_NAME is created and host's loaded into it.Refer [Makefile.variable](#makefilevariable). + +# Example make + +``` + +$ make +pip install virtualenv +Requirement already satisfied: virtualenv in /Library/Python/2.7/site-packages +rm -rf ./awx ./awx-env /private/tmp/pgdocker +virtualenv awx-env --no-site-packages +New python executable in /private/tmp/ansible-junos-awx/awx-env/bin/python +Installing setuptools, pip, wheel...done. +. awx-env/bin/activate && \ + pip install ansible docker-py +Collecting ansible +Collecting docker-py + Using cached docker_py-1.10.6-py2.py3-none-any.whl +Collecting jinja2 (from ansible) + Using cached Jinja2-2.10-py2.py3-none-any.whl +Collecting PyYAML (from ansible) +Collecting cryptography (from ansible) + Using cached cryptography-2.1.4-cp27-cp27m-macosx_10_6_intel.whl +Collecting paramiko (from ansible) + Using cached paramiko-2.4.0-py2.py3-none-any.whl +Requirement already satisfied: setuptools in ./awx-env/lib/python2.7/site-packages (from ansible) +Collecting websocket-client>=0.32.0 (from docker-py) + Using cached websocket_client-0.47.0-py2.py3-none-any.whl +Collecting backports.ssl-match-hostname>=3.5; python_version < "3.5" (from docker-py) +Collecting ipaddress>=1.0.16; python_version < "3.3" (from docker-py) +Collecting six>=1.4.0 (from docker-py) + Using cached six-1.11.0-py2.py3-none-any.whl +Collecting requests!=2.11.0,>=2.5.2 (from docker-py) + Using cached requests-2.18.4-py2.py3-none-any.whl +Collecting docker-pycreds>=0.2.1 (from docker-py) + Using cached docker_pycreds-0.2.2-py2.py3-none-any.whl +Collecting MarkupSafe>=0.23 (from jinja2->ansible) +Collecting cffi>=1.7; platform_python_implementation != "PyPy" (from cryptography->ansible) + Using cached cffi-1.11.5-cp27-cp27m-macosx_10_6_intel.whl +Collecting enum34; python_version < "3" (from cryptography->ansible) + Using cached enum34-1.1.6-py2-none-any.whl +Collecting idna>=2.1 (from cryptography->ansible) + Using cached idna-2.6-py2.py3-none-any.whl +Collecting asn1crypto>=0.21.0 (from cryptography->ansible) + Using cached asn1crypto-0.24.0-py2.py3-none-any.whl +Collecting pynacl>=1.0.1 (from paramiko->ansible) + Using cached PyNaCl-1.2.1-cp27-cp27m-macosx_10_6_intel.whl +Collecting bcrypt>=3.1.3 (from paramiko->ansible) + Using cached bcrypt-3.1.4-cp27-cp27m-macosx_10_6_intel.whl +Collecting pyasn1>=0.1.7 (from paramiko->ansible) + Using cached pyasn1-0.4.2-py2.py3-none-any.whl +Collecting urllib3<1.23,>=1.21.1 (from requests!=2.11.0,>=2.5.2->docker-py) + Using cached urllib3-1.22-py2.py3-none-any.whl +Collecting certifi>=2017.4.17 (from requests!=2.11.0,>=2.5.2->docker-py) + Using cached certifi-2018.1.18-py2.py3-none-any.whl +Collecting chardet<3.1.0,>=3.0.2 (from requests!=2.11.0,>=2.5.2->docker-py) + Using cached chardet-3.0.4-py2.py3-none-any.whl +Collecting pycparser (from cffi>=1.7; platform_python_implementation != "PyPy"->cryptography->ansible) +Installing collected packages: MarkupSafe, jinja2, PyYAML, pycparser, cffi, enum34, idna, asn1crypto, ipaddress, six, cryptography, pynacl, bcrypt, pyasn1, paramiko, ansible, websocket-client, backports.ssl-match-hostname, urllib3, certifi, chardet, requests, docker-pycreds, docker-py +Successfully installed MarkupSafe-1.0 PyYAML-3.12 ansible-2.4.3.0 asn1crypto-0.24.0 backports.ssl-match-hostname-3.5.0.1 bcrypt-3.1.4 certifi-2018.1.18 cffi-1.11.5 chardet-3.0.4 cryptography-2.1.4 docker-py-1.10.6 docker-pycreds-0.2.2 enum34-1.1.6 idna-2.6 ipaddress-1.0.19 jinja2-2.10 paramiko-2.4.0 pyasn1-0.4.2 pycparser-2.18 pynacl-1.2.1 requests-2.18.4 six-1.11.0 urllib3-1.22 websocket-client-0.47.0 +. awx-env/bin/activate && \ + git clone https://github.com/ansible/awx.git --single-branch --depth 1 +Cloning into 'awx'... +remote: Counting objects: 2596, done. +remote: Compressing objects: 100% (2295/2295), done. +remote: Total 2596 (delta 603), reused 1034 (delta 237), pack-reused 0 +Receiving objects: 100% (2596/2596), 7.48 MiB | 482.00 KiB/s, done. +Resolving deltas: 100% (603/603), done. +Checking out files: 100% (2317/2317), done. +mkdir -p /private/tmp/pgdocker +. awx-env/bin/activate && \ + ansible-playbook -i /private/tmp/ansible-junos-awx/awx/installer/inventory /private/tmp/ansible-junos-awx/awx/installer/install.yml +[DEPRECATION WARNING]: DEFAULT_SUDO_USER option, In favor of become which is a generic framework . This feature will be removed in version 2.8. Deprecation +warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. + +PLAY [Build and deploy AWX] ************************************************************************************************************************************ + +TASK [check_vars : include_tasks] ****************************************************************************************************************************** +skipping: [localhost] + +TASK [check_vars : include_tasks] ****************************************************************************************************************************** +included: /private/tmp/ansible-junos-awx/awx/installer/check_vars/tasks/check_docker.yml for localhost + +TASK [check_vars : postgres_data_dir should be defined] ******************************************************************************************************** +ok: [localhost] => { + "changed": false, + "msg": "All assertions passed" +} + +TASK [check_vars : host_port should be defined] **************************************************************************************************************** +ok: [localhost] => { + "changed": false, + "msg": "All assertions passed" +} + +TASK [image_build : Get Version from checkout if not provided] ************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Set global version if not provided] ******************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Verify awx-logos directory exists for official install] ************************************************************************************ +skipping: [localhost] + +TASK [image_build : Copy logos for inclusion in sdist] ********************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Set sdist file name] *********************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : AWX Distribution] ************************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Stat distribution file] ******************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Clean distribution] ************************************************************************************************************************ +skipping: [localhost] + +TASK [image_build : Build sdist builder image] ***************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Build AWX distribution using container] **************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Build AWX distribution locally] ************************************************************************************************************ +skipping: [localhost] + +TASK [image_build : Set docker build base path] **************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Set awx_web image name] ******************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Set awx_task image name] ******************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Ensure directory exists] ******************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Stage sdist] ******************************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Template web Dockerfile] ******************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Template task Dockerfile] ****************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Stage launch_awx] ************************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Stage launch_awx_task] ********************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Stage nginx.conf] ************************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Stage supervisor.conf] ********************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Stage supervisor_task.conf] **************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Stage settings.py] ************************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Stage requirements] ************************************************************************************************************************ +skipping: [localhost] + +TASK [image_build : Stage config watcher] ********************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Stage Makefile] **************************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Stage ansible repo] ************************************************************************************************************************ +skipping: [localhost] + +TASK [image_build : Stage ansible repo key] ******************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Build base web image] ********************************************************************************************************************** +skipping: [localhost] + +TASK [image_build : Build base task image] ********************************************************************************************************************* +skipping: [localhost] + +TASK [image_build : Clean docker base directory] *************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Authenticate with OpenShift via user and password] ******************************************************************************************* +skipping: [localhost] + +TASK [openshift : Authenticate with OpenShift via token] ******************************************************************************************************* +skipping: [localhost] + +TASK [openshift : Get Project Detail] ************************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Get Postgres Service Detail] ***************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Create AWX Openshift Project] **************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Mark Openshift User as Admin] **************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Set docker registry password] **************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Set docker registry password] **************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Authenticate with Docker registry] *********************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Wait for Openshift] ************************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Tag and push web image to registry] ********************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Wait for the registry to settle] ************************************************************************************************************* +skipping: [localhost] + +TASK [openshift : Tag and push task image to registry] ********************************************************************************************************* +skipping: [localhost] + +TASK [openshift : Enable image stream lookups for awx images] ************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Set full web image path] ********************************************************************************************************************* +skipping: [localhost] + +TASK [openshift : Set full task image path] ******************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Set DockerHub Image Paths] ******************************************************************************************************************* +skipping: [localhost] + +TASK [openshift : Deploy and Activate Postgres] **************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Wait for Postgres to activate] *************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Set openshift base path] ********************************************************************************************************************* +skipping: [localhost] + +TASK [openshift : Ensure directory exists] ********************************************************************************************************************* +skipping: [localhost] + +TASK [openshift : Template Openshift AWX Config] *************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Template Openshift AWX Deployment] *********************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Template Openshift AWX etcd2] **************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Apply etcd deployment] *********************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Apply Configmap] ***************************************************************************************************************************** +skipping: [localhost] + +TASK [openshift : Apply Deployment] **************************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Set the Kubernetes Context] ***************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Get Namespace Detail] *********************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Get Postgres Service Detail] **************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Create AWX Kubernetes Project] ************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Authenticate with Docker registry] ********************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Wait for Openshift] ************************************************************************************************************************* +skipping: [localhost] + +TASK [kubernetes : Tag and push web image to registry] ********************************************************************************************************* +skipping: [localhost] + +TASK [kubernetes : Wait for the registry to settle] ************************************************************************************************************ +skipping: [localhost] + +TASK [kubernetes : Tag and push task image to registry] ******************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Set full web image path] ******************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Set full task image path] ******************************************************************************************************************* +skipping: [localhost] + +TASK [kubernetes : Set DockerHub Image Paths] ****************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Deploy and Activate Postgres] *************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Set postgresql hostname to helm package service] ******************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Wait for Postgres to activate] ************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Set kubernetes base path] ******************************************************************************************************************* +skipping: [localhost] + +TASK [kubernetes : Ensure directory exists] ******************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Template Kubernetes AWX etcd2] ************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Template Kubernetes AWX Config] ************************************************************************************************************* +skipping: [localhost] + +TASK [kubernetes : Template Kubernetes AWX Deployment] ********************************************************************************************************* +skipping: [localhost] + +TASK [kubernetes : Apply etcd deployment] ********************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Apply Configmap] **************************************************************************************************************************** +skipping: [localhost] + +TASK [kubernetes : Apply Deployment] *************************************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Export Docker web image if it isnt local and there isnt a registry defined] *************************************************************** +skipping: [localhost] + +TASK [local_docker : Export Docker task image if it isnt local and there isnt a registry defined] ************************************************************** +skipping: [localhost] + +TASK [local_docker : Authenticate with Docker registry if registry password given] ***************************************************************************** +skipping: [localhost] + +TASK [local_docker : Set docker base path] ********************************************************************************************************************* +skipping: [localhost] + +TASK [local_docker : Ensure directory exists] ****************************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Copy web image to docker execution] ******************************************************************************************************* +skipping: [localhost] + +TASK [local_docker : Copy task image to docker execution] ****************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Load web image] *************************************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Load task image] ************************************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : include_role] ***************************************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Set full image path for local install] **************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Set DockerHub Image Paths] **************************************************************************************************************** +ok: [localhost] + +TASK [local_docker : Activate postgres container] ************************************************************************************************************** +changed: [localhost] + +TASK [local_docker : Activate rabbitmq container] ************************************************************************************************************** +changed: [localhost] + +TASK [local_docker : Activate memcached container] ************************************************************************************************************* +changed: [localhost] + +TASK [local_docker : Wait for postgres and rabbitmq to activate] *********************************************************************************************** +Pausing for 15 seconds +(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort) +ok: [localhost] + +TASK [local_docker : Set properties without postgres for awx_web] ********************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Set properties with postgres for awx_web] ************************************************************************************************* +ok: [localhost] + +TASK [local_docker : Set properties without postgres for awx_task] ********************************************************************************************* +skipping: [localhost] + +TASK [local_docker : Set properties with postgres for awx_task] ************************************************************************************************ +ok: [localhost] + +TASK [local_docker : Activate AWX Web Container] *************************************************************************************************************** +changed: [localhost] + +TASK [local_docker : Activate AWX Task Container] ************************************************************************************************************** +changed: [localhost] + +TASK [local_docker : Create /var/lib/awx directory] ************************************************************************************************************ +skipping: [localhost] + +TASK [local_docker : Create docker-compose.yml file] *********************************************************************************************************** +skipping: [localhost] + +TASK [local_docker : Start the containers] ********************************************************************************************************************* +skipping: [localhost] + +PLAY RECAP ***************************************************************************************************************************************************** +localhost : ok=12 changed=5 unreachable=0 failed=0 + +sleep 120 +docker exec -it awx_task pip install jsnapy jxmlease junos-eznc +Collecting jsnapy + Downloading jsnapy-1.3.1.tar.gz (50kB) + 100% |################################| 51kB 399kB/s +Collecting jxmlease + Downloading jxmlease-1.0.1-py2.py3-none-any.whl +Collecting junos-eznc + Downloading junos_eznc-2.1.7-py2.py3-none-any.whl (150kB) + 100% |################################| 153kB 1.6MB/s +Collecting colorama (from jsnapy) + Downloading colorama-0.3.9-py2.py3-none-any.whl +Collecting configparser (from jsnapy) + Downloading configparser-3.5.0.tar.gz +Collecting pyparsing (from jsnapy) + Downloading pyparsing-2.2.0-py2.py3-none-any.whl (56kB) + 100% |################################| 61kB 5.5MB/s +Collecting icdiff (from jsnapy) + Downloading icdiff-1.9.1.tar.gz +Collecting future (from jsnapy) + Downloading future-0.16.0.tar.gz (824kB) + 100% |################################| 829kB 937kB/s +Collecting ncclient>=0.5.3 (from junos-eznc) + Downloading ncclient-0.5.3.tar.gz (63kB) + 100% |################################| 71kB 6.5MB/s +Requirement already satisfied (use --upgrade to upgrade): paramiko>=1.15.2 in /usr/lib/python2.7/site-packages (from junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): six in /usr/lib/python2.7/site-packages (from junos-eznc) +Collecting scp>=0.7.0 (from junos-eznc) + Downloading scp-0.10.2-py2.py3-none-any.whl +Requirement already satisfied (use --upgrade to upgrade): jinja2>=2.7.1 in /usr/lib/python2.7/site-packages (from junos-eznc) +Collecting lxml>=3.2.4 (from junos-eznc) + Downloading lxml-4.1.1-cp27-cp27mu-manylinux1_x86_64.whl (5.6MB) + 100% |################################| 5.6MB 229kB/s +Collecting pyserial (from junos-eznc) + Downloading pyserial-3.4-py2.py3-none-any.whl (193kB) + 100% |################################| 194kB 4.3MB/s +Requirement already satisfied (use --upgrade to upgrade): PyYAML>=3.10 in /usr/lib64/python2.7/site-packages (from junos-eznc) +Collecting netaddr (from junos-eznc) + Downloading netaddr-0.7.19-py2.py3-none-any.whl (1.6MB) + 100% |################################| 1.6MB 815kB/s +Requirement already satisfied (use --upgrade to upgrade): setuptools>0.6 in /usr/lib/python2.7/site-packages (from ncclient>=0.5.3->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): cryptography>=1.1 in /usr/lib64/python2.7/site-packages (from paramiko>=1.15.2->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): pyasn1>=0.1.7 in /usr/lib/python2.7/site-packages (from paramiko>=1.15.2->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): MarkupSafe in /usr/lib64/python2.7/site-packages (from jinja2>=2.7.1->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): idna>=2.0 in /usr/lib/python2.7/site-packages (from cryptography>=1.1->paramiko>=1.15.2->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): enum34 in /usr/lib/python2.7/site-packages (from cryptography>=1.1->paramiko>=1.15.2->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): ipaddress in /usr/lib/python2.7/site-packages (from cryptography>=1.1->paramiko>=1.15.2->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): cffi>=1.4.1 in /usr/lib64/python2.7/site-packages (from cryptography>=1.1->paramiko>=1.15.2->junos-eznc) +Requirement already satisfied (use --upgrade to upgrade): pycparser in /usr/lib/python2.7/site-packages (from cffi>=1.4.1->cryptography>=1.1->paramiko>=1.15.2->junos-eznc) +Installing collected packages: lxml, ncclient, scp, pyserial, netaddr, junos-eznc, colorama, configparser, pyparsing, icdiff, future, jsnapy, jxmlease + Running setup.py install for ncclient ... done + Running setup.py install for configparser ... done + Running setup.py install for icdiff ... done + Running setup.py install for future ... done + Running setup.py install for jsnapy ... done +Successfully installed colorama-0.3.9 configparser-3.5.0 future-0.16.0 icdiff-1.9.1 jsnapy-1.3.1 junos-eznc-2.1.7 jxmlease-1.0.1 lxml-4.1.1 ncclient-0.5.3 netaddr-0.7.19 pyparsing-2.2.0 pyserial-3.4 scp-0.10.2 +You are using pip version 8.1.2, however version 9.0.1 is available. +You should consider upgrading via the 'pip install --upgrade pip' command. +docker exec -it awx_task ansible-galaxy install juniper.junos, -p /etc/ansible/roles +- downloading role 'junos', owned by Juniper +- downloading role from https://github.com/Juniper/ansible-junos-stdlib/archive/2.0.2.tar.gz +- extracting juniper.junos to /etc/ansible/roles/juniper.junos +- juniper.junos (2.0.2) was installed successfully +docker exec -it awx_task /bin/bash -c 'sed -i '/roles_path/s/^#//g' /etc/ansible/ansible.cfg' + + +``` + +After it has finished executing, check whether all containers are up. + +``` +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +ee36bb9312bc ansible/awx_task:latest "/tini -- /bin/sh -c…" About an hour ago Up About an hour 8052/tcp awx_task +bc8652bcf6ea ansible/awx_web:latest "/tini -- /bin/sh -c…" About an hour ago Up About an hour 0.0.0.0:80->8052/tcp awx_web +fb820f201e0c memcached:alpine "docker-entrypoint.s…" About an hour ago Up About an hour 11211/tcp memcached +c0b5bfd1bd85 rabbitmq:3 "docker-entrypoint.s…" About an hour ago Up About an hour 4369/tcp, 5671-5672/tcp, 25672/tcp rabbitmq +4411bd57f8d3 postgres:9.6 "docker-entrypoint.s…" About an hour ago Up About an hour 5432/tcp postgres + +``` +Log into 0.0.0.0:80 or localhost to acess the AWX web UI. + +``` +make docker-remove + This command will stop and remove the docker container + +$ make docker-remove +docker stop awx_task +awx_task +docker stop awx_web +awx_web +docker stop memcached +memcached +docker stop rabbitmq +rabbitmq +docker stop postgres +postgres +docker rm awx_task +awx_task +docker rm awx_web +awx_web +docker rm memcached +memcached +docker rm rabbitmq +rabbitmq +docker rm postgres +postgres + +``` + +## Makefile.variable + +This file helps to pass arguments to make file.User can specific the path, name of the project and postgres data +directory.Docker hub version and ansible junos version helps to control the version of the docker and juniper +ansible-galaxy respectively. + +``` +Example: + +PROJECT_DATA_DIR = +AWX_TASK_TAG = +POSTGRES_DATA_DIR = +ANSIBLE_JUNOS_VERSION = +HOST_FILE = /etc/ansible/hosts +INVENTORY_NAME = Junos + +``` +1. `PROJECT_DATA_DIR` : Provide absolute path to directory where the ansible projects reside.If the directory is not present Makefile will create the path. +2. `AWX_TASK_TAG`: Mention the awx_task tag to be installed.For available versions refer [Dockerhub](https://hub.docker.com/r/ansible/awx_task/tags/). +3. `POSTGRES_DATA_DIR`: Provide absolute path to postgres directory.If the directory is not present Makefile will create the path and create folders required for postgres to run. +4. `ANSIBLE_JUNOS_VERSION`: Mention the juniper.junos version to be installed.By default, it installs the latest version. +5. `HOST_FILE`: Provide the absolute path to the host file.This option can be only used if PROJECT_DATA_DIR is mentioned. +By default, it doesnot load any host file.Please ensure that a unique INVENTORY_NAME is mentioned to avoid errors e.g Hosts. +6. `INVENTORY_NAME`: The name of the inventory to which HOST_FILE is to be loaded. + +Note: +- `PROJECT_DATA_DIR` is the location where the Ansible projects will be manually sourced from. But it doesn't directly contain any playbook, AWX expects Ansible project folders at this location. +For more information about Ansible project structure, please refer [here](http://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#content-organization)
+
A sample directory structure is explained below. +``` +XYZ/ +└── Ansible_Project_1 + └── getFacts.yml +``` +In the above case, the `PROJECT_DATA_DIR = XYZ` +- If a variable is left blank, it is considered to be built with default values. +- If postgres container keeps on restarting, source the `POSTGRES_DATA_DIR` into any other location other than /tmp. +- Ensure docker has permission to bind the location mentioned in Makefile.variable + +## LICENSE + +Apache 2.0 + +## CONTRIBUTORS + +Juniper Networks is actively contributing to and maintaining this repo. Please contact jnpr-community-netdev@juniper.net for any queries. + +*Contributors:* + +- v0.0.1: [Raja Shekar M](https://github.com/rsmekala),[Dinesh Babu R](https://github.com/dineshbaburam91), [Jasminderpal Sidhu](https://github.com/sidhujasminder) diff --git a/ansible_collections/juniper/device/README.md b/ansible_collections/juniper/device/README.md new file mode 120000 index 00000000..8a33348c --- /dev/null +++ b/ansible_collections/juniper/device/README.md @@ -0,0 +1 @@ +../../../README.md \ No newline at end of file diff --git a/ansible_collections/juniper/device/changelogs/config.yaml b/ansible_collections/juniper/device/changelogs/config.yaml new file mode 100644 index 00000000..1052ffc5 --- /dev/null +++ b/ansible_collections/juniper/device/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: Juniper.Device +trivial_section_name: trivial +use_fqcn: true diff --git a/ansible_collections/juniper/device/docs/.readthedocs.yaml b/ansible_collections/juniper/device/docs/.readthedocs.yaml new file mode 100644 index 00000000..ce9e7926 --- /dev/null +++ b/ansible_collections/juniper/device/docs/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.8" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: ansible_collections/juniper/device/docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: ansible_collections/juniper/device/docs/docreq.txt diff --git a/ansible_collections/juniper/device/docs/Makefile b/ansible_collections/juniper/device/docs/Makefile new file mode 100644 index 00000000..7b3aac77 --- /dev/null +++ b/ansible_collections/juniper/device/docs/Makefile @@ -0,0 +1,234 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +LOGFILE = sphinx.log +FULL_TRACEBACKS = -T +CPUS ?= 4 +VERBOSITY ?= -v +FORCE_REBUILD = -a -E +CONFIG_DIR = -c . +NITPICK ?= -n +SPHINXOPTS = -j $(CPUS) -w $(LOGFILE) $(FULL_TRACEBACKS) $(FORCE_REBUILD) $(NITPICK) $(VERBOSITY) $(CONFIG_DIR) +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build +RSTDIR = . +MODULES_PATH = ../library +EXCLUDE_PATHS = ../library/_junos* +DOC_PROJECTS = "Ansible API" + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + rm -rf $(RSTDIR)/*.rst + rm -rf $(LOGFILE) + +.PHONY: apidoc +apidoc: + sphinx-apidoc --module-first --doc-project $(DOC_PROJECT) --force --maxdepth 7 -o $(RSTDIR) $(MODULES_PATH) $(EXCLUDE_PATHS) + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(RSTDIR) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: docs +docs: clean apidoc html + +.PHONY: webdocs +webdocs: clean apidoc html + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Ansible.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Ansible.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Ansible" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Ansible" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/ansible_collections/juniper/device/docs/_static/juniper-junos-modules.css b/ansible_collections/juniper/device/docs/_static/juniper-junos-modules.css new file mode 100644 index 00000000..29d867d0 --- /dev/null +++ b/ansible_collections/juniper/device/docs/_static/juniper-junos-modules.css @@ -0,0 +1,7 @@ +td, th { + padding: 20px; +} +code { + color: #3a87ad; + background-color: #d9edf7; +} diff --git a/docs/juniper.png b/ansible_collections/juniper/device/docs/_static/juniper.png similarity index 100% rename from docs/juniper.png rename to ansible_collections/juniper/device/docs/_static/juniper.png diff --git a/ansible_collections/juniper/device/docs/ansible2rst.py b/ansible_collections/juniper/device/docs/ansible2rst.py new file mode 100755 index 00000000..73971a03 --- /dev/null +++ b/ansible_collections/juniper/device/docs/ansible2rst.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python +# (c) 2012, Jan-Piet Mens +# +# This file is part of Ansible +# +# Modified to support stand-alone Galaxy documentation +# Copyright (c) 2014, 2017-2018 Juniper Networks Inc. +# 2014, Rick Sherman +# +# 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 . +# + +import cgi +import datetime +import os +import re +import warnings +import yaml +from jinja2 import Environment, FileSystemLoader +from six import print_ + +from collections.abc import MutableMapping, MutableSet, MutableSequence + +from ansible.module_utils._text import to_bytes +from ansible.module_utils.six import iteritems, string_types +from ansible.parsing.plugin_docs import read_docstring +from ansible.parsing.yaml.loader import AnsibleLoader +from ansible.plugins.loader import fragment_loader +from ansible.errors import AnsibleError + +try: + from html import escape as html_escape +except ImportError: + # Python-3.5 or later + import cgi + + def html_escape(text, quote=True): + return cgi.escape(text, quote) + + +from ansible import __version__ as ansible_version + +##################################################################################### +# constants and paths + +# if a module is added in a version of Ansible older than this, don't print the version added information +# in the module documentation because everyone is assumed to be running something newer than this already. +TO_OLD_TO_BE_NOTABLE = 1.3 + +_ITALIC = re.compile(r"I\(([^)]+)\)") +_BOLD = re.compile(r"B\(([^)]+)\)") +_MODULE = re.compile(r"M\(([^)]+)\)") +_URL_W_TEXT = re.compile(r"U\(([^)^|]+)\|([^)]+)\)") +_URL = re.compile(r"U\(([^)^|]+)\)") +_CONST = re.compile(r"C\(([^)]+)\)") +_UNDERSCORE = re.compile(r"_") +DEPRECATED = b" (D)" + +MODULE_NAME_STARTS_WITH = "" +MODULEDIR = "../plugins/modules/" +OUTPUTDIR = "./" + +##################################################################################### + + +def too_old(added): + if not added: + return False + try: + added_tokens = str(added).split(".") + readded = added_tokens[0] + "." + added_tokens[1] + added_float = float(readded) + except ValueError as e: + warnings.warn("Could not parse %s: %s" % (added, str(e))) + return False + return added_float < TO_OLD_TO_BE_NOTABLE + + +##################################################################################### + + +def rst_ify(text): + """convert symbols like I(this is in italics) to valid restructured text""" + + try: + t = _ITALIC.sub(r"*" + r"\1" + r"*", text) + t = _BOLD.sub(r"**" + r"\1" + r"**", t) + t = _MODULE.sub(r":ref:`" + r"\1 <\1>" + r"`", t) + t = _URL_W_TEXT.sub(r"`" + r"\1" + r" <" + r"\2" + r">`_", t) + t = _URL.sub(r"`" + r"\1" + r" <" + r"\1" + r">`_", t) + t = _CONST.sub(r"``" + r"\1" + r"``", t) + except Exception as e: + raise AnsibleError("Could not process (%s) : %s" % (str(text), str(e))) + + return t + + +##################################################################################### + + +def module_to_html(matchobj): + if matchobj.group(1) is not None: + module_name = matchobj.group(1) + module_href = _UNDERSCORE.sub("-", module_name) + return ( + '' + + module_name + + "" + ) + return "" + + +def html_ify(text): + """convert symbols like I(this is in italics) to valid HTML""" + + t = html_escape(text) + t = _ITALIC.sub("" + r"\1" + "", t) + t = _BOLD.sub("" + r"\1" + "", t) + t = _MODULE.sub(module_to_html, t) + t = _URL_W_TEXT.sub("" + r"\1" + "", t) + t = _URL.sub("" + r"\1" + "", t) + t = _CONST.sub("" + r"\1" + "", t) + + return t + + +##################################################################################### + + +def rst_fmt(text, fmt): + """helper for Jinja2 to do format strings""" + + return fmt % (text) + + +##################################################################################### + + +def rst_xline(width, char="="): + """return a restructured text line of a given length""" + + return char * width + + +##################################################################################### + + +def write_data(text, outputname, module, output_dir=None): + """dumps module output to a file or the screen, as requested""" + + if output_dir is not None: + if not os.path.exists(output_dir): + os.makedirs(output_dir) + fname = os.path.join(output_dir, outputname % (module)) + with open(fname, "wb") as f: + f.write(to_bytes(text)) + else: + print(text) + + +##################################################################################### + + +def jinja2_environment(template_dir, template_type): + + env = Environment( + loader=FileSystemLoader(template_dir), + variable_start_string="@{", + variable_end_string="}@", + trim_blocks=True, + ) + env.globals["xline"] = rst_xline + + if template_type == "rst": + env.filters["convert_symbols_to_format"] = rst_ify + env.filters["html_ify"] = html_ify + env.filters["fmt"] = rst_fmt + env.filters["xline"] = rst_xline + template = env.get_template("rst.j2") + outputname = "%s.rst" + else: + raise Exception("unknown module format type: %s" % template_type) + + return env, template, outputname + + +##################################################################################### + + +def add_fragments(doc, filename): + + fragments = doc.get("extends_documentation_fragment", []) + + if isinstance(fragments, string_types): + fragments = [fragments] + + # Allow the module to specify a var other than DOCUMENTATION + # to pull the fragment from, using dot notation as a separator + for fragment_slug in fragments: + fragment_slug = fragment_slug.lower() + if "." in fragment_slug: + fragment_name, fragment_var = fragment_slug.split(".", 1) + fragment_var = fragment_var.upper() + else: + fragment_name, fragment_var = fragment_slug, "DOCUMENTATION" + + fragment_loader.add_directory("../plugins/module_utils/") + fragment_class = fragment_loader.get(fragment_name) + assert fragment_class is not None + + fragment_yaml = getattr(fragment_class, fragment_var, "{}") + fragment = AnsibleLoader(fragment_yaml, file_name=filename).get_single_data() + + if "notes" in fragment: + notes = fragment.pop("notes") + if notes: + if "notes" not in doc: + doc["notes"] = [] + doc["notes"].extend(notes) + + if ( + "options" not in fragment + and "logging_options" not in fragment + and "connection_options" not in fragment + ): + raise Exception( + "missing options in fragment (%s), possibly misformatted?: %s" + % (fragment_name, filename) + ) + + for key, value in iteritems(fragment): + if key in doc: + # assumes both structures have same type + if isinstance(doc[key], MutableMapping): + value.update(doc[key]) + elif isinstance(doc[key], MutableSet): + value.add(doc[key]) + elif isinstance(doc[key], MutableSequence): + value = sorted(frozenset(value + doc[key])) + else: + raise Exception( + "Attempt to extend a documentation fragement (%s) of unknown type: %s" + % (fragment_name, filename) + ) + doc[key] = value + + +def get_docstring(filename, verbose=False): + """ + DOCUMENTATION can be extended using documentation fragments loaded by the PluginLoader from the module_docs_fragments directory. + """ + + data = read_docstring(filename, verbose=verbose) + + # add fragments to documentation + # if data.get('doc', False): + # add_fragments(data['doc'], filename) + + return data["doc"], data["plainexamples"], data["returndocs"], data["metadata"] + + +def process_module(fname, template, outputname, aliases=None): + + module_name = fname.replace(".py", "") + + print_("Processing module %s" % (MODULEDIR + fname)) + doc, examples, returndocs, metadata = get_docstring(MODULEDIR + fname, verbose=True) + + # add some defaults for plugins that dont have most of the info + doc["module"] = doc.get("module", module_name) + doc["version_added"] = doc.get("version_added", "historical") + doc["plugin_type"] = "module" + + required_fields = ("short_description",) + for field in required_fields: + if field not in doc: + print_("%s: WARNING: MODULE MISSING field '%s'" % (fname, field)) + + not_nullable_fields = ("short_description",) + for field in not_nullable_fields: + if field in doc and doc[field] in (None, ""): + print_( + "%s: WARNING: MODULE field '%s' DOCUMENTATION is null/empty value=%s" + % (fname, field, doc[field]) + ) + + # + # The present template gets everything from doc so we spend most of this + # function moving data into doc for the template to reference + # + + if aliases: + doc["aliases"] = aliases + + # don't show version added information if it's too old to be called out + added = 0 + if doc["version_added"] == "historical": + del doc["version_added"] + else: + added = doc["version_added"] + + # Strip old version_added for the module + if too_old(added): + del doc["version_added"] + + option_names = [] + if "options" in doc and doc["options"]: + for k, v in iteritems(doc["options"]): + # Error out if there's no description + if "description" not in doc["options"][k]: + raise AnsibleError( + "Missing required description for option %s in %s " % (k, module_name) + ) + + # Error out if required isn't a boolean (people have been putting + # information on when something is required in here. Those need + # to go in the description instead). + required_value = doc["options"][k].get("required", False) + if not isinstance(required_value, bool): + raise AnsibleError( + "Invalid required value '%s' for option '%s' in '%s' (must be truthy)" + % (required_value, k, module_name) + ) + + # Strip old version_added information for options + if "version_added" in doc["options"][k] and too_old( + doc["options"][k]["version_added"] + ): + del doc["options"][k]["version_added"] + + # Make sure description is a list of lines for later formatting + if not isinstance(doc["options"][k]["description"], list): + doc["options"][k]["description"] = [doc["options"][k]["description"]] + option_names.append(k) + option_names.sort() + doc["option_keys"] = option_names + + connection_option_names = [] + if "connection_options" in doc and doc["connection_options"]: + for k, v in iteritems(doc["connection_options"]): + # Error out if there's no description + if "description" not in doc["connection_options"][k]: + raise AnsibleError( + "Missing required description for connection_option %s in %s " + % (k, module_name) + ) + + # Error out if required isn't a boolean (people have been putting + # information on when something is required in here. Those need + # to go in the description instead). + required_value = doc["connection_options"][k].get("required", False) + if not isinstance(required_value, bool): + raise AnsibleError( + "Invalid required value '%s' for connection_option '%s' in '%s' (must be truthy)" + % (required_value, k, module_name) + ) + + # Strip old version_added information for options + if "version_added" in doc["connection_options"][k] and too_old( + doc["connection_options"][k]["version_added"] + ): + del doc["connection_options"][k]["version_added"] + + # Make sure description is a list of lines for later formatting + if not isinstance(doc["connection_options"][k]["description"], list): + doc["connection_options"][k]["description"] = [ + doc["connection_options"][k]["description"] + ] + connection_option_names.append(k) + connection_option_names.sort() + doc["connection_option_keys"] = connection_option_names + + logging_option_names = [] + if "logging_options" in doc and doc["logging_options"]: + for k, v in iteritems(doc["logging_options"]): + # Error out if there's no description + if "description" not in doc["logging_options"][k]: + raise AnsibleError( + "Missing required description for logging_option %s in %s " + % (k, module_name) + ) + + # Error out if required isn't a boolean (people have been putting + # information on when something is required in here. Those need + # to go in the description instead). + required_value = doc["logging_options"][k].get("required", False) + if not isinstance(required_value, bool): + raise AnsibleError( + "Invalid required value '%s' for logging_option '%s' in '%s' (must be truthy)" + % (required_value, k, module_name) + ) + + # Strip old version_added information for options + if "version_added" in doc["logging_options"][k] and too_old( + doc["logging_options"][k]["version_added"] + ): + del doc["logging_options"][k]["version_added"] + + # Make sure description is a list of lines for later formatting + if not isinstance(doc["logging_options"][k]["description"], list): + doc["logging_options"][k]["description"] = [ + doc["logging_options"][k]["description"] + ] + logging_option_names.append(k) + logging_option_names.sort() + doc["logging_option_keys"] = logging_option_names + + doc["filename"] = fname + doc["docuri"] = doc["module"].replace("_", "-") + doc["now_date"] = datetime.date.today().strftime("%Y-%m-%d") + doc["ansible_version"] = ansible_version + doc["plainexamples"] = examples # plain text + doc["metadata"] = metadata + + if returndocs: + try: + doc["returndocs"] = yaml.safe_load(returndocs) + returndocs_keys = list(doc["returndocs"].keys()) + returndocs_keys.sort() + doc["returndocs_keys"] = returndocs_keys + except Exception as e: + print_( + "%s:%s:yaml error:%s:returndocs=%s" + % (fname, module_name, e, returndocs) + ) + doc["returndocs"] = None + doc["returndocs_keys"] = None + else: + doc["returndocs"] = None + doc["returndocs_keys"] = None + + doc["author"] = doc.get("author", ["UNKNOWN"]) + if isinstance(doc["author"], string_types): + doc["author"] = [doc["author"]] + + # here is where we build the table of contents... + template.render(doc) + # write_data(text, outputname, module_name, OUTPUTDIR) + + +##################################################################################### + + +def main(): + + env, template, outputname = jinja2_environment(".", "rst") + module_names = [] + + for module in os.listdir(MODULEDIR): + if module.startswith(MODULE_NAME_STARTS_WITH): + process_module(module, template, outputname) + module_names.append(module.replace(".py", "")) + + index_file_path = os.path.join(OUTPUTDIR, "index.rst") + index_file = open(index_file_path, "w") + index_file.write("juniper.device Ansible Modules\n") + index_file.write("==============================\n") + index_file.write("\n") + index_file.write("Contents:\n") + index_file.write("\n") + index_file.write(".. toctree::\n") + index_file.write(" :maxdepth: 1\n") + index_file.write("\n") + + for module_name in module_names: + index_file.write(" %s\n" % module_name) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/docs/command.rst b/ansible_collections/juniper/device/docs/command.rst new file mode 100644 index 00000000..daf7fe62 --- /dev/null +++ b/ansible_collections/juniper/device/docs/command.rst @@ -0,0 +1,566 @@ +.. _command: + +command ++++++++ +Execute one or more CLI commands on a Junos device + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Execute one or more CLI commands on a Junos device. +* Alias command +* This module does NOT use the Junos CLI to execute the CLI command. Instead, it uses the ```` RPC over a NETCONF channel. The ```` RPC takes a CLI command as it's input and is very similar to executing the command on the CLI, but you can NOT include any pipe modifies (i.e. ``| match``, ``| count``, etc.) with the CLI commands executed by this module. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
commands
listyesnone +
A list of one or more CLI commands to execute on the Junos device.
+
aliases: cli, command, cmd, cmds
+
dest
pathnoNone +
The path to a file, on the Ansible control machine, where the output of the cli command will be saved.
+
The file must be writeable. If the file already exists, it is overwritten.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the value of the dest option. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the dest_dir option in new playbooks. The dest and dest_dir options are mutually exclusive.
+
aliases: destination
+
dest_dir
pathnoNone +
The path to a directory, on the Ansible control machine, where the output of the cli command will be saved. The output will be logged to a file named {{ inventory_hostname }}_command.format in the directory specified by the value of the dest_dir option.
+
The destination file must be writeable. If the file already exists, it is overwritten. It is the users responsibility to ensure a unique dest_dir value is provided for each execution of this module within a playbook.
+
The dest_dir and dest options are mutually exclusive. The dest_dir option is recommended for all new playbooks.
+
aliases: destination_dir, destdir
+
formats
str or list of strnotext
  • text
  • xml
  • json
+
The format of the reply for the CLI command(s) specified by the commands option. The specified format(s) must be supported by the target Junos device. The value of this option can either be a single format, or a list of formats. If a single format is specified, it applies to all command(s) specified by the commands option. If a list of formats are specified, there must be one value in the list for each command specified by the commands option. Specifying the value xml for the formats option is similar to appending | display xml to a CLI command, and specifying the value json for the formats option is similar to appending | display json to a CLI command.
+
aliases: format, display, output
+
return_output
boolnoTrue
  • yes
  • no
+
Indicates if the output of the command should be returned in the module's response. You might want to set this option to false, and set the dest_dir option, if the command output is very large and you only need to save the output rather than using it's content in subsequent tasks/plays of your playbook.
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _command-examples-label: + +Examples +-------- + +:: + + + - name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: "Execute single command in text format" + command: + commands: "show configuration system services netconf traceoptions" + format: text + + - name: "Execute command with login credentials" + command: + host: "10.x.x.x." + user: "user" + passwd: "user123" + commands: + - "show system storage" + register: junos_result + + - name: Execute three commands. + command: + commands: + - "show version" + - "show system uptime" + - "show interface terse" + register: response + + - name: Print the command output of each. + debug: + var: item.stdout + with_items: "{{ response.results }}" + + - name: show route with XML output - show version with JSON output + command: + commands: + - "show route" + - "show version" + formats: + - "xml" + - "json" + + - name: Multiple commands, save outputs, but don't return them + command: + commands: + - "show route" + - "show version" + formats: + - "xml" + dest_dir: "../Output" + return_output: false + + - name: save output to dest + command: + command: + - "show route" + - "show lldp neighbors" + dest: "/tmp/{{ inventory_hostname }}.commands.output" + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
changed +
Indicates if the device's state has changed. Since this module does not change the operational or configuration state of the device, the value is always set to false.
+
You could use this module to execute a command which changes the operational state of the the device. For example, clear ospf neighbors. Beware, this module is unable to detect this situation, and will still return the value false for changed in this case.
+
successboolFalse
command +
The CLI command which was executed.
+
alwaysstr
failed +
Indicates if the task failed. See the results key for additional details.
+
alwaysbool
format +
The format of the command response.
+
alwaysstr
msg +
A human-readable message indicating the result.
+
alwaysstr
parsed_output +
The command reply from the Junos device parsed into a JSON data structure. For XML replies, the response is parsed into JSON using the jxmlease library. For JSON the response is parsed using the Python json library.
+
When Ansible converts the jxmlease or native Python data structure into JSON, it does not guarantee that the order of dictionary/object keys are maintained.
+
when command executed successfully, return_output is true, and the value of the formats option is xml or json.dict
results +
The other keys are returned when a single command is specified for the commands option. When the value of the commands option is a list of commands, this key is returned instead. The value of this key is a list of dictionaries. Each element in the list corresponds to the commands in the commands option. The keys for each element in the list include all of the other keys listed. The failed key indicates if the individual command failed. In this case, there is also a top-level failed key. The top-level failed key will have a value of false if ANY of the commands ran successfully. In this case, check the value of the failed key for each element in the results list for the results of individual commands.
+
when the commands option is a list value.list of dict
stdout +
The command reply from the Junos device as a single multi-line string.
+
when command executed successfully and return_output is true.str
stdout_lines +
The command reply from the Junos device as a list of single-line strings.
+
when command executed successfully and return_output is true.list of str
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/docs/conf.py b/ansible_collections/juniper/device/docs/conf.py similarity index 71% rename from docs/conf.py rename to ansible_collections/juniper/device/docs/conf.py index 397f09e0..b2155e6f 100644 --- a/docs/conf.py +++ b/ansible_collections/juniper/device/docs/conf.py @@ -12,24 +12,32 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys + import sphinx_bootstrap_theme + +def setup(app): + app.add_css_file("juniper-junos-modules.css") + + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(1, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath(".")) +sys.path.insert(1, os.path.abspath("..")) -# Import ansible2rst so that RST files are generated. +# Import ansible2rst so that RST files can be generated. import ansible2rst + +# Call ansible2rst.main() to generate RST files. ansible2rst.main() # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -37,26 +45,27 @@ extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +# templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Junos Ansible Modules' -copyright = u'2014, Juniper Networks, Inc' +project = "Junos Ansible Collection Modules" +copyright = "2014-2017, Juniper Networks, Inc" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # from version import VERSION + # The short X.Y version. version = VERSION # The full version, including alpha/beta/rc tags. @@ -64,13 +73,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -78,167 +87,170 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'bootstrap' +html_theme = "bootstrap" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - 'bootswatch_theme': "spacelab", - 'navbar_sidebarrel': False, - 'navbar_site_name': "Modules", - 'source_link_position': "footer", - 'navbar_links': [ + "bootswatch_theme": "spacelab", + "navbar_sidebarrel": False, + "navbar_site_name": "Modules", + "source_link_position": "footer", + "navbar_links": [ ("Wiki", "https://techwiki.juniper.net/Automation_Scripting", True), ("Forum", "http://groups.google.com/group/junos-python-ez", True), - ], - } + ], +} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = 'juniper.png' +html_logo = "_static/juniper.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { - '**': [ - 'globaltoc.html', - ] + "**": [ + "globaltoc.html", + ] } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'JunosAnsibleModulesdoc' +htmlhelp_basename = "JunosAnsibleCollectionModulesdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'JunosAnsibleModules.tex', u'Junos Ansible Modules Documentation', - u'Jeremy Schulman - Juniper Networks, Inc.', 'manual'), + ( + "index", + "JunosAnsibleCollectionModules.tex", + "Junos Ansible Collection Modules Documentation", + "Juniper Networks, Inc.", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -246,12 +258,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'junosansiblemodules', u'Junos Ansible Modules Documentation', - [u'Jeremy Schulman - Juniper Networks, Inc.'], 1) + ( + "index", + "junosansiblecollectionmodules", + "Junos Ansible Collection Modules Documentation", + ["Juniper Networks, Inc."], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -260,19 +277,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'JunosAnsibleModules', u'Junos Ansible Modules Documentation', - u'Jeremy Schulman - Juniper Networks, Inc.', 'JunosAnsibleModules', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "JunosAnsibleCollectionModules", + "Junos Ansible Collection Modules Documentation", + "Juniper Networks, Inc.", + "JunosAnsibleCollectionModules", + "Ansible Modules for Junos", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/ansible_collections/juniper/device/docs/config.rst b/ansible_collections/juniper/device/docs/config.rst new file mode 100644 index 00000000..8d1aac89 --- /dev/null +++ b/ansible_collections/juniper/device/docs/config.rst @@ -0,0 +1,1018 @@ +.. _config: + +config +++++++ +Manipulate the configuration of a Junos device + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Manipulate the configuration of a Junos device. This module allows a combination of loading or rolling back, checking, diffing, retrieving, and committing the configuration of a Junos device. It performs the following steps in order: +#. Open a candidate configuration database. + + * If the *config_mode* option has a value of ``exclusive``, the default, + take a lock on the candidate configuration database. If the lock fails + the module fails and reports an error. + * If the *config_mode* option has a value of ``private``, open a private + candidate configuration database. If opening the private configuration + database fails the module fails and reports an error. +#. Load configuration data into the candidate configuration database. + + * Configuration data may be loaded using the *load* or *rollback* + options. If either of these options are specified, new configuration + data is loaded. If neither option is specified, this step is skipped. + * If the *rollback* option is specified, replace the candidate + configuration with the previous configuration specified by the value + of the *rollback* option. + * If the *load* option is specified, load new configuration data. + * The value of the *load* option defines the type of load which is + performed. + * The source of the new configuration data is one of the following: + + * *src* - A file path on the local Ansible control machine. + * *lines* - A list of strings containing the configuration data. + * *template* - A file path to a Jinja2 template on the local + Ansible control machine. This template is rendered with the variables + specified by the *vars* option. If the *template* option is + specified, the *vars* option must also be specified. + * *url* - A URL reachable from the target Junos device. + * If the *format* option is specified, the configuration file being + loaded is in the specified format, rather than the format determined + from the file name. +#. Check the validity of the candidate configuration database. + + * If the *check* option is ``true``, the default, check the validity + of the configuration by performing a "commit check" operation. + * This option may be specified with *diff* ``false`` and *commit* + ``false`` to confirm a previous "commit confirmed " operation + without actually performing an additional commit. + * If the configuration check fails, further processing stops, the module + fails, and an error is reported. +#. Determine differences between the candidate and committed configuration + databases. + + * If step 2 was not skipped, and the *diff* option is ``true``, + the default, perform a diff between the candidate and committed + configuration databases. + * If the *diffs_file* or *dest_dir* option is specified, save the + generated configuration differences. + * If the *return_output* option is ``true``, the default, include the + generated configuration difference in the *diff* and *diff_lines* + keys of the module's response. +#. Retrieve the configuration database from the Junos device. + + * If the *retrieve* option is specified, retrieve the configuration + database specified by the *retrieve* value from the target Junos + device to the local Ansible control machine. + * The format in which the configuration is retrieved is specified by the + value of the *format* option. + * The optional *filter* controls which portions of the configuration + are retrieved. + * If *options* are specified, they control the content of the + configuration retrieved. + * If the *dest* or *dest_dir* option is specified, save the + retrieved configuration to a file on the local Ansible control + machine. + * If the *return_output* option is ``true``, the default, include the + retrieved configuration in the *config*, *config_lines*, and + *config_parsed* keys of the module's response. +#. Commit the configuration changes. + + * If the *commit* option is ``true``, the default, commit the + configuration changes. + * This option may be specified with *diff* ``false`` and *check* + ``false`` to confirm a previous "commit confirmed " operation. + * If the *comment* option is specified, add the comment to the commit. + * If the *confirmed* option is specified, perform a + ``commit confirmed`` *min* operation where *min* is the value of the + *confirmed* option. + * If the *check* option is ``true`` and the *check_commit_wait* + option is specified, wait *check_commit_wait* seconds before + performing the commit. +#. Close the candidate configuration database. + + * Close and discard the candidate configuration database. + * If the *config_mode* option has a value of ``exclusive``, the default, + unlock the candidate configuration database. + + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
check
boolnotrue (false if retrieve is set and load and rollback are not set)
  • yes
  • no
+
Perform a commit check operation.
+
aliases: check_commit, commit_check
+
check_commit_wait
intnonone +
The number of seconds to wait between check and commit operations.
+
This option is only valid if check is true and commit is true.
+
This option should not normally be needed. It works around an issue in some versions of Junos.
+
comment
strnonone +
Provide a comment to be used with the commit operation.
+
This option is only valid if the commit option is true.
+
commit
boolnotrue (false if retrieve is set and load and rollback are not set)
  • yes
  • no
+
Perform a commit operation.
+
commit_empty_changes
boolnoFalse
  • yes
  • no
+
Perform a commit operation, even if there are no changes between the candidate configuration and the committed configuration.
+
config_mode
strnoexclusive
  • exclusive
  • private
+
The mode used to access the candidate configuration database.
+
aliases: config_access, edit_mode, edit_access
+
confirmed
intnonone +
Provide a confirmed timeout, in minutes, to be used with the commit operation.
+
This option is only valid if the commit option is true.
+
The value of this option is the number of minutes to wait for another commit operation before automatically rolling back the configuration change performed by this task. In other words, this option causes the module to perform a commit confirmed min where min is the value of the confirmed option. This option DOES NOT confirm a previous commit confirmed min operation. To confirm a previous commit operation, invoke this module with the check or commit option set to true.
+
aliases: confirm
+
dest
pathnonone +
The path to a file, on the local Ansible control machine, where the configuration will be saved if the retrieve option is specified.
+
The file must be writeable. If the file already exists, it is overwritten.
+
This option is only valid if the retrieve option is not none.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the dest value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the dest_dir option in new playbooks. The dest and dest_dir options are mutually exclusive.
+
aliases: destination
+
dest_dir
pathnonone +
The path to a directory, on the Ansible control machine. This is the directory where the configuration will be saved if the retrieve option is specified. It is also the directory where the configuration diff will be specified if the diff option is true.
+
This option is only valid if the retrieve option is not none or the diff option is true.
+
The retrieved configuration will be saved to a file named {{ inventory_hostname }}.format_extension in the dest_dir directory. Where format_extension is conf for text format, xml for XML format, json for JSON format, and set for set format.
+
If the diff option is true, the configuration diff will be saved to a file named {{ inventory_hostname }}.diff in the dest_dir directory.
+
The destination file must be writeable. If the file already exists, it is overwritten. It is the users responsibility to ensure a unique dest_dir value is provided for each execution of this module within a playbook.
+
The dest_dir and dest options are mutually exclusive. The dest_dir option is recommended for all new playbooks.
+
The dest_dir and diff_file options are mutually exclusive. The dest_dir option is recommended for all new playbooks.
+
aliases: destination_dir, destdir, savedir, save_dir
+
diff
boolnotrue (false if retrieve is set and load and rollback are not set)
  • yes
  • no
+
Perform a configuration compare (aka diff) operation.
+
aliases: compare, diffs
+
diffs_file
pathnoNone +
The path to a file, on the Ansible control machine, where the configuration differences will be saved if the diff option is specified.
+
The file must be writeable. If the file already exists, it is overwritten.
+
This option is only valid if the diff option is true.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the diffs_file value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the dest_dir option in new playbooks.
+
The diffs_file and dest_dir options are mutually exclusive.
+
filter
strnonone +
A string of XML, or '/'-separated configuration hierarchies, which specifies a filter used to restrict the portions of the configuration which are retrieved. See PyEZ's get_config method documentation for details on the value of this option.
+
aliases: filter_xml
+
format
strnonone (auto-detect on load, text on retrieve)
  • xml
  • set
  • text
  • json
+
Specifies the format of the configuration retrieved, if retrieve is not none.
+
Specifies the format of the configuration to be loaded, if load is not none.
+
The specified format must be supported by the target Junos device.
+
ignore_warning
bool, str, or list of strnonone +
A boolean, string or list of strings. If the value is true, ignore all warnings regardless of the warning message. If the value is a string, it will ignore warning(s) if the message of each warning matches the string. If the value is a list of strings, ignore warning(s) if the message of each warning matches at least one of the strings in the list. The value of the ignore_warning option is applied to the load and commit operations performed by this module.
+
lines
listnonone +
Used with the load option. Specifies a list of list of configuration strings containing the configuration to be loaded.
+
The src, lines, template, and url options are mutually exclusive.
+
By default, the format of the configuration data is auto-dectected by the content of the first line in the lines list.
+
If the format option is specified, the format value overrides the format auto-detection.
+
load
strnonone
  • none
  • set
  • merge
  • update
  • replace
  • override
  • overwrite
+
Specifies the type of load operation to be performed.
+
The load and rollback options are mutually exclusive.
+
The choices have the following meanings: +
+
none - Do not perform a load operation.
+
merge - Combine the new configuration with the existing configuration. If statements in the new configuration conflict with statements in the existing configuration, the statements in the new configuration replace those in the existing configuration.
+
replace - This option is a superset of the merge option. It combines the new configuration with the existing configuration. If the new configuration is in text format and a hierarchy level in the new configuartion is prefixed with the string replace:, then the hierarchy level in the new configuration replaces the entire corresponding hierarchy level in the existing configuration, regardles of the existence or content of that hierarchy level in the existing configuration. If the configuration is in XML format, the XML attribute replace = "replace" is equivalent to the text format's replace: prefix. If a configuration hierarchy in the new configuration is not prefixed with replace:, then the merge behavior is used. Specifically, for any statements in the new configuration which conflict with statements in the existing configuration, the statements in the new configuration replace those in the existing configuration.
+
override - Discard the entire existing configuration and replace it with the new configuration. When the configuration is later committed, all system processes are notified and the entire new configuration is marked as 'changed' even if some statements previously existed in the configuration. The value overwrite is a synonym for override.
+
update - This option is similar to the override option. The new configuration completely replaces the existing configuration. The difference comes when the configuration is later committed. This option performs a 'diff' between the new candidate configuration and the existing committed configuration. It then only notifies system processes repsonsible for the changed portions of the configuration, and only marks the actual configuration changes as 'changed'.
+
set - This option is used when the new configuration data is in set format (a series of configuration mode commands). The new configuration data is loaded line by line and may contain any configuration mode commands, such as set, delete, edit, or deactivate. This value must be specified if the new configuration is in set format.
+
options
dictnoNone +
Additional options, specified as a dictionary of key/value pairs, used when retrieving the configuration. See the <get-configuration> RPC documentation for information on available options.
+
retrieve
strnonone
  • none
  • candidate
  • committed
+
The configuration database to be retrieved.
+
return_output
boolnoTrue
  • yes
  • no
+
Indicates if the output of the diff and retreive options should be returned in the module's response. You might want to set this option to false, and set the dest_dir option, if the configuration or diff output is very large and you only need to save the output rather than using it's content in subsequent tasks/plays of your playbook.
+
rollback
int or strnonone
  • 0-49
  • rescue
+
Populate the candidate configuration from a previously committed configuration. This value can be a configuration number between 0 and 49, or the keyword rescue to load the previously saved rescue configuration.
+
By default, some Junos platforms store fewer than 50 previous configurations. Specifying a value greater than the number of previous configurations available, or specifying rescue when no rescue configuration has been saved, will result in an error when the module attempts to perform the rollback.
+
The rollback and load options are mutually exclusive.
+
src
pathnonone +
Used with the load option. Specifies the path to a file, on the local Ansible control machine, containing the configuration to be loaded.
+
The src, lines, template, and url options are mutually exclusive.
+
By default, the format of the configuration data is determined by the file extension of this path name. If the file has a .conf extension, the content is treated as text format. If the file has a .xml extension, the content is treated as XML format. If the file has a .set extension, the content is treated as Junos set commands.
+
If the format option is specified, the format value overrides the file-extension based format detection.
+
aliases: source, file
+
template
pathnonone +
The path to a Jinja2 template file, on the local Ansible control machine. This template file, along with the vars option, is used to generate the configuration to be loaded on the target Junos device.
+
The src, lines, template, and url options are mutually exclusive.
+
The template and vars options are required together. If one is specified, the other must be specified.
+
aliases: template_path
+
url
strnonone +
A URL which specifies the configuration data to load on the target Junos device.
+
The Junos device uses this URL to load the configuration, therefore this URL must be reachable by the target Junos device.
+
The possible formats of this value are documented in the 'url' section of the <load-configuration> RPC documentation.
+
The src, lines, template, and url options are mutually exclusive.
+
vars
dictnonone +
A dictionary of keys and values used to render the Jinja2 template specified by the template option.
+
The template and vars options are required together. If one is specified, the other must be specified.
+
aliases: template_vars
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _config-examples-label: + +Examples +-------- + +:: + + + --- + - name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: Retrieve the committed configuration + config: + retrieve: 'committed' + diff: false + check: false + commit: false + register: response + + - name: Print the lines in the config. + debug: + var: response.config_lines + + - name: Append .foo to the hostname using private config mode. + config: + config_mode: 'private' + load: 'merge' + lines: + - "set system host-name {{ inventory_hostname }}.foo" + register: response + + - name: Print the config changes. + debug: + var: response.diff_lines + + - name: Rollback to the previous config. + config: + config_mode: 'private' + rollback: 1 + register: response + + - name: Print the config changes. + debug: + var: response.diff_lines + + - name: Rollback to the rescue config. + config: + rollback: 'rescue' + register: response + - name: Print the complete response. + debug: + var: response + + - name: Load override from a file. + config: + load: 'override' + src: "{{ inventory_hostname }}.conf" + register: response + + - name: Print the complete response. + debug: + var: response + + - name: Load from a Jinja2 template. + config: + load: 'merge' + format: 'xml' + template: "{{ inventory_hostname }}.j2" + vars: + host: "{{ inventory_hostname }}" + register: response + - name: Print the complete response. + debug: + var: response + + - name: Load from a file on the Junos device. + config: + load: 'merge' + url: "{{ inventory_hostname }}.conf" + register: response + - name: Print the complete response. + debug: + var: response + + - name: Load from a file on the Junos device, skip the commit check + config: + load: 'merge' + url: "{{ inventory_hostname }}.conf" + check: false + register: response + - name: Print the msg. + debug: + var: response.msg + + - name: Print diff between current and rollback 10. No check. No commit. + config: + rollback: 11 + diff: true + check: false + commit: false + register: response + + - name: Print the msg. + debug: + var: response + + - name: Retrieve [edit system services] of current committed config. + config: + retrieve: 'committed' + filter: 'system/services' + diff: true + check: false + commit: false + register: response + + - name: Print the resulting config lines. + debug: + var: response.config_lines + + - name: Enable NETCONF SSH and traceoptions, save config, and diffs. + config: + load: 'merge' + lines: + - 'set system services netconf ssh' + - 'set system services netconf traceoptions flag all' + - 'set system services netconf traceoptions file netconf.log' + format: 'set' + retrieve: 'candidate' + filter: 'system/services' + comment: 'Enable NETCONF with traceoptions' + dest_dir: './output' + register: response + + - name: Print the complete response + debug: + var: response + + - name: Load conf. Confirm within 5 min. Wait 3 secs between chk and commit + config: + load: 'merge' + url: "{{ inventory_hostname }}.conf" + confirm: 5 + check_commit_wait: 3 + register: response + + - name: Print the complete response + debug: + var: response + + - name: Confirm the previous commit with a commit check (but no commit) + config: + check: true + diff: false + commit: false + register: response + + - name: Print the complete response + debug: + var: response + + - name: fetch config from the device with filter and login credentials + config: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "22" + retrieve: 'committed' + format: xml + commit: no + check: no + diff: no + dest_dir: "/tmp/" + filter: re0 + return_output: True + register: config_output + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
changed +
Indicates if the device's configuration has changed, or would have changed when in check mode.
+
successbool
config +
The retrieved configuration. The value is a single multi-line string in the format specified by the format option.
+
when retrieved is not none and return_output is true.str
config_lines +
The retrieved configuration. The value is a list of single-line strings in the format specified by the format option.
+
when retrieved is not none and return_output is true.list
config_parsed +
The retrieved configuration parsed into a JSON datastructure. For XML replies, the response is parsed into JSON using the jxmlease library. For JSON the response is parsed using the Python json library.
+
When Ansible converts the jxmlease or native Python data structure into JSON, it does not guarantee that the order of dictionary/object keys are maintained.
+
when retrieved is not none, the format option is xml or json and return_output is true.dict
diff +
The configuration differences between the previous and new configurations. The value is a dict that contains a single key named "prepared". Value associated with that key is a single multi-line string in "diff" format.
+
when load or rollback is specified, diff is true, and return_output is true.dict
diff_lines +
The configuration differences between the previous and new configurations. The value is a list of single-line strings in "diff" format.
+
when load or rollback is specified, diff is true, and return_output is true.list
failed +
Indicates if the task failed.
+
alwaysbool
file +
The value of the src option.
+
when load is not none and src is not nonestr
msg +
A human-readable message indicating the result.
+
alwaysstr
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/docs/docreq.txt b/ansible_collections/juniper/device/docs/docreq.txt similarity index 62% rename from docs/docreq.txt rename to ansible_collections/juniper/device/docs/docreq.txt index bc96b3b0..2a15208e 100644 --- a/docs/docreq.txt +++ b/ansible_collections/juniper/device/docs/docreq.txt @@ -1,2 +1,7 @@ git+https://github.com/ryan-roemer/sphinx-bootstrap-theme.git#egg=sphinx-bootstrap-theme ansible +six +jxmlease +xmltodict +junos-eznc >= 2.5.4 +jsnapy>=1.3.4 diff --git a/ansible_collections/juniper/device/docs/facts.rst b/ansible_collections/juniper/device/docs/facts.rst new file mode 100644 index 00000000..1c5a272a --- /dev/null +++ b/ansible_collections/juniper/device/docs/facts.rst @@ -0,0 +1,509 @@ +.. _facts: + +facts ++++++ +Retrieve facts from a Junos device + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Retrieve facts from a Junos device using the `PyEZ fact gathering system `_. +* Also returns the committed configuration of the Junos device if the *config_format* option has a value other than ``none``. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
config_format
nonone
  • none
  • xml
  • set
  • text
  • json
+
The format of the configuration returned. The specified format must be supported by the target Junos device.
+
savedir
pathnonone +
A path to a directory, on the Ansible control machine, where facts will be stored in a JSON file.
+
The resulting JSON file is saved in savedir/hostname-facts.json.
+
The savedir directory is the value of the savedir option.
+
The hostname-facts.json filename begins with the value of the hostname fact returned from the Junos device, which might be different than the value of the host option passed to the module.
+
If the value of the savedir option is none, the default, then facts are NOT saved to a file.
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _facts-examples-label: + +Examples +-------- + +:: + + + --- + - name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: "Get facts" + facts: + register: response + + - name: Facts with login credentials + facts: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "22" + + - name: Facts in telnet mode + facts: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "23" + mode: "telnet" + + # Print a fact + + # Using config_format option + + # Print the config + + # Using savedir option + + # Print the saved JSON file + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
ansible_facts.junos +
Facts collected from the Junos device. This dictionary contains the keys listed in the contains section of this documentation PLUS all of the keys returned from PyEZ's fact gathering system. See PyEZ facts for a complete list of these keys and their meaning.
+
successcomplex
contains: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
config +
The device's committed configuration, in the format specified by config_format, as a single multi-line string.
+
when config_format is not none.str
has_2RE +
Indicates if the device has more than one Routing Engine installed. Because Ansible does not allow keys to begin with a number, this fact is returned in place of PyEZ's 2RE fact.
+
successbool
re_name +
The name of the current Routing Engine to which Ansible is connected.
+
successstr
master_state +
The mastership state of the Routing Engine to which Ansible is connected. true if the RE is the master Routing Engine. false if the RE is not the master Routing Engine.
+
successbool
+
changed +
Indicates if the device's state has changed. Since this module does not change the operational or configuration state of the device, the value is always set to false.
+
successboolFalse
facts +
Returned for backwards compatibility. Returns the same keys and values which are returned under ansible_facts.junos.
+
successdict
failed +
Indicates if the task failed.
+
alwaysboolFalse
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/file_copy.rst b/ansible_collections/juniper/device/docs/file_copy.rst new file mode 100644 index 00000000..74913ac0 --- /dev/null +++ b/ansible_collections/juniper/device/docs/file_copy.rst @@ -0,0 +1,129 @@ +.. _file_copy: + +file_copy ++++++++++ +File put and get module + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Copy file over SCP to and from a Juniper device + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
action
stryes +
Type of operation to execute, currently only support get and put
+
file
stryes +
Name of the file to copy to/from the remote device
+
local_dir
stryes +
path of the local directory where the file is located or needs to be copied to
+
remote_dir
stryes +
path of the directory on the remote device where the file is located or needs to be copied to
+
+
+ +.. _file_copy-examples-label: + +Examples +-------- + +:: + + + --- + - name: Examples of juniper_device_file_copy + hosts: all + connection: local + gather_facts: false + tasks: + - name: Copy a log file on a remote device locally + juniper.device.file_copy: + remote_dir: /var/log + local_dir: /tmp + action: get + file: log.txt + - name: Copy a local file into /var/tmp on the remote device + juniper.device.file_copy: + remote_dir: /var/tmp + local_dir: /tmp + action: put + file: license.txt + + + + + +Author +~~~~~~ + +* Juniper Networks - Dinesh Babu (@dineshbaburam91) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/jsnapy.rst b/ansible_collections/juniper/device/docs/jsnapy.rst new file mode 100644 index 00000000..865fc6cb --- /dev/null +++ b/ansible_collections/juniper/device/docs/jsnapy.rst @@ -0,0 +1,497 @@ +.. _jsnapy: + +jsnapy +++++++ +Execute JSNAPy tests on a Junos device + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Execute Junos SNAPshot Adminsitrator (JSNAPy) tests against a Junos device. JSNAPy is documented on `Github `_ and this `Day One Book `_ +* This module only reports ``failed`` if the module encounters an error and fails to execute the JSNAPy tests. If does NOT report ``failed`` if one or more of the JSNAPy tests fail. To check the test results, register the module's response and use the assert module to verify the expected result in the response. (See :ref:`jsnapy-examples-label`.) +* A callback plugin which formats and prints JSNAPy test results for human consumption is also available. This callback plugin is enabled by adding ``callback_whitelist = jsnapy`` to the Ansible configuration file. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
action
stryesnone
  • check
  • snapcheck
  • snap_pre
  • snap_post
+
The JSNAPy action to perform.
+
config_file
pathnonone +
The filename of a JSNAPy configuration file (in YAML format). The test_files option and the config_file option are mutually exclusive. Either the test_files option or the config_file option is required.
+
dir
pathno/etc/jsnapy/testfiles +
The path to the directory containing the JSNAPy test file(s) specified by the test_files option or the JSNAPy configuration file specified by the config_file option.
+
aliases: directory
+
test_files
list of pathnonone +
The filename of file(s) in the dir directory. Each file contains JSNAPy test case definitions. The test_files option and the config_file option are mutually exclusive. Either the test_files option or the config_file option is required.
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _jsnapy-examples-label: + +Examples +-------- + +:: + + + --- + - name: Examples of jsnapy + hosts: junos-all + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: JUNOS Post Checklist + jsnapy: + action: "snap_post" + config_file: "first_test.yml" + logfile: "migration_post.log" + register: test1 + - name: Verify all JSNAPy tests passed + assert: + that: + - "test1.passPercentage == 100" + - name: Print the full test response + debug: + var: test1 + + - name: Test based on a test_file directly + jsnapy: + action: "snapcheck" + test_files: "tests/test_junos_interface.yaml" + register: test2 + - name: Verify all JSNAPy tests passed + assert: + that: + - "test2.passPercentage == 100" + - name: Print the full test response + debug: + var: test2 + + - name: "Collect Pre Snapshot" + jsnapy: + action: "snap_pre" + test_files: "tests/test_loopback.yml" + + - name: "Collect Post Snapshot" + jsnapy: + action: "snap_post" + test_files: "tests/test_loopback.yml" + + - name: "Check after Pre and Post Snapshots" + jsnapy: + action: "check" + test_files: "tests/test_loopback.yml" + register: test3 + - name: Verify all JSNAPy tests passed + assert: + that: + - "test3.|succeeded" + - "test3.passPercentage == 100" + - name: Print the full test response + debug: + var: test3 + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
action +
The JSNAPy action performed as specified by the action option.
+
successstr
changed +
Indicates if the device's state has changed. Since this module doesn't change the operational or configuration state of the device, the value is always set to false.
+
successbool
failed +
Indicates if the task failed.
+
alwaysbool
msg +
A human-readable message indicating the result of the JSNAPy tests.
+
alwaysstr
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks +* Roslan Zaki +* Damien Garros +* Stacy Smith (@stacywsmith)" + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/ping.rst b/ansible_collections/juniper/device/docs/ping.rst new file mode 100644 index 00000000..b647dccd --- /dev/null +++ b/ansible_collections/juniper/device/docs/ping.rst @@ -0,0 +1,771 @@ +.. _ping: + +ping +++++ +Execute ping from a Junos device + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Execute the ping command from a Junos device to a specified destination in order to test network reachability from the Junos device . + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
acceptable_percent_loss
intno0 +
Maximum percentage of packets that may be lost and still consider the task not to have failed.
+
aliases: acceptable_packet_loss
+
count
intno5 +
Number of packets to send.
+
dest
stryesnone +
The IP address, or hostname if DNS is configured on the Junos device, used as the destination of the ping.
+
aliases: dest_ip, dest_host, destination, destination_ip, destination_host
+
do_not_fragment
boolnoFalse
  • yes
  • no
+
Set Do Not Fragment bit on ping packets.
+
interface
strnonone +
The source interface from which the the ping is sent. If not specified, the default Junos algorithm for determining the source interface is used.
+
rapid
boolnoTrue
  • yes
  • no
+
Send ping requests rapidly
+
routing_instance
strnonone +
Name of the source routing instance from which the ping is originated. If not specified, the default routing instance is used.
+
size
intnonone (default size for device) +
The size of the ICMP payload of the ping.
+
Total size of the IP packet is size + the 20 byte IP header + the 8 byte ICMP header. Therefore, size of 1472 generates an IP packet of size 1500.
+
source
strnonone +
The IP address, or hostname if DNS is configured on the Junos device, used as the source address of the ping. If not specified, the Junos default algorithm for determining the source address is used.
+
aliases: source_ip, source_host, src, src_ip, src_host
+
ttl
intnonone (default ttl for device) +
Maximum number of IP routers (hops) allowed between source and destination.
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _ping-examples-label: + +Examples +-------- + +:: + + + --- + - name: Examples of ping + hosts: junos-all + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: Ping 192.68.1.1 with default parameters. Fails if any packets lost. + ping: + dest: "192.68.1.1" + + - name: Ping 192.68.1.1 Allow 50% packet loss. Register response. + ping: + dest: "192.68.1.1" + acceptable_percent_loss: 50 + register: response + - name: Print all keys in the response. + debug: + var: response + + - name: Ping 192.68.1.1. Send 20 packets. Register response. + ping: + dest: "192.68.1.1" + count: 20 + register: response + - name: Print packet sent from the response. + debug: + var: response.packets_sent + + - name: Ping 192.68.1.1. Send 10 packets wihtout rapid. Register response. + ping: + dest: "192.68.1.1" + count: 10 + rapid: false + register: response + - name: Print the average round-trip-time from the response. + debug: + var: response.rtt_average + + - name: Ping www.juniper.net with ttl 15. Register response. + ping: + dest: "www.juniper.net" + ttl: 15 + register: response + - name: Print the packet_loss percentage from the response. + debug: + var: response.packet_loss + + - name: Ping 192.68.1.1 with IP packet size of 1500. Register response. + ping: + dest: "192.68.1.1" + size: 1472 + register: response + - name: Print the packets_received from the response. + debug: + var: response.packets_received + + - name: Ping 192.68.1.1 with do-not-fragment bit set. Register response. + ping: + dest: "192.68.1.1" + do_not_fragment: true + register: response + - name: Print the maximum round-trip-time from the response. + debug: + var: response.rtt_maximum + + - name: Ping 192.68.1.1 with source set to 192.68.1.2. Register response. + ping: + dest: "192.68.1.1" + source: "192.68.1.2" + register: response + - name: Print the source from the response. + debug: + var: response.source + + - name: Ping 192.168.1.1 from the red routing-instance. + ping: + dest: "192.168.1.1" + routing_instance: "red" + + - name: Ping the all-hosts multicast address from the ge-0/0/0.0 interface + ping: + dest: "224.0.0.1" + interface: "ge-0/0/0.0" + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
acceptable_percent_loss +
The acceptable packet loss (as a percentage) for this task as specified by the acceptable_percent_loss option.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.str
changed +
Indicates if the device's state has changed. Since this module doesn't change the operational or configuration state of the device, the value is always set to false.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.bool
count +
The number of pings sent, as specified by the count option.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.str
do_not_fragment +
Whether or not the do not fragment bit was set on the pings sent, as specified by the do_not_fragment option.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.bool
failed +
Indicates if the task failed.
+
alwaysbool
host +
The destination IP/host of the pings sent as specified by the dest option.
+
Keys dest and dest_ip are also returned for backwards compatibility.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.str
interface +
The source interface of the pings sent as specified by the interface option.
+
when ping successfully executed and the interface option was specified, even if the acceptable_percent_loss was exceeded.str
msg +
A human-readable message indicating the result.
+
alwaysstr
packet_loss +
The percentage of packets lost.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.str
packets_received +
The number of packets received.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.str
packets_sent +
The number of packets sent.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.str
rapid +
Whether or not the pings were sent rapidly, as specified by the rapid option.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.bool
routing_instance +
The routing-instance from which the pings were sent as specified by the routing_instance option.
+
when ping successfully executed and the routing_instance option was specified, even if the acceptable_percent_loss was exceeded.str
rtt_average +
The average round-trip-time, in microseconds, of all ping responses received.
+
when ping successfully executed, and packet_loss < 100%.str
rtt_maximum +
The maximum round-trip-time, in microseconds, of all ping responses received.
+
when ping successfully executed, and packet_loss < 100%.str
rtt_minimum +
The minimum round-trip-time, in microseconds, of all ping responses received.
+
when ping successfully executed, and packet_loss < 100%.str
rtt_stddev +
The standard deviation of round-trip-time, in microseconds, of all ping responses received.
+
when ping successfully executed, and packet_loss < 100%.str
size +
The size in bytes of the ICMP payload on the pings sent as specified by the size option.
+
Total size of the IP packet is size + the 20 byte IP header + the 8 byte ICMP header. Therefore, size of 1472 generates an IP packet of size 1500.
+
when ping successfully executed and the size option was specified, even if the acceptable_percent_loss was exceeded.str
source +
The source IP/host of the pings sent as specified by the source option.
+
Key source_ip is also returned for backwards compatibility.
+
when ping successfully executed and the source option was specified, even if the acceptable_percent_loss was exceeded.str
timeout +
The number of seconds to wait for a response from the ping RPC.
+
when ping successfully executed, even if the acceptable_percent_loss was exceeded.str
ttl +
The time-to-live set on the pings sent as specified by the ttl option.
+
when ping successfully executed and the ttl option was specified, even if the acceptable_percent_loss was exceeded.str
warnings +
A list of warning strings, if any, produced from the ping.
+
when warnings are presentlist
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/pmtud.rst b/ansible_collections/juniper/device/docs/pmtud.rst new file mode 100644 index 00000000..8f165cc2 --- /dev/null +++ b/ansible_collections/juniper/device/docs/pmtud.rst @@ -0,0 +1,559 @@ +.. _pmtud: + +pmtud ++++++ +Perform path MTU discovery from a Junos device to a destination + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Determine the maximum IP MTU supported along a path from a Junos device to a user-specified destination by performing path MTU discovery (PMTUD) using the ping command. The reported MTU will be between min_test_size and *max_size* where *min_test_size* = (*max_size* - *max_range* + 1). If the actual path MTU is greater than *max_size*, then *max_size* will be reported. If the actual path MTU is less than *min_test_size*, then a failure will be reported. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
dest
stryesnone +
The IPv4 address, or hostname if DNS is configured on the Junos device, used as the destination of the PMTUD.
+
aliases: dest_ip, dest_host, destination, destination_ip, destination_host
+
interface
strnonone +
The source interface from which the the PMTUD is performed. If not specified, the default Junos algorithm for determining the source interface is used.
+
max_range
intno512 +
The maximum range of MTU values, in bytes, which will be searched when performing path MTU discovery. This value must be 0 or a power of 2 (2^n) between 2 and 65536. The minimum IPv4 MTU value attempted when performing path MTU discovery is min_test_size = (max_size - max_range + 1)
+
max_size
intno1500 +
The maximum IPv4 MTU, in bytes, to attempt when performing path MTU discovery.
+
The value returned for inet_mtu will be no more than this value even if the path actually supports a higher MTU.
+
This value must be between 68 and 65496.
+
routing_instance
strnonone +
Name of the source routing instance from which the ping is originated.
+
If not specified, the default routing instance is used.
+
source
strnonone +
The IPv4 address, or hostname if DNS is configured on the Junos device, used as the source address of the PMTUD. If not specified, the Junos default algorithm for determining the source address is used.
+
aliases: source_ip, source_host, src, src_ip, src_host
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _pmtud-examples-label: + +Examples +-------- + +:: + + + --- + - name: Examples of pmtud + hosts: junos-all + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: Perform PMTUD to 192.68.1.1 with default parameters. + pmtud: + dest: "192.68.1.1" + + - name: Perform PMTUD to 192.68.1.1. Register response. + pmtud: + dest: "192.68.1.1" + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Search all possible MTU values. + pmtud: + dest: "192.68.1.1" + max_size: 65496 + max_range: 65536 + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Source from ge-0/0/0.0 interface. + pmtud: + dest: "192.68.1.1" + interface: "ge-0/0/0.0" + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Source from 192.168.1.2. + pmtud: + dest: "192.68.1.1" + source: "192.168.1.2" + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Source from the red routing-instance. + pmtud: + dest: "192.68.1.1" + routing_instance: "red" + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
changed +
Indicates if the device's state has changed. Since this module doesn't change the operational or configuration state of the device, the value is always set to false.
+
when PMTUD successfully executed.bool
failed +
Indicates if the task failed.
+
alwaysbool
host +
The destination IP/host of the PMTUD as specified by the dest option.
+
Keys dest and dest_ip are also returned for backwards compatibility.
+
when PMTUD successfully executed.str
inet_mtu +
The IPv4 path MTU size in bytes to the dest. This is the lesser of max_size and the actual path MTU to dest. If the actual path MTU is less than min_test_size, then a failure is reported. Where min_test_size = (max_size - max_range + 1)
+
when PMTUD successfully executed.str
interface +
The source interface of the PMTUD as specified by the interface option.
+
when the interface option was specified.str
routing_instance +
The routing-instance from which the PMTUD was performed as specified by the routing_instance option.
+
when the routing_instance option was specified.str
source +
The source IP/host of the PMTUD as specified by the source option.
+
Key source_ip is also returned for backwards compatibility.
+
when the source option was specified.str
warnings +
A list of warning strings, if any, produced from the ping.
+
when warnings are presentlist
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Martin Komon (@mkomon) +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/rpc.rst b/ansible_collections/juniper/device/docs/rpc.rst new file mode 100644 index 00000000..94a79e14 --- /dev/null +++ b/ansible_collections/juniper/device/docs/rpc.rst @@ -0,0 +1,637 @@ +.. _rpc: + +rpc ++++ +Execute one or more NETCONF RPCs on a Junos device + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Execute one or more NETCONF RPCs on a Junos device. +* Use the ``| display xml rpc`` modifier to determine the equivalent RPC name for a Junos CLI command. For example, ``show version | display xml rpc`` reveals the equivalent RPC name is ``get-software-information``. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attrs
dict or list of dictnonone +
The attributes and values to the RPCs specified by the rpcs option. The value of this option can either be a single dictionary of keywords and values, or a list of dictionaries containing keywords and values.
+
There is a one-to-one correspondence between the elements in the kwargs list and the RPCs in the rpcs list. In other words, the two lists must always contain the same number of elements.
+
aliases: attr
+
dest
pathnoNone +
The path to a file, on the Ansible control machine, where the output of the RPC will be saved.
+
The file must be writeable. If the file already exists, it is overwritten.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the dest value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the dest_dir option in new playbooks. The dest and dest_dir options are mutually exclusive.
+
aliases: destination
+
dest_dir
pathnoNone +
The path to a directory, on the Ansible control machine, where the output of the RPC will be saved. The output will be logged to a file named {{ inventory_hostname }}_rpc.format in the dest_dir directory.
+
The destination file must be writeable. If the file already exists, it is overwritten. It is the users responsibility to ensure a unique dest_dir value is provided for each execution of this module within a playbook.
+
The dest_dir and dest options are mutually exclusive. The dest_dir option is recommended for all new playbooks.
+
aliases: destination_dir, destdir
+
filter
strnonone +
This argument only applies if the rpcs option contains a single RPC with the value get-config. When used, this value specifies an XML filter used to restrict the portions of the configuration which are retrieved. See the PyEZ get_config method for details on the value of this option.
+
aliases: filter_xml
+
formats
str or list of strnoxml
  • text
  • xml
  • json
+
The format of the reply for the RPCs specified by the rpcs option.
+
The specified format(s) must be supported by the target Junos device.
+
The value of this option can either be a single format, or a list of formats. If a single format is specified, it applies to all RPCs specified by the rpcs option. If a list of formats are specified, there must be one value in the list for each RPC specified by the rpcs option.
+
aliases: format, display, output
+
ignore_warning
bool, str, or list of strnonone +
A boolean, string or list of strings. If the value is true, ignore all warnings regardless of the warning message. If the value is a string, it will ignore warning(s) if the message of each warning matches the string. If the value is a list of strings, ignore warning(s) if the message of each warning matches at least one of the strings in the list. The value of the ignore_warning option is applied to the load and commit operations performed by this module.
+
kwargs
dict or list of dictnonone +
The keyword arguments and values to the RPCs specified by the rpcs option. The value of this option can either be a single dictionary of keywords and values, or a list of dictionaries containing keywords and values.
+
There must be a one-to-one correspondence between the elements in the kwargs list and the RPCs in the rpcs list. In other words, the two lists must always contain the same number of elements. For RPC arguments which do not require a value, specify the value of True as shown in the :ref:`rpc-examples-label`.
+
aliases: kwarg, args, arg
+
return_output
boolnoTrue
  • yes
  • no
+
Indicates if the output of the RPC should be returned in the module's response. You might want to set this option to false, and set the dest_dir option, if the RPC output is very large and you only need to save the output rather than using it's content in subsequent tasks/plays of your playbook.
+
rpcs
listyesnone +
A list of one or more NETCONF RPCs to execute on the Junos device.
+
aliases: rpc
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _rpc-examples-label: + +Examples +-------- + +:: + + + --- + - name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: "Execute RPC with filters" + rpc: + rpcs: + - "get-config" + format: xml + filter: re0 + attr: name=re0 + register: test1 + ignore_errors: True + + - name: Check TEST 1 + debug: + var: test1 + + - name: "Execute RPC with host data and store logging" + rpc: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "22" + rpcs: + - "get-software-information" + logfile: "/var/tmp/rpc.log" + ignore_warning: true + register: test1 + ignore_errors: True + + - name: "Print results - summary" + debug: + var: test1.stdout_lines + + - name: "Execute multiple RPC" + rpc: + rpcs: + - "get-config" + - "get-software-information" + + - name: Get Device Configuration for vlan - 1 + rpc: + rpc: "get-config" + filter_xml: "" + dest: "get_config_vlan.conf" + register: junos + + - name: Get interface information with kwargs + rpc: + rpc: get-interface-information + kwargs: + interface_name: em1 + media: True + format: json + dest: get_interface_information.conf + register: junos + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
attrs +
The RPC attributes and values from the list of dictionaries in the attrs option. This will be none if no attributes are applied to the RPC.
+
alwaysdict
changed +
Indicates if the device's state has changed. Since this module doesn't change the operational or configuration state of the device, the value is always set to false.
+
You could use this module to execute an RPC which changes the operational state of the the device. For example, clear-ospf-neighbor-information. Beware, this module is unable to detect this situation, and will still return a changed value of false in this case.
+
successbool
failed +
Indicates if the task failed. See the results key for additional details.
+
alwaysbool
format +
The format of the RPC response from the list of formats in the formats option.
+
alwaysstr
kwargs +
The keyword arguments from the list of dictionaries in the kwargs option. This will be none if no kwargs are applied to the RPC.
+
alwaysdict
msg +
A human-readable message indicating the result.
+
alwaysstr
parsed_output +
The RPC reply from the Junos device parsed into a JSON datastructure. For XML replies, the response is parsed into JSON using the jxmlease library. For JSON the response is parsed using the Python json library.
+
When Ansible converts the jxmlease or native Python data structure into JSON, it does not guarantee that the order of dictionary/object keys are maintained.
+
when RPC executed successfully, return_output is true, and the RPC format is xml or json.dict
results +
The other keys are returned when a single RPC is specified for the rpcs option. When the value of the rpcs option is a list of RPCs, this key is returned instead. The value of this key is a list of dictionaries. Each element in the list corresponds to the RPCs in the rpcs option. The keys for each element in the list include all of the other keys listed. The failed key indicates if the individual RPC failed. In this case, there is also a top-level failed key. The top-level failed key will have a value of false if ANY of the RPCs ran successfully. In this case, check the value of the failed key for each element in the results list for the results of individual RPCs.
+
when the rpcs option is a list value.list of dict
rpc +
The RPC which was executed from the list of RPCs in the rpcs option.
+
alwaysstr
stdout +
The RPC reply from the Junos device as a single multi-line string.
+
when RPC executed successfully and return_output is true.str
stdout_lines +
The RPC reply from the Junos device as a list of single-line strings.
+
when RPC executed successfully and return_output is true.list of str
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/rst.j2 b/ansible_collections/juniper/device/docs/rst.j2 new file mode 100644 index 00000000..3b7338f0 --- /dev/null +++ b/ansible_collections/juniper/device/docs/rst.j2 @@ -0,0 +1,560 @@ +.. _@{ module }@: + +{% set title = module %} +{% set title_len = title|length %} +@{ title }@ +@{ '+' * title_len }@ +{% if short_description %} +@{ short_description|convert_symbols_to_format }@ +{% endif %} + +{% if version_added is defined and version_added != '' -%} +.. versionadded:: @{ version_added | default('') }@ + + +{% endif %} + + +.. contents:: + :local: + :depth: 2 + +{# ------------------------------------------ + # + # Please note: this looks like a core dump + # but it isn't one. + # + --------------------------------------------#} +{% if deprecated is defined -%} + + +DEPRECATED +---------- + +{# use unknown here? skip the fields? #} +:In: version: @{ deprecated['version'] | default('') | string | convert_symbols_to_format }@ +:Why: @{ deprecated['why'] | default('') | convert_symbols_to_format }@ +:Alternative: @{ deprecated['alternative'] | default('')| convert_symbols_to_format }@ + + +{% endif %} + +Synopsis +-------- + +{% if description %} + +{% for desc in description -%} +* @{ desc | convert_symbols_to_format }@ +{% endfor %} + + +{% endif %} +{% if aliases is defined -%} + +Aliases: @{ ','.join(aliases) }@ + + +{% endif %} +{% if requirements %} + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +{% for req in requirements %} +* @{ req | convert_symbols_to_format }@ +{% endfor %} + + + +{% endif %} +{% if options -%} + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + +{% for k in option_keys -%} +{% set v = options[k] -%} +{% if not v['suboptions'] %} + + + + + + +{% if v.get('type', 'not_bool') == 'bool' %} + +{% else %} + +{% endif %} + + + + + + + + + + + +{% endif %} + + +{% endfor %} + +
parametertyperequireddefaultchoicescomments
@{ k }@
{% if v['version_added'] -%} (added in @{v['version_added']}@){% endif -%}
{% if v['type'] -%}@{ v['type'] }@{% endif -%}{% if v.get('required', False) -%}yes{% else %}no{% endif -%}{% if v.get('default', None) is not none -%}@{ v['default'] | string | html_ify }@{% endif -%}
  • yes
  • no
{% if v['choices'] -%}
    {% for choice in v.get('choices',[]) -%}
  • @{ choice }@
  • {% endfor -%}
{% endif -%}
+{% if v.description is string %} +
@{ v.description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in v.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +{% if 'aliases' in v and v.aliases %} +
aliases: @{ v.aliases|join(', ') }@
+{% endif %} +{% else %} + +
@{ k }@
{% if v['version_added'] -%} (added in @{v['version_added']}@){% endif -%}
{% if v['type'] -%}@{ v['type'] }@{% endif -%}{% if v.get('required', False) -%}yes{% else -%}no{% endif -%} +{% for desc in v.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% if 'aliases' in v and v.aliases %} +
aliases: @{ v.aliases|join(', ') }@
+{% endif %} +
+ + + + + + + + + + + +{% for k2 in v['suboptions'] %} +{% set v2 = v['suboptions'] [k2] %} + + + + + + +{% if v2.get('type', 'not_bool') == 'bool' %} + +{% else %} + +{% endif %} + + +{% endfor %} + +
Dictionary object @{ k }@
parametertyperequireddefaultchoicescomments
@{ k2 }@
{% if v2['version_added'] -%} (added in @{v2['version_added']}@){% endif -%}
{% if v2['type'] -%}@{ v2['type'] }@{% endif -%}{% if v2.get('required', False) -%}yes{% else -%}no{% endif -%}{% if v2.get('default', None) is not none -%}@{ v2['default'] | string | html_ify }@{% endif -%}
  • yes
  • no
{% if v2['choices'] -%}
    {% for choice in v2.get('choices',[]) -%}
  • @{ choice }@
  • {% endfor -%}
{% endif -%}
+{% if v2.description is string %} +
@{ v2.description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in v2.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +{% if 'aliases' in v and v2.aliases %} +
aliases: @{ v2.aliases|join(', ') }@
+{% endif %} +
+ +
+
+ +{% endif %} +{% if connection_options -%} + + +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + +{% for k in connection_option_keys -%} +{% set v = connection_options[k] -%} +{% if not v['suboptions'] %} + + + + + + +{% if v.get('type', 'not_bool') == 'bool' %} + +{% else %} + +{% endif %} + + + + + + + + + + + +{% endif %} + + +{% endfor %} + +
parametertyperequireddefaultchoicescomments
@{ k }@
{% if v['version_added'] -%} (added in @{v['version_added']}@){% endif -%}
{% if v['type'] -%}@{ v['type'] }@{% endif -%}{% if v.get('required', False) -%}yes{% else %}no{% endif -%}{% if v.get('default', None) is not none -%}@{ v['default'] | string | html_ify }@{% endif -%}
  • yes
  • no
{% if v['choices'] -%}
    {% for choice in v.get('choices',[]) -%}
  • @{ choice }@
  • {% endfor -%}
{% endif -%}
+{% if v.description is string %} +
@{ v.description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in v.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +{% if 'aliases' in v and v.aliases %} +
aliases: @{ v.aliases|join(', ') }@
+{% endif %} +{% else %} + +
@{ k }@
{% if v['version_added'] -%} (added in @{v['version_added']}@){% endif -%}
{% if v['type'] -%}@{ v['type'] }@{% endif -%}{% if v.get('required', False) -%}yes{% else -%}no{% endif -%} +{% for desc in v.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% if 'aliases' in v and v.aliases %} +
aliases: @{ v.aliases|join(', ') }@
+{% endif %} +
+ + + + + + + + + + + +{% for k2 in v['suboptions'] %} +{% set v2 = v['suboptions'] [k2] %} + + + + + + +{% if v2.get('type', 'not_bool') == 'bool' %} + +{% else %} + +{% endif %} + + +{% endfor %} + +
Dictionary object @{ k }@
parametertyperequireddefaultchoicescomments
@{ k2 }@
{% if v2['version_added'] -%} (added in @{v2['version_added']}@){% endif -%}
{% if v2['type'] -%}@{ v2['type'] }@{% endif -%}{% if v2.get('required', False) -%}yes{% else -%}no{% endif -%}{% if v2.get('default', None) is not none -%}@{ v2['default'] | string | html_ify }@{% endif -%}
  • yes
  • no
{% if v2['choices'] -%}
    {% for choice in v2.get('choices',[]) -%}
  • @{ choice }@
  • {% endfor -%}
{% endif -%}
+{% if v2.description is string %} +
@{ v2.description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in v2.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +{% if 'aliases' in v and v2.aliases %} +
aliases: @{ v2.aliases|join(', ') }@
+{% endif %} +
+ +
+
+ +{% endif %} +{% if logging_options -%} + + +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + +{% for k in logging_option_keys -%} +{% set v = logging_options[k] -%} +{% if not v['suboptions'] %} + + + + + + +{% if v.get('type', 'not_bool') == 'bool' %} + +{% else %} + +{% endif %} + + + + + + + + + + + +{% endif %} + + +{% endfor %} + +
parametertyperequireddefaultchoicescomments
@{ k }@
{% if v['version_added'] -%} (added in @{v['version_added']}@){% endif -%}
{% if v['type'] -%}@{ v['type'] }@{% endif -%}{% if v.get('required', False) -%}yes{% else %}no{% endif -%}{% if v.get('default', None) is not none -%}@{ v['default'] | string | html_ify }@{% endif -%}
  • yes
  • no
{% if v['choices'] -%}
    {% for choice in v.get('choices',[]) -%}
  • @{ choice }@
  • {% endfor -%}
{% endif -%}
+{% if v.description is string %} +
@{ v.description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in v.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +{% if 'aliases' in v and v.aliases %} +
aliases: @{ v.aliases|join(', ') }@
+{% endif %} +{% else %} + +
@{ k }@
{% if v['version_added'] -%} (added in @{v['version_added']}@){% endif -%}
{% if v['type'] -%}@{ v['type'] }@{% endif -%}{% if v.get('required', False) -%}yes{% else -%}no{% endif -%} +{% for desc in v.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% if 'aliases' in v and v.aliases %} +
aliases: @{ v.aliases|join(', ') }@
+{% endif %} +
+ + + + + + + + + + + +{% for k2 in v['suboptions'] %} +{% set v2 = v['suboptions'] [k2] %} + + + + + + +{% if v2.get('type', 'not_bool') == 'bool' %} + +{% else %} + +{% endif %} + + +{% endfor %} + +
Dictionary object @{ k }@
parametertyperequireddefaultchoicescomments
@{ k2 }@
{% if v2['version_added'] -%} (added in @{v2['version_added']}@){% endif -%}
{% if v2['type'] -%}@{ v2['type'] }@{% endif -%}{% if v2.get('required', False) -%}yes{% else -%}no{% endif -%}{% if v2.get('default', None) is not none -%}@{ v2['default'] | string | html_ify }@{% endif -%}
  • yes
  • no
{% if v2['choices'] -%}
    {% for choice in v2.get('choices',[]) -%}
  • @{ choice }@
  • {% endfor -%}
{% endif -%}
+{% if v2.description is string %} +
@{ v2.description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in v2.description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +{% if 'aliases' in v and v2.aliases %} +
aliases: @{ v2.aliases|join(', ') }@
+{% endif %} +
+ +
+
+ +{% endif %} +{% if examples or plainexamples -%} +.. _@{ title }@-examples-label: + +Examples +-------- + +:: + +{% for example in examples %} +{% if example['description'] %} +@{ example['description'] }@ +{% endif %} +@{ example['code'] | escape | indent(4, True) }@ +{% endfor %} +{% if plainexamples %} +@{ plainexamples | indent(4, True) }@ +{% endif %} +{% endif %} + + +{% if returndocs -%} + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + +{% for entry in returndocs_keys %} + + + + + + + + +{% if returndocs[entry].type == 'complex' %} + + + + + +{% endif %} +{% endfor %} + +
namedescriptionreturnedtypesample
@{ entry }@ +{% if returndocs[entry].description is string %} +
@{ returndocs[entry].description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in returndocs[entry].description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +
@{ returndocs[entry].returned | html_ify }@@{ returndocs[entry].type | html_ify }@@{ returndocs[entry].sample | replace('\n', '\n ') | html_ify }@
contains: + + + + + + + + + +{% for sub in returndocs[entry].contains %} + + + + + + + + +{% endfor %} + +
namedescriptionreturnedtypesample
@{ sub }@ +{% if returndocs[entry].contains[sub].description is string %} +
@{ returndocs[entry].contains[sub].description | replace('\n', '\n ') | html_ify }@
+{% else %} +{% for desc in returndocs[entry].contains[sub].description %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+{% endfor %} +{% endif %} +
@{ returndocs[entry].contains[sub].returned | html_ify }@@{ returndocs[entry].contains[sub].type | html_ify }@@{ returndocs[entry].contains[sub].sample }@
+
+
+
+{% endif %} + + +{% if notes -%} + + +Notes +----- + +.. note:: +{% for note in notes %} + - @{ note | convert_symbols_to_format }@ +{% endfor %} + + +{% endif %} +{% if author is defined -%} + + +Author +~~~~~~ + +{% for author_name in author %} +* @{ author_name }@ +{% endfor %} + + +{% endif %} +{% if not deprecated %} +{% set support = { 'core': 'The Ansible Core Team', 'network': 'The Ansible Network Team', 'certified': 'an Ansible Partner', 'community': 'The Ansible Community', 'curated': 'A Third Party'} %} +{% set module_states = { 'preview': 'it is not guaranteed to have a backwards compatible interface', 'stableinterface': 'the maintainers for this module guarantee that no backward incompatible interface changes will be made'} %} +{% if metadata %} +{% if metadata.status %} + + +Status +~~~~~~ + +{% for cur_state in metadata.status %} +This module is flagged as **@{cur_state}@** which means that @{module_states[cur_state]}@. +{% endfor %} + + +{% endif %} +{% endif %} +{% endif %} diff --git a/ansible_collections/juniper/device/docs/software.rst b/ansible_collections/juniper/device/docs/software.rst new file mode 100644 index 00000000..a25aacac --- /dev/null +++ b/ansible_collections/juniper/device/docs/software.rst @@ -0,0 +1,684 @@ +.. _software: + +software +++++++++ +Install software on a Junos device + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Install a Junos OS image, or other software package, on a Junos device. This action is generally equivalent to the ``request system software add`` operational-mode CLI command. It performs the following steps in order: + +#. Compare the currently installed Junos version to the desired version + specified by the *version* option. + + * If the current and desired versions are the same, stop and return + *changed* with a value of ``false``. + * If running in check mode, and the current and desired versions differ, + stop and return *changed* with a value of ``true``. + * Otherwise, proceed. +#. If the *local_package* option is specified, compute the MD5 checksum + of the *local_package* file on the local Ansible control machine. +#. Check if the file exists at the *remote_package* location on the target + Junos device. If so, compute the MD5 checksum of the file on the target + Junos device. +#. If the *cleanfs* option is ``true``, the default, then perform the + equivalent of the ``request system storage cleanup`` CLI command. +#. If the checksums computed in steps 2 and 3 differ, or if the + *remote_package* file does not exist on the target Junos device, then + copy the package from *local_package* on the local Ansible control + machine to *remote_package* on the target Junos device. +#. Install the software pacakge from the *remote_package* location on the + target Junos device using the options specified. +#. If the *reboot* option is ``true``, the default, initiate a reboot of + the target Junos device. + + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
all_re
boolnoTrue
  • yes
  • no
+
Whether or not to install the software on all Routing Engines of the target Junos device. If true, and the device has multiple Routing Engines, the software is installed on all Routing Engines. If false, the software is only installed on the current Routing Engine.
+
checksum
strnonone +
The pre-calculated checksum, using the checksum_algorithm of the file specified by the local_package option. Specifying this option is simply an optimization to avoid repeatedly computing the checksum of the local_package file once for each target Junos host.
+
checksum_algorithm
strnomd5 +
The algorithm to use when calculating the checksum of the local and remote software packages.
+
checksum_timeout
intno300 (5 minutes) +
The number of seconds to wait for the calculation of the checksum to complete on the target Junos device.
+
cleanfs
boolnotrue (unless no_copy is true, then false)
  • yes
  • no
+
Whether or not to perform a request system storage cleanup prior to copying or installing the software.
+
cleanfs_timeout
intno300 (5 minutes) +
The number of seconds to wait for the request system storage cleanup to complete on the target Junos device.
+
force_host
boolnoFalse
  • yes
  • no
+
Forces the upgrade of the Host Software package on QFX-series devices.
+
install_timeout
intno1800 (30 minutes) +
The number of seconds to wait for the software installation to complete on the target Junos device.
+
issu
boolnoFalse
  • yes
  • no
+
Indicates if a unified in-service software upgrade (ISSU) should be attempted. ISSU enables the upgrade between two different Junos OS releases with no control plane disruption and minimal data plane traffic disruption.
+
In order for an ISSU to succeed, ISSU must be supported. This includes support for the current to desired Junos versions, the hardware of the target Junos device, and the current software configuration of the target Junos device.
+
The issu and nssu options are mutually exclusive.
+
kwargs
dictnonone +
Additional keyword arguments and values which are passed to the <request-package-add> RPC used to install the software package. The value of this option is a dictionary of keywords and values.
+
aliases: kwarg, args, arg
+
local_package
pathnonone +
The path, on the local Ansible control machine, of a Junos software package. This Junos software package will be installed on the target Junos device.
+
If this option is specified, and a file with the same MD5 checksum doesn't already exist at the remote_package location on the target Junos device, then the file is copied from the local Ansible control machine to the target Junos device.
+
If this option is not specified, it is assumed that the software package already exists on the target Junos device. In this case, the remote_package option must be specified.
+
aliases: package
+
no_copy
boolnoFalse
  • yes
  • no
+
Indicates if the file containing the software package should be copied from the local_package location on the local Ansible control machine to the remote_package location on the target Junos device.
+
If the value is true, or if the local_package option is not specified, then the copy is skipped and the file must already exist at the remote_package location on the target Junos device.
+
nssu
boolnoFalse
  • yes
  • no
+
Indicates if a non-stop software upgrade (NSSU) should be attempted. NSSU enables the upgrade between two different Junos OS releases with minimal data plane traffic disruption.
+
NSSU is specific to EX-series Virtual Chassis systems or EX-series stand-alone systems with redundant Routing Engines.
+
In order for an NSSU to succeed, NSSU must be supported. This includes support for the current to desired Junos versions, the hardware of the target Junos device, and the current software configuration of the target Junos device.
+
The nssu and issu options are mutually exclusive.
+
pkg_set
listnoFalse +
install software on the members in a mixed Virtual Chassis. Currently we are not doing target package check this option is provided.
+
reboot
boolnoTrue
  • yes
  • no
+
Indicates if the target Junos device should be rebooted after performing the software install.
+
reboot_pause
intno10 +
The amount of time, in seconds, to wait after the reboot is issued before the module returns. This gives time for the reboot to begin. The default value of 10 seconds is designed to ensure the device is no longer reachable (because the reboot has begun) when the next task begins. The value must be an integer greater than or equal to 0.
+
remote_package
pathno/var/tmp/ + filename portion of local_package +
This option may take one of two formats.
+
The first format is a URL, from the perspective of the target Junos device, from which the device retrieves the software package to be installed. The acceptable formats for the URL value may be found here.
+
When using the URL format, the local_package and no_copy options must not be specified.
+
The second format is a file path, on the taget Junos device, to the software package.
+
If the local_package option is also specified, and the no_copy option is false, the software package will be copied from local_package to remote_package, if necessary.
+
If the no_copy option is true or the local_package option is not specified, then the file specified by this option must already exist on the target Junos device.
+
If this option is not specified, it is assumed that the software package will be copied into the /var/tmp directory on the target Junos device using the filename portion of the local_package option. In this case, the local_package option must be specified.
+
Specifying the remote_package option and not specifying the local_package option is equivalent to specifying the local_package option and the no_copy option. In this case, you no longer have to explicitly specify the no_copy option.
+
If the remote_package value is a directory (ends with /), then the filename portion of local_package will be appended to the remote_package value.
+
If the remote_package value is a file (does not end with /), then the filename portion of remote_package must be the same as the filename portion of local_package.
+
validate
boolnoFalse
  • yes
  • no
+
Whether or not to have the target Junos device should validate the current configuration against the new software package.
+
version
strnoAttempt to extract the version from the file name specified by the local_package or remote_package option values IF the package appears to be a Junos software package. Otherwise, none. +
The version of software contained in the file specified by the local_package and/or remote_package options. This value should match the Junos version which will be reported by the device once the new software is installed. If the device is already running a version of software which matches the version option value, the software install is not necessary. In this case the module returns a changed value of false and an failed value of false and does not attempt to perform the software install.
+
aliases: target_version, new_version, desired_version
+
vmhost
boolnoFalse
  • yes
  • no
+
Whether or not this is a vmhost software installation.
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _software-examples-label: + +Examples +-------- + +:: + + + --- + - name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: Execute a basic Junos software upgrade. + software: + local_package: "./images/" + register: response + + - name: Print the complete response. + debug: + var: response + + - name: Upgrade Junos OS from package copied at device + software: + host: "10.x.x.x" + user: "user" + passwd: "user123" + remote_package: "/var/tmp/junos-install-mx-x86-64-20.1R1.5.tgz" + no_copy: false + cleanfs: false + validate: true + register: response + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
changed +
Indicates if the device's state has changed, or if the state would have changed when executing in check mode. This value is set to true when the version of software currently running on the target Junos device does not match the desired version of software specified by the version option. If the current and desired software versions match, the value of this key is set to false.
+
successbool
check_mode +
Indicates whether or not the module ran in check mode.
+
successbool
failed +
Indicates if the task failed.
+
alwaysbool
msg +
A human-readable message indicating the result of the software installation.
+
alwaysstr
+
+
+ + +Notes +----- + +.. note:: + - This module does support connecting to the console of a Junos device, but does not support copying the software package from the local Ansible control machine to the target Junos device while connected via the console. In this situation, the *remote_package* option must be specified, and the specified software package must already exist on the target Junos device. + - This module returns after installing the software and, optionally, initiating a reboot of the target Junos device. It does not wait for the reboot to complete, and it does not verify that the desired version of software specified by the *version* option is actually activated on the target Junos device. It is the user's responsibility to confirm the software installation using additional follow on tasks in their playbook. + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Jeremy Schulman +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/srx_cluster.rst b/ansible_collections/juniper/device/docs/srx_cluster.rst new file mode 100644 index 00000000..f5993882 --- /dev/null +++ b/ansible_collections/juniper/device/docs/srx_cluster.rst @@ -0,0 +1,452 @@ +.. _srx_cluster: + +srx_cluster ++++++++++++ +Add or remove SRX chassis cluster configuration + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Add an SRX chassis cluster configuration and reboot the device. Assuming the device is capable of forming an SRX cluster and has the correct cables connected, this will form an SRX cluster. +* If an SRX chassis cluster is already present, setting *cluster_enable* to ``false`` will remove the SRX chassis cluster configuration and reboot the device causing the SRX cluster to be broken and the device to return to stand-alone mode. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
cluster_id
intnonone +
The cluster ID to configure.
+
Required when enable is true.
+
aliases: cluster
+
enable
boolyesnone
  • yes
  • no
+
Enable or disable cluster mode. When true cluster mode is enabled and cluster_id and node_id must also be specified. When false cluster mode is disabled and the device returns to stand-alone mode.
+
aliases: cluster_enable
+
node_id
intnonone +
The node ID to configure. (0 or 1)
+
Required when enable is true.
+
aliases: node
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _srx_cluster-examples-label: + +Examples +-------- + +:: + + + --- + - name: Manipulate the SRX cluster configuration of Junos SRX devices + hosts: junos-all + connection: local + gather_facts: no + collections: + - juniper.device + tasks: + - name: Enable an SRX cluster + srx_cluster: + enable: true + cluster_id: 4 + node_id: 0 + register: response + - name: Print the response. + debug: + var: response.config_lines + + - name: Disable an SRX cluster + srx_cluster: + enable: false + register: response + - name: Print the response. + debug: + var: response.config_lines + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
changed +
Indicates if the device's configuration has changed, or would have changed when in check mode.
+
successbool
failed +
Indicates if the task failed.
+
alwaysbool
msg +
A human-readable message indicating the result.
+
alwaysstr
reboot +
Indicates if a reboot of the device has been initiated.
+
successbool
+
+
+ + +Notes +----- + +.. note:: + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/system.rst b/ansible_collections/juniper/device/docs/system.rst new file mode 100644 index 00000000..661f3abb --- /dev/null +++ b/ansible_collections/juniper/device/docs/system.rst @@ -0,0 +1,563 @@ +.. _system: + +system +++++++ +Initiate operational actions on the Junos system + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Initiate an operational action (shutdown, reboot, halt or zeroize) on a Junos system. The particular action to execute is defined by the mandatory *action* option. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
action
stryesnone
  • shutdown
  • halt
  • reboot
  • zeroize
  • off
  • power-off
  • power_off
+
The action performed by the module.
+
The following actions are supported: +
+
shutdown - Power off the Junos devices. The values off, power-off, and power_off are aliases for this value. This is the equivalent of the request system power-off CLI command.
+
halt - Stop the Junos OS running on the RE, but do not power off the system. Once the system is halted, it will reboot if a keystroke is entered on the console. This is the equivalent of the request system halt CLI command.
+
reboot - Reboot the system. This is the equivalent of the request system reboot CLI command.
+
zeroize - Restore the system (configuration, log files, etc.) to a factory default state. This is the equivalent of the request system zeroize CLI command.
+
all_re
boolnoTrue
  • yes
  • no
+
If the system has multiple Routing Engines and this option is true, then the action is performed on all REs in the system. If the system does not have multiple Routing Engines, then this option has no effect.
+
This option applies to all action values.
+
The all_re option is mutually exclusive with the other_re option.
+
at
strnonone +
The time at which to shutdown, halt, or reboot the system.
+
The value may be specified in one of the following ways: +
+
now - The action takes effect immediately.
+
+minutes — The action takes effect in minutes minutes from now.
+
yymmddhhmm — The action takes effect at yymmddhhmm absolute time, specified as year, month, day, hour, and minute.
+
hh:mm — The action takes effect at hh:mm absolute time on the current day, specified in 24-hour time.
+
The at option can not be used when the action option has a value of zeroize. The at option is mutually exclusive with the in_min option.
+
in_min
intno0 +
Specify a delay, in minutes, before the shutdown, halt, or reboot.
+
The in_min option can not be used when the action option has a value of zeroize. The in_min option is mutually exclusive with the at option.
+
media
boolnoFalse
  • yes
  • no
+
Overwrite media when performing the zeroize operation. This option is only valid when the action option has a value of zeroize.
+
other_re
boolnoFalse
  • yes
  • no
+
If the system has dual Routing Engines and this option is true, then the action is performed on the other REs in the system. If the system does not have dual Routing Engines, then this option has no effect.
+
The other_re option can not be used when the action option has a value of zeroize.
+
The other_re option is mutually exclusive with the all_re option.
+
vmhost
boolnoFalse
  • yes
  • no
+
Whether or not this is a vmhost reboot.
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _system-examples-label: + +Examples +-------- + +:: + + + --- + - name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: Reboot all REs of the device + system: + action: "reboot" + + - name: Power off the other RE of the device. + system: + action: "shutdown" + othe_re: True + + - name: Reboot this RE at 8pm today. + system: + action: "reboot" + all_re: False + at: "20:00" + + - name: Halt the system on 25 January 2018 at 4pm. + system: + action: "halt" + at: "1801251600" + + - name: Reboot the system in 30 minutes. + system: + action: "reboot" + in_min: 30 + + - name: Reboot the system in 30 minutes. + system: + action: "reboot" + at: "+30m" + + - name: Zeroize the local RE only. + system: + action: "zeroize" + all_re: False + + - name: Zeroize all REs and overwrite medea. + system: + action: "zeroize" + media: True + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
action +
The value of the action option.
+
alwaysstr
all_re +
The value of the all_re option.
+
alwaysstr
changed +
Indicates if the device's state has changed. If the action is performed (or if it would have been performed when in check mode) then the value will be true. If there was an error before the action, then the value will be false.
+
alwaysbool
failed +
Indicates if the task failed.
+
alwaysbool
media +
The value of the media option.
+
alwaysstr
msg +
A human-readable message indicating the result.
+
alwaysstr
other_re +
The value of the other_re option.
+
alwaysstr
+
+
+ + +Notes +----- + +.. note:: + - This module only **INITIATES** the action. It does **NOT** wait for the action to complete. + - Some Junos devices are effected by a Junos defect which causes this Ansible module to hang indefinitely when connected to the Junos device via the console. This problem is not seen when connecting to the Junos device using the normal NETCONF over SSH transport connection. Therefore, it is recommended to use this module only with a NETCONF over SSH transport connection. However, this module does still permit connecting to Junos devices via the console port and this functionality may still be used for Junos devices running Junos versions less than 15.1. + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/docs/table.rst b/ansible_collections/juniper/device/docs/table.rst new file mode 100644 index 00000000..fe2d5fb2 --- /dev/null +++ b/ansible_collections/juniper/device/docs/table.rst @@ -0,0 +1,574 @@ +.. _table: + +table ++++++ +Retrieve data from a Junos device using a PyEZ table/view + + + +.. contents:: + :local: + :depth: 2 + + +Synopsis +-------- + + +* Retrieve data from a Junos device using PyEZ's operational table/views. This module may be used with the tables/views which are included in the PyEZ distribution or it may be used with user-defined tables/views. + + + +Requirements +------------ +The following software packages must be installed on hosts that execute this module: + +* `junos-eznc `_ >= 2.5.2 +* Python >= 3.5 + + + +.. _module-specific-options-label: + +Module-specific Options +----------------------- +The following options may be specified for this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
file
pathyesnone +
Name of the YAML file, relative to the path option, that contains the table/view definition. The file name must end with the .yml or .yaml extension.
+
kwargs
dictnonone +
Optional keyword arguments and values to the table's get() method. The value of this option is a dictionary of keywords and values which are used to refine the data return from performing a get() on the table. The exact keywords and values which are supported are specific to the table's definition and the underlying RPC which the table invokes.
+
aliases: kwarg, args, arg
+
path
pathnoop directory in jnpr.junos.op +
The directory containing the YAML table/view definition file as specified by the file option. The default value is the op directory in jnpr.junos.op. This is the directory containing the table/view definitions which are included in the PyEZ distribution.
+
aliases: directory, dir
+
response_type
strnolist_of_dicts
  • list_of_dicts
  • juniper_items
+
Defines the format of data returned by the module. See RETURN. The value of the resource key in the module's response is either a list of dictionaries list_of_dicts or PyEZ's native return format juniper_items. Because Ansible module's may only return JSON data, PyEZ's native return format juniper_items is translated into a list of lists.
+
table
strnoThe name of the table defined in the file option. +
Name of the PyEZ table used to retrieve data. If not specified, defaults to the name of the table defined in the file option. Any table names in file which begin with _ are ignored. If more than one table is defined in file, the module fails with an error message. In this case, you must manually specify the name of the table by setting this option.
+
+
+ +Common Connection-related Options +--------------------------------- +In addition to the :ref:`module-specific-options-label`, the following connection-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
attempts
intno10 +
The number of times to try connecting and logging in to the Junos device. This option is only applicable when using mode = 'telnet' or mode = 'serial'. Mutually exclusive with the console option.
+
baud
intno9600 +
The serial baud rate, in bits per second, used to connect to the Junos device. This option is only applicable when using mode = 'serial'. Mutually exclusive with the console option.
+
console
strnonone +
An alternate method of specifying a NETCONF over serial console connection to the Junos device using Telnet to a console server. The value of this option must be a string in the format --telnet <console_hostname>,<console_port_number>. This option is deprecated. It is present only for backwards compatibility. The string value of this option is exactly equivalent to specifying host with a value of <console_hostname>, mode with a value of telnet, and port with a value of <console_port_number>. Mutually exclusive with the mode, port, baud, and attempts options.
+
cs_passwd
strno +
The password used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_password
+
cs_user
strno +
The username used to authenticate with the console server over SSH. This option is only required if you want to connect to a device over console using SSH as transport. Mutually exclusive with the console option.
+
aliases: console_username
+
host
stryes{{ inventory_hostname }} +
The hostname or IP address of the Junos device to which the connection should be established. This is normally the Junos device itself, but is the hostname or IP address of a console server when connecting to the console of the device by setting the mode option to the value telnet. This option is required, but does not have to be specified explicitly by the user because it defaults to {{ inventory_hostname }}.
+
aliases: hostname, ip
+
mode
strnonone
  • none
  • telnet
  • serial
+
The PyEZ mode used to establish a NETCONF connection to the Junos device. A value of none uses the default NETCONF over SSH mode. Depending on the values of the host and port options, a value of telnet results in either a direct NETCONF over Telnet connection to the Junos device, or a NETCONF over serial console connection to the Junos device using Telnet to a console server. A value of serial results in a NETCONF over serial console connection to the Junos device. Mutually exclusive with the console option.
+
passwd
strnoThe first defined value from the following list 1) The ANSIBLE_NET_PASSWORD environment variable. (used by Ansible Tower) 2) The value specified using the -k or --ask-pass command line arguments to the ansible or ansible-playbook command. 3) none (An empty password/passphrase) +
The password, or ssh key's passphrase, used to authenticate with the Junos device. If this option is not specified, authentication is attempted using an empty password, or ssh key passphrase.
+
aliases: password
+
port
int or strno830 if mode = none, 23 if mode = 'telnet', '/dev/ttyUSB0' if (mode = 'serial') +
The TCP port number or serial device port used to establish the connection. Mutually exclusive with the console option.
+
ssh_config
pathno +
The path to the SSH client configuration file. If this option is not specified, then the PyEZ Device instance by default queries file ~/.ssh/config.
+
ssh_private_key_file
pathnoThe first defined value from the following list 1) The ANSIBLE_NET_SSH_KEYFILE environment variable. (used by Ansible Tower) 2) The value specified using the --private-key or --key-file command line arguments to the ansible or ansible-playbook command. 3) none (the file specified in the user's SSH configuration, or the operating-system-specific default) +
The path to the SSH private key file used to authenticate with the Junos device. If this option is not specified, and no default value is found using the algorithm below, then the SSH private key file specified in the user's SSH configuration, or the operating-system-specific default is used.
+
This must be in the RSA PEM format, and not the newer OPENSSH format. To check if the private key is in the correct format, issue the command `head -n1 ~/.ssh/some_private_key` and ensure that it's RSA and not OPENSSH. To create a key in the RSA PEM format, issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m PEM -f ~/.ssh/some_private_key`
+
aliases: ssh_keyfile
+
timeout
intno30 +
The maximum number of seconds to wait for RPC responses from the Junos device. This option does NOT control the initial connection timeout value.
+
user
stryesThe first defined value from the following list 1) The ANSIBLE_NET_USERNAME environment variable. (used by Ansible Tower) 2) The remote_user as defined by Ansible. Ansible sets this value via several methods including a) -u or --user command line arguments to the ansible or ansible-playbook command. b) ANSIBLE_REMOTE_USER environment variable. c) remote_user configuration setting. See the Ansible documentation for the precedence used to set the remote_user value. 3) The USER environment variable. +
The username used to authenticate with the Junos device. This option is required, but does not have to be specified explicitly by the user due to the algorithm for determining the default value.
+
aliases: username
+
+
+ +Common Logging-related Options +------------------------------ +In addition to the :ref:`module-specific-options-label`, the following logging-related options are also supported by this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parametertyperequireddefaultchoicescomments
level
strnoWARNING
  • INFO
  • DEBUG
+
The level of information to be logged can be modified using this option
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
logdir
pathnonone +
The path to a directory, on the Ansible control machine, where debugging information for the particular task is logged.
+
If this option is specified, debugging information is logged to a file named {{ inventory_hostname }}.log in the directory specified by the logdir option.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
The logfile and logdir options are mutually exclusive. The logdir option is recommended for all new playbooks.
+
aliases: log_dir
+
logfile
pathnonone +
The path to a file, on the Ansible control machine, where debugging information for the particular task is logged.
+
The log file must be writeable. If the file already exists, it is appended. It is the users responsibility to delete/rotate log files.
+
The level of information logged in this file is controlled by Ansible's verbosity, debug options and level option in task
+
1) By default, messages at level WARNING or higher are logged.
+
2) If the -v or --verbose command-line options to the ansible-playbook command are specified, messages at level INFO or higher are logged.
+
3) If the -vv (or more verbose) command-line option to the ansible-playbook command is specified, or the ANSIBLE_DEBUG environment variable is set, then messages at level DEBUG or higher are logged.
+
4) If level is mentioned then messages at level level or more are logged.
+
When tasks are executed against more than one target host, one process is forked for each target host. (Up to the maximum specified by the forks configuration. See forks for details.) This means that the value of this option must be unique per target host. This is usually accomplished by including {{ inventory_hostname }} in the logfile value. It is the user's responsibility to ensure this value is unique per target host.
+
For this reason, this option is deprecated. It is maintained for backwards compatibility. Use the logdir option in new playbooks. The logfile and logdir options are mutually exclusive.
+
aliases: log_file
+
+
+ +.. _table-examples-label: + +Examples +-------- + +:: + + + --- + - name: Retrieve data from a Junos device using a PyEZ table/view. + hosts: junos-all + connection: local + gather_facts: no + collections: + - juniper.device + + tasks: + - name: Retrieve LLDP Neighbor Information Using PyEZ-included Table + table: + file: "lldp.yml" + register: response + - name: Print response + debug: + var: response + + - name: Retrieve routes within 192.68.1/8 + table: + file: "routes.yml" + table: "RouteTable" + kwargs: + destination: "192.68.1.0/8" + response_type: "juniper_items" + register: response + - name: Print response + debug: + var: response + + - name: Retrieve from custom table in playbook directory + table: + file: "fpc.yaml" + path: "." + register: response + - name: Print response + debug: + var: response + + + +Return Values +------------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescriptionreturnedtypesample
changed +
Indicates if the device's configuration has changed. Since this module does not change the operational or configuration state of the device, the value is always set to false.
+
successbool
failed +
Indicates if the task failed.
+
alwaysbool
msg +
A human-readable message indicating a summary of the result.
+
alwaysstr
resource +
The items retrieved by the table/view.
+
successlist of dicts if response_type is list_of_dicts or list of lists if respsonse_type is juniper_items.# when response_type == 'list_of_dicts' + [ + { + "local_int": "ge-0/0/3", + "local_parent": "-", + "remote_chassis_id": "00:05:86:08:d4:c0", + "remote_port_desc": null, + "remote_port_id": "ge-0/0/0", + "remote_sysname": "r5", + "remote_type": "Mac address" + }, + { + "local_int": "ge-0/0/0", + "local_parent": "-", + "remote_chassis_id": "00:05:86:18:f3:c0", + "remote_port_desc": null, + "remote_port_id": "ge-0/0/2", + "remote_sysname": "r4", + "remote_type": "Mac address" + } + ] + # when response_type == 'juniper_items' + [ + [ + "ge-0/0/3", + [ + [ + "local_parent", + "-" + ], + [ + "remote_port_id", + "ge-0/0/0" + ], + [ + "remote_chassis_id", + "00:05:86:08:d4:c0" + ], + [ + "remote_port_desc", + null + ], + [ + "remote_type", + "Mac address" + ], + [ + "local_int", + "ge-0/0/3" + ], + [ + "remote_sysname", + "r5" + ] + ] + ], + [ + "ge-0/0/0", + [ + [ + "local_parent", + "-" + ], + [ + "remote_port_id", + "ge-0/0/2" + ], + [ + "remote_chassis_id", + "00:05:86:18:f3:c0" + ], + [ + "remote_port_desc", + null + ], + [ + "remote_type", + "Mac address" + ], + [ + "local_int", + "ge-0/0/0" + ], + [ + "remote_sysname", + "r4" + ] + ] + ] + ] +
+
+
+ + +Notes +----- + +.. note:: + - This module only works with operational tables/views; it does not work with configuration tables/views. + - The NETCONF system service must be enabled on the target Junos device. + + +Author +~~~~~~ + +* Jason Edelman (@jedelman8) +* Updated by Juniper Networks - Stacy Smith (@stacywsmith) + + + + +Status +~~~~~~ + +This module is flagged as **stableinterface** which means that the maintainers for this module guarantee that no backward incompatible interface changes will be made. diff --git a/ansible_collections/juniper/device/galaxy.yml b/ansible_collections/juniper/device/galaxy.yml new file mode 100644 index 00000000..12004f75 --- /dev/null +++ b/ansible_collections/juniper/device/galaxy.yml @@ -0,0 +1,50 @@ +### 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 characters and underscores. Additionally namespaces cannot start with +# underscores or numbers and cannot contain consecutive underscores +namespace: juniper + +# The name of the collection. Has the same character restrictions as 'namespace' +name: device + +# The version of the collection. Must be compatible with semantic versioning +version: 1.0.7 + +# 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: +- Juniper Networks + +# A short summary description of the collection +description: set of Ansible modules that perform specific operational and configuration tasks on devices running Junos OS. + +# 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: +- Apache-2.0 + +# 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: ['juniper', 'junos', 'network'] + +# 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: {} + +# The URL of the originating SCM repository +repository: https://github.com/Juniper/ansible-junos-stdlib + +# The URL to any online docs +documentation: https://ansible-juniper-collection.readthedocs.io/ + +# The URL to the homepage of the collection/project +homepage: https://github.com/Juniper/ansible-junos-stdlib + +# The URL to the collection issue tracker +issues: https://github.com/Juniper/ansible-junos-stdlib/issues diff --git a/ansible_collections/juniper/device/meta/runtime.yml b/ansible_collections/juniper/device/meta/runtime.yml new file mode 100644 index 00000000..d15cfe2b --- /dev/null +++ b/ansible_collections/juniper/device/meta/runtime.yml @@ -0,0 +1 @@ +requires_ansible: ">=2.10" diff --git a/ansible_collections/juniper/device/plugins/README.md b/ansible_collections/juniper/device/plugins/README.md new file mode 100644 index 00000000..06857260 --- /dev/null +++ b/ansible_collections/juniper/device/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.9/plugins/plugins.html). diff --git a/ansible_collections/juniper/device/plugins/action/command.py b/ansible_collections/juniper/device/plugins/action/command.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/command.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/config.py b/ansible_collections/juniper/device/plugins/action/config.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/config.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/extract_data.py b/ansible_collections/juniper/device/plugins/action/extract_data.py new file mode 100644 index 00000000..bda01219 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/extract_data.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +import os + +connection_spec_fallbacks = { + "host": ["host", "hostname", "ip", "ansible_host", "inventory_hostname"], + "user": [ + "user", + "username", + "ansible_connection_user", + "ansible_ssh_user", + "ansible_user", + ], + "passwd": ["passwd", "password", "ansible_ssh_pass", "ansible_pass"], + "port": ["port", "ansible_ssh_port", "ansible_port"], + "ssh_private_key_file": [ + "ssh_private_key_file", + "ansible_ssh_private_key_file", + "ansible_private_key_file", + "ssh_keyfile", + ], + "ssh_config": ["ssh_config"], + "cs_user": ["cs_user", "console_username"], + "cs_passwd": ["cs_passwd", "console_password"], + "attempts": ["attempts"], + "baud": ["baud"], + "console": ["console"], + "mode": ["mode"], + "timeout": ["timeout", "ansible_timeout"], +} + + +class ExtractData: + def extract(self, tmp=None, task_vars=None): + # The new connection arguments based on fallback/defaults. + new_connection_args = dict() + + connection_args = self._task.args + + self._task.args["_connection"] = self._play_context.connection + new_connection_args["_connection"] = self._play_context.connection + + # The environment variables used by Ansible Tower + if "user" not in connection_args: + net_user = os.getenv("ANSIBLE_NET_USERNAME") + if net_user is not None: + new_connection_args["user"] = net_user + connection_args["user"] = net_user + if "passwd" not in connection_args: + net_passwd = os.getenv("ANSIBLE_NET_PASSWORD") + if net_passwd is not None: + new_connection_args["passwd"] = net_passwd + connection_args["passwd"] = net_passwd + if "ssh_private_key_file" not in connection_args: + net_key = os.getenv("ANSIBLE_NET_SSH_KEYFILE") + if net_key is not None: + new_connection_args["ssh_private_key_file"] = net_key + connection_args["ssh_private_key_file"] = net_key + + # The values set by Ansible command line arguments, configuration + # settings, or environment variables. + for key in connection_spec_fallbacks: + if key not in connection_args: + for task_var_key in connection_spec_fallbacks[key]: + if task_var_key in task_vars: + new_connection_args[key] = task_vars[task_var_key] + # check if the value is in form of a variable {{var}} + # In case of variables, resolve it to the value + index = str(new_connection_args[key]).find("{{") + if index == 0: + tempKey = new_connection_args[key][2:-2].strip() + new_connection_args[key] = task_vars[tempKey] + break + + # Fix for ansible-core>=2.13 when -u, -k or --private-key are command line arguments + if "user" not in connection_args: + if self._play_context.remote_user is not None: + new_connection_args["user"] = self._play_context.remote_user + connection_args["user"] = self._play_context.remote_user + if "passwd" not in connection_args: + if self._play_context.password is not None: + new_connection_args["passwd"] = self._play_context.password + connection_args["passwd"] = self._play_context.password + if "ssh_private_key_file" not in connection_args: + if self._play_context.private_key_file is not None: + new_connection_args["ssh_private_key_file"] = ( + self._play_context.private_key_file + ) + connection_args["ssh_private_key_file"] = ( + self._play_context.private_key_file + ) + + # Backwards compatible behavior to fallback to USER env. variable. + if "user" not in connection_args and "user" not in new_connection_args: + user = os.getenv("USER") + if user is not None: + new_connection_args["user"] = user + + self._task.args.update(new_connection_args) + + # Pass the hidden _module_utils_path option + module_utils_path = os.path.normpath(os.path.dirname(__file__)) + self._task.args["_module_utils_path"] = module_utils_path + # Pass the hidden _module_name option + self._task.args["_module_name"] = self._task.action + # Pass the hidden _inventory_hostname option + self._task.args["_inventory_hostname"] = task_vars["inventory_hostname"] diff --git a/ansible_collections/juniper/device/plugins/action/facts.py b/ansible_collections/juniper/device/plugins/action/facts.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/facts.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/file_copy.py b/ansible_collections/juniper/device/plugins/action/file_copy.py new file mode 100755 index 00000000..912bdca7 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/file_copy.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2024, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + + +from ansible.plugins.action.normal import ActionModule as ActionNormal + +from ansible_collections.juniper.device.plugins.action.extract_data import ExtractData + + +# The Ansible core engine will call ActionModule.run() +class ActionModule(ExtractData, ActionNormal): + """A subclass of ansible.plugins.action.network.ActionModule used by all modules. + + All modules share common behavior which is implemented in + this class. This includes specific option fallback/default behavior and + passing the "hidden" _module_utils_path option to the module. + + """ + + def run(self, tmp=None, task_vars=None): + super().extract(tmp, task_vars) + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/ansible_collections/juniper/device/plugins/action/jsnapy.py b/ansible_collections/juniper/device/plugins/action/jsnapy.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/jsnapy.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/juniper_junos_common_action.py b/ansible_collections/juniper/device/plugins/action/juniper_junos_common_action.py new file mode 100755 index 00000000..7f0e5a0f --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/juniper_junos_common_action.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from ansible.plugins.action.normal import ActionModule as ActionNormal + +from ansible_collections.juniper.device.plugins.action.extract_data import ExtractData + + +# The Ansible core engine will call ActionModule.run() +class ActionModule(ExtractData, ActionNormal): + """A subclass of ansible.plugins.action.network.ActionModule used by all modules. + + All modules share common behavior which is implemented in + this class. This includes specific option fallback/default behavior and + passing the "hidden" _module_utils_path option to the module. + + """ + + def run(self, tmp=None, task_vars=None): + super().extract(tmp, task_vars) + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/ansible_collections/juniper/device/plugins/action/ping.py b/ansible_collections/juniper/device/plugins/action/ping.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/ping.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/pmtud.py b/ansible_collections/juniper/device/plugins/action/pmtud.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/pmtud.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/rpc.py b/ansible_collections/juniper/device/plugins/action/rpc.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/rpc.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/software.py b/ansible_collections/juniper/device/plugins/action/software.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/software.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/srx_cluster.py b/ansible_collections/juniper/device/plugins/action/srx_cluster.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/srx_cluster.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/system.py b/ansible_collections/juniper/device/plugins/action/system.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/system.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/action/table.py b/ansible_collections/juniper/device/plugins/action/table.py new file mode 120000 index 00000000..7470277b --- /dev/null +++ b/ansible_collections/juniper/device/plugins/action/table.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/ansible_collections/juniper/device/plugins/callback/jsnapy.py b/ansible_collections/juniper/device/plugins/callback/jsnapy.py new file mode 100644 index 00000000..b7be564c --- /dev/null +++ b/ansible_collections/juniper/device/plugins/callback/jsnapy.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +import pprint + +from ansible.module_utils.six import iteritems +from ansible import constants as C +from ansible.plugins.callback import CallbackBase + + +class CallbackModule(CallbackBase): + """ + This callback add extra logging for the module junos_jsnapy . + """ + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = "aggregate" + CALLBACK_NAME = "jsnapy" + + # callback needs to be enabled with config-file to use jsnapy callback during execution + CALLBACK_NEEDS_WHITELIST = True + + # useful links regarding Callback + # https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/callback/__init__.py + + def __init__(self): + self._pp = pprint.PrettyPrinter(indent=4) + self._results = {} + + super(CallbackModule, self).__init__() + + def v2_runner_on_ok(self, result): + """ + Collect test results for all tests executed if action is snapcheck or check + """ + + # Extract module name + module_args = {} + if "invocation" in result._result: + if "module_args" in result._result["invocation"]: + module_args = result._result["invocation"]["module_args"] + + # Check if dic return has all valid information + if "action" not in module_args: + return None + + if module_args["action"] == "snapcheck" or module_args["action"] == "check": + + # Check if dict entry already exist for this host + host = result._host.name + if host not in self._results.keys(): + self._results[host] = [] + + self._results[host].append(result) + + def v2_playbook_on_stats(self, stats): + + # Go over all results for all hosts + for host, results in iteritems(self._results): + has_printed_banner = False + for result in results: + # self._pp.pprint(result.__dict__) + res = result._result + if res.get("final_result") == "Failed": + for test_name, test_results in iteritems(res["test_results"]): + for testlet in test_results: + if ("count" in testlet) and testlet["count"]["fail"] != 0: + if not has_printed_banner: + self._display.banner( + "JSNAPy Results for: " + str(host) + ) + has_printed_banner = True + + for test in testlet["failed"]: + + # Check if POST exist in the response + data = "" + if "post" in test: + data = test["post"] + else: + data = test + + self._display.display( + "Value of '{0}' not '{1}' at '{2}' with {3}".format( + str(testlet["node_name"]), + str(testlet["testoperation"]), + str(testlet["xpath"]), + json.dumps(data), + ), + color=C.COLOR_ERROR, + ) + + elif testlet["count"]["pass"] != 0: + if not has_printed_banner: + self._display.banner( + "JSNAPy Results for: " + str(host) + ) + has_printed_banner = True + + for test in testlet["passed"]: + + # Check if POST exist in the response + data = "" + if "post" in test: + data = test["post"] + else: + data = test + + self._display.display( + "Value of '{0}' '{1}' at '{2}' with {3}".format( + str(testlet["node_name"]), + str(testlet["testoperation"]), + str(testlet["xpath"]), + json.dumps(data), + ), + color=C.COLOR_DEBUG, + ) diff --git a/ansible_collections/juniper/device/plugins/connection/pyez.py b/ansible_collections/juniper/device/plugins/connection/pyez.py new file mode 100644 index 00000000..40fef9bb --- /dev/null +++ b/ansible_collections/juniper/device/plugins/connection/pyez.py @@ -0,0 +1,953 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +try: + import xmltodict + + HAS_XMLTODICT = True +except ImportError: + HAS_XMLTODICT = False + +__metaclass__ = type + +DOCUMENTATION = """author: Juniper Automation Team +connection: pyez +short_description: Use pyez to run command on JUNOS appliances +description: +- This connection plugin provides a connection to remote devices over the junos-pyez library. +options: + host: + description: + - Specifies the remote device FQDN or IP address to establish the SSH connection + to. + default: inventory_hostname + vars: + - name: ansible_host + - name: host + - name: hostname + - name: ip + port: + description: + - Specifies the port on the remote device that listens for connections when establishing + the SSH connection. + ini: + - section: defaults + key: remote_port + env: + - name: ANSIBLE_REMOTE_PORT + vars: + - name: ansible_port + - name: port + mode: + description: + - Specifies the mode for the remote device connections. + vars: + - name: mode + baud: + description: + - The serial baud rate. + vars: + - name: baud + attempts: + description: + - The number of times to try connecting and logging in to the Junos device. + vars: + - name: attempts + remote_user: + description: + - The username used to authenticate to the remote device when the SSH connection + is first established. If the remote_user is not specified, the connection will + use the username of the logged in user. + - Can be configured from the CLI via the C(--user) or C(-u) options. + ini: + - section: defaults + key: remote_user + env: + - name: ANSIBLE_REMOTE_USER + vars: + - name: ansible_user + - name: user + - name: username + password: + description: + - Configures the user password used to authenticate to the remote device when + first establishing the SSH connection. + vars: + - name: ansible_password + - name: ansible_ssh_pass + - name: ansible_ssh_password + - name: passwd + - name: password + pyez_console: + description: + - console option. + ini: + - section: pyez_connection + key: console + env: + - name: ANSIBLE_PYEZ_CONSOLE + vars: + - name: ansible_pyez_console + private_key_file: + description: + - The private SSH key or certificate file used to authenticate to the remote device + when first establishing the SSH connection. + ini: + - section: defaults + key: private_key_file + env: + - name: ANSIBLE_PRIVATE_KEY_FILE + vars: + - name: ansible_private_key_file + - name: ssh_private_key_file + - name: ssh_keyfile + host_key_auto_add: + type: boolean + description: + - By default, Ansible will prompt the user before adding SSH keys to the known + hosts file. Since persistent connections such as network_cli run in background + processes, the user will never be prompted. By enabling this option, unknown + host keys will automatically be added to the known hosts file. + - Be sure to fully understand the security implications of enabling this option + on production systems as it could create a security vulnerability. + default: false + ini: + - section: pyez_connection + key: host_key_auto_add + env: + - name: ANSIBLE_HOST_KEY_AUTO_ADD + persistent_connect_timeout: + type: int + description: + - Configures, in seconds, the amount of time to wait when trying to initially + establish a persistent connection. If this value expires before the connection + to the remote device is completed, the connection will fail. + default: 30 + ini: + - section: persistent_connection + key: connect_timeout + env: + - name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT + vars: + - name: ansible_connect_timeout + - name: timeout + persistent_command_timeout: + type: int + description: + - Configures, in seconds, the amount of time to wait for a command to return from + the remote device. If this timer is exceeded before the command returns, the + connection plugin will raise an exception and close. + default: 30 + ini: + - section: persistent_connection + key: command_timeout + env: + - name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT + vars: + - name: ansible_command_timeout + persistent_log_messages: + type: boolean + description: + - This flag will enable logging the command executed and response received from + target device in the ansible log file. For this option to work 'log_path' ansible + configuration option is required to be set to a file path with write access. + - Be sure to fully understand the security implications of enabling this option + as it could create a security vulnerability by logging sensitive information + in log file. + default: false + ini: + - section: persistent_connection + key: log_messages + env: + - name: ANSIBLE_PERSISTENT_LOG_MESSAGES + vars: + - name: ansible_persistent_log_messages + pyez_ssh_config: + description: + - This variable is used to enable bastion/jump host with netconf connection. If + set to True the bastion/jump host ssh settings should be present in ~/.ssh/config + file, alternatively it can be set to custom ssh configuration file path to read + the bastion/jump host settings. + ini: + - section: pyez_connection + key: ssh_config + env: + - name: ANSIBLE_PYEZ_SSH_CONFIG + vars: + - name: ansible_pyez_ssh_config + - name: ssh_config + +""" +import json +import logging + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_bytes +from ansible.plugins.connection import NetworkConnectionBase, ensure_connect + +# Non-standard library imports and checks +try: + from jnpr.junos.version import VERSION + + HAS_PYEZ_VERSION = VERSION +except ImportError: + HAS_PYEZ_VERSION = None + +try: + import jnpr.junos.device + + HAS_PYEZ_DEVICE = True +except ImportError: + HAS_PYEZ_DEVICE = False + +try: + import jnpr.junos.utils.sw + + HAS_PYEZ_SW = True +except ImportError: + HAS_PYEZ_SW = False + +try: + from jnpr.junos.utils.scp import SCP + + HAS_PYEZ_SCP = True +except ImportError: + HAS_PYEZ_SCP = False + +try: + import jnpr.junos.utils.config + + HAS_PYEZ_CONFIG = True +except ImportError: + HAS_PYEZ_CONFIG = False + +try: + import jnpr.junos.factory.factory_loader + import jnpr.junos.factory.table + import jnpr.junos.op + + HAS_PYEZ_OP_TABLE = True +except ImportError: + HAS_PYEZ_OP_TABLE = False + +try: + import jnpr.junos.exception as pyez_exception + + HAS_PYEZ_EXCEPTIONS = True +except ImportError: + HAS_PYEZ_EXCEPTIONS = False + +try: + from jnpr.jsnapy import __version__ + + HAS_JSNAPY_VERSION = __version__ +except ImportError: + HAS_JSNAPY_VERSION = None +# Most likely JSNAPy 1.2.0 with https://github.com/Juniper/jsnapy/issues/263 +except TypeError: + HAS_JSNAPY_VERSION = "possibly 1.2.0" + +try: + from lxml import etree + + HAS_LXML_ETREE_VERSION = ".".join(map(str, etree.LXML_VERSION)) +except ImportError: + HAS_LXML_ETREE_VERSION = None + +try: + import jxmlease + + HAS_JXMLEASE_VERSION = jxmlease.__version__ +except ImportError: + HAS_JXMLEASE_VERSION = None + +try: + import yaml + + HAS_YAML_VERSION = yaml.__version__ +except ImportError: + HAS_YAML_VERSION = None + +# import q +logging.getLogger("ncclient").setLevel(logging.INFO) + + +# Supported configuration modes +CONFIG_MODE_CHOICES = ["exclusive", "private", "dynamic", "batch", "ephemeral"] + + +class Connection(NetworkConnectionBase): + """NetConf connections""" + + transport = "juniper.device.pyez" + has_pipelining = False + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + self.dev = None + self.config = None + + @property + @ensure_connect + def manager(self): + return self.dev + + def _connect(self): + """Connect with the device. + + Establish the connection with the device. + """ + self.queue_message("log", "ssh connection done, starting junos-eznc") + self.open() + if not self.dev.connected: + return 1, b"", b"not connected" + + self._connected = True + + super(Connection, self)._connect() + + self._sub_plugin = {"name": "pyez", "obj": self.dev} + self.queue_message("vvvv", "created pyez connection type") + return ( + 0, + to_bytes(self.dev._conn.session_id, errors="surrogate_or_strict"), + b"", + ) + + def open(self): + """Open the self.dev PyEZ Device instance. + Failures: + - ConnectError: When unable to make a PyEZ connection. + """ + # Move all of the connection arguments into connect_args + connect_args = {} + + # check for mode + if self.get_option("port") is None: + if self.get_option("mode") == "telnet": + connect_args["port"] = 23 + elif self.get_option("mode") == "serial": + connect_args["port"] = "/dev/ttyUSB0" + else: + connect_args["port"] = 830 + else: + connect_args["port"] = self.get_option("port") + + if self.get_option("mode") == "telnet" or self.get_option("mode") == "serial": + if self.get_option("baud") is None: + # Default baud if serial or telnet mode + connect_args["baud"] = 9600 + if self.get_option("attempts") is None: + # Default attempts if serial or telnet mode + connect_args["attempts"] = 10 + + connect_args["host"] = self.get_option("host") + # connect_args['port'] = self.get_option('port') + connect_args["user"] = self.get_option("remote_user") + connect_args["passwd"] = self.get_option("password") + connect_args["ssh_private_key_file"] = self.get_option("private_key_file") + connect_args["ssh_config"] = self.get_option("pyez_ssh_config") + connect_args["timeout"] = self.get_option("persistent_connect_timeout") + try: + log_connect_args = dict(connect_args) + log_connect_args["passwd"] = "NOT_LOGGING_PARAMETER" + + self.queue_message( + "vvvv", "Creating device parameters: %s" % log_connect_args + ) + timeout = connect_args.pop("timeout") + self.dev = jnpr.junos.device.Device(**connect_args) + self.queue_message("vvvv", "Opening device.") + self.dev.open() + self.queue_message("vvvv", "Device opened.") + + self.dev.timeout = self.get_option("persistent_command_timeout") + self.queue_message( + "vvvv", "Setting default device timeout to %d." % timeout + ) + # Exceptions raised by close() or open() are all sub-classes of + # ConnectError, so this should catch all connection-related exceptions + # raised from PyEZ. + except pyez_exception.ConnectError as ex: + raise AnsibleError("Unable to make a PyEZ connection: %s" % (str(ex))) + + def close(self): + """Close the self.dev PyEZ Device instance.""" + if self.dev is not None: + try: + # Because self.fail_json() calls self.close(), we must set + # self.dev = None BEFORE calling dev.close() in order to avoid + # the infinite recursion which would occur if dev.close() + # raised a ConnectError. + dev = self.dev + self.dev = None + dev.close() + # Exceptions raised by close() are all sub-classes of + # ConnectError or RpcError, so this should catch all + # exceptions raised from PyEZ. + except (pyez_exception.ConnectError, pyez_exception.RpcError) as ex: + # Ignore exceptions from closing. We're about to exit + # anyway and they will just mask the real error that + # happened. + pass + super(Connection, self).close() + + @ensure_connect + def get_capabilities(self): + """Get the capabilities for network api..""" + return json.dumps({"network_api": "pyez"}) + + def get_config( + self, + filter_xml=None, + options=None, + model=None, + namespace=None, + remove_ns=True, + **kwarg + ): + """Get Configuration. + + Args: + filter_xml: A string of XML, or '/'-separated configuration hierarchies, + which specifies a filter used to restrict the portions of the + configuration which are retrieved. + options: Additional options, specified as a dictionary of key/value pairs, used + when retrieving the configuration. + model: the model of the configuration + namespace: namespace to be used. + remove_ns: if namespace is to be removed from the end output. + + Returns: + - The configuration in the requested format as a single + multi-line string. Returned for all formats. + + Fails: + - Invalid database. + - Invalid filter. + - Format not understood by device. + """ + resp = self.dev.rpc.get_config( + filter_xml, options, model, namespace, remove_ns, **kwarg + ) + if options['format'] == 'json': + return resp + else: + return etree.tostring(resp) + + def get_rpc_resp(self, rpc, ignore_warning, format): + """Execute rpc on the device and get response. + + Args: + rpc: the rpc to be executed on the device + ignore_warning: flag to check if warning received by device are to be ignored or not. + format: the format of the response received. + + Returns: + - Response in the requested format as a single multi-line string. + - if format is json then return in json format + + Fails: + - If the RPC produces an exception. + """ + # data comes in JSON format, needs to be converted + rpc_val = xmltodict.unparse(rpc) + rpc_val = rpc_val.encode("utf-8") + parser = etree.XMLParser(ns_clean=True, recover=True, encoding="utf-8") + rpc_etree = etree.fromstring(rpc_val, parser=parser) + resp = self.dev.rpc( + rpc_etree, normalize=bool(format == "xml"), ignore_warning=ignore_warning + ) + if format == "json": + return resp + return etree.tostring(resp) + + def get_facts(self): + """Get device facts.""" + return dict(self.dev.facts) + + def ping_device(self, normalize, **params): + """Ping the device. + + Args: + params: dict of parameters passed directly to the ping RPC. + normalize: flag to check if to normalize the results. + + Returns: + Response in the requested format as a single multi-line string. + + Fails: + - If the ping RPC produces an exception. + """ + resp = self.dev.rpc.ping(normalize=normalize, **params) + rpc_str = etree.tostring(resp) + return rpc_str + + def get_chassis_inventory(self): + """Get chassis inventory details from the device.""" + + resp = self.dev.rpc.get_chassis_inventory() + return etree.tostring(resp) + + def get_checksum_information(self, remote_file): + """Get checksum information of the file""" + resp = self.dev.rpc.get_checksum_information(path=remote_file) + return etree.tostring(resp) + + def get_re_name(self): + """Get re name from the device.""" + return self.dev.re_name + + def set_chassis_cluster_enable(self, cluster_id, node_id): + """send set chassis cluster enable rpc to the device.""" + return self.dev.rpc.set_chassis_cluster_enable( + cluster_id=cluster_id, node=node_id, reboot=True, normalize=True + ) + + def set_chassis_cluster_disable(self): + """send set chassis cluster disable rpc to the device.""" + return self.dev.rpc.set_chassis_cluster_disable(reboot=True, normalize=True) + + def invoke_jsnapy(self, data, action): + """invoke jsnapy for persistent connection.""" + try: + self.queue_message("vvvv", "Creating jnpr.jsnapy.SnapAdmin instance.") + jsa = jnpr.jsnapy.SnapAdmin() + self.queue_message("vvvv", "Executing %s action." % action) + if action == "check": + responses = jsa.check( + data=data, dev=self.dev, pre_file="PRE", post_file="POST" + ) + elif action == "snapcheck": + responses = jsa.snapcheck(data=data, dev=self.dev, pre_file="PRE") + elif action == "snap_pre": + responses = jsa.snap(data=data, dev=self.dev, file_name="PRE") + elif action == "snap_post": + responses = jsa.snap(data=data, dev=self.dev, file_name="POST") + else: + raise AnsibleError("Unexpected action: %s." % (action)) + self.queue_message("vvvv", "The %s action executed successfully" % action) + except (pyez_exception.RpcError, pyez_exception.ConnectError) as ex: + raise AnsibleError("Error communicating with the device: %s" % str(ex)) + + results = {} + if isinstance(responses, list) and len(responses) == 1: + if action in ("snapcheck", "check"): + for response in responses: + results["device"] = response.device + results["router"] = response.device + results["final_result"] = response.result + results["total_passed"] = response.no_passed + results["total_failed"] = response.no_failed + results["test_results"] = response.test_results + total_tests = int(response.no_passed) + int(response.no_failed) + results["total_tests"] = total_tests + pass_percentage = 0 + if total_tests > 0: + pass_percentage = (int(response.no_passed) * 100) // total_tests + results["passPercentage"] = pass_percentage + results["pass_percentage"] = pass_percentage + if results["final_result"] == "Failed": + results["msg"] = "Test Failed: Passed %s, Failed %s" % ( + results["total_passed"], + results["total_failed"], + ) + else: + results["msg"] = "Test Passed: Passed %s, Failed %s" % ( + results["total_passed"], + results["total_failed"], + ) + elif action in ("snap_pre", "snap_post"): + results["msg"] = "The %s action successfully executed." % (action) + else: + raise AnsibleError( + "Unexpected JSNAPy responses. Type: %s." + "Responses: %s" % (type(responses), str(responses)) + ) + return results + + def open_configuration(self, mode, ignore_warn=None, ephemeral_instance=None): + """Open candidate configuration database in exclusive or private mode. + + Failures: + - When there's a problem with the PyEZ connection. + - When there's a RPC problem including an already locked + config or an already opened private config. + """ + if self.config is None: + if mode not in CONFIG_MODE_CHOICES: + raise AnsibleError("Invalid configuration mode: %s" % mode) + if mode != "ephemeral" and ephemeral_instance is not None: + self.fail_json( + msg="Ephemeral instance is specified while the mode " + "is not ephemeral. Specify the mode as ephemeral " + "or do not specify the instance." + ) + if self.dev is None: + self.open() + config = jnpr.junos.utils.config.Config(self.dev, mode=mode) + try: + if config.mode == "exclusive": + config.lock() + elif config.mode == "private": + self.dev.rpc.open_configuration( + private=True, ignore_warning=ignore_warn + ) + elif config.mode == "dynamic": + self.dev.rpc.open_configuration( + dynamic=True, ignore_warning=ignore_warn + ) + elif config.mode == "batch": + self.dev.rpc.open_configuration( + batch=True, ignore_warning=ignore_warn + ) + elif config.mode == "ephemeral": + if ephemeral_instance is None: + self.dev.rpc.open_configuration( + ephemeral=True, ignore_warning=ignore_warn + ) + else: + self.dev.rpc.open_configuration( + ephemeral_instance=ephemeral_instance, + ignore_warning=ignore_warn, + ) + except (pyez_exception.ConnectError, pyez_exception.RpcError) as ex: + raise AnsibleError( + "Unable to open the configuration in %s " + "mode: %s" % (config.mode, str(ex)) + ) + self.config = config + self.queue_message("log", "Configuration opened in %s mode." % config.mode) + + def close_configuration(self): + """Close candidate configuration database. + + Failures: + - When there's a problem with the PyEZ connection. + - When there's a RPC problem closing the config. + """ + if self.config is not None: + config = self.config + self.config = None + try: + if config.mode == "exclusive": + config.unlock() + elif config.mode in ["batch", "dynamic", "private", "ephemeral"]: + self.dev.rpc.close_configuration() + self.queue_message("log", "Configuration closed.") + except (pyez_exception.ConnectError, pyez_exception.RpcError) as ex: + raise AnsibleError("Unable to close the configuration: %s" % (str(ex))) + + def rollback_configuration(self, id): + """Rollback the device configuration to the specified id. + + Rolls back the configuration to the specified id. Assumes the + configuration is already opened. Does NOT commit the configuration. + + Args: + id: The id to which the configuration should be rolled back. Either + an integer rollback value or the string 'rescue' to roll back + to the previously saved rescue configuration. + + Failures: + - Unable to rollback the configuration due to an RpcError or ConnectError + """ + if self.dev is None or self.config is None: + raise AnsibleError("The device or configuration is not open.") + + if id == "rescue": + self.queue_message("log", "Rolling back to the rescue configuration.") + try: + self.config.rescue(action="reload") + self.queue_message("log", "Rescue configuration loaded.") + except ( + self.pyez_exception.RpcError, + self.pyez_exception.ConnectError, + ) as ex: + raise AnsibleError( + "Unable to load the rescue configuraton: " "%s" % (str(ex)) + ) + elif id >= 0 and id <= 49: + self.queue_message("log", "Loading rollback {} configuration.".format(id)) + try: + self.config.rollback(rb_id=id) + self.queue_message( + "log", "Rollback {} configuration loaded.".format(id) + ) + except ( + self.pyez_exception.RpcError, + self.pyez_exception.ConnectError, + ) as ex: + raise AnsibleError( + "Unable to load the rollback %d " "configuraton: %s" % (id, str(ex)) + ) + else: + raise AnsibleError("Unrecognized rollback configuraton value: %s" % (id)) + + def check_configuration(self): + """Check the candidate configuration. Assumes the configuration is already opened. + Performs the equivalent of a "commit check", but does NOT commit the + configuration. + + Failures: + - An error returned from checking the configuration. + """ + try: + self.config.commit_check() + self.queue_message("log", "Configuration checked.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError("Failure checking the configuraton: %s" % (str(ex))) + + def diff_configuration(self, ignore_warning=False): + """Diff the candidate and committed configurations. + + Returns: + A string with the configuration differences in text "diff" format. + + Failures: + - An error returned from diffing the configuration. + """ + try: + diff = self.config.diff(rb_id=0, ignore_warning=ignore_warning) + self.queue_message("log", "Configuration diff completed.") + return diff + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError("Failure diffing the configuraton: %s" % (str(ex))) + + def load_configuration(self, config, load_args): + """Load the candidate configuration from the specified src file using the + specified action. + + Failures: + - An error returned from loading the configuration. + """ + try: + if config is not None: + self.config.load(config, **load_args) + else: + self.queue_message("log", "Load args %s." % str(load_args)) + self.config.load(**load_args) + self.queue_message("log", "Configuration loaded.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError("Failure loading the configuraton: %s" % (str(ex))) + + def commit_configuration( + self, + ignore_warning=None, + comment=None, + confirmed=None, + timeout=30, + full=False, + force_sync=False, + sync=False, + ): + """Commit the candidate configuration. + Assumes the configuration is already opened. + + Args: + ignore_warning - Which warnings to ignore. + comment - The commit comment + confirmed - Number of minutes for commit confirmed. + + Failures: + - An error returned from committing the configuration. + """ + if self.dev.timeout: + timeout = self.dev.timeout + try: + self.config.commit( + ignore_warning=ignore_warning, + comment=comment, + confirm=confirmed, + timeout=timeout, + full=full, + force_sync=force_sync, + sync=sync, + ) + self.queue_message("log", "Configuration committed.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError("Failure committing the configuraton: %s" % (str(ex))) + + def system_api( + self, action, in_min, at, all_re, vmhost, other_re, media, member_id=None + ): + """Triggers the system calls like reboot, shutdown, halt and zeroize to device.""" + msg = None + if action != "zeroize": + if at == "now" or (in_min == 0 and at is None): + if self.dev.timeout > 5: + self.queue_message( + "log", "Decreasing device RPC timeout to 5 seconds." + ) + self.dev.timeout = 5 + + try: + self.sw = jnpr.junos.utils.sw.SW(self.dev) + if action == "reboot": + if member_id is not None: + for m_id in member_id: + got = self.sw.reboot( + in_min, at, all_re, None, vmhost, other_re, member_id=m_id + ) + else: + got = self.sw.reboot(in_min, at, all_re, None, vmhost, other_re) + elif action == "shutdown": + got = self.sw.poweroff(in_min, at, None, all_re, other_re, vmhost) + elif action == "halt": + got = self.sw.halt(in_min, at, all_re, other_re) + elif action == "zeroize": + got = self.sw.zeroize(all_re, media) + else: + raise AnsibleError("Relevant action not found") + + self.queue_message("log", "RPC executed") + if got is None: + msg = "Did not find expected RPC response." + else: + msg = "%s successfully initiated. Response got %s" % (action, got) + except self.pyez_exception.RpcTimeoutError as ex: + try: + self.close(raise_exceptions=True) + # This means the device wasn't already disconnected. + raise AnsibleError( + "%s failed. %s may not have been " "initiated." % (action, action) + ) + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError): + # This is expected. The device has already disconnected. + msg = "%s succeeded." % (action) + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError("%s failed. Error: %s" % (action, str(ex))) + return msg + + def software_api(self, install_params): + """Installs package to device.""" + try: + self.sw = jnpr.junos.utils.sw.SW(self.dev) + ok, msg_ret = self.sw.install(**install_params) + if ok is not True: + raise AnsibleError("Unable to install the software %s" % msg_ret) + msg = "Package %s successfully installed. Response from device is: %s" % ( + install_params.get("package") or install_params.get("pkg_set"), + msg_ret, + ) + self.queue_message("log", str(msg)) + return msg + except (self.pyez_exception.ConnectError, self.pyez_exception.RpcError) as ex: + raise AnsibleError("Installation failed. Error: %s" % str(ex)) + + def reboot_api(self, all_re, vmhost, member_id=None): + """reboots the device.""" + msg = None + try: + restore_timeout = self.dev.timeout + if self.dev.timeout > 5: + self.dev.timeout = 5 + try: + if member_id is not None: + for m_id in member_id: + got = self.sw.reboot( + 0, None, all_re, None, vmhost, member_id=m_id + ) + else: + got = self.sw.reboot(0, None, all_re, None, vmhost) + self.dev.timeout = restore_timeout + except Exception: # pylint: disable=broad-except + self.dev.timeout = restore_timeout + raise + self.queue_message("log", "Reboot RPC executed.") + + if got is not None: + msg += " Reboot successfully initiated. " "Reboot message: %s" % got + else: + raise AnsibleError( + " Did not find expected response from reboot RPC. " + ) + except self.pyez_exception.RpcTimeoutError as ex: + try: + self.close(raise_exceptions=True) + # This means the device wasn't already disconnected. + raise AnsibleError(" Reboot failed. It may not have been initiated.") + except ( + self.pyez_exception.RpcError, + self.pyez_exception.RpcTimeoutError, + self.pyez_exception.ConnectError, + ): + # This is expected. The device has already disconnected. + msg += " Reboot succeeded." + except self.ncclient_exception.TimeoutExpiredError: + # This is not really expected. Still consider reboot success as + # Looks like rpc was consumed but no response as its rebooting. + msg += " Reboot succeeded. Ignoring close error." + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError(" Reboot failed. Error: %s" % (str(ex))) + else: + try: + self.close() + except self.ncclient_exception.TimeoutExpiredError: + self.queue_message("log", "Ignoring TimeoutError for close call") + + self.queue_message("log", "Reboot RPC successfully initiated.") + + return msg + + def scp_file_copy_put(self, local_file, remote_file): + """Copy the file using scp. + + Args: + local_file local file path/name. + remote_file: remote file path/name. + """ + try: + with SCP(self.dev, progress=True) as scp: + scp.put(local_file, remote_file) + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError( + "Failure checking the configuraton: {0}".format(str(ex)) + ) from ex + + def scp_file_copy_get(self, remote_file, local_file): + """Copy the file using scp. + + Args: + local_file local file path/name. + remote_file: remote file path/name. + """ + try: + with SCP(self.dev, progress=True) as scp: + scp.get(remote_file, local_file) + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + raise AnsibleError( + "Failure checking the configuraton: {0}".format(str(ex)) + ) from ex diff --git a/library/__init__.py b/ansible_collections/juniper/device/plugins/module_utils/__init__.py similarity index 100% rename from library/__init__.py rename to ansible_collections/juniper/device/plugins/module_utils/__init__.py diff --git a/ansible_collections/juniper/device/plugins/module_utils/configuration.py b/ansible_collections/juniper/device/plugins/module_utils/configuration.py new file mode 100644 index 00000000..5bf98e87 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/module_utils/configuration.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +try: + from looseversion import LooseVersion + + HAS_LOOSE_VERSION = True +except ImportError: + HAS_LOOSE_VERSION = False +# Non-standard library imports and checks +try: + from jnpr.junos.version import VERSION + + HAS_PYEZ_VERSION = VERSION +except ImportError: + HAS_PYEZ_VERSION = None + +try: + import jnpr.junos.factory.factory_loader + import jnpr.junos.factory.table + import jnpr.junos.op + + HAS_PYEZ_OP_TABLE = True +except ImportError: + HAS_PYEZ_OP_TABLE = False + +try: + import ncclient + + HAS_NCCLIENT_VERSION = ncclient.__version__ +except ImportError: + HAS_NCCLIENT_VERSION = None + +try: + import jnpr.jsnapy + + HAS_JSNAPY_VERSION = jnpr.jsnapy.__version__ +except ImportError: + HAS_JSNAPY_VERSION = None +# Most likely JSNAPy 1.2.0 with https://github.com/Juniper/jsnapy/issues/263 +except TypeError: + HAS_JSNAPY_VERSION = "possibly 1.2.0" + +try: + from lxml import etree + + HAS_LXML_ETREE_VERSION = ".".join(map(str, etree.LXML_VERSION)) +except ImportError: + HAS_LXML_ETREE_VERSION = None + +try: + import jxmlease + + HAS_JXMLEASE_VERSION = jxmlease.__version__ +except ImportError: + HAS_JXMLEASE_VERSION = None + +try: + import yaml + + HAS_YAML_VERSION = yaml.__version__ +except ImportError: + HAS_YAML_VERSION = None + +# Constants +# Minimum PyEZ version required by shared code. +MIN_PYEZ_VERSION = "2.5.2" +# Installation URL for PyEZ. +PYEZ_INSTALLATION_URL = "https://github.com/Juniper/py-junos-eznc#installation" +# Minimum lxml version required by shared code. +MIN_LXML_ETREE_VERSION = "3.2.4" +# Installation URL for LXML. +LXML_ETREE_INSTALLATION_URL = "http://lxml.de/installation.html" +# Minimum JSNAPy version required by shared code. +MIN_JSNAPY_VERSION = "1.3.4" +# Installation URL for JSNAPy. +JSNAPY_INSTALLATION_URL = "https://github.com/Juniper/jsnapy#installation" +# Minimum jxmlease version required by shared code. +MIN_JXMLEASE_VERSION = "1.0.1" +# Installation URL for jxmlease. +JXMLEASE_INSTALLATION_URL = "http://jxmlease.readthedocs.io/en/stable/install.html" +# Minimum yaml version required by shared code. +MIN_YAML_VERSION = "3.08" +YAML_INSTALLATION_URL = "http://pyyaml.org/wiki/PyYAMLDocumentation" + + +def _check_library( + library_name, + installed_version, + installation_url, + minimum=None, + library_nickname=None, +): + """Check if library_name is installed and version is >= minimum. + + Args: + library_name: The name of the library to check. + installed_version: The currently installed version, or None if it's + not installed. + installation_url: The URL with instructions on installing + library_name + minimum: The minimum version required. + Default = None which means no version check. + library_nickname: The library name with any nickname. + Default = library_name. + Failures: + - library_name not installed (unable to import). + - library_name installed_version < minimum. + """ + if library_nickname is None: + library_nickname = library_name + if installed_version is None: + if minimum is not None: + return ( + "%s >= %s is required for this module. " + "However, %s does not appear to be " + "currently installed. See %s for " + "details on installing %s." + % ( + library_nickname, + minimum, + library_name, + installation_url, + library_name, + ) + ) + else: + return ( + "%s is required for this module. However, " + "%s does not appear to be currently " + "installed. See %s for details on " + "installing %s." + % (library_nickname, library_name, installation_url, library_name) + ) + elif installed_version is not None and minimum is not None: + if not LooseVersion(installed_version) >= LooseVersion(minimum): + return ( + "%s >= %s is required for this module. Version %s of " + "%s is currently installed. See %s for details on " + "upgrading %s." + % ( + library_nickname, + minimum, + installed_version, + library_name, + installation_url, + library_name, + ) + ) + return "success" + + +def check_pyez(minimum=None): + """Check PyEZ is available and version is >= minimum. + + Args: + minimum: The minimum PyEZ version required. + Default = None which means no version check. + check_device: Indicates whether to check for PyEZ Device object. + check_exception: Indicates whether to check for PyEZ exceptions. + + Failures: + - PyEZ not installed (unable to import). + - PyEZ version < minimum. + """ + if HAS_NCCLIENT_VERSION is None: + return "ncclient module could not " "be imported." + return _check_library( + "junos-eznc", + HAS_PYEZ_VERSION, + PYEZ_INSTALLATION_URL, + minimum=minimum, + library_nickname="junos-eznc (aka PyEZ)", + ) + + +def check_jsnapy(minimum=None): + """Check jsnapy is available and version is >= minimum. + + Args: + minimum: The minimum jsnapy version required. + Default = None which means no version check. + + Failures: + - jsnapy not installed. + - jsnapy version < minimum. + """ + return _check_library( + "jsnapy", HAS_JSNAPY_VERSION, JSNAPY_INSTALLATION_URL, minimum=minimum + ) + + +def check_jxmlease(minimum=None): + """Check jxmlease is available and version is >= minimum. + + Args: + minimum: The minimum jxmlease version required. + Default = None which means no version check. + + Failures: + - jxmlease not installed. + - jxmlease version < minimum. + """ + return _check_library( + "jxmlease", HAS_JXMLEASE_VERSION, JXMLEASE_INSTALLATION_URL, minimum=minimum + ) + + +def check_lxml_etree(minimum=None): + """Check lxml etree is available and version is >= minimum. + + Args: + minimum: The minimum lxml version required. + Default = None which means no version check. + + Failures: + - lxml not installed. + - lxml version < minimum. + """ + return _check_library( + "lxml Etree", + HAS_LXML_ETREE_VERSION, + LXML_ETREE_INSTALLATION_URL, + minimum=minimum, + ) + + +def check_yaml(minimum=None): + """Check yaml is available and version is >= minimum. + + Args: + minimum: The minimum PyYAML version required. + Default = None which means no version check. + + Failures: + - yaml not installed. + - yaml version < minimum. + """ + return _check_library( + "yaml", HAS_YAML_VERSION, YAML_INSTALLATION_URL, minimum=minimum + ) + + +def check_sw_compatibility( + min_pyez_version, + min_lxml_etree_version, + min_jsnapy_version=None, + min_jxmlease_version=None, + min_yaml_version=None, +): + """Check yaml is available and version is >= minimum. + + Args: + minimum: The minimum PyYAML version required. + Default = None which means no version check. + Returns: + string as success or the error + """ + ret_output = check_pyez(min_pyez_version) + if ret_output != "success": + return ret_output + + ret_output = check_lxml_etree(min_lxml_etree_version) + if ret_output != "success": + return ret_output + + if min_jsnapy_version is not None: + ret_output = check_jsnapy(min_jsnapy_version) + if ret_output != "success": + return ret_output + + if min_jxmlease_version is not None: + ret_output = check_jxmlease(min_jxmlease_version) + if ret_output != "success": + return ret_output + + if min_yaml_version is not None: + ret_output = check_yaml(min_yaml_version) + + return ret_output diff --git a/ansible_collections/juniper/device/plugins/module_utils/juniper_junos_common.py b/ansible_collections/juniper/device/plugins/module_utils/juniper_junos_common.py new file mode 100644 index 00000000..ca3282bb --- /dev/null +++ b/ansible_collections/juniper/device/plugins/module_utils/juniper_junos_common.py @@ -0,0 +1,2052 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import hashlib +import json +import logging +import os + +# Standard library imports +from argparse import ArgumentParser + +try: + import jnpr + + HAS_PYEZ_JNPR = True +except ImportError: + HAS_PYEZ_JNPR = False +try: + import xmltodict + + HAS_XMLTODICT = True +except ImportError: + HAS_XMLTODICT = False +from ansible.module_utils._text import to_bytes +from ansible.module_utils.basic import AnsibleModule, boolean +from ansible.module_utils.common.validation import check_type_dict +from ansible.module_utils.six import string_types + +# Ansible imports +from ansible.module_utils.connection import Connection +try: + from jnpr.junos.utils.sw import SW + + HAS_PYEZ_SW = True +except ImportError: + HAS_PYEZ_SW = False +try: + from jnpr.junos.utils.scp import SCP + + HAS_PYEZ_SW = True +except ImportError: + HAS_PYEZ_SW = False +try: + from jnpr.junos import exception as pyez_exception + + HAS_PYEZ_EXCEPTION = True +except ImportError: + HAS_PYEZ_EXCEPTION = False +try: + from ncclient.operations.errors import TimeoutExpiredError + + HAS_NCCLIENT = True +except ImportError: + HAS_NCCLIENT = False +try: + import ncclient.operations.errors as ncclient_exception + + HAS_NCCLIENT_EXCEPTIONS = True +except ImportError: + HAS_NCCLIENT_EXCEPTIONS = False + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg + + +class ModuleDocFragment(object): + """Documentation fragment for connection-related parameters. + + All modules share a common set of connection parameters + which are documented in this class. + + Attributes: + CONNECTION_DOCUMENTATION: The documentation string defining the + connection-related parameters for the + modules. + LOGGING_DOCUMENTATION: The documentation string defining the + logging-related parameters for the + modules. + """ + + # The connection-specific options. Defined here so it can be re-used as + # suboptions in provider. + _CONNECT_DOCUMENTATION = """ + attempts: + description: + - The number of times to try connecting and logging in to the Junos + device. This option is only applicable when using C(mode = 'telnet') + or C(mode = 'serial'). Mutually exclusive with the I(console) + option. + required: false + default: 10 + type: int + baud: + description: + - The serial baud rate, in bits per second, used to connect to the + Junos device. This option is only applicable when using + C(mode = 'serial'). Mutually exclusive with the I(console) option. + required: false + default: 9600 + type: int + console: + description: + - An alternate method of specifying a NETCONF over serial console + connection to the Junos device using Telnet to a console server. + The value of this option must be a string in the format + C(--telnet ,). + This option is deprecated. It is present only for backwards + compatibility. The string value of this option is exactly equivalent + to specifying I(host) with a value of C(), + I(mode) with a value of C(telnet), and I(port) with a value of + C(). Mutually exclusive with the I(mode), + I(port), I(baud), and I(attempts) options. + required: false + default: none + type: str + host: + description: + - The hostname or IP address of the Junos device to which the + connection should be established. This is normally the Junos device + itself, but is the hostname or IP address of a console server when + connecting to the console of the device by setting the I(mode) + option to the value C(telnet). This option is required, but does not + have to be specified explicitly by the user because it defaults to + C({{ inventory_hostname }}). + required: true + default: C({{ inventory_hostname }}) + type: str + aliases: + - hostname + - ip + mode: + description: + - The PyEZ mode used to establish a NETCONF connection to the Junos + device. A value of C(none) uses the default NETCONF over SSH mode. + Depending on the values of the I(host) and I(port) options, a value + of C(telnet) results in either a direct NETCONF over Telnet + connection to the Junos device, or a NETCONF over serial console + connection to the Junos device using Telnet to a console server. + A value of C(serial) results in a NETCONF over serial console + connection to the Junos device. Mutually exclusive with the + I(console) option. + required: false + default: none + type: str + choices: + - none + - telnet + - serial + passwd: + description: + - The password, or ssh key's passphrase, used to authenticate with the + Junos device. If this option is not specified, authentication is + attempted using an empty password, or ssh key passphrase. + required: false + default: The first defined value from the following list + 1) The C(ANSIBLE_NET_PASSWORD) environment variable. + (used by Ansible Tower) + 2) The value specified using the C(-k) or C(--ask-pass) + command line arguments to the C(ansible) or + C(ansible-playbook) command. + 3) none (An empty password/passphrase) + type: str + aliases: + - password + port: + description: + - The TCP port number or serial device port used to establish the + connection. Mutually exclusive with the I(console) option. + required: false + default: C(830) if C(mode = none), C(23) if C(mode = 'telnet'), + C('/dev/ttyUSB0') if (mode = 'serial') + type: int or str + ssh_private_key_file: + description: + - The path to the SSH private key file used to authenticate with the + Junos device. If this option is not specified, and no default value + is found using the algorithm below, then the SSH private key file + specified in the user's SSH configuration, or the + operating-system-specific default is used. + - This must be in the RSA PEM format, and not the newer OPENSSH + format. To check if the private key is in the correct format, issue + the command `head -n1 ~/.ssh/some_private_key` and ensure that + it's RSA and not OPENSSH. To create a key in the RSA PEM format, + issue the command `ssh-keygen -m PEM -t rsa -b 4096`. To convert + an OPENSSH key to an RSA key, issue the command `ssh-keygen -p -m + PEM -f ~/.ssh/some_private_key` + required: false + default: The first defined value from the following list + 1) The C(ANSIBLE_NET_SSH_KEYFILE) environment variable. + (used by Ansible Tower) + 2) The value specified using the C(--private-key) or + C(--key-file) command line arguments to the C(ansible) or + C(ansible-playbook) command. + 3) none (the file specified in the user's SSH configuration, + or the operating-system-specific default) + type: path + aliases: + - ssh_keyfile + ssh_config: + description: + - The path to the SSH client configuration file. If this option is not + specified, then the PyEZ Device instance by default queries file + ~/.ssh/config. + required: false + type: path + timeout: + description: + - The maximum number of seconds to wait for RPC responses from the + Junos device. This option does NOT control the initial connection + timeout value. + required: false + default: 30 + type: int + user: + description: + - The username used to authenticate with the Junos device. This option + is required, but does not have to be specified explicitly by the + user due to the algorithm for determining the default value. + required: true + default: The first defined value from the following list + 1) The C(ANSIBLE_NET_USERNAME) environment variable. + (used by Ansible Tower) + 2) The C(remote_user) as defined by Ansible. Ansible sets this + value via several methods including + a) C(-u) or C(--user) command line arguments to the + C(ansible) or C(ansible-playbook) command. + b) C(ANSIBLE_REMOTE_USER) environment variable. + c) C(remote_user) configuration setting. + See the Ansible documentation for the precedence used to set + the C(remote_user) value. + 3) The C(USER) environment variable. + type: str + aliases: + - username + cs_user: + description: + - The username used to authenticate with the console server over SSH. + This option is only required if you want to connect to a device over console + using SSH as transport. Mutually exclusive with the I(console) option. + required: false + type: str + aliases: + - console_username + cs_passwd: + description: + - The password used to authenticate with the console server over SSH. + This option is only required if you want to connect to a device over console + using SSH as transport. Mutually exclusive with the I(console) option. + required: false + type: str + aliases: + - console_password + huge_tree: + description: + - Parse XML with very deep trees and long text content. + required: false + type: bool + default: false +""" + + LOGGING_DOCUMENTATION = """ + logging_options: + logdir: + description: + - The path to a directory, on the Ansible control machine, where + debugging information for the particular task is logged. + - If this option is specified, debugging information is logged to a + file named C({{ inventory_hostname }}.log) in the directory + specified by the I(logdir) option. + - The log file must be writeable. If the file already exists, it is + appended. It is the users responsibility to delete/rotate log files. + - The level of information logged in this file is controlled by + Ansible's verbosity, debug options and level option in task + - 1) By default, messages at level C(WARNING) or higher are logged. + - 2) If the C(-v) or C(--verbose) command-line options to the + C(ansible-playbook) command are specified, messages at level + C(INFO) or higher are logged. + - 3) If the C(-vv) (or more verbose) command-line option to the + C(ansible-playbook) command is specified, or the C(ANSIBLE_DEBUG) + environment variable is set, then messages at level C(DEBUG) or + higher are logged. + - 4) If C(level) is mentioned then messages at level C(level) or more are + logged. + - The I(logfile) and I(logdir) options are mutually exclusive. The + I(logdir) option is recommended for all new playbooks. + required: false + default: none + type: path + aliases: + - log_dir + logfile: + description: + - The path to a file, on the Ansible control machine, where debugging + information for the particular task is logged. + - The log file must be writeable. If the file already exists, it is + appended. It is the users responsibility to delete/rotate log files. + - The level of information logged in this file is controlled by + Ansible's verbosity, debug options and level option in task + - 1) By default, messages at level C(WARNING) or higher are logged. + - 2) If the C(-v) or C(--verbose) command-line options to the + C(ansible-playbook) command are specified, messages at level + C(INFO) or higher are logged. + - 3) If the C(-vv) (or more verbose) command-line option to the + C(ansible-playbook) command is specified, or the C(ANSIBLE_DEBUG) + environment variable is set, then messages at level C(DEBUG) or + higher are logged. + - 4) If C(level) is mentioned then messages at level C(level) or more are + logged. + - When tasks are executed against more than one target host, + one process is forked for each target host. (Up to the maximum + specified by the forks configuration. See + U(forks|http://docs.ansible.com/ansible/latest/intro_configuration.html#forks) + for details.) This means that the value of this option must be + unique per target host. This is usually accomplished by including + C({{ inventory_hostname }}) in the I(logfile) value. It is the + user's responsibility to ensure this value is unique per target + host. + - For this reason, this option is deprecated. It is maintained for + backwards compatibility. Use the I(logdir) option in new playbooks. + The I(logfile) and I(logdir) options are mutually exclusive. + required: false + default: none + type: path + aliases: + - log_file + level: + description: + - The level of information to be logged can be modified using this option + - 1) By default, messages at level C(WARNING) or higher are logged. + - 2) If the C(-v) or C(--verbose) command-line options to the + C(ansible-playbook) command are specified, messages at level + C(INFO) or higher are logged. + - 3) If the C(-vv) (or more verbose) command-line option to the + C(ansible-playbook) command is specified, or the C(ANSIBLE_DEBUG) + environment variable is set, then messages at level C(DEBUG) or + higher are logged. + - 4) If C(level) is mentioned then messages at level C(level) or more are + logged. + required: false + default: WARNING + type: str + choices: + - INFO + - DEBUG + + +""" + + # _SUB_CONNECT_DOCUMENTATION is just _CONNECT_DOCUMENTATION with each + # line indented. + _SUB_CONNECT_DOCUMENTATION = "" + for line in _CONNECT_DOCUMENTATION.splitlines(True): + _SUB_CONNECT_DOCUMENTATION += " " + line + + # Build actual DOCUMENTATION string by putting the pieces together. + CONNECTION_DOCUMENTATION = ( + """ + connection_options:""" + + _CONNECT_DOCUMENTATION + + """ + requirements: + - U(junos-eznc|https://github.com/Juniper/py-junos-eznc) >= """ + + cfg.MIN_PYEZ_VERSION + + """ + - Python >= 3.5 + notes: + - The NETCONF system service must be enabled on the target Junos device. +""" + ) + + +# The common argument specification for connecting to Junos devices. +connection_spec = { + "host": dict( + type="str", + # Required at top-level. + required=False, + aliases=["hostname", "ip"], + # See documentation for real default behavior. + # Default behavior coded in JuniperJunosActionModule.run() + default=None, + ), + "user": dict( + type="str", + # Required at top-level. + required=False, + aliases=["username"], + # See documentation for real default behavior. + # Default behavior coded in JuniperJunosActionModule.run() + default=None, + ), + "passwd": dict( + type="str", + required=False, + aliases=["password"], + # See documentation for real default behavior. + # Default behavior coded in JuniperJunosActionModule.run() + default=None, + no_log=True, + ), + "cs_user": dict( + type="str", aliases=["console_username"], required=False, default=None + ), + "cs_passwd": dict( + type="str", + aliases=["console_password"], + required=False, + default=None, + no_log=True, + ), + "ssh_private_key_file": dict( + type="path", + required=False, + aliases=["ssh_keyfile"], + # See documentation for real default behavior. + # Default behavior coded in + # JuniperJunosActionModule.run() + default=None, + ), + "ssh_config": dict(type="path", required=False, default=None), + "mode": dict(choices=[None, "telnet", "serial"], default=None), + "console": dict(type="str", required=False, default=None), + "port": dict( + type="str", + required=False, + # See documentation for real default behavior. + # Default behavior coded in JuniperJunosModule.__init__() + default=None, + ), + "baud": dict( + type="int", + required=False, + # See documentation for real default behavior. + # Default behavior coded in JuniperJunosModule.__init__() + default=None, + ), + "attempts": dict( + type="int", + required=False, + # See documentation for real default behavior. + # Default behavior coded in JuniperJunosModule.__init__() + default=None, + ), + "timeout": dict(type="int", required=False, default=30), + "huge_tree": dict(type="bool", required=False, default=False), +} + +# Connection arguments which are mutually exclusive. +connection_spec_mutually_exclusive = [ + ["mode", "console"], + ["port", "console"], + ["baud", "console"], + ["attempts", "console"], + ["cs_user", "console"], + ["cs_passwd", "console"], +] + +# Specify the logging spec. +logging_spec = { + "logfile": dict(type="path", required=False, default=None), + "logdir": dict(type="path", required=False, default=None), + "level": dict(choices=[None, "INFO", "DEBUG"], required=False, default=None), +} + +# The logdir and logfile options are mutually exclusive. +logging_spec_mutually_exclusive = ["logfile", "logdir"] + +# Other logging names which should be logged to the logfile +additional_logger_names = ["ncclient", "paramiko"] + +# top_spec is connection_spec + logging_spec +top_spec = connection_spec + +top_spec.update(logging_spec) +top_spec_mutually_exclusive = connection_spec_mutually_exclusive +top_spec_mutually_exclusive += logging_spec_mutually_exclusive + +# "Hidden" arguments which are passed between the action plugin and the +# Junos module, but which should not be visible to users. +internal_spec = { + "_module_utils_path": dict(type="path", default=None), + "_module_name": dict(type="str", default=None), + "_inventory_hostname": dict(type="str", default=None), + "_connection": dict(type="str", default=None), +} + +# Known RPC output formats +RPC_OUTPUT_FORMAT_CHOICES = ["text", "xml", "json"] + +# Known configuration formats +CONFIG_FORMAT_CHOICES = ["xml", "set", "text", "json"] +# Known configuration databases +CONFIG_DATABASE_CHOICES = ["candidate", "committed"] +# Known configuration actions +CONFIG_ACTION_CHOICES = [ + "set", + "merge", + "update", + "replace", + "override", + "overwrite", + "patch", +] +# Supported configuration modes +CONFIG_MODE_CHOICES = ["exclusive", "private", "dynamic", "batch", "ephemeral"] +# Supported configuration models +CONFIG_MODEL_CHOICES = ["openconfig", "custom", "ietf", "True"] + + +class JuniperJunosModule(AnsibleModule): + """A subclass of AnsibleModule used by all modules. + + All modules share common behavior which is implemented in + this class. + + Attributes: + dev: An instance of a PyEZ Device() object. + + Public Methods: + exit_json: Close self.dev and call parent's exit_json(). + fail_json: Close self.dev and call parent's fail_json(). + check_pyez: Verify the PyEZ library is present and functional. + check_jsnapy: Verify the JSNAPy library is present and functional. + check_jxmlease: Verify the Jxmlease library is present and functional. + check_lxml_etree: Verify the lxml Etree library is present and + functional. + check_yaml: Verify the YAML library is present and functional. + parse_arg_to_list_of_dicts: Parses string_val into a list of dicts. + parse_ignore_warning_option: Parses the ignore_warning option. + parse_rollback_option: Parses the rollback option. + open: Open self.dev. + close: Close self.dev. + add_sw: Add an instance of jnp.junos.utils.sw.SW() to self. + open_configuration: Open cand. conf. db in exclusive/private/dynamic/batch/ephemeral mode. + close_configuration: Close candidate configuration database. + get_configuration: Return the device config. in the specified format. + rollback_configuration: Rollback device config. to the specified id. + check_configuration: Check the candidate configuration. + diff_configuration: Diff the candidate and committed configurations. + load_configuration: Load the candidate configuration. + commit_configuration: Commit the candidate configuration. + ping: Execute a ping command from a Junos device. + save_text_output: Save text output into a file. + """ + + # Method overrides + def __init__( + self, + argument_spec=None, + mutually_exclusive=None, + min_pyez_version=cfg.MIN_PYEZ_VERSION, + min_lxml_etree_version=cfg.MIN_LXML_ETREE_VERSION, + min_jsnapy_version=None, + min_jxmlease_version=None, + min_yaml_version=None, + **kwargs + ): + """Initialize a new JuniperJunosModule instance. + + Combines module-specific parameters with the common parameters shared + by all modules. Performs additional checks on options. + Checks the minimum PyEZ version. Creates and opens the PyEZ Device instance. + + Args: + agument_spec: Module-specific argument_spec added to top_spec. + mutually_exclusive: Module-specific mutually exclusive added to + top_spec_mutually_exclusive. + min_pyez_version: The minimum PyEZ version required by the module. + Since all modules require PyEZ this defaults to + MIN_PYEZ_VERSION. + min_lxml_etree_version: The minimum lxml Etree version required by + the module. Since most modules require + lxml Etree this defaults to + MIN_LXML_ETREE_VERSION. + min_jsnapy_version: The minimum JSNAPy version required by the + module. If this is None, the default, it + means the module does not explicitly require + jsnapy. + min_jxmlease_version: The minimum Jxmlease version required by the + module. If this is None, the default, it + means the module does not explicitly require + jxmlease. + min_yanml_version: The minimum YAML version required by the + module. If this is None, the default, it + means the module does not explicitly require + yaml. + **kwargs: All additional keyword arguments are passed to + AnsibleModule.__init__(). + + Returns: + A JuniperJunosModule instance object. + """ + + if mutually_exclusive is None: + mutually_exclusive = [] + if argument_spec is None: + argument_spec = {} + # initialize default values here for error scenario while super is called + + # by default local + self.conn_type = "local" + # Initialize the dev attribute + self.dev = None + # Initialize the config attribute + self.config = None + + # Update argument_spec with the internal_spec + argument_spec.update(internal_spec) + # Update argument_spec with the top_spec + argument_spec.update(top_spec) + # Extend mutually_exclusive with connection_mutually_exclusive + mutually_exclusive += top_spec_mutually_exclusive + # Call parent's __init__() + super(JuniperJunosModule, self).__init__( + argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, **kwargs + ) + + # initialize the parameters + self.initialize_params() + + # Remove any arguments in internal_spec + for arg_name in internal_spec: + self.params.pop(arg_name) + + # check software compatibility for various 3rd party tools used + ret_output = cfg.check_sw_compatibility( + min_pyez_version, + min_lxml_etree_version, + min_jsnapy_version, + min_jxmlease_version, + min_yaml_version, + ) + + if ret_output != "success": + self.fail_json(msg="%s" % ret_output) + + self.pyez_factory_loader = jnpr.junos.factory.factory_loader + self.pyez_factory_table = jnpr.junos.factory.table + self.pyez_op_table = jnpr.junos.op + self.pyez_exception = pyez_exception + self.ncclient_exception = ncclient_exception + self.etree = cfg.etree + self.jxmlease = cfg.jxmlease + self.yaml = cfg.yaml + + if min_jsnapy_version is not None: + self.jsnapy = jnpr.jsnapy + + # Setup logging. + self.logger = self._setup_logging() + + # Open the PyEZ connection + if self.conn_type == "local": + self.open() + else: + self._pyez_conn = self.get_connection() + + def initialize_params(self): + """ + Initalize the parameters in common module + """ + # priority for arguments is inventory < tasks < console + + self.module_name = self.params.get("_module_name") + self.inventory_hostname = self.params.get("_inventory_hostname") + self.conn_type = self.params.get("_connection") + + # Parse the console option + self._parse_console_options() + + # Check that we have a user and host + if not self.params.get("host"): + self.fail_json(msg="missing required arguments: host") + if not self.params.get("user"): + self.fail_json(msg="missing required arguments: user") + + # Default port based on mode. + if self.params.get("port") is None: + if self.params.get("mode") == "telnet": + self.params["port"] = 23 + elif self.params.get("mode") == "serial": + self.params["port"] = "/dev/ttyUSB0" + else: + self.params["port"] = 830 + else: + if self.params.get("mode") != "serial": + try: + self.params["port"] = int(self.params["port"]) + except ValueError: + self.fail_json( + msg="The port option (%s) must be an " + "integer value." % (self.params["port"]) + ) + else: + self.params["port"] = self.params["port"] + + if self.params.get("mode") == "telnet" or self.params.get("mode") == "serial": + if self.params.get("baud") is None: + # Default baud if serial or telnet mode + self.params["baud"] = 9600 + if self.params.get("attempts") is None: + # Default attempts if serial or telnet mode + self.params["attempts"] = 10 + else: + if self.params.get("baud") is not None: + self.fail_json( + msg="The baud option (%s) is not valid when " + "mode == none." % (self.params.get("baud")) + ) + if self.params.get("attempts") is not None: + self.fail_json( + msg="The attempts option (%s) is not valid when " + "mode == none." % (self.params.get("attempts")) + ) + + def get_connection(self): + if hasattr(self, "_pyez_connection"): + return self._pyez_connection + try: + capabilities = self.get_capabilities() + except ConnectionError: + self.logger.debug("Connection might be local") + return + # module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + network_api = capabilities.get("network_api") + if network_api == "pyez": + self._pyez_connection = Connection(self._socket_path) + else: + self.fail_json(msg="Invalid connection type %s" % network_api) + return self._pyez_connection + + def get_capabilities(self): + if hasattr(self, "_pyez_junos_capabilities"): + return self._pyez_junos_capabilities + try: + capabilities = Connection(self._socket_path).get_capabilities() + except ConnectionError as exc: + raise exc + # module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + self._pyez_junos_capabilities = json.loads(capabilities) + return self._pyez_junos_capabilities + + def exit_json(self, **kwargs): + """Close self.dev and call parent's exit_json(). + + Args: + **kwargs: All keyword arguments are passed to + AnsibleModule.exit_json(). + """ + # Close the connection. + if self.conn_type == "local": + try: + self.close() + except TimeoutExpiredError: + if hasattr(self, "logger"): + self.logger.debug("Ignoring dev.close() timeout error") + if hasattr(self, "logger"): + self.logger.debug("Exit JSON: %s", kwargs) + # Call the parent's exit_json() + super(JuniperJunosModule, self).exit_json(**kwargs) + + def fail_json(self, **kwargs): + """Close self.dev and call parent's fail_json(). + + Args: + **kwargs: All keyword arguments are passed to + AnsibleModule.fail_json(). + """ + # Close the configuration + self.close_configuration() + # Close the connection. + # if self.conn_type == "local": + try: + self.close() + except TimeoutExpiredError: + if hasattr(self, "logger"): + self.logger.debug("Ignoring dev.close() timeout error") + if hasattr(self, "logger"): + self.logger.debug("Fail JSON: %s", kwargs) + # Call the parent's fail_json() + super(JuniperJunosModule, self).fail_json(**kwargs) + + # JuniperJunosModule-specific methods below this point. + + def _parse_console_options(self): + """Parse the console option value. + + Parse the console option value and turn it into the equivalent: + host, mode, baud, attempts, and port options. + """ + if self.params.get("console") is not None: + try: + console_string = self.params.get("console") + + # Subclass ArgumentParser to simply raise a ValueError + # rather than printing to stderr and calling sys.exit() + class QuiteArgumentParser(ArgumentParser): + def error(self, message): + raise ValueError(message) + + # Parse the console_string. + parser = QuiteArgumentParser(add_help=False) + parser.add_argument("-t", "--telnet", default=None) + parser.add_argument("-p", "--port", default=None) + parser.add_argument("-b", "--baud", default=None) + parser.add_argument("-a", "--attempts", default=None) + parser.add_argument("--timeout", default=None) + con_params = vars(parser.parse_args(console_string.split())) + + telnet_params = con_params.get("telnet", None) + # mode == 'telnet' + if telnet_params is not None: + # Split on , + host_port = telnet_params.split(",", 1) + # Strip any leading/trailing whitespace or equal sign + # from host + host = host_port[0].strip(" ") + # Try to convert port to an int. + port = int(host_port[1]) + # Successfully parsed. Set params values + self.params["mode"] = "telnet" + self.params["host"] = host + self.params["port"] = port + # mode == serial + else: + port = con_params.get("port", None) + baud = con_params.get("baud", None) + attempts = con_params.get("attempts", None) + timeout = con_params.get("timeout", None) # not used + self.params["mode"] = "serial" + if port is not None: + self.params["port"] = port + if baud is not None: + self.params["baud"] = baud + if attempts is not None: + self.params["attempts"] = attempts + + # Remove the console option. + self.params.pop("console") + + except ValueError as ex: + self.fail_json( + msg="Unable to parse the console value (%s). " + "Error: %s" % (console_string, str(ex)) + ) + except Exception: + self.fail_json( + msg="Unable to parse the console value (%s). " + "The value of the console argument is " + "typically in the format '--telnet " + ",'." % (console_string) + ) + + def _setup_logging(self): + """Setup logging for the module. + + Performs several tasks to setup logging for the module. This includes: + 1) Creating a Logger instance object for the name + jnpr.ansible_module.. + 2) Sets the level for the Logger object depending on verbosity and + debug settings specified by the user. + 3) Sets the level for other Logger objects specified in + additional_logger_names depending on verbosity and + debug settings specified by the user. + 4) If the logfile or logdir option is specified, attach a FileHandler + instance which logs messages from jnpr.ansible_module. or + any of the names in additional_logger_names. + + Returns: + Logger instance object for the name jnpr.ansible_module.. + """ + + class CustomAdapter(logging.LoggerAdapter): + """ + Prepend the hostname, in brackets, to the log message. + """ + + def process(self, msg, kwargs): + return "[%s] %s" % (self.extra["host"], msg), kwargs + + # Default level to log. + level = logging.WARNING + # Log more if ANSIBLE_DEBUG or -v[v] is set. + if self._debug is True: + level = logging.DEBUG + elif self._verbosity == 1: + level = logging.INFO + elif self._verbosity > 1: + level = logging.DEBUG + # Set level as mentioned in task + elif self.params.get("level") is not None: + level = self.params.get("level") + # Get the logger object to be used for our logging. + logger = logging.getLogger("jnpr.ansible_module." + self.module_name) + # Attach the NullHandler to avoid any errors if no logging is needed. + logger.addHandler(logging.NullHandler()) + # Set the logging level for the modules logging. This will also control + # the amount of logging which goes into Ansible's log file. + logger.setLevel(level) + # Set the logging level for additional names. This will also control + # the amount of logging which goes into Ansible's log file. + for name in additional_logger_names: + logging.getLogger(name).setLevel(level) + # Get the name of the logfile based on logfile or logdir options. + logfile = None + if self.params.get("logfile") is not None: + logfile = self.params.get("logfile") + elif self.params.get("logdir") is not None: + logfile = os.path.normpath( + self.params.get("logdir") + "/" + self.params.get("host") + ".log" + ) + # Create the FileHandler and attach it. + if logfile is not None: + try: + handler = logging.FileHandler(logfile, mode="a") + handler.setLevel(level) + # Create a custom formatter. + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + # add formatter to handler + handler.setFormatter(formatter) + # Handler should log anything from the 'jnpr.ansible_module.' namespace to + # catch PyEZ, JSNAPY, etc. logs. + logger.addHandler(handler) + for name in additional_logger_names: + logging.getLogger(name).addHandler(handler) + except IOError as ex: + self.fail_json( + msg="Unable to open the log file %s. %s" % (logfile, str(ex)) + ) + # Use the CustomAdapter to add host information. + return CustomAdapter(logger, {"host": self.params.get("host")}) + + def parse_arg_to_list_of_dicts( + self, option_name, string_val, allow_bool_values=False + ): + """Parses string_val into a list of dicts with bool and/or str values. + + In order to handle all of the different ways that list of dict type + options may be specified, the arg_spec must set the option type to + 'str'. This requires us to parse the string_val ourselves into the + required list of dicts. Handles Ansible-style keyword=value format for + specifying dictionaries. Also handles Ansible aliases for boolean + values if allow_bool_values is True. + + Args: + option_name - The name of the option being parsed. + string_val - The string to be parse. + allow_bool_values - Whether or not boolean values are allowed. + + Returns: + The list of dicts + + Fails: + If there is an error parsing + """ + # Nothing to do if no string_val were specified. + if string_val is None: + return None + + # Evaluate the string + kwargs = self.safe_eval(string_val) + + if isinstance(kwargs, string_types): + # This might be a keyword1=value1 keyword2=value2 type string. + # The _check_type_dict method will parse this into a dict for us. + try: + kwargs = check_type_dict(kwargs) + except TypeError as exc: + self.fail_json( + msg="The value of the %s option (%s) is " + "invalid. Unable to translate into " + "a list of dicts. %s" % (option_name, string_val, str(exc)) + ) + + # Now, if it's a dict, let's make it a list of one dict + if isinstance(kwargs, dict): + kwargs = [kwargs] + # Now, if it's not a list, we've got a problem. + if not isinstance(kwargs, list): + self.fail_json( + msg="The value of the %s option (%s) is invalid. " + "Unable to translate into a list of dicts." % (option_name, string_val) + ) + # We've got a list, traverse each element to make sure it's a dict. + return_val = [] + for kwarg in kwargs: + # If it's now a string, see if it can be parsed into a dictionary. + if isinstance(kwarg, string_types): + # This might be a keyword1=value1 keyword2=value2 type string. + # The _check_type_dict method will parse this into a dict. + try: + kwarg = check_type_dict(kwarg) + except TypeError as exc: + self.fail_json( + msg="The value of the %s option (%s) is " + "invalid. Unable to translate into a " + "list of dicts %s." % (option_name, string_val, str(exc)) + ) + # Now if it's not a dict, there's a problem. + if not isinstance(kwarg, dict): + self.fail_json( + msg="The value of the kwargs option (%s) is " + "%s. Unable to translate into a list " + "of dicts." % (option_name, string_val) + ) + # check if allow_bool_values passed in kwargs + if "allow_bool_values" in kwarg: + allow_bool_values = kwarg.pop("allow_bool_values") + + # Now we just need to make sure the key is a string and the value + # is a string or bool. + return_item = {} + for k, v in kwarg.items(): + if not isinstance(k, string_types): + self.fail_json( + msg="The value of the %s option (%s) " + "is invalid. Unable to translate into " + "a list of dicts." % (option_name, string_val) + ) + if allow_bool_values is True: + # Try to convert it to a boolean value. Will be None if it + # can't be converted. + try: + bool_val = boolean(v) + except TypeError: + bool_val = None + if bool_val is not None: + v = bool_val + return_item[k] = v + return_val.append(return_item) + return return_val + + def parse_ignore_warning_option(self): + """Parses the ignore_warning option. + + ignore_warning may be a bool, str, or list of str. The Ansible type + checking doesn't support the possibility of more than one type. + + Returns: + The validated value of the ignore_warning option. None if + ignore_warning is not specified. + + Fails: + If there is an error parsing ignore_warning. + """ + # Nothing to do if ignore_warning wasn't specified. + ignore_warn_list = self.params.get("ignore_warning") + if ignore_warn_list is None: + return ignore_warn_list + if len(ignore_warn_list) == 1: + try: + bool_val = boolean(ignore_warn_list[0]) + if bool_val is not None: + return bool_val + except TypeError: + if isinstance(ignore_warn_list[0], string_types): + return ignore_warn_list[0] + self.fail_json( + msg="The value of the ignore_warning option " + "(%s) is invalid. Unexpected type (%s)." + % (ignore_warn_list[0], type(ignore_warn_list[0])) + ) + elif len(ignore_warn_list) > 1: + for ignore_warn in ignore_warn_list: + if not isinstance(ignore_warn, string_types): + self.fail_json( + msg="The value of the ignore_warning " + "option (%s) is invalid. " + "Element (%s) has unexpected " + "type (%s)." + % (str(ignore_warn_list), ignore_warn, type(ignore_warn)) + ) + return ignore_warn_list + else: + self.fail_json( + msg="The value of the ignore_warning option " + "(%s) is invalid." % (ignore_warn_list) + ) + + def parse_rollback_option(self): + """Parses the rollback option. + + rollback may be a str of 'rescue' or an int between 0 and 49. The + Ansible type checking doesn't support the possibility of more than + one type. + + Returns: + The validate value of the rollback option. None if + rollback is not specified. + + Fails: + If there is an error parsing rollback. + """ + # Nothing to do if rollback wasn't specified or is 'rescue'. + rollback = self.params.get("rollback") + if rollback is None or rollback == "rescue": + return rollback + if isinstance(rollback, string_types): + try: + # Is it an int between 0 and 49? + int_val = int(rollback) + if int_val >= 0 and int_val <= 49: + return int_val + except ValueError: + # Fall through to fail_json() + pass + self.fail_json( + msg="The value of the rollback option (%s) is invalid. " + "Must be the string 'rescue' or an int between " + "0 and 49." % (str(rollback)) + ) + + def open(self): + """Open the self.dev PyEZ Device instance. + + Failures: + - ConnectError: When unable to make a PyEZ connection. + """ + # Move all of the connection arguments into connect_args + connect_args = {} + for key in connection_spec: + if self.params.get(key) is not None: + connect_args[key] = self.params.get(key) + + try: + # self.close() + log_connect_args = dict(connect_args) + log_connect_args["passwd"] = "NOT_LOGGING_PARAMETER" + if "cs_passwd" in log_connect_args: + log_connect_args["cs_passwd"] = "NOT_LOGGING_PARAMETER" + + self.logger.debug("Creating device parameters: %s", log_connect_args) + timeout = connect_args.pop("timeout") + self.dev = jnpr.junos.device.Device(**connect_args) + self.logger.debug("Opening device.") + self.dev.open() + self.logger.debug("Device opened.") + self.logger.debug("Setting default device timeout to %d.", timeout) + self.dev.timeout = timeout + self.logger.debug("Device timeout set.") + # Exceptions raised by close() or open() are all sub-classes of + # ConnectError, so this should catch all connection-related exceptions + # raised from PyEZ. + except pyez_exception.ConnectError as ex: + self.fail_json(msg="Unable to make a PyEZ connection: %s" % (str(ex))) + + def close(self, raise_exceptions=False): + """Close the self.dev PyEZ Device instance.""" + if self.dev is not None: + try: + # Because self.fail_json() calls self.close(), we must set + # self.dev = None BEFORE calling dev.close() in order to avoid + # the infinite recursion which would occur if dev.close() + # raised a ConnectError. + dev = self.dev + self.dev = None + dev.close() + self.logger.debug("Device closed.") + # Exceptions raised by close() are all sub-classes of + # ConnectError or RpcError, so this should catch all + # exceptions raised from PyEZ. + except (pyez_exception.ConnectError, pyez_exception.RpcError) as ex: + if raise_exceptions is True: + raise ex + else: + # Ignore exceptions from closing. We're about to exit + # anyway and they will just mask the real error that + # happened. + pass + + def add_sw(self): + """Add an instance of jnpr.junos.utils.sw.SW() to self.""" + self.sw = SW(self.dev) + + def open_configuration(self, mode, ignore_warning=None, ephemeral_instance=None): + """Open candidate configuration database in exclusive or private mode. + + Failures: + - ConnectError: When there's a problem with the PyEZ connection. + - RpcError: When there's a RPC problem including an already locked + config or an already opened private config. + """ + + ignore_warn = ["uncommitted changes will be discarded on exit"] + # if ignore_warning is a bool, pass the bool + # if ignore_warning is a string add to the list + # if ignore_warning is a list, merge them + if ignore_warning is not None and isinstance(ignore_warning, bool): + ignore_warn = ignore_warning + elif ignore_warning is not None and isinstance(ignore_warning, str): + ignore_warn.append(ignore_warning) + elif ignore_warning is not None and isinstance(ignore_warning, list): + ignore_warn = ignore_warn + ignore_warning + + if self.conn_type != "local": + self._pyez_conn.open_configuration(mode, ignore_warn) + return + + if self.config is None: + if mode not in CONFIG_MODE_CHOICES: + self.fail_json(msg="Invalid configuration mode: %s" % (mode)) + if mode != "ephemeral" and ephemeral_instance is not None: + self.fail_json( + msg="Ephemeral instance is specified while the mode " + "is not ephemeral.Specify the mode as ephemeral or " + "do not specify the instance." + ) + if self.dev is None: + self.open() + config = jnpr.junos.utils.config.Config(self.dev, mode=mode) + try: + if config.mode == "exclusive": + config.lock() + elif config.mode == "private": + self.dev.rpc.open_configuration( + private=True, ignore_warning=ignore_warn + ) + elif config.mode == "dynamic": + self.dev.rpc.open_configuration( + dynamic=True, ignore_warning=ignore_warn + ) + elif config.mode == "batch": + self.dev.rpc.open_configuration( + batch=True, ignore_warning=ignore_warn + ) + elif config.mode == "ephemeral": + if ephemeral_instance is None: + self.dev.rpc.open_configuration( + ephemeral=True, ignore_warning=ignore_warn + ) + else: + self.dev.rpc.open_configuration( + ephemeral_instance=ephemeral_instance, + ignore_warning=ignore_warn, + ) + except (pyez_exception.ConnectError, pyez_exception.RpcError) as ex: + self.fail_json( + msg="Unable to open the configuration in %s " + "mode: %s" % (config.mode, str(ex)) + ) + self.config = config + self.logger.debug("Configuration opened in %s mode.", config.mode) + + def close_configuration(self): + """Close candidate configuration database. + + Failures: + - ConnectError: When there's a problem with the PyEZ connection. + - RpcError: When there's a RPC problem closing the config. + """ + if self.conn_type != "local": + self._pyez_conn.close_configuration() + return + + if self.config is not None: + # Because self.fail_json() calls self.close_configuration(), we + # must set self.config = None BEFORE closing the config in order to + # avoid the infinite recursion which would occur if closing the + # configuration raised an exception. + config = self.config + self.config = None + try: + if config.mode == "exclusive": + config.unlock() + elif config.mode in ["batch", "dynamic", "private", "ephemeral"]: + self.dev.rpc.close_configuration() + self.logger.debug("Configuration closed.") + except (pyez_exception.ConnectError, pyez_exception.RpcError) as ex: + self.fail_json(msg="Unable to close the configuration: %s" % (str(ex))) + + def get_configuration( + self, + database="committed", + format="text", + options=None, + filter=None, + model=None, + namespace=None, + remove_ns=True, + ): + """Return the device configuration in the specified format. + + Return the database device configuration database in the format format. + Pass the options specified in the options dict and the filter specified + in the filter argument. + + Args: + database: The configuration database to return. Choices are defined + in CONFIG_DATABASE_CHOICES. + format: The format of the configuration to return. Choices are + defined in CONFIG_FORMAT_CHOICES. + model: The namespace of the configuration to return. Choices are defined + in CONFIG_MODEL_CHOICES. + namespace: User can have their own defined namespace in the + custom yang models, In such cases they need to provide that + namespace so that it can be used to fetch yang modeled configs + remove_ns: Flag to check if namespaces should be removed or not. + filter: A string of XML, or '/'-separated configuration hierarchies, + which specifies a filter used to restrict the portions of the + configuration which are retrieved. + options: Additional options, specified as a dictionary of key/value pairs, used + `when retrieving the configuration. + Returns: + A tuple containing: + - The configuration in the requested format as a single + multi-line string. Returned for all formats. + - The "parsed" configuration as a JSON string. Set when + format == 'xml' or format == 'json'. None when format == 'text' + or format == 'set' + Failures: + - Invalid database. + - Invalid format. + - Options not a dict. + - Invalid filter. + - Format not understood by device. + """ + if options is None: + options = {} + if database not in CONFIG_DATABASE_CHOICES: + self.fail_json( + msg="The configuration database % is not in the " + "list of recognized configuration databases: " + "%s." % (database, str(CONFIG_DATABASE_CHOICES)) + ) + + if format not in CONFIG_FORMAT_CHOICES: + self.fail_json( + msg="The configuration format % is not in the list " + "of recognized configuration formats: %s." + % (format, str(CONFIG_FORMAT_CHOICES)) + ) + + options.update({"database": database, "format": format}) + + if self.conn_type == "local": + if self.dev is None: + self.open() + + self.logger.debug( + "Retrieving device configuration. Options: %s Filter %s", + str(options), + str(filter), + ) + config = None + try: + if self.conn_type == "local": + config = self.dev.rpc.get_config( + options=options, + filter_xml=filter, + model=model, + remove_ns=remove_ns, + namespace=namespace, + ) + else: + config = self.get_config( + options=options, + filter_xml=filter, + model=model, + remove_ns=remove_ns, + namespace=namespace, + ) + self.logger.debug("Configuration retrieved.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + self.fail_json(msg="Unable to retrieve the configuration: %s" % (str(ex))) + + return_val = (None, None) + if format == "text": + if not isinstance(config, self.etree._Element): + self.fail_json( + msg="Unexpected configuration type returned. " + "Configuration is: %s" % (str(config)) + ) + if model is None and config.tag != "configuration-text": + self.fail_json( + msg="Unexpected XML tag returned. " + "Configuration is: %s" + % (self.etree.tostring(config, pretty_print=True)) + ) + return_val = (config.text, None) + elif format == "set": + if not isinstance(config, self.etree._Element): + self.fail_json( + msg="Unexpected configuration type returned. " + "Configuration is: %s" % (str(config)) + ) + if model is None and config.tag != "configuration-set": + self.fail_json( + msg="Unexpected XML tag returned. " + "Configuration is: %s" + % (self.etree.tostring(config, pretty_print=True)) + ) + return_val = (config.text, config.text.splitlines()) + elif format == "xml": + if not isinstance(config, self.etree._Element): + self.fail_json( + msg="Unexpected configuration type returned. " + "Configuration is: %s" % (str(config)) + ) + if model is None and config.tag != "configuration": + self.fail_json( + msg="Unexpected XML tag returned. " + "Configuration is: %s" + % (self.etree.tostring(config, pretty_print=True)) + ) + return_val = ( + self.etree.tostring(config, pretty_print=True), + self.jxmlease.parse_etree(config), + ) + elif format == "json": + return_val = (json.dumps(config), config) + else: + self.fail_json( + msg="Unable to return configuration in %s format." % (format) + ) + return return_val + + def rollback_configuration(self, id): + """Rollback the device configuration to the specified id. + + Rolls back the configuration to the specified id. Assumes the + configuration is already opened. Does NOT commit the configuration. + + Args: + id: The id to which the configuration should be rolled back. Either + an integer rollback value or the string 'rescue' to roll back + to the previously saved rescue configuration. + + Failures: + - Unable to rollback the configuration due to an RpcError or + ConnectError. + """ + if self.conn_type != "local": + self._pyez_conn.rollback_configuration(id) + return + + if self.dev is None or self.config is None: + self.fail_json(msg="The device or configuration is not open.") + + if id == "rescue": + self.logger.debug("Rolling back to the rescue configuration.") + try: + self.config.rescue(action="reload") + self.logger.debug("Rescue configuration loaded.") + except ( + self.pyez_exception.RpcError, + self.pyez_exception.ConnectError, + ) as ex: + self.fail_json( + msg="Unable to load the rescue configuraton: " "%s" % (str(ex)) + ) + elif id >= 0 and id <= 49: + self.logger.debug("Loading rollback %d configuration.", id) + try: + self.config.rollback(rb_id=id) + self.logger.debug("Rollback %d configuration loaded.", id) + except ( + self.pyez_exception.RpcError, + self.pyez_exception.ConnectError, + ) as ex: + self.fail_json( + msg="Unable to load the rollback %d " + "configuraton: %s" % (id, str(ex)) + ) + else: + self.fail_json(msg="Unrecognized rollback configuraton value: %s" % (id)) + + def check_configuration(self): + """Check the candidate configuration. + + Check the configuration. Assumes the configuration is already opened. + Performs the equivalent of a "commit check", but does NOT commit the + configuration. + + Failures: + - An error returned from checking the configuration. + """ + self.logger.debug("Checking the configuration.") + if self.conn_type != "local": + self._pyez_conn.check_configuration() + return + + if self.dev is None or self.config is None: + self.fail_json(msg="The device or configuration is not open.") + + try: + self.config.commit_check() + self.logger.debug("Configuration checked.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + self.fail_json(msg="Failure checking the configuraton: %s" % (str(ex))) + + def diff_configuration(self, ignore_warning=False): + """Diff the candidate and committed configurations. + + Diff the candidate and committed configurations. + + Returns: + A string with the configuration differences in text "diff" format. + + Failures: + - An error returned from diffing the configuration. + """ + self.logger.debug("Diffing candidate and committed configurations.") + if self.conn_type != "local": + diff = self._pyez_conn.diff_configuration(ignore_warning) + return diff + if self.dev is None or self.config is None: + self.fail_json(msg="The device or configuration is not open.") + + try: + diff = self.config.diff(rb_id=0, ignore_warning=ignore_warning) + self.logger.debug("Configuration diff completed.") + return diff + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + self.fail_json(msg="Failure diffing the configuraton: %s" % (str(ex))) + + def load_configuration( + self, + action, + lines=None, + src=None, + template=None, + vars=None, + url=None, + ignore_warning=None, + format=None, + ): + """Load the candidate configuration. + + Load the candidate configuration from the specified src file using the + specified action. + + Args: + action - The type of load to perform: 'merge', 'replace', 'set', + 'override', 'overwrite', and + 'update', 'patch' + lines - A list of strings containing the configuration. + src - The file path on the local Ansible control machine to the + configuration to be loaded. + template - The Jinja2 template used to renter the configuration. + vars - The variables used to render the template. + url - The URL to the candidate configuration. + ignore_warning - What warnings to ignore. + format - The format of the configuration being loaded. + + Failures: + - An error returned from loading the configuration. + """ + if self.conn_type == "local": + if self.dev is None or self.config is None: + self.fail_json(msg="The device or configuration is not open.") + + load_args = {} + config = None + if ignore_warning is not None: + load_args["ignore_warning"] = ignore_warning + if action == "set": + format = "set" + if format is not None: + load_args["format"] = format + if action == "merge": + load_args["merge"] = True + if action == "override" or action == "overwrite": + load_args["overwrite"] = True + if action == "update": + load_args["update"] = True + if action == "patch": + load_args["patch"] = True + if lines is not None: + config = "\n".join(map(lambda line: line.rstrip("\n"), lines)) + self.logger.debug("Loading the supplied configuration.") + if src is not None: + abs_path_src = os.path.abspath(src) # For PyEZ persistent + load_args["path"] = abs_path_src + self.logger.debug("Loading the configuration from: %s.", src) + if template is not None: + load_args["template_path"] = template + load_args["template_vars"] = vars + self.logger.debug( + "Loading the configuration from the %s template.", template + ) + if url is not None: + load_args["url"] = url + self.logger.debug("Loading the configuration from %s.", url) + + if self.conn_type != "local": + self._pyez_conn.load_configuration(config, load_args) + return + + try: + if config is not None: + self.config.load(config, **load_args) + else: + self.logger.debug("Load args %s.", str(load_args)) + self.config.load(**load_args) + self.logger.debug("Configuration loaded.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + self.fail_json(msg="Failure loading the configuraton: %s" % (str(ex))) + + def commit_configuration( + self, + ignore_warning=None, + comment=None, + confirmed=None, + timeout=30, + full=False, + sync=False, + force_sync=False, + ): + """Commit the candidate configuration. + + Commit the configuration. Assumes the configuration is already opened. + + Args: + ignore_warning - Which warnings to ignore. + comment - The commit comment + confirmed - Number of minutes for commit confirmed. + timeout - Timeout for commit configuration. Default timeout value is 30s. + full - apply full commit + sync - Check for commit syntax and sync between RE's + force_sync - Ignore syntax check and force to sync between RE's + + Failures: + - An error returned from committing the configuration. + """ + if self.conn_type == "local": + if self.dev.timeout: + timeout = self.dev.timeout + + if self.conn_type != "local": + self._pyez_conn.commit_configuration( + ignore_warning=ignore_warning, + comment=comment, + confirmed=confirmed, + timeout=timeout, + full=full, + force_sync=force_sync, + sync=sync, + ) + return + + if self.dev is None or self.config is None: + self.fail_json(msg="The device or configuration is not open.") + + self.logger.debug("Committing the configuration.") + try: + self.config.commit( + ignore_warning=ignore_warning, + comment=comment, + confirm=confirmed, + timeout=timeout, + full=full, + force_sync=force_sync, + sync=sync, + ) + self.logger.debug("Configuration committed.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + self.fail_json(msg="Failure committing the configuraton: %s" % (str(ex))) + + def ping(self, params, acceptable_percent_loss=0, results=None): + """Execute a ping command with the parameters specified in params. + + Args: + params: dict of parameters passed directly to the ping RPC. + acceptable_percent_loss: integer specifying maximum percentage of + packets that may be lost and still + consider the ping not to have failed. + results: dict of results which should be included in the return + value, or which should be included if fail_json() is + called due to a failure. + + Returns: + A dict of results. It contains all key/value pairs in the results + argument plus the keys below. (The keys below will overwrite + any corresponding key which exists in the results argument): + + msg: (str) A human-readable message indicating the result. + packet_loss: (str) The percentage of packets lost. + packets_sent: (str) The number of packets sent. + packets_received: (str) The number of packets received. + rtt_minimum: (str) The minimum round-trip-time, in microseconds, + of all ping responses received. + rtt_maximum: (str) The maximum round-trip-time, in microseconds, + of all ping responses received. + rtt_average: (str) The average round-trip-time, in microseconds, + of all ping responses received. + rtt_stddev: (str) The standard deviation of round-trip-time, in + microseconds, of all ping responses received. + warnings: (list of str) A list of warning strings, if any, produced + from the ping. + failed: (bool) Indicates if the ping failed. The ping fails + when packet_loss > acceptable_percent_loss. + + Fails: + - If the ping RPC produces an exception. + - If there are errors present in the results. + """ + if results is None: + results = {} + # Assume failure until we know success. + results["failed"] = True + + # Execute the ping. + try: + self.logger.debug("Executing ping with parameters: %s", str(params)) + if self.conn_type == "local": + resp = self.dev.rpc.ping(normalize=True, **params) + else: + response = self._pyez_conn.ping_device(normalize=True, **params) + resp = self.etree.fromstring(response) + self.logger.debug("Ping executed.") + except (self.pyez_exception.RpcError, self.pyez_exception.ConnectError) as ex: + self.fail_json(msg="Unable to execute ping: %s" % (str(ex))) + + if not isinstance(resp, self.etree._Element): + self.fail_json(msg="Unexpected ping response: %s" % (str(resp))) + + resp_xml = self.etree.tostring(resp, pretty_print=True) + + # Fail if any errors in the results + errors = resp.findall("rpc-error[error-severity='error']/error-message") + if len(errors) != 0: + # Create a comma-plus-space-seperated string of the errors. + # Calls the text attribute of each element in the errors list. + err_msg = ", ".join(map(lambda err: err.text, errors)) + results["msg"] = "Ping returned errors: %s" % (err_msg) + self.exit_json(**results) + + # Add any warnings into the results + warnings = resp.findall("rpc-error[error-severity='warning']/error-message") + if len(warnings) != 0: + # Create list of the text attributes of each element in the + # warnings list. + results["warnings"] = list(map(lambda warn: warn.text, warnings)) + + # Try to find probe summary + probe_summary = resp.find("probe-results-summary") + if probe_summary is None: + results["msg"] = "Probe-results-summary not found in response: " "%s" % ( + resp_xml + ) + self.exit_json(**results) + + # Extract some required fields and some optional fields + r_fields = {} + r_fields["packet_loss"] = probe_summary.findtext("packet-loss") + r_fields["packets_sent"] = probe_summary.findtext("probes-sent") + r_fields["packets_received"] = probe_summary.findtext("responses-received") + o_fields = {} + o_fields["rtt_minimum"] = probe_summary.findtext("rtt-minimum") + o_fields["rtt_maximum"] = probe_summary.findtext("rtt-maximum") + o_fields["rtt_average"] = probe_summary.findtext("rtt-average") + o_fields["rtt_stddev"] = probe_summary.findtext("rtt-stddev") + + # Make sure we got values for required fields. + for key in r_fields: + if r_fields[key] is None: + results["msg"] = "Expected field %s not found in " "response: %s" % ( + key, + resp_xml, + ) + self.exit_json(**results) + # Add the required fields to the result. + results.update(r_fields) + + # Extract integer packet loss + packet_loss = 100 + if results["packet_loss"] is not None: + try: + packet_loss = round(float(results["packet_loss"])) + except ValueError: + results["msg"] = "Packet loss %s not an integer. " "Response: %s" % ( + results["packet_loss"], + resp_xml, + ) + self.exit_json(**results) + + if packet_loss < 100: + # Optional fields are present if packet_loss < 100 + for key in o_fields: + if o_fields[key] is None: + results["msg"] = ( + "Expected field %s not found in " + "response: %s" % (key, resp_xml) + ) + self.exit_json(**results) + # Add the o_fields to the result (even if they're None) + results.update(o_fields) + + # Set the result message. + results["msg"] = "Loss %s%%, (Sent %s | Received %s)" % ( + results["packet_loss"], + results["packets_sent"], + results["packets_received"], + ) + + # Was packet loss within limits? If so, we didn't fail. + if packet_loss <= acceptable_percent_loss: + results["failed"] = False + + return results + + def save_text_output(self, name, format, text): + """Save text output into a file based on 'dest' and 'dest_dir' params. + + The text provided in the text parameter is saved to a file on the + local Ansible control machine based on the 'diffs_file', 'dest', and + 'dest_dir' module parameters. If neither parameter is specified, + then this method is a no-op. If the 'dest' or 'diffs_file' parameter is + specified, the value of the 'dest' or 'diffs_file' parameter is used as + the path name for the destination file. In this case, the name and + format parameters are ignored. If the 'dest_dir' parameter is + specified, the path name for the destination file is: + _.. If the destination file already exists, + and the 'dest_dir' option is specified, or the 'dest' parameter is + specified and the self.destfile attribute is not present, the file is + overwritten. If the 'dest' parameter is specified and the + self.destfile attribute is present, then the file is appended. This + allows multiple text outputs to be written to the same file. + + Args: + name: The name portion of the destination filename when the + 'dest_dir' parameter is specified. + format: The format portion of the destination filename when the + 'dest_dir' parameter is specified. + text: The text to be written into the destination file. + + Fails: + - If the destination file is not writable. + """ + file_path = None + mode = "wb" + if name == "diff": + if self.params.get("diffs_file") is not None: + file_path = os.path.normpath(self.params.get("diffs_file")) + elif self.params.get("dest_dir") is not None: + dest_dir = self.params.get("dest_dir") + file_name = "%s.diff" % (self.inventory_hostname,) + file_path = os.path.normpath(os.path.join(dest_dir, file_name)) + else: + if self.params.get("dest") is not None: + file_path = os.path.normpath(self.params.get("dest")) + if getattr(self, "destfile", None) is None: + self.destfile = self.params.get("dest") + else: + mode = "ab" + elif self.params.get("dest_dir") is not None: + dest_dir = self.params.get("dest_dir") + # Substitute underscore for spaces. + name = name.replace(" ", "_") + # Substitute underscore for pipe + name = name.replace("|", "_") + name = "" if name == "config" else "_" + name + file_name = "%s%s.%s" % (self.inventory_hostname, name, format) + file_path = os.path.normpath(os.path.join(dest_dir, file_name)) + if file_path is not None: + try: + # Use ansible utility to convert objects to bytes + # to achieve Python2/3 compatibility + with open(file_path, mode) as save_file: + save_file.write(to_bytes(text, encoding="utf-8")) + self.logger.debug("Output saved to: %s.", file_path) + except IOError: + self.fail_json( + msg="Unable to save output. Failed to " + "open the %s file." % (file_path) + ) + + def get_config( + self, + filter_xml=None, + options=None, + model=None, + namespace=None, + remove_ns=True, + **kwarg + ): + response = self._pyez_conn.get_config( + filter_xml, options, model, namespace, remove_ns, **kwarg + ) + if options['format'] == 'json': + return response + else: + return self.etree.fromstring(response) + + def get_rpc(self, rpc, ignore_warning=None, format=None): + rpc_1 = self.etree.tostring(rpc) + rpc_str = xmltodict.parse(rpc_1) + # json.dumps(rpc_str) + response = self._pyez_conn.get_rpc_resp( + rpc_str, ignore_warning=ignore_warning, format=format + ) + if format == "json": + return response + return self.etree.fromstring(response) + + def get_facts(self): + facts = self._pyez_conn.get_facts() + return facts + + def get_chassis_inventory(self): + chassis = self._pyez_conn.get_chassis_inventory() + return self.etree.fromstring(chassis) + + def _hashfile(self, afile, hasher, blocksize=65536): + buf = afile.read(blocksize) + while len(buf) > 0: + hasher.update(buf) + buf = afile.read(blocksize) + return hasher.hexdigest() + + def local_md5(self, package, action): + """ + Computes the MD5 checksum value on the local package file. + :param str package: + File-path to the package (tgz) file on the local server + :returns: MD5 checksum (str) + :raises IOError: when **package** file does not exist + """ + try: + checksum = self._hashfile(open(package, "rb"), hashlib.md5()) + except Exception as err: + self.logger.error("unable to get the hash due to:{0}".format(err)) + if ("No such file" in format(err)) and action == "get": + checksum = "no_file" + else: + raise err + return checksum + + self.logger.info("local hash calculated") + return checksum + + def remote_md5(self, remote_file, action): + try: + if self.conn_type == "local": + rpc_reply = self.dev.rpc.get_checksum_information(path=remote_file) + checksum = rpc_reply.findtext(".//checksum").strip() + else: + rpc_reply = self._pyez_conn.get_checksum_information(remote_file) + resp = self.etree.fromstring(rpc_reply) + checksum = resp.findtext(".//checksum").strip() + except Exception as err: + self.logger.error("unable to get rpc due to:{0}".format(str(err))) + if ("No such file or directory" in format(err)) and (action == "put"): + checksum = "no_file" + else: + raise err + return checksum + self.logger.info("rpc response received") + return checksum + + def scp_file_copy_put(self, local_file, remote_file): + self.logger.info("Computing local MD5 checksum on: {0}".format(local_file)) + local_checksum = self.local_md5(local_file, "put") + self.logger.info("Local checksum: {0}".format(local_checksum)) + remote_checksum = self.remote_md5(remote_file, "put") + if remote_checksum == "no_file" or remote_checksum != local_checksum: + status = "File not present, need to transfer" + self.logger.info(status) + if self.conn_type == "local": + with SCP(self.dev, progress=True) as scp1: + scp1.put(local_file, remote_file) + else: + self._pyez_conn.scp_file_copy_put(local_file, remote_file) + self.logger.info( + "computing remote MD5 checksum on: {0}".format(remote_file) + ) + remote_checksum = self.remote_md5(remote_file, "put") + self.logger.info("Remote checksum: {0}".format(remote_checksum)) + if remote_checksum != local_checksum: + status = "Transfer failed (different MD5 between local and remote) {0} | {1}".format( + local_checksum, remote_checksum + ) + self.logger.error(status) + self.fail_json(msg=status) + return [status, False] + else: + status = "File pushed OK" + self.logger.info("Checksum check passed.") + self.logger.info(status) + return [status, True] + else: + status = "File already present, skipping the scp" + self.logger.info(status) + return [status, False] + + def scp_file_copy_get(self, remote_file, local_file): + self.logger.info("Computing remote MD5 checksum on: {0}".format(remote_file)) + remote_checksum = self.remote_md5(remote_file, "get") + self.logger.info("Remote checksum: {0}".format(remote_checksum)) + local_checksum = self.local_md5(local_file, "get") + if local_checksum == "no_file" or local_checksum != remote_checksum: + status = "File not present, need to transfer" + self.logger.info(status) + if self.conn_type == "local": + with SCP(self.dev, progress=True) as scp1: + scp1.get(remote_file, local_file) + else: + self._pyez_conn.scp_file_copy_get(remote_file, local_file) + self.logger.info("computing local MD5 checksum on: {0}".format(local_file)) + local_checksum = self.local_md5(local_file, "get") + self.logger.info("Local checksum: {0}".format(local_checksum)) + if remote_checksum != local_checksum: + status = "Transfer failed (different MD5 between local and remote) {0} | {1}".format( + local_checksum, remote_checksum + ) + self.logger.error(status) + self.fail_json(msg=status) + return [status, False] + else: + status = "File pushed OK" + self.logger.info("Checksum check passed.") + self.logger.info(status) + return [status, True] + else: + status = "File already present, skipping the scp" + self.logger.info(status) + return [status, False] diff --git a/ansible_collections/juniper/device/plugins/modules/command.py b/ansible_collections/juniper/device/plugins/modules/command.py new file mode 100644 index 00000000..454eec2c --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/command.py @@ -0,0 +1,504 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: command +author: "Juniper Networks - Stacy Smith (@stacywsmith)" +short_description: Execute one or more CLI commands on a Junos device +description: + - Execute one or more CLI commands on a Junos device. + - Alias command + - This module does NOT use the Junos CLI to execute the CLI command. + Instead, it uses the C() RPC over a NETCONF channel. The + C() RPC takes a CLI command as it's input and is very similar to + executing the command on the CLI, but you can NOT include any pipe modifies + (i.e. C(| match), C(| count), etc.) with the CLI commands executed by this + module. +options: + commands: + description: + - A list of one or more CLI commands to execute on the Junos device. + required: true + default: none + type: list + aliases: + - cli + - command + - cmd + - cmds + dest: + description: + - The path to a file, on the Ansible control machine, where the output of + the cli command will be saved. + - The file must be writeable. If the file already exists, it is + overwritten. + - When tasks are executed against more than one target host, + one process is forked for each target host. (Up to the maximum + specified by the forks configuration. See + U(forks|http://docs.ansible.com/ansible/latest/intro_configuration.html#forks) + for details.) This means that the value of this option must be unique + per target host. This is usually accomplished by including + C({{ inventory_hostname }}) in the value of the I(dest) option. It is + the user's responsibility to ensure this value is unique per target + host. + - For this reason, this option is deprecated. It is maintained for + backwards compatibility. Use the I(dest_dir) option in new playbooks. + The I(dest) and I(dest_dir) options are mutually exclusive. + required: false + default: None + type: path + aliases: + - destination + dest_dir: + description: + - The path to a directory, on the Ansible control machine, where + the output of the cli command will be saved. The output will be logged + to a file named C({{ inventory_hostname }}_)I(command)C(.)I(format) + in the directory specified by the value of the I(dest_dir) option. + - The destination file must be writeable. If the file already exists, + it is overwritten. It is the users responsibility to ensure a unique + I(dest_dir) value is provided for each execution of this module + within a playbook. + - The I(dest_dir) and I(dest) options are mutually exclusive. The + I(dest_dir) option is recommended for all new playbooks. + required: false + default: None + type: path + aliases: + - destination_dir + - destdir + formats: + description: + - The format of the reply for the CLI command(s) specified by the + I(commands) option. The specified format(s) must be supported by the + target Junos device. The value of this option can either be a single + format, or a list of formats. If a single format is specified, it + applies to all command(s) specified by the I(commands) option. If a + list of formats are specified, there must be one value in the list for + each command specified by the I(commands) option. Specifying the value + C(xml) for the I(formats) option is similar to appending + C(| display xml) to a CLI command, and specifying the value C(json) + for the I(formats) option is similar to appending C(| display json) to + a CLI command. + required: false + default: text + type: str or list of str + choices: + - text + - xml + - json + aliases: + - format + - display + - output + return_output: + description: + - Indicates if the output of the command should be returned in the + module's response. You might want to set this option to C(false), + and set the I(dest_dir) option, if the command output is very large + and you only need to save the output rather than using it's content in + subsequent tasks/plays of your playbook. + required: false + default: true + type: bool +""" + +EXAMPLES = """ +- name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: false + + tasks: + - name: "Execute single command in text format" + juniper.device.command: + commands: "show configuration system services netconf traceoptions" + format: text + + - name: "Execute command with login credentials" + juniper.device.command: + host: "10.x.x.x." + user: "user" + passwd: "user123" + commands: + - "show system storage" + register: junos_result + + - name: Execute three commands. + juniper.device.command: + commands: + - "show version" + - "show system uptime" + - "show interface terse" + register: response + + - name: Print the command output of each. + ansible.builtin.debug: + var: item.stdout + with_items: "{{ response.results }}" + + - name: show route with XML output - show version with JSON output + juniper.device.command: + commands: + - "show route" + - "show version" + formats: + - "xml" + - "json" + + - name: Multiple commands, save outputs, but don't return them + juniper.device.command: + commands: + - "show route" + - "show version" + formats: + - "xml" + dest_dir: "../Output" + return_output: false + + - name: save output to dest + juniper.device.command: + command: + - "show route" + - "show lldp neighbors" + dest: "/tmp/{{ inventory_hostname }}.commands.output" +""" + +RETURN = """ +changed: + description: + - Indicates if the device's state has changed. Since this module does not + change the operational or configuration state of the device, the value + is always set to false. + - You could use this module to execute a command which + changes the operational state of the the device. For example, + C(clear ospf neighbors). Beware, this module is unable to detect + this situation, and will still return the value C(false) for I(changed) + in this case. + returned: success + type: bool + sample: false +command: + description: + - The CLI command which was executed. + returned: always + type: str +failed: + description: + - Indicates if the task failed. See the I(results) key for additional + details. + returned: always + type: bool +format: + description: + - The format of the command response. + returned: always + type: str +msg: + description: + - A human-readable message indicating the result. + returned: always + type: str +parsed_output: + description: + - The command reply from the Junos device parsed into a JSON data structure. + For XML replies, the response is parsed into JSON using the + U(jxmlease|https://github.com/Juniper/jxmlease) + library. For JSON the response is parsed using the Python + U(json|https://docs.python.org/2/library/json.html) library. + - When Ansible converts the jxmlease or native Python data structure + into JSON, it does not guarantee that the order of dictionary/object keys + are maintained. + returned: when command executed successfully, I(return_output) is true, + and the value of the I(formats) option is C(xml) or C(json). + type: dict +results: + description: + - The other keys are returned when a single command is specified for the + I(commands) option. When the value of the I(commands) option is a list + of commands, this key is returned instead. The value of this key is a + list of dictionaries. Each element in the list corresponds to the + commands in the I(commands) option. The keys for each element in the list + include all of the other keys listed. The I(failed) key indicates if the + individual command failed. In this case, there is also a top-level + I(failed) key. The top-level I(failed) key will have a value of C(false) + if ANY of the commands ran successfully. In this case, check the value + of the I(failed) key for each element in the I(results) list for the + results of individual commands. + returned: when the I(commands) option is a list value. + type: list of dict +stdout: + description: + - The command reply from the Junos device as a single multi-line string. + returned: when command executed successfully and I(return_output) is C(true). + type: str +stdout_lines: + description: + - The command reply from the Junos device as a list of single-line strings. + returned: when command executed successfully and I(return_output) is C(true). + type: list of str +""" + +import sys + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + commands=dict( + required=True, + type="list", + aliases=["cli", "command", "cmd", "cmds"], + default=None, + ), + formats=dict( + required=False, + type="list", + aliases=["format", "display", "output"], + default=None, + ), + dest=dict( + required=False, type="path", aliases=["destination"], default=None + ), + dest_dir=dict( + required=False, + type="path", + aliases=["destination_dir", "destdir"], + default=None, + ), + ignore_warning=dict(required=False, type="list", default=None), + return_output=dict(required=False, type="bool", default=True), + ), + # Since this module doesn't change the device's configuration, there is + # no additional work required to support check mode. It's inherently + # supported. Well, that's not completely true. It does depend on the + # command executed. See the I(changed) key in the RETURN documentation + # for more details. + supports_check_mode=True, + min_jxmlease_version=cfg.MIN_JXMLEASE_VERSION, + ) + + # Parse ignore_warning value + ignore_warning = junos_module.parse_ignore_warning_option() + + # Check over commands + commands = junos_module.params.get("commands") + # Ansible allows users to specify a commands argument with no value. + if commands is None: + junos_module.fail_json(msg="The commands option must have a value.") + # Make sure the commands don't include any pipe modifiers. + for command in commands: + pipe_index = command.find("|") + if pipe_index != -1 and command[pipe_index:].strip() != "display xml rpc": + # Allow "show configuration | display set" + if ( + "show configuration" in command + and "display set" in command[pipe_index:] + and "|" not in command[pipe_index + 1 :] + ): + continue + # Any other "| display " should use the format option instead. + for valid_format in juniper_junos_common.RPC_OUTPUT_FORMAT_CHOICES: + if "display " + valid_format in command[pipe_index:]: + junos_module.fail_json( + msg="The pipe modifier (%s) in the command " + '(%s) is not supported. Use format: "%s" ' + "instead." % (command[pipe_index:], command, valid_format) + ) + # Any other "| " is going to produce an error anyway, so fail + # with a meaningful message. + junos_module.fail_json( + msg="The pipe modifier (%s) in the command " + "(%s) is not supported." % (command[pipe_index:], command) + ) + + # Check over formats + formats = junos_module.params.get("formats") + if formats is None: + # Default to text format + formats = ["text"] + valid_formats = juniper_junos_common.RPC_OUTPUT_FORMAT_CHOICES + # Check format values + for format in formats: + # Is it a valid format? + if format not in valid_formats: + junos_module.fail_json( + msg="The value %s in formats is invalid. " + "Must be one of: %s" % (format, ", ".join(map(str, valid_formats))) + ) + # Correct number of format values? + if len(formats) != 1 and len(formats) != len(commands): + junos_module.fail_json( + msg="The formats option must have a single " + "value, or one value per command. There " + "are %d commands and %d formats." % (len(commands), len(formats)) + ) + # Same format for all commands + elif len(formats) == 1 and len(commands) > 1: + formats = formats * len(commands) + + results = list() + for command, format in zip(commands, formats): + # Set initial result values. Assume failure until we know it's success. + result = { + "msg": "", + "command": command, + "format": format, + "changed": False, + "failed": True, + } + + # Execute the CLI command + try: + junos_module.logger.debug('Executing command "%s".', command) + rpc = junos_module.etree.Element("command", format=format) + rpc.text = command + if junos_module.conn_type == "local": + resp = junos_module.dev.rpc( + rpc, ignore_warning=ignore_warning, normalize=bool(format == "xml") + ) + else: + resp = junos_module.get_rpc( + rpc, ignore_warning=ignore_warning, format=format + ) + result["msg"] = "The command executed successfully." + junos_module.logger.debug("Command %s executed successfully.", command) + except ( + junos_module.pyez_exception.ConnectError, + junos_module.pyez_exception.RpcError, + ) as ex: + junos_module.logger.debug( + 'Unable to execute "%s". Error: %s', command, str(ex) + ) + result["msg"] = "Unable to execute the command: %s. Error: %s" % ( + command, + str(ex), + ) + results.append(result) + continue + + text_output = None + parsed_output = None + if resp is True: + text_output = "" + elif (isinstance(resp, junos_module.etree._Element)) or (isinstance(resp, dict)): + # Handle the output based on format + if format == "text": + if resp.tag in ["output", "rpc-reply"]: + text_output = resp.text + junos_module.logger.debug("Text output set.") + elif resp.tag == "configuration-information": + text_output = resp.findtext("configuration-output") + junos_module.logger.debug("Text configuration output set.") + else: + result["msg"] = "Unexpected text response tag: %s." % ((resp.tag)) + results.append(result) + junos_module.logger.debug( + "Unexpected text response tag %s.", resp.tag + ) + continue + elif format == "xml": + encode = None if sys.version < "3" else "unicode" + text_output = junos_module.etree.tostring( + resp, pretty_print=True, encoding=encode + ) + parsed_output = junos_module.jxmlease.parse_etree(resp) + junos_module.logger.debug("XML output set.") + elif format == "json": + text_output = str(resp) + parsed_output = resp + junos_module.logger.debug("JSON output set.") + else: + result["msg"] = "Unexpected format %s." % (format) + results.append(result) + junos_module.logger.debug("Unexpected format %s.", format) + continue + else: + result["msg"] = "Unexpected response type %s." % (type(resp)) + results.append(result) + junos_module.logger.debug("Unexpected response type %s.", type(resp)) + continue + + # Set the output keys + if junos_module.params["return_output"] is True: + if text_output is not None: + result["stdout"] = text_output + result["stdout_lines"] = text_output.splitlines() + if parsed_output is not None: + result["parsed_output"] = parsed_output + # Save the output + junos_module.save_text_output(command, format, text_output) + # This command succeeded. + result["failed"] = False + # Append to the list of results + results.append(result) + + # Return response. + if len(results) == 1: + junos_module.exit_json(**results[0]) + else: + # Calculate the overall failed. Only failed if all commands failed. + failed = True + for result in results: + if result.get("failed") is False: + failed = False + break + junos_module.exit_json(results=results, changed=False, failed=failed) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/config.py b/ansible_collections/juniper/device/plugins/modules/config.py new file mode 100644 index 00000000..8e74a09d --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/config.py @@ -0,0 +1,1247 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: config +author: "Juniper Networks - Stacy Smith (@stacywsmith)" +short_description: Manipulate the configuration of a Junos device +description: + - > + Manipulate the configuration of a Junos device. This module allows a + combination of loading or rolling back, checking, diffing, retrieving, and + committing the configuration of a Junos device. It performs the following + steps in order: + + #. Open a candidate configuration database. + + * If the I(config_mode) option has a value of C(exclusive), the default, + take a lock on the candidate configuration database. If the lock fails + the module fails and reports an error. + * If the I(config_mode) option has a value of C(private), open a private + candidate configuration database. If opening the private configuration + database fails the module fails and reports an error. + * If the I(config_mode) option has a value of C(dynamic), open a dynamic + candidate configuration database. If opening the dynamic configuration + database fails the module fails and reports an error. + * If the I(config_mode) option has a value of C(batch), open a batch + candidate configuration database. If opening the batch configuration + database fails the module fails and reports an error. + * If the I(config_mode) option has a value of C(ephemeral), open a default + ephemeral candidate configuration database. If opening the ephemeral + configuration database fails the module fails and reports an error. + * If the I(ephemeral_instance) option has a value of C(ephemeral instance + name) along with I(config_mode) option has a value of C(ephemeral), open + a user defined ephemeral instance candidate configuration database. If + opening the ephemeral onfiguration database fails the module fails + and reports an error. + #. Load configuration data into the candidate configuration database. + + * Configuration data may be loaded using the I(load) or I(rollback) + options. If either of these options are specified, new configuration + data is loaded. If neither option is specified, this step is skipped. + * If the I(rollback) option is specified, replace the candidate + configuration with the previous configuration specified by the value + of the I(rollback) option. + * If the I(load) option is specified, load new configuration data. + * The value of the I(load) option defines the type of load which is + performed. + * The source of the new configuration data is one of the following: + + * I(src) - A file path on the local Ansible control machine. + * I(lines) - A list of strings containing the configuration data. + * I(template) - A file path to a Jinja2 template on the local + Ansible control machine. This template is rendered with the variables + specified by the I(vars) option. If the I(template) option is + specified, the I(vars) option must also be specified. + * I(url) - A URL reachable from the target Junos device. + * If the I(format) option is specified, the configuration file being + loaded is in the specified format, rather than the format determined + from the file name. + #. Check the validity of the candidate configuration database. + + * If the I(check) option is C(true), the default, check the validity + of the configuration by performing a "commit check" operation. + * This option may be specified with I(diff) C(false) and I(commit) + C(false) to confirm a previous "commit confirmed " operation + without actually performing an additional commit. + * If the configuration check fails, further processing stops, the module + fails, and an error is reported. + #. Determine differences between the candidate and committed configuration + databases. + + * If step 2 was not skipped, and the I(diff) option is C(true), + the default, perform a diff between the candidate and committed + configuration databases. + * If the I(diffs_file) or I(dest_dir) option is specified, save the + generated configuration differences. + * If the I(return_output) option is C(true), the default, include the + generated configuration difference in the I(diff) and I(diff_lines) + keys of the module's response. + #. Retrieve the configuration database from the Junos device. + + * If the I(retrieve) option is specified, retrieve the configuration + database specified by the I(retrieve) value from the target Junos + device to the local Ansible control machine. + * The format in which the configuration is retrieved is specified by the + value of the I(format) option. + * The optional I(filter) controls which portions of the configuration + are retrieved. + * If I(options) are specified, they control the content of the + configuration retrieved. + * If the I(dest) or I(dest_dir) option is specified, save the + retrieved configuration to a file on the local Ansible control + machine. + * If the I(return_output) option is C(true), the default, include the + retrieved configuration in the I(config), I(config_lines), and + I(config_parsed) keys of the module's response. + #. Commit the configuration changes. + + * If the I(commit) option is C(true), the default, commit the + configuration changes. + * This option may be specified with I(diff) C(false) and I(check) + C(false) to confirm a previous "commit confirmed " operation. + * If the I(comment) option is specified, add the comment to the commit. + * If the I(confirmed) option is specified, perform a + C(commit confirmed) I(min) operation where I(min) is the value of the + I(confirmed) option. + * If the I(check) option is C(true) and the I(check_commit_wait) + option is specified, wait I(check_commit_wait) seconds before + performing the commit. + #. Close the candidate configuration database. + + * Close and discard the candidate configuration database. + * If the I(config_mode) option has a value of C(exclusive), the default, + unlock the candidate configuration database. +options: + check: + description: + - Perform a commit check operation. + required: false + default: true (false if retrieve is set and load and rollback are not set) + type: bool + aliases: + - check_commit + - commit_check + check_commit_wait: + description: + - The number of seconds to wait between check and commit operations. + - This option is only valid if I(check) is C(true) and I(commit) is + C(true). + - This option should not normally be needed. It works around an issue in + some versions of Junos. + required: false + default: none + type: int + comment: + description: + - Provide a comment to be used with the commit operation. + - This option is only valid if the I(commit) option is true. + required: false + default: none + type: str + commit: + description: + - Perform a commit operation. + required: false + default: true (false if retrieve is set and load and rollback are not set) + type: bool + commit_empty_changes: + description: + - Perform a commit operation, even if there are no changes between the + candidate configuration and the committed configuration. + required: false + default: false + type: bool + commit_full: + description: + - Coupled with commit=True, this will perform a C(commit full) operation. + required: false + default: false + type: bool + commit_sync: + description: + - On dual control plane systems, requests that the candidate configuration + on one control plane be copied to the other control plane, checked for + correct syntax, and committed on both Routing engines. + required: false + default: false + type: bool + commit_force_sync: + description: + - On dual control plane systems, forces the candidate configuration + on one control plane to be copied to the other control plane. + required: false + default: false + type: bool + config_mode: + description: + - The mode used to access the candidate configuration database. + required: false + default: exclusive + type: str + choices: + - exclusive + - private + - dynamic + - batch + - ephemeral + aliases: + - config_access + - edit_mode + - edit_access + ephemeral_instance: + description: + - To open a user-defined instance of the ephemeral database + required: false + default: None + type: str + confirmed: + description: + - Provide a confirmed timeout, in minutes, to be used with the commit + operation. + - This option is only valid if the I(commit) option is C(true). + - The value of this option is the number of minutes to wait for another + commit operation before automatically rolling back the configuration + change performed by this task. In other words, this option causes the + module to perform a C(commit confirmed )I(min) where I(min) is the + value of the I(confirmed) option. This option DOES NOT confirm a + previous C(commit confirmed )I(min) operation. To confirm a previous + commit operation, invoke this module with the I(check) or I(commit) + option set to C(true). + required: false + default: none + type: int + aliases: + - confirm + dest: + description: + - The path to a file, on the local Ansible control machine, where the + configuration will be saved if the I(retrieve) option is specified. + - The file must be writeable. If the file already exists, it is + overwritten. + - This option is only valid if the I(retrieve) option is not C(none). + - When tasks are executed against more than one target host, + one process is forked for each target host. (Up to the maximum + specified by the forks configuration. See + U(forks|http://docs.ansible.com/ansible/latest/intro_configuration.html#forks) + for details.) This means that the value of this option must be unique + per target host. This is usually accomplished by including + C({{ inventory_hostname }}) in the I(dest) value. It is the user's + responsibility to ensure this value is unique per target host. + - For this reason, this option is deprecated. It is maintained for + backwards compatibility. Use the I(dest_dir) option in new playbooks. + The I(dest) and I(dest_dir) options are mutually exclusive. + required: false + default: none + type: path + aliases: + - destination + dest_dir: + description: + - The path to a directory, on the Ansible control machine. This is the + directory where the configuration will be saved if the I(retrieve) + option is specified. It is also the directory where the configuration + diff will be specified if the I(diff) option is C(true). + - This option is only valid if the I(retrieve) option is not C(none) or + the I(diff) option is C(true). + - The retrieved configuration will be saved to a file named + C({{ inventory_hostname }}.)I(format_extension) in the I(dest_dir) + directory. Where I(format_extension) is C(conf) for text format, C(xml) + for XML format, C(json) for JSON format, and C(set) for set format. + - If the I(diff) option is C(true), the configuration diff will be saved + to a file named C({{ inventory_hostname }}.diff) in the I(dest_dir) + directory. + - The destination file must be writeable. If the file already exists, + it is overwritten. It is the users responsibility to ensure a unique + I(dest_dir) value is provided for each execution of this module + within a playbook. + - The I(dest_dir) and I(dest) options are mutually exclusive. The + I(dest_dir) option is recommended for all new playbooks. + - The I(dest_dir) and I(diff_file) options are mutually exclusive. The + I(dest_dir) option is recommended for all new playbooks. + required: false + default: none + type: path + aliases: + - destination_dir + - destdir + - savedir + - save_dir + diff: + description: + - Perform a configuration compare (aka diff) operation. + required: false + default: true (false if retrieve is set and load and rollback are not set) + type: bool + aliases: + - compare + - diffs + diffs_file: + description: + - The path to a file, on the Ansible control machine, where the + configuration differences will be saved if the I(diff) option is + specified. + - The file must be writeable. If the file already exists, it is + overwritten. + - This option is only valid if the I(diff) option is C(true). + - When tasks are executed against more than one target host, + one process is forked for each target host. (Up to the maximum + specified by the forks configuration. See + U(forks|http://docs.ansible.com/ansible/latest/intro_configuration.html#forks) + for details.) This means that the value of this option must be unique + per target host. This is usually accomplished by including + C({{ inventory_hostname }}) in the I(diffs_file) value. It is the + user's responsibility to ensure this value is unique per target host. + - For this reason, this option is deprecated. It is maintained for + backwards compatibility. Use the I(dest_dir) option in new playbooks. + - The I(diffs_file) and I(dest_dir) options are mutually exclusive. + required: false + default: None + type: path + format: + description: + - Specifies the format of the configuration retrieved, if I(retrieve) + is not C(none). + - Specifies the format of the configuration to be loaded, if I(load) is + not C(none). + - The specified format must be supported by the target Junos device. + required: false + default: none (auto-detect on load, text on retrieve) + type: str + choices: + - xml + - set + - text + - json + filter: + description: + - A string of XML, or '/'-separated configuration hierarchies, + which specifies a filter used to restrict the portions of the + configuration which are retrieved. See + U(PyEZ's get_config method documentation|http://junos-pyez.readthedocs.io/en/stable/jnpr.junos.html#jnpr.junos.rpcmeta._RpcMetaExec.get_config) + for details on the value of this option. + required: false + default: none + type: 'str' + aliases: + - filter_xml + ignore_warning: + description: + - A boolean, string or list of strings. If the value is C(true), + ignore all warnings regardless of the warning message. If the value + is a string, it will ignore warning(s) if the message of each warning + matches the string. If the value is a list of strings, ignore + warning(s) if the message of each warning matches at least one of the + strings in the list. The value of the I(ignore_warning) option is + applied to the load and commit operations performed by this module. + required: false + default: none + type: bool, str, or list of str + model: + description: + - Specifies yang model openconfig/custom/ietf to fetch. + - When model is True and filter_xml is None, xml is enclosed under + so that we get junos as well as other model configurations. + - In case of custom, user will have to provide the namespace to be fetched + using I(namespace) option. + required: false + default: none + choices: + - openconfig + - ietf + - custom + namespace: + description: + - Used with I(model) option. Specifies the custom namespace to be fetched + from the database. + required: false + default: none + type: str + lines: + description: + - Used with the I(load) option. Specifies a list of list of + configuration strings containing the configuration to be loaded. + - The I(src), I(lines), I(template), and I(url) options are mutually + exclusive. + - By default, the format of the configuration data is auto-dectected by + the content of the first line in the I(lines) list. + - If the I(format) option is specified, the I(format) value overrides the + format auto-detection. + required: false + default: none + type: list + load: + description: + - Specifies the type of load operation to be performed. + - The I(load) and I(rollback) options are mutually exclusive. + - > + The choices have the following meanings: + - B(none) - Do not perform a load operation. + - B(merge) - Combine the new configuration with the existing + configuration. If statements in the new configuration conflict with + statements in the existing configuration, the statements in + the new configuration replace those in the existing + configuration. + - B(replace) - This option is a superset of the B(merge) option. It + combines the new configuration with the existing configuration. If the + new configuration is in text format and a hierarchy level in the new + configuartion is prefixed with the string C(replace:), then the + hierarchy level in the new configuration replaces the entire + corresponding hierarchy level in the existing configuration, regardles + of the existence or content of that hierarchy level in the existing + configuration. If the configuration is in XML format, the XML attribute + C(replace = "replace") is equivalent to the text format's C(replace:) + prefix. If a configuration hierarchy in the new configuration is not + prefixed with C(replace:), then the B(merge) behavior is used. + Specifically, for any statements in the new configuration which + conflict with statements in the existing configuration, the statements + in the new configuration replace those in the existing configuration. + - B(override) - Discard the entire existing configuration and replace it + with the new configuration. When the configuration is later committed, + all system processes are notified and the entire new configuration is + marked as 'changed' even if some statements previously existed in the + configuration. The value B(overwrite) is a synonym for B(override). + - B(update) - This option is similar to the B(override) option. The new + configuration completely replaces the existing configuration. The + difference comes when the configuration is later committed. This option + performs a 'diff' between the new candidate configuration and the + existing committed configuration. It then only notifies system + processes repsonsible for the changed portions of the configuration, + and only marks the actual configuration changes as 'changed'. + - B(set) - This option is used when the new configuration data is in set + format (a series of configuration mode commands). The new configuration + data is loaded line by line and may contain any configuration mode + commands, such as set, delete, edit, or deactivate. This value must be + specified if the new configuration is in set format. + required: false + default: none + choices: + - none + - set + - merge + - update + - replace + - override + - overwrite + type: str + options: + description: + - Additional options, specified as a dictionary of key/value pairs, used + when retrieving the configuration. See the + U( RPC documentation| + https://www.juniper.net/documentation/en_US/junos/topics/reference/tag-summary/junos-xml-protocol-get-configuration.html) + for information on available options. + required: false + default: None + type: dict + retrieve: + description: + - The configuration database to be retrieved. + required: false + default: none + choices: + - none + - candidate + - committed + type: str + return_output: + description: + - Indicates if the output of the I(diff) and I(retreive) options should + be returned in the module's response. You might want to set this option + to C(false), and set the I(dest_dir) option, if the configuration or + diff output is very large and you only need to save the output rather + than using it's content in subsequent tasks/plays of your playbook. + required: false + default: true + type: bool + rollback: + description: + - Populate the candidate configuration from a previously committed + configuration. This value can be a configuration number between 0 and + 49, or the keyword C(rescue) to load the previously saved rescue + configuration. + - By default, some Junos platforms store fewer than 50 previous + configurations. Specifying a value greater than the number + of previous configurations available, or specifying C(rescue) when no + rescue configuration has been saved, will result in an error when the + module attempts to perform the rollback. + - The I(rollback) and I(load) options are mutually exclusive. + required: false + default: none + choices: + - 0-49 + - rescue + type: int or str + src: + description: + - Used with the I(load) option. Specifies the path to a file, on the + local Ansible control machine, containing the configuration to be + loaded. + - The I(src), I(lines), I(template), and I(url) options are mutually + exclusive. + - By default, the format of the configuration data is determined by the + file extension of this path name. If the file has a C(.conf) + extension, the content is treated as text format. If the file has a + C(.xml) extension, the content is treated as XML format. If the file + has a C(.set) extension, the content is treated as Junos B(set) + commands. + - If the I(format) option is specified, the I(format) value overrides the + file-extension based format detection. + required: false + default: none + type: 'path' + aliases: + - source + - file + template: + description: + - The path to a Jinja2 template file, on the local Ansible control + machine. This template file, along with the I(vars) option, is used to + generate the configuration to be loaded on the target Junos device. + - The I(src), I(lines), I(template), and I(url) options are mutually + exclusive. + - The I(template) and I(vars) options are required together. If one is + specified, the other must be specified. + required: false + default: none + type: path + aliases: + - template_path + url: + description: + - A URL which specifies the configuration data to load on the target + Junos device. + - The Junos device uses this URL to load the configuration, therefore + this URL must be reachable by the target Junos device. + - The possible formats of this value are documented in the 'url' section + of the + U( RPC documentation| + https://www.juniper.net/documentation/en_US/junos/topics/reference/tag-summary/junos-xml-protocol-load-configuration.html). + - The I(src), I(lines), I(template), and I(url) options are mutually + exclusive. + required: false + default: none + type: str + vars: + description: + - A dictionary of keys and values used to render the Jinja2 template + specified by the I(template) option. + - The I(template) and I(vars) options are required together. If one is + specified, the other must be specified. + required: false + default: none + type: dict + aliases: + - template_vars +""" + +EXAMPLES = """ +--- +- name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: false + + tasks: + - name: Retrieve the committed configuration + juniper.device.config: + retrieve: 'committed' + diff: false + check: false + commit: false + register: response + + - name: Print the lines in the config. + ansible.builtin.debug: + var: response.config_lines + + - name: Append .foo to the hostname using private config mode. + juniper.device.config: + config_mode: 'private' + load: 'merge' + lines: + - "set system host-name {{ inventory_hostname }}.foo" + register: response + + - name: Print the config changes. + ansible.builtin.debug: + var: response.diff_lines + + - name: Rollback to the previous config. + juniper.device.config: + config_mode: 'private' + rollback: 1 + register: response + + - name: Print the config changes. + ansible.builtin.debug: + var: response.diff_lines + + - name: Rollback to the rescue config. + juniper.device.config: + rollback: 'rescue' + register: response + - name: Print the complete response. + ansible.builtin.debug: + var: response + + - name: Load override from a file. + juniper.device.config: + load: 'override' + src: "{{ inventory_hostname }}.conf" + register: response + + - name: Print the complete response. + ansible.builtin.debug: + var: response + + - name: Load from a Jinja2 template. + juniper.device.config: + load: 'merge' + format: 'xml' + template: "{{ inventory_hostname }}.j2" + vars: + host: "{{ inventory_hostname }}" + register: response + - name: Print the complete response. + ansible.builtin.debug: + var: response + + - name: Load from a file on the Junos device. + juniper.device.config: + load: 'merge' + url: "{{ inventory_hostname }}.conf" + register: response + - name: Print the complete response. + ansible.builtin.debug: + var: response + + - name: Load from a file on the Junos device, skip the commit check + juniper.device.config: + load: 'merge' + url: "{{ inventory_hostname }}.conf" + check: false + register: response + - name: Print the msg. + ansible.builtin.debug: + var: response.msg + + - name: Print diff between current and rollback 10. No check. No commit. + juniper.device.config: + rollback: 11 + diff: true + check: false + commit: false + register: response + + - name: Print the msg. + ansible.builtin.debug: + var: response + + - name: Retrieve [edit system services] of current committed config. + juniper.device.config: + retrieve: 'committed' + filter: 'system/services' + diff: true + check: false + commit: false + register: response + + - name: Print the resulting config lines. + ansible.builtin.debug: + var: response.config_lines + + - name: Enable NETCONF SSH and traceoptions, save config, and diffs. + juniper.device.config: + load: 'merge' + lines: + - 'set system services netconf ssh' + - 'set system services netconf traceoptions flag all' + - 'set system services netconf traceoptions file netconf.log' + format: 'set' + retrieve: 'candidate' + filter: 'system/services' + comment: 'Enable NETCONF with traceoptions' + dest_dir: './output' + register: response + + - name: Print the complete response + ansible.builtin.debug: + var: response + + - name: Load conf. Confirm within 5 min. Wait 3 secs between chk and commit + juniper.device.config: + load: 'merge' + url: "{{ inventory_hostname }}.conf" + confirm: 5 + check_commit_wait: 3 + register: response + + - name: Print the complete response + ansible.builtin.debug: + var: response + + - name: Confirm the previous commit with a commit check (but no commit) + juniper.device.config: + check: true + diff: false + commit: false + register: response + + - name: Print the complete response + ansible.builtin.debug: + var: response + + - name: fetch config from the device with filter and login credentials + juniper.device.config: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "22" + retrieve: 'committed' + format: xml + commit: no + check: no + diff: no + dest_dir: "/tmp/" + filter: re0 + return_output: True + register: config_output +""" + +RETURN = """ +changed: + description: + - Indicates if the device's configuration has changed, or would have + changed when in check mode. + returned: success + type: bool +config: + description: + - The retrieved configuration. The value is a single multi-line + string in the format specified by the I(format) option. + returned: when I(retrieved) is not C(none) and I(return_output) is C(true). + type: str +config_lines: + description: + - The retrieved configuration. The value is a list of single-line + strings in the format specified by the I(format) option. + returned: when I(retrieved) is not C(none) and I(return_output) is C(true). + type: list +config_parsed: + description: + - The retrieved configuration parsed into a JSON datastructure. + For XML replies, the response is parsed into JSON using the + jxmlease library. For JSON the response is parsed using the + Python json library. + - When Ansible converts the jxmlease or native Python data + structure into JSON, it does not guarantee that the order of + dictionary/object keys are maintained. + returned: when I(retrieved) is not C(none), the I(format) option is C(xml) or + C(json) and I(return_output) is C(true). + type: dict +diff: + description: + - The configuration differences between the previous and new + configurations. The value is a dict that contains a single key named + "prepared". Value associated with that key is a single multi-line string + in "diff" format. + returned: when I(load) or I(rollback) is specified, I(diff) is C(true), and + I(return_output) is C(true). + type: dict +diff_lines: + description: + - The configuration differences between the previous and new + configurations. The value is a list of single-line strings in "diff" + format. + returned: when I(load) or I(rollback) is specified, I(diff) is C(true), and + I(return_output) is C(true). + type: list +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +file: + description: + - The value of the I(src) option. + returned: when I(load) is not C(none) and I(src) is not C(none) + type: str +msg: + description: + - A human-readable message indicating the result. + returned: always + type: str +""" + + +# Standard library imports +import time + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + # Choices which are defined in the common module. + config_format_choices = juniper_junos_common.CONFIG_FORMAT_CHOICES + config_database_choices = [None] + juniper_junos_common.CONFIG_DATABASE_CHOICES + config_action_choices = [None] + juniper_junos_common.CONFIG_ACTION_CHOICES + config_mode_choices = juniper_junos_common.CONFIG_MODE_CHOICES + config_model_choices = juniper_junos_common.CONFIG_MODEL_CHOICES + + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + ignore_warning=dict(required=False, type="list", default=None), + config_mode=dict( + choices=config_mode_choices, + type="str", + required=False, + aliases=["config_access", "edit_mode", "edit_access"], + default="exclusive", + ), + ephemeral_instance=dict(type="str", required=False, default=None), + rollback=dict(type="str", required=False, default=None), + load=dict( + choices=config_action_choices, type="str", required=False, default=None + ), + src=dict( + type="path", required=False, aliases=["source", "file"], default=None + ), + lines=dict(type="list", required=False, default=None), + template=dict( + type="path", required=False, aliases=["template_path"], default=None + ), + vars=dict( + type="dict", required=False, aliases=["template_vars"], default=None + ), + url=dict(type="str", required=False, default=None), + format=dict( + choices=config_format_choices, type="str", required=False, default=None + ), + model=dict( + required=False, choices=config_model_choices, type="str", default=None + ), + remove_ns=dict(required=False, type="bool", default=None), + namespace=dict(required=False, type="str", default=None), + check=dict( + required=False, + type="bool", + aliases=["check_commit", "commit_check"], + default=None, + ), + diff=dict( + required=False, type="bool", aliases=["compare", "diffs"], default=None + ), + diffs_file=dict(type="path", required=False, default=None), + dest_dir=dict( + required=False, + type="path", + aliases=["destination_dir", "destdir", "savedir", "save_dir"], + default=None, + ), + return_output=dict(required=False, type="bool", default=True), + retrieve=dict( + choices=config_database_choices, + type="str", + required=False, + default=None, + ), + options=dict(type="dict", required=False, default={}), + filter=dict( + required=False, type="str", aliases=["filter_xml"], default=None + ), + dest=dict( + type="path", required=False, aliases=["destination"], default=None + ), + commit=dict(required=False, type="bool", default=None), + commit_empty_changes=dict(required=False, type="bool", default=False), + commit_full=dict(required=False, type="bool", default=False), + commit_sync=dict(required=False, type="bool", default=False), + commit_force_sync=dict(required=False, type="bool", default=False), + confirmed=dict( + required=False, type="int", aliases=["confirm"], default=None + ), + timeout=dict(required=False, type="int", default=30), + comment=dict(required=False, type="str", default=None), + check_commit_wait=dict(required=False, type="int", default=None), + ), + # Mutually exclusive options. + mutually_exclusive=[ + ["load", "rollback"], + ["src", "lines", "template", "url"], + ["diffs_file", "dest_dir"], + ["dest", "dest_dir"], + ], + # Required together options. + required_together=[["template", "vars"]], + # Check mode is implemented. + supports_check_mode=True, + min_jxmlease_version=cfg.MIN_JXMLEASE_VERSION, + ) + # Do additional argument verification. + + # Parse ignore_warning value + ignore_warning = junos_module.parse_ignore_warning_option() + + # Straight from params + config_mode = junos_module.params.get("config_mode") + ephemeral_instance = junos_module.params.get("ephemeral_instance") + + # Parse rollback value + rollback = junos_module.parse_rollback_option() + + # Straight from params + load = junos_module.params.get("load") + src = junos_module.params.get("src") + lines = junos_module.params.get("lines") + template = junos_module.params.get("template") + vars = junos_module.params.get("vars") + url = junos_module.params.get("url") + format = junos_module.params.get("format") + check = junos_module.params.get("check") + diff = junos_module.params.get("diff") + diffs_file = junos_module.params.get("diffs_file") + dest_dir = junos_module.params.get("dest_dir") + return_output = junos_module.params.get("return_output") + retrieve = junos_module.params.get("retrieve") + options = junos_module.params.get("options") + filter = junos_module.params.get("filter") + dest = junos_module.params.get("dest") + commit = junos_module.params.get("commit") + commit_empty_changes = junos_module.params.get("commit_empty_changes") + commit_full = junos_module.params.get("commit_full") + commit_sync = junos_module.params.get("commit_sync") + commit_force_sync = junos_module.params.get("commit_force_sync") + confirmed = junos_module.params.get("confirmed") + timeout = junos_module.params.get("timeout") + comment = junos_module.params.get("comment") + check_commit_wait = junos_module.params.get("check_commit_wait") + model = junos_module.params.get("model") + remove_ns = junos_module.params.get("remove_ns") + namespace = junos_module.params.get("namespace") + + # Ephemeral database doesn't support "show | compare", + # so setting diff to False. + if config_mode == "ephemeral": + diff = False + + # If retrieve is set and load and rollback are not set, then + # check, diff, and commit default to False. + if retrieve is not None and load is None and rollback is None: + if diff is None: + diff = False + if check is None: + check = False + if commit is None: + commit = False + # Otherwise, diff, check, and commit default to True. + else: + if diff is None: + diff = True + if check is None: + check = True + if commit is None: + commit = True + + # If load is not None, must have one of src, template, url, lines + if load is not None: + for option in ["src", "lines", "template", "url"]: + if junos_module.params.get(option) is not None: + break + # for/else only executed if we didn't break out of the loop. + else: + junos_module.fail_json( + msg="The load option (%s) is specified, " + "but none of 'src', 'lines', " + "'template', or 'url' are specified. " + "Must specify one of the 'src', " + "'lines', 'template', or 'url' options." % (load) + ) + + # format is valid if retrieve is not None or load is not None. + if format is not None: + if load is None and retrieve is None: + junos_module.fail_json( + msg="The format option (%s) is specified, " + "but neither 'load' or 'retrieve' are " + "specified. Must specify one of " + "'load' or 'retrieve' options." % (format) + ) + + # dest_dir is valid if retrieve is not None or diff is True. + if dest_dir is not None: + if retrieve is None and diff is False: + junos_module.fail_json( + msg="The dest_dir option (%s) is specified," + " but neither 'retrieve' or 'diff' " + "are specified. Must specify one of " + "'retrieve' or 'diff' options." % (dest_dir) + ) + + # dest is valid if retrieve is not None + if dest is not None: + if retrieve is None: + junos_module.fail_json( + msg="The dest option (%s) is specified," + " but 'retrieve' is not specified. " + "Must specify the 'retrieve' option." % (dest) + ) + + # diffs_file is valid if diff is True + if diffs_file is not None: + if diff is False: + junos_module.fail_json( + msg="The diffs_file option (%s) is " + "specified, but 'diff' is false." % (diffs_file) + ) + + # commit_empty_changes is valid if commit is True + if commit_empty_changes is True: + if commit is False: + junos_module.fail_json( + msg="The commit_empty_changes option " + "is true, but 'commit' is false. " + "The commit_empty_changes option " + "may only be specified when " + "'commit' is true." + ) + + # comment is valid if commit is True + if comment is not None: + if commit is False: + junos_module.fail_json( + msg="The comment option (%s) is " + "specified, but 'commit' is false." % (comment) + ) + + # confirmed is valid if commit is True + if confirmed is not None: + if commit is False: + junos_module.fail_json( + msg="The confirmed option (%s) is " + "specified, but 'commit' is false." % (confirmed) + ) + # Must be greater >= 1. + if confirmed < 1: + junos_module.fail_json( + msg="The confirmed option (%s) must have a " + "positive integer value." % (confirmed) + ) + + # check_commit_wait is valid if check is True and commit is True + if check_commit_wait is not None: + if commit is False: + junos_module.fail_json( + msg="The check_commit_wait option (%s) is " + "specified, but 'commit' is false." % (check_commit_wait) + ) + if check is False: + junos_module.fail_json( + msg="The check_commit_wait option (%s) is " + "specified, but 'check' is false." % (check_commit_wait) + ) + # Must be greater >= 1. + if check_commit_wait < 1: + junos_module.fail_json( + msg="The check_commit_wait option (%s) " + "must have a positive integer value." % (check_commit_wait) + ) + + # Initialize the results. Assume failure until we know it's success. + results = {"msg": "Configuration has been: ", "changed": False, "failed": True} + + junos_module.logger.debug("Step 1 - Open a candidate configuration database.") + junos_module.open_configuration( + mode=config_mode, + ignore_warning=ignore_warning, + ephemeral_instance=ephemeral_instance, + ) + results["msg"] += "opened" + + junos_module.logger.debug( + "Step 2 - Load configuration data into the candidate configuration database." + ) + if rollback is not None: + junos_module.rollback_configuration(id=rollback) + # Assume configuration changed in case we don't perform a diff later. + # If diff is set, we'll check for actual differences later. + results["changed"] = True + results["msg"] += ", rolled back" + elif load is not None: + if src is not None: + junos_module.load_configuration( + action=load, src=src, ignore_warning=ignore_warning, format=format + ) + results["file"] = src + elif lines is not None: + junos_module.load_configuration( + action=load, lines=lines, ignore_warning=ignore_warning, format=format + ) + elif template is not None: + junos_module.load_configuration( + action=load, + template=template, + vars=vars, + ignore_warning=ignore_warning, + format=format, + ) + elif url is not None: + junos_module.load_configuration( + action=load, url=url, ignore_warning=ignore_warning, format=format + ) + else: + junos_module.fail_json( + msg="The load option was set to: %s, but " + "no 'src', 'lines', 'template', or " + "'url' option was set." % (load) + ) + # Assume configuration changed in case we don't perform a diff later. + # If diff is set, we'll check for actual differences later. + results["changed"] = True + results["msg"] += ", loaded" + + junos_module.logger.debug( + "Step 3 - Check the validity of the candidate configuration database." + ) + if check is True: + junos_module.check_configuration() + results["msg"] += ", checked" + + junos_module.logger.debug( + "Step 4 - Determine differences between the " + "candidate and committed configuration " + "databases." + ) + if diff is True or junos_module._diff: + diff = junos_module.diff_configuration(ignore_warning) + if diff is not None: + results["changed"] = True + if return_output is True or junos_module._diff: + results["diff"] = {"prepared": diff} + results["diff_lines"] = diff.splitlines() + # Save the diff output + junos_module.save_text_output("diff", "diff", diff) + else: + results["changed"] = False + results["msg"] += ", diffed" + + junos_module.logger.debug( + "Step 5 - Retrieve the configuration database from the Junos device." + ) + if retrieve is not None: + if format is None: + format = "text" + (config, config_parsed) = junos_module.get_configuration( + database=retrieve, + format=format, + options=options, + filter=filter, + model=model, + namespace=namespace, + remove_ns=remove_ns, + ) + if return_output is True: + if config is not None: + results["config"] = config + results["config_lines"] = config.splitlines() + if config_parsed is not None: + results["config_parsed"] = config_parsed + # Save the output + format_extension = "config" if format == "text" else format + junos_module.save_text_output("config", format_extension, config) + results["msg"] += ", retrieved" + + junos_module.logger.debug("Step 6 - Commit the configuration changes.") + if commit is True and not junos_module.check_mode: + # Perform the commit if: + # 1) commit_empty_changes is True + # 2) Neither rollback or load is set. i.e. confirming a previous commit + # 3) rollback or load is set, and there were actual changes. + if ( + commit_empty_changes is True + or (rollback is None and load is None) + or ( + (rollback is not None or load is not None) + and results["changed"] is True + ) + ): + if check_commit_wait is not None: + time.sleep(check_commit_wait) + junos_module.commit_configuration( + ignore_warning=ignore_warning, + comment=comment, + confirmed=confirmed, + timeout=timeout, + full=commit_full, + sync=commit_sync, + force_sync=commit_force_sync, + ) + results["msg"] += ", committed" + else: + junos_module.logger.debug("Skipping commit. Nothing changed.") + + junos_module.logger.debug("Step 7 - Close the candidate configuration database.") + junos_module.close_configuration() + results["msg"] += ", closed." + + # If we made it this far, everything was successful. + results["failed"] = False + + # Return response. + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/facts.py b/ansible_collections/juniper/device/plugins/modules/facts.py new file mode 100644 index 00000000..fec4df04 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/facts.py @@ -0,0 +1,373 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: facts +author: "Juniper Networks - Stacy Smith (@stacywsmith)" +short_description: Retrieve facts from a Junos device +description: + - Retrieve facts from a Junos device using the + U(PyEZ fact gathering system|http://junos-pyez.readthedocs.io/en/stable/jnpr.junos.facts.html). + - Also returns the committed configuration of the Junos device if the + I(config_format) option has a value other than C(none). +options: + config_format: + description: + - The format of the configuration returned. The specified format must be + supported by the target Junos device. + required: false + default: none + choices: + - none + - xml + - set + - text + - json + savedir: + description: + - A path to a directory, on the Ansible control machine, where facts + will be stored in a JSON file. + - The resulting JSON file is saved in + I(savedir)C(/)I(hostname)C(-facts.json). + - The I(savedir) directory is the value of the I(savedir) option. + - The I(hostname)C(-facts.json) filename begins with the value of the + C(hostname) fact returned from the Junos device, which might be + different than the value of the I(host) option passed to the module. + - If the value of the I(savedir) option is C(none), the default, then + facts are NOT saved to a file. + required: false + default: none + type: path +""" + +EXAMPLES = """ +--- +- name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: false + + tasks: + - name: "Get facts" + juniper.device.facts: + register: response + + - name: Facts with login credentials + juniper.device.facts: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "22" + + - name: Facts in telnet mode + juniper.device.facts: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "23" + mode: "telnet" + +# Print a fact + +# Using config_format option + +# Print the config + +# Using savedir option + +# Print the saved JSON file +""" + +RETURN = """ +ansible_facts.junos: + description: + - Facts collected from the Junos device. This dictionary contains the + keys listed in the I(contains) section of this documentation PLUS all + of the keys returned from PyEZ's fact gathering system. See + U(PyEZ facts|http://junos-pyez.readthedocs.io/en/stable/jnpr.junos.facts.html) + for a complete list of these keys and their meaning. + returned: success + type: complex + contains: + config: + description: + - The device's committed configuration, in the format specified by + I(config_format), as a single multi-line string. + returned: when I(config_format) is not C(none). + type: str + has_2RE: + description: + - Indicates if the device has more than one Routing Engine installed. + Because Ansible does not allow keys to begin with a number, this fact + is returned in place of PyEZ's C(2RE) fact. + returned: success + type: bool + re_name: + description: + - The name of the current Routing Engine to which Ansible is connected. + returned: success + type: str + master_state: + description: + - The mastership state of the Routing Engine to which Ansible is + connected. C(true) if the RE is the master Routing Engine. C(false) + if the RE is not the master Routing Engine. + returned: success + type: bool +changed: + description: + - Indicates if the device's state has changed. Since this module does not + change the operational or configuration state of the device, the value is + always set to C(false). + returned: success + type: bool + sample: false +facts: + description: + - Returned for backwards compatibility. Returns the same keys and values + which are returned under I(ansible_facts.junos). + returned: success + type: dict +failed: + description: + - Indicates if the task failed. + returned: always + type: bool + sample: false +""" + +# Standard library imports +import json +import os.path + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +from ansible.module_utils._text import to_bytes + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def get_facts_dict(junos_module): + """Retreive PyEZ facts and convert to a standard dict w/o custom types. + + Ansible >= 2.0 doesn't like custom objects in a modules return value. + Because PyEZ facts are a custom object rather than a true dict they must be + converted to a standard dict. Since facts are read-only, we must begin by + copying facts into a dict. Since PyEZ facts are "on-demand", the + junos_module.dev instance must be an open PyEZ Device instance ojbect + before this function is called. + + Args: + junos_module: An instance of a JuniperJunosModule. + + Returns: + A dict containing the device facts. + """ + if junos_module.conn_type == "local": + dev = junos_module.dev + # Retrieve all PyEZ-supported facts and copy to a standard dict. + facts = dict(dev.facts) + # Add two useful facts that are implement as PyEZ Device attributes. + facts["re_name"] = dev.re_name + facts["master_state"] = dev.master + else: + facts = junos_module.get_facts() + # Ansible doesn't allow keys starting with numbers. + # Replace the '2RE' key with the 'has_2RE' key. + if "2RE" in facts: + facts["has_2RE"] = facts["2RE"] + del facts["2RE"] + # The value of the 'version_info' key is a custom junos.version_info + # object. Convert this value to a dict. + if "version_info" in facts and facts["version_info"] is not None: + facts["version_info"] = dict(facts["version_info"]) + # The values of the ['junos_info'][re_name]['object'] keys are + # custom junos.version_info objects. Convert all of these to dicts. + if "junos_info" in facts and facts["junos_info"] is not None: + for key in facts["junos_info"]: + facts["junos_info"][key]["object"] = dict( + facts["junos_info"][key]["object"] + ) + return facts + + +def save_facts(junos_module, facts): + """If the savedir option was specified, save the facts into a JSON file. + + If the savedir option was specified, save the facts into a JSON file named + savedir/hostname-facts.json. The filename begins with the value of the + hostname fact returned from the Junos device, which might be different than + the value of the host option passed to the module. + + Args: + junos_module: An instance of a JuniperJunosModule. + facts: The facts dict returned by get_facts_dict(). + + Raises: + IOError: Calls junos_module.fail_json if unable to open the facts + file for writing. + """ + if junos_module.params.get("savedir") is not None: + save_dir = junos_module.params.get("savedir") + file_name = "%s-facts.json" % (facts["hostname"]) + file_path = os.path.normpath(os.path.join(save_dir, file_name)) + junos_module.logger.debug("Saving facts to: %s.", file_path) + try: + # TODO: Verify does this work with Python3 + with open(file_path, "w") as fact_file: + json.dump(facts, fact_file) + junos_module.logger.debug("Facts saved to: %s.", file_path) + except IOError: + junos_module.fail_json( + msg="Unable to save facts. Failed to open " "the %s file." % (file_path) + ) + + +def save_inventory(junos_module, inventory): + """If the savedir option was specified, save the XML inventory. + + If the savedir option was specified, save the inventory XML output into + an XML file named savedir/hostname-inventory.xml. The filename begins with + the value of the hostname fact returned from the Junos device, which might + be different than the value of the host option passed to the module. + + Args: + junos_module: An instance of a JuniperJunosModule. + inventory: The XML string of inventory to save. + + Raises: + IOError: Calls junos_module.fail_json if unable to open the inventory + file for writing. + """ + if junos_module.conn_type == "local": + dev = junos_module.dev + file_name = "%s-inventory.xml" % (dev.facts["hostname"]) + else: + facts = junos_module._pyez_conn.get_facts() + file_name = "%s-inventory.xml" % (facts["hostname"]) + if junos_module.params.get("savedir") is not None: + save_dir = junos_module.params.get("savedir") + file_path = os.path.normpath(os.path.join(save_dir, file_name)) + junos_module.logger.debug("Saving inventory to: %s.", file_path) + try: + with open(file_path, "wb") as fact_file: + fact_file.write(to_bytes(inventory, encoding="utf-8")) + junos_module.logger.debug("Inventory saved to: %s.", file_path) + except IOError: + junos_module.fail_json( + msg="Unable to save inventory. Failed to " + "open the %s file." % (file_path) + ) + + +def main(): + config_format_choices = [None] + config_format_choices += juniper_junos_common.CONFIG_FORMAT_CHOICES + + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + config_format=dict( + choices=config_format_choices, required=False, default=None + ), + savedir=dict(type="path", required=False, default=None), + ), + # Since this module doesn't change the device's configuration, there is + # no additional work required to support check mode. It's inherently + # supported. + supports_check_mode=True, + min_jxmlease_version=cfg.MIN_JXMLEASE_VERSION, + ) + + junos_module.logger.debug("Gathering facts.") + # Get the facts dictionary from the device. + facts = get_facts_dict(junos_module) + junos_module.logger.debug("Facts gathered.") + + if junos_module.params.get("savedir") is not None: + # Save the facts. + save_facts(junos_module, facts) + + # Get and save the inventory + try: + junos_module.logger.debug("Gathering inventory.") + if junos_module.conn_type == "local": + inventory = junos_module.dev.rpc.get_chassis_inventory() + else: + inventory = junos_module.get_chassis_inventory() + junos_module.logger.debug("Inventory gathered.") + save_inventory( + junos_module, junos_module.etree.tostring(inventory, pretty_print=True) + ) + except junos_module.pyez_exception.RpcError as ex: + junos_module.fail_json( + msg="Unable to retrieve hardware " "inventory: %s" % (str(ex)) + ) + + config_format = junos_module.params.get("config_format") + if config_format is not None: + (config, config_parsed) = junos_module.get_configuration(format=config_format) + if config is not None: + facts.update({"config": config}) + # Need to wait until the ordering issues are figured out before + # using config_parsed. + # if config_parsed is not None: + # facts.update({'config_parsed': config_parsed}) + + # Return response. + junos_module.exit_json( + changed=False, failed=False, ansible_facts={"junos": facts}, facts=facts + ) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/file_copy.py b/ansible_collections/juniper/device/plugins/modules/file_copy.py new file mode 100644 index 00000000..7cdc6872 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/file_copy.py @@ -0,0 +1,158 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2024, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: file_copy +author: "Juniper Networks - Dinesh Babu (@dineshbaburam91)" +short_description: File put and get over SCP module +description: + - Copy file over SCP to and from a Juniper device +options: + local_dir: + description: + - path of the local directory where the file is located + or needs to be copied to + required: true + type: str + remote_dir: + description: + - path of the directory on the remote device where the file is located + or needs to be copied to + required: true + type: str + file: + description: + - Name of the file to copy to/from the remote device + required: true + type: str + action: + description: + - Type of operation to execute, currently only support get and put + required: true + type: str +""" + +EXAMPLES = """ +--- +- name: Examples of juniper_device_file_copy + hosts: all + connection: local + gather_facts: false + tasks: + - name: Copy a log file on a remote device locally + juniper.device.file_copy: + remote_dir: /var/log + local_dir: /tmp + action: get + file: log.txt + - name: Copy a local file into /var/tmp on the remote device + juniper.device.file_copy: + remote_dir: /var/tmp + local_dir: /tmp + action: put + file: license.txt +""" + +RETURN = """ +changed: + description: + - Indicates if the device's state has changed. + returned: when the file has been successfully copied. + type: bool +""" + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + + # The argument spec for the module. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + local_dir=dict(type="str", required=True, default=None), + remote_dir=dict(type="str", required=True, default=None), + file=dict(type="str", required=True, default=None), + action=dict( + type="str", choices=["put", "get"], required=True, default=None + ), + ), + supports_check_mode=True, + min_jxmlease_version=cfg.MIN_JXMLEASE_VERSION, + ) + + # Set initial results values. Assume failure until we know it's success. + results = {"msg": "", "changed": False, "failed": False} + output = [] + # We're going to be using params a lot + params = junos_module.params + + params["remote_dir"] + local_file = params["local_dir"] + "/" + params["file"] + remote_file = params["remote_dir"] + "/" + params["file"] + + if params["action"] == "put": + output = junos_module.scp_file_copy_put(local_file, remote_file) + results["msg"] = output[0] + results["changed"] = output[1] + elif params["action"] == "get": + output = junos_module.scp_file_copy_get(remote_file, local_file) + results["msg"] = output[0] + results["changed"] = output[1] + + # Return results. + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/jsnapy.py b/ansible_collections/juniper/device/plugins/modules/jsnapy.py new file mode 100644 index 00000000..1a66ba20 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/jsnapy.py @@ -0,0 +1,446 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: jsnapy +author: + - Juniper Networks + - Roslan Zaki + - Damien Garros + - Stacy Smith (@stacywsmith)" +short_description: Execute JSNAPy tests on a Junos device +description: + - Execute Junos SNAPshot Adminsitrator (JSNAPy) tests against a Junos device. + JSNAPy is documented on U(Github|https://github.com/Juniper/jsnapy) and + this + U(Day One Book|https://www.juniper.net/documentation/en_US/day-one-books/DO_JSNAPy.zip) + - This module only reports C(failed) if the module encounters an error and + fails to execute the JSNAPy tests. If does NOT report C(failed) if one or + more of the JSNAPy tests fail. To check the test results, register the + module's response and use the assert module to verify the expected result + in the response. (See :ref:`jsnapy-examples-label`.) + - A callback plugin which formats and prints JSNAPy test results for human + consumption is also available. This callback plugin is enabled by adding + C(callback_whitelist = jsnapy) to the Ansible configuration file. +options: + action: + description: + - The JSNAPy action to perform. + required: true + default: none + type: str + choices: + - check + - snapcheck + - snap_pre + - snap_post + config_file: + description: + - The filename of a JSNAPy configuration file (in YAML format). The + I(test_files) option and the I(config_file) option are mutually + exclusive. Either the I(test_files) option or the I(config_file) + option is required. + required: false + type: path + default: none + dir: + description: + - The path to the directory containing the JSNAPy test file(s) specified + by the I(test_files) option or the JSNAPy configuration file specified + by the I(config_file) option. + required: false + type: path + default: /etc/jsnapy/testfiles + aliases: + - directory + test_files: + description: + - The filename of file(s) in the I(dir) directory. Each file contains + JSNAPy test case definitions. The I(test_files) option and the + I(config_file) option are mutually exclusive. Either the I(test_files) + option or the I(config_file) option is required. + required: false + type: list of path + default: none +""" + + +EXAMPLES = """ +--- +- name: Examples of jsnapy + hosts: junos-all + connection: local + gather_facts: false + + tasks: + - name: JUNOS Post Checklist + juniper.device.jsnapy: + action: "snap_post" + config_file: "first_test.yml" + logfile: "migration_post.log" + register: test1 + - name: Verify all JSNAPy tests passed + ansible.builtin.assert: + that: + - "test1.passPercentage == 100" + - name: Print the full test response + ansible.builtin.debug: + var: test1 + + - name: Test based on a test_file directly + juniper.device.jsnapy: + action: "snapcheck" + test_files: "tests/test_junos_interface.yaml" + register: test2 + - name: Verify all JSNAPy tests passed + ansible.builtin.assert: + that: + - "test2.passPercentage == 100" + - name: Print the full test response + ansible.builtin.debug: + var: test2 + + - name: "Collect Pre Snapshot" + juniper.device.jsnapy: + action: "snap_pre" + test_files: "tests/test_loopback.yml" + + - name: "Collect Post Snapshot" + juniper.device.jsnapy: + action: "snap_post" + test_files: "tests/test_loopback.yml" + + - name: "Check after Pre and Post Snapshots" + juniper.device.jsnapy: + action: "check" + test_files: "tests/test_loopback.yml" + register: test3 + - name: Verify all JSNAPy tests passed + ansible.builtin.assert: + that: + - "test3.|succeeded" + - "test3.passPercentage == 100" + - name: Print the full test response + ansible.builtin.debug: + var: test3 +""" + +RETURN = """ +action: + description: + - The JSNAPy action performed as specified by the I(action) option. + returned: success + type: str +changed: + description: + - Indicates if the device's state has changed. Since this module doesn't + change the operational or configuration state of the device, the value + is always set to C(false). + returned: success + type: bool +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +# final_result: +msg: + description: + - A human-readable message indicating the result of the JSNAPy tests. + returned: always + type: str +# total_passed: +# total_failed: +""" + +# Standard Library imports +import os.path + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + JSNAPY_ACTION_CHOICES = ["check", "snapcheck", "snap_pre", "snap_post"] + + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + action=dict( + required=True, choices=JSNAPY_ACTION_CHOICES, type="str", default=None + ), + test_files=dict(required=False, type="list", default=None), + config_file=dict(required=False, type="path", default=None), + dest_dir=dict( + required=False, + type="path", + aliases=["destination_dir", "destdir"], + default=None, + ), + dir=dict( + required=False, + type="path", + aliases=["directory"], + default="/etc/jsnapy/testfiles", + ), + ), + # Mutually exclusive options. + mutually_exclusive=[["test_files", "config_file"]], + # One of test_files or config_file is required. + required_one_of=[["test_files", "config_file"]], + supports_check_mode=True, + min_jsnapy_version=cfg.MIN_JSNAPY_VERSION, + ) + + # Straight from params + action = junos_module.params.get("action") + test_files = junos_module.params.get("test_files") + config_file = junos_module.params.get("config_file") + dir = junos_module.params.get("dir") + dest_dir = junos_module.params.get("dest_dir") + + # Initialize the results. Assume failure until we know otherwise. + results = {"msg": "", "action": action, "changed": False, "failed": True} + + if config_file is not None: + junos_module.logger.debug("Checking config file: %s.", config_file) + config_file_path = os.path.abspath(config_file) + config_dir_file_path = os.path.abspath(os.path.join(dir, config_file)) + if os.path.isfile(config_file_path): + data = config_file_path + elif os.path.isfile(config_dir_file_path): + data = config_dir_file_path + else: + junos_module.fail_json( + msg="Unable to locate the %s config file " + "at %s or %s." % (config_file, config_file_path, config_dir_file_path) + ) + elif test_files is not None and len(test_files) > 0: + data = {"tests": []} + for test_file in test_files: + junos_module.logger.debug("Checking test file: %s.", test_file) + test_file_path = os.path.abspath(test_file) + test_dir_file_path = os.path.abspath(os.path.join(dir, test_file)) + if os.path.isfile(test_file_path): + data["tests"].append(test_file_path) + elif os.path.isfile(test_dir_file_path): + data["tests"].append(test_dir_file_path) + else: + junos_module.fail_json( + msg="Unable to locate the %s test file " + "at %s or %s." % (test_file, test_file_path, test_dir_file_path) + ) + else: + junos_module.fail_json(msg="No config_file or test_files specified.") + + try: + junos_module.logger.debug("Creating jnpr.jsnapy.SnapAdmin instance.") + jsa = junos_module.jsnapy.SnapAdmin() + junos_module.logger.debug("Executing %s action.", action) + if action == "check": + if junos_module.conn_type != "local": + responses = junos_module._pyez_conn.invoke_jsnapy( + data=data, action="check" + ) + else: + responses = jsa.check( + data=data, dev=junos_module.dev, pre_file="PRE", post_file="POST" + ) + elif action == "snapcheck": + if junos_module.conn_type != "local": + responses = junos_module._pyez_conn.invoke_jsnapy( + data=data, action="snapcheck" + ) + else: + responses = jsa.snapcheck( + data=data, dev=junos_module.dev, pre_file="PRE" + ) + elif action == "snap_pre": + if junos_module.conn_type != "local": + responses = junos_module._pyez_conn.invoke_jsnapy( + data=data, action="snap_pre" + ) + else: + responses = jsa.snap(data=data, dev=junos_module.dev, file_name="PRE") + elif action == "snap_post": + if junos_module.conn_type != "local": + responses = junos_module._pyez_conn.invoke_jsnapy( + data=data, action="snap_post" + ) + else: + responses = jsa.snap(data=data, dev=junos_module.dev, file_name="POST") + else: + junos_module.fail_json(msg="Unexpected action: %s." % (action)) + junos_module.logger.debug("The %s action executed successfully.", action) + except ( + junos_module.pyez_exception.RpcError, + junos_module.pyez_exception.ConnectError, + ) as ex: + junos_module.fail_json( + msg="Error communicating with the device: %s" % (str(ex)) + ) + except Exception as ex: + junos_module.fail_json(msg="Uncaught exception - please report: %s" % (str(ex))) + + if isinstance(responses, list) and len(responses) == 1: + if action in ("snapcheck", "check"): + for response in responses: + results["device"] = response.device + results["router"] = response.device + results["final_result"] = response.result + results["total_passed"] = response.no_passed + results["total_failed"] = response.no_failed + results["test_results"] = response.test_results + total_tests = int(response.no_passed) + int(response.no_failed) + results["total_tests"] = total_tests + pass_percentage = 0 + if total_tests > 0: + pass_percentage = (int(response.no_passed) * 100) // total_tests + results["passPercentage"] = pass_percentage + results["pass_percentage"] = pass_percentage + if results["final_result"] == "Failed": + results["msg"] = "Test Failed: Passed %s, Failed %s" % ( + results["total_passed"], + results["total_failed"], + ) + else: + results["msg"] = "Test Passed: Passed %s, Failed %s" % ( + results["total_passed"], + results["total_failed"], + ) + elif action in ("snap_pre", "snap_post"): + results["msg"] = "The %s action successfully executed." % (action) + elif isinstance(responses, dict) and len(responses) >= 1: + if action in ("snapcheck", "check"): + results["device"] = responses["device"] + results["router"] = responses["router"] + results["final_result"] = responses["final_result"] + results["total_passed"] = responses["total_passed"] + results["total_failed"] = responses["total_failed"] + results["test_results"] = responses["test_results"] + total_tests = int(responses["total_passed"]) + int( + responses["total_failed"] + ) + results["total_tests"] = total_tests + pass_percentage = 0 + if total_tests > 0: + pass_percentage = (int(responses["total_passed"]) * 100) // total_tests + results["passPercentage"] = pass_percentage + results["pass_percentage"] = pass_percentage + if results["final_result"] == "Failed": + results["msg"] = "Test Failed: Passed %s, Failed %s" % ( + results["total_passed"], + results["total_failed"], + ) + else: + results["msg"] = "Test Passed: Passed %s, Failed %s" % ( + results["total_passed"], + results["total_failed"], + ) + elif action in ("snap_pre", "snap_post"): + results["msg"] = "The %s action successfully executed." % (action) + else: + junos_module.fail_json( + msg="Unexpected JSNAPy responses. Type: %s." + "Responses: %s" % (type(responses), str(responses)) + ) + + # If we made it this far, it's success. + results["failed"] = False + + # To save the failed test_name details to dest_dir + # for connection local + if dest_dir is not None: + if action in ("snapcheck", "check"): + if junos_module.conn_type == "local": + for response_loc in responses: + results_loc = response_loc.test_details + for cmd, data in results_loc.items(): + for data1 in data: + if ("test_name" in data1.keys()) and ( + "result" in data1.keys() + ): + if data1["result"] is False: + test_name = ( + str(data1["test_name"]) + + "_" + + str(data1["result"]) + ) + text_msg = str(data) + junos_module.save_text_output( + test_name, "text", text_msg + ) + else: + # For connection pyez + for res in results["test_results"]: + for data in results["test_results"][res]: + for key, value in data.items(): + if ("test_name" in data.keys()) and ( + "result" in data.keys() + ): + if data["result"] is False: + test_name = ( + str(data["test_name"]) + + "_" + + str(data["result"]) + ) + text_msg = str(data) + junos_module.save_text_output( + test_name, "text", text_msg + ) + + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/ping.py b/ansible_collections/juniper/device/plugins/modules/ping.py new file mode 100644 index 00000000..da4688e0 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/ping.py @@ -0,0 +1,499 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: ping +author: Juniper Networks - Stacy Smith (@stacywsmith) +short_description: Execute ping from a Junos device +description: + - Execute the ping command from a Junos device to a specified destination in + order to test network reachability from the Junos device . +options: + acceptable_percent_loss: + description: + - Maximum percentage of packets that may be lost and still consider the + task not to have failed. + required: false + default: 0 + type: int + aliases: + - acceptable_packet_loss + count: + description: + - Number of packets to send. + required: false + default: 5 + type: int + dest: + description: + - The IP address, or hostname if DNS is configured on the Junos device, + used as the destination of the ping. + required: true + default: none + type: str + aliases: + - dest_ip + - dest_host + - destination + - destination_ip + - destination_host + do_not_fragment: + description: + - Set Do Not Fragment bit on ping packets. + required: false + default: false + type: bool + interface: + description: + - The source interface from which the the ping is sent. If not + specified, the default Junos algorithm for determining the source + interface is used. + required: false + default: none + type: str + rapid: + description: + - Send ping requests rapidly + required: false + default: true + type: bool + routing_instance: + description: + - Name of the source routing instance from which the ping is + originated. If not specified, the default routing instance is used. + required: false + default: none + type: str + size: + description: + - The size of the ICMP payload of the ping. + - Total size of the IP packet is I(size) + the 20 byte IP header + + the 8 byte ICMP header. Therefore, I(size) of C(1472) generates an IP + packet of size 1500. + required: false + default: none (default size for device) + type: int + source: + description: + - The IP address, or hostname if DNS is configured on the Junos device, + used as the source address of the ping. If not specified, the Junos + default algorithm for determining the source address is used. + required: false + default: none + type: str + aliases: + - source_ip + - source_host + - src + - src_ip + - src_host + ttl: + description: + - Maximum number of IP routers (hops) allowed between source and + destination. + required: false + default: none (default ttl for device) + type: int +""" + +EXAMPLES = """ +--- +- name: Examples of ping + hosts: junos-all + connection: local + gather_facts: false + + tasks: + - name: Ping 192.68.1.1 with default parameters. Fails if any packets lost. + juniper.device.ping: + dest: "192.68.1.1" + + - name: Ping 192.68.1.1 Allow 50% packet loss. Register response. + juniper.device.ping: + dest: "192.68.1.1" + acceptable_percent_loss: 50 + register: response + - name: Print all keys in the response. + ansible.builtin.debug: + var: response + + - name: Ping 192.68.1.1. Send 20 packets. Register response. + juniper.device.ping: + dest: "192.68.1.1" + count: 20 + register: response + - name: Print packet sent from the response. + ansible.builtin.debug: + var: response.packets_sent + + - name: Ping 192.68.1.1. Send 10 packets wihtout rapid. Register response. + juniper.device.ping: + dest: "192.68.1.1" + count: 10 + rapid: false + register: response + - name: Print the average round-trip-time from the response. + ansible.builtin.debug: + var: response.rtt_average + + - name: Ping www.juniper.net with ttl 15. Register response. + juniper.device.ping: + dest: "www.juniper.net" + ttl: 15 + register: response + - name: Print the packet_loss percentage from the response. + ansible.builtin.debug: + var: response.packet_loss + + - name: Ping 192.68.1.1 with IP packet size of 1500. Register response. + juniper.device.ping: + dest: "192.68.1.1" + size: 1472 + register: response + - name: Print the packets_received from the response. + ansible.builtin.debug: + var: response.packets_received + + - name: Ping 192.68.1.1 with do-not-fragment bit set. Register response. + juniper.device.ping: + dest: "192.68.1.1" + do_not_fragment: true + register: response + - name: Print the maximum round-trip-time from the response. + ansible.builtin.debug: + var: response.rtt_maximum + + - name: Ping 192.68.1.1 with source set to 192.68.1.2. Register response. + juniper.device.ping: + dest: "192.68.1.1" + source: "192.68.1.2" + register: response + - name: Print the source from the response. + ansible.builtin.debug: + var: response.source + + - name: Ping 192.168.1.1 from the red routing-instance. + juniper.device.ping: + dest: "192.168.1.1" + routing_instance: "red" + + - name: Ping the all-hosts multicast address from the ge-0/0/0.0 interface + juniper.device.ping: + dest: "224.0.0.1" + interface: "ge-0/0/0.0" +""" + +RETURN = """ +acceptable_percent_loss: + description: + - The acceptable packet loss (as a percentage) for this task as specified + by the I(acceptable_percent_loss) option. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: str +changed: + description: + - Indicates if the device's state has changed. Since this module + doesn't change the operational or configuration state of the + device, the value is always set to C(false). + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: bool +count: + description: + - The number of pings sent, as specified by the I(count) option. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: str +do_not_fragment: + description: + - Whether or not the do not fragment bit was set on the pings sent, as + specified by the I(do_not_fragment) option. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: bool +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +host: + description: + - The destination IP/host of the pings sent as specified by the I(dest) + option. + - Keys I(dest) and I(dest_ip) are also returned for backwards + compatibility. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: str +interface: + description: + - The source interface of the pings sent as specified by the + I(interface) option. + returned: when ping successfully executed and the I(interface) option was + specified, even if the I(acceptable_percent_loss) was exceeded. + type: str +msg: + description: + - A human-readable message indicating the result. + returned: always + type: str +packet_loss: + description: + - The percentage of packets lost. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: str +packets_sent: + description: + - The number of packets sent. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: str +packets_received: + description: + - The number of packets received. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: str +rapid: + description: + - Whether or not the pings were sent rapidly, as specified by the + I(rapid) option. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: bool +routing_instance: + description: + - The routing-instance from which the pings were sent as specified by + the I(routing_instance) option. + returned: when ping successfully executed and the I(routing_instance) + option was specified, even if the I(acceptable_percent_loss) was + exceeded. + type: str +rtt_average: + description: + - The average round-trip-time, in microseconds, of all ping responses + received. + returned: when ping successfully executed, and I(packet_loss) < 100%. + type: str +rtt_maximum: + description: + - The maximum round-trip-time, in microseconds, of all ping responses + received. + returned: when ping successfully executed, and I(packet_loss) < 100%. + type: str +rtt_minimum: + description: + - The minimum round-trip-time, in microseconds, of all ping responses + received. + returned: when ping successfully executed, and I(packet_loss) < 100%. + type: str +rtt_stddev: + description: + - The standard deviation of round-trip-time, in microseconds, of all ping + responses received. + returned: when ping successfully executed, and I(packet_loss) < 100%. + type: str +size: + description: + - The size in bytes of the ICMP payload on the pings sent as specified + by the I(size) option. + - Total size of the IP packet is I(size) + the 20 byte IP header + the 8 + byte ICMP header. Therefore, I(size) of 1472 generates an IP packet of + size 1500. + returned: when ping successfully executed and the I(size) option was + specified, even if the I(acceptable_percent_loss) was exceeded. + type: str +source: + description: + - The source IP/host of the pings sent as specified by the I(source) + option. + - Key I(source_ip) is also returned for backwards compatibility. + returned: when ping successfully executed and the I(source) option was + specified, even if the I(acceptable_percent_loss) was exceeded. + type: str +timeout: + description: + - The number of seconds to wait for a response from the ping RPC. + returned: when ping successfully executed, even if the + I(acceptable_percent_loss) was exceeded. + type: str +ttl: + description: + - The time-to-live set on the pings sent as specified by the + I(ttl) option. + returned: when ping successfully executed and the I(ttl) option was + specified, even if the I(acceptable_percent_loss) was exceeded. + type: str +warnings: + description: + - A list of warning strings, if any, produced from the ping. + returned: when warnings are present + type: list +""" + + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + # The argument spec for the module. + argument_spec = dict( + dest=dict( + type="str", + required=True, + aliases=[ + "dest_ip", + "dest_host", + "destination", + "destination_ip", + "destination_host", + ], + default=None, + ), + acceptable_percent_loss=dict( + type="int", required=False, aliases=["acceptable_packet_loss"], default=0 + ), + ) + + # The portion of the argument spec that's specifically a parameter + # to the ping RPC. + ping_argument_spec = dict( + count=dict(type="int", required=False, default=5), + rapid=dict(type="bool", required=False, default=True), + ttl=dict(type="int", required=False, default=None), + size=dict(type="int", required=False, default=None), + do_not_fragment=dict(type="bool", required=False, default=False), + source=dict( + type="str", + required=False, + aliases=["source_ip", "source_host", "src", "src_ip", "src_host"], + default=None, + ), + interface=dict(type="str", required=False, default=None), + routing_instance=dict(type="str", required=False, default=None), + ) + + # Add the ping RPC parameter argument spec fo the full argument_spec. + argument_spec.update(ping_argument_spec) + + argument_spec_keys = list(argument_spec.keys()) + + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=argument_spec, + # Since this module doesn't change the device's configuration, there is + # no additional work required to support check mode. It's inherently + # supported. + supports_check_mode=True, + ) + + # We're going to be using params a lot + params = junos_module.params + + # acceptable packet loss is a percentage. Check to make sure it's between + # 0 and 100 inclusive + if params["acceptable_percent_loss"] > 100 or params["acceptable_percent_loss"] < 0: + junos_module.fail_json( + msg="The value of the acceptable_percent_loss" + "option (%d) is a percentage and must have " + "a value between 0 and 100." % (params["acceptable_percent_loss"]) + ) + + # All of the params keys which are also keys in ping_argument_spec are the + # ping_params. Omit None and False values because they don't need to be + # passed to the RPC. + ping_params = {"host": params.get("dest")} + for key in ping_argument_spec: + value = params.get(key) + # Convert int (but not bool) to str + if not isinstance(value, bool) and isinstance(value, int): + params[key] = str(params[key]) + value = params.get(key) + # None and False values are the default for the RPC and shouldn't be + # passed to the device. + if value is not None and value is not False: + ping_params.update({key: value}) + + # Set initial results values. Assume failure until we know it's success. + results = {"msg": "", "changed": False, "failed": True} + # Results should include all the ping params in argument_spec_keys. + for key in argument_spec_keys: + results[key] = params.get(key) + # Overwrite to be a string in the results + results["acceptable_percent_loss"] = str(params.get("acceptable_percent_loss")) + # Add timeout to the response even though it's a connect parameter. + results["timeout"] = str(params.get("timeout")) + # Add aliases for backwards compatibility + results.update( + { + "host": params.get("dest"), + "dest_ip": params.get("dest"), + "source_ip": params.get("source"), + } + ) + + # Execute the ping. + results = junos_module.ping( + ping_params, + acceptable_percent_loss=params["acceptable_percent_loss"], + results=results, + ) + + # Return results. + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/pmtud.py b/ansible_collections/juniper/device/plugins/modules/pmtud.py new file mode 100644 index 00000000..c4110b25 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/pmtud.py @@ -0,0 +1,416 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: pmtud +author: + - Martin Komon (@mkomon) + - Juniper Networks - Stacy Smith (@stacywsmith) +short_description: Perform path MTU discovery from a Junos device to a + destination +description: + - Determine the maximum IP MTU supported along a path from a Junos device to + a user-specified destination by performing path MTU discovery (PMTUD) using + the ping command. The reported MTU will be between min_test_size and + I(max_size) where I(min_test_size) = (I(max_size) - I(max_range) + 1). + If the actual path MTU is greater than I(max_size), then I(max_size) will + be reported. If the actual path MTU is less than I(min_test_size), then a + failure will be reported. +options: + dest: + description: + - The IPv4 address, or hostname if DNS is configured on the Junos device, + used as the destination of the PMTUD. + required: true + default: none + type: str + aliases: + - dest_ip + - dest_host + - destination + - destination_ip + - destination_host + interface: + description: + - The source interface from which the the PMTUD is performed. If not + specified, the default Junos algorithm for determining the source + interface is used. + required: false + default: none + type: str + max_range: + description: + - The maximum range of MTU values, in bytes, which will be searched + when performing path MTU discovery. This value must be C(0) or + a power of 2 (2^n) between C(2) and C(65536). The minimum IPv4 MTU + value attempted when performing path MTU discovery is + I(min_test_size) = (I(max_size) - I(max_range) + 1) + required: false + default: 512 + type: int + max_size: + description: + - The maximum IPv4 MTU, in bytes, to attempt when performing path MTU + discovery. + - The value returned for I(inet_mtu) will be no more + than this value even if the path actually supports a higher MTU. + - This value must be between 68 and 65496. + required: false + default: 1500 + type: int + routing_instance: + description: + - Name of the source routing instance from which the ping is + originated. + - If not specified, the default routing instance is used. + required: false + default: none + type: str + source: + description: + - The IPv4 address, or hostname if DNS is configured on the Junos device, + used as the source address of the PMTUD. If not specified, the Junos + default algorithm for determining the source address is used. + required: false + default: none + type: str + aliases: + - source_ip + - source_host + - src + - src_ip + - src_host +""" + +EXAMPLES = """ +--- +- name: Examples of pmtud + hosts: junos-all + connection: local + gather_facts: false + + tasks: + - name: Perform PMTUD to 192.68.1.1 with default parameters. + juniper.device.pmtud: + dest: "192.68.1.1" + + - name: Perform PMTUD to 192.68.1.1. Register response. + juniper.device.pmtud: + dest: "192.68.1.1" + register: response + - name: Print the discovered MTU. + ansible.builtin.debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Search all possible MTU values. + juniper.device.pmtud: + dest: "192.68.1.1" + max_size: 65496 + max_range: 65536 + register: response + - name: Print the discovered MTU. + ansible.builtin.debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Source from ge-0/0/0.0 interface. + juniper.device.pmtud: + dest: "192.68.1.1" + interface: "ge-0/0/0.0" + register: response + - name: Print the discovered MTU. + ansible.builtin.debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Source from 192.168.1.2. + juniper.device.pmtud: + dest: "192.68.1.1" + source: "192.168.1.2" + register: response + - name: Print the discovered MTU. + ansible.builtin.debug: + var: response.inet_mtu + + - name: Perform PMTUD to 192.68.1.1. Source from the red routing-instance. + juniper.device.pmtud: + dest: "192.68.1.1" + routing_instance: "red" + register: response + - name: Print the discovered MTU. + ansible.builtin.debug: + var: response.inet_mtu +""" + +RETURN = """ +changed: + description: + - Indicates if the device's state has changed. Since this module + doesn't change the operational or configuration state of the + device, the value is always set to C(false). + returned: when PMTUD successfully executed. + type: bool +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +host: + description: + - The destination IP/host of the PMTUD as specified by the I(dest) + option. + - Keys I(dest) and I(dest_ip) are also returned for backwards + compatibility. + returned: when PMTUD successfully executed. + type: str +inet_mtu: + description: + - The IPv4 path MTU size in bytes to the I(dest). This is the lesser of + I(max_size) and the actual path MTU to I(dest). If the actual path + MTU is less than I(min_test_size), then a failure is reported. Where + I(min_test_size) = (I(max_size) - I(max_range) + 1) + returned: when PMTUD successfully executed. + type: str +interface: + description: + - The source interface of the PMTUD as specified by the I(interface) + option. + returned: when the I(interface) option was specified. + type: str +routing_instance: + description: + - The routing-instance from which the PMTUD was performed as specified by + the I(routing_instance) option. + returned: when the I(routing_instance) option was specified. + type: str +source: + description: + - The source IP/host of the PMTUD as specified by the I(source) + option. + - Key I(source_ip) is also returned for backwards compatibility. + returned: when the I(source) option was specified. + type: str +warnings: + description: + - A list of warning strings, if any, produced from the ping. + returned: when warnings are present + type: list +""" + + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + # Constants for MTU size + INET_MIN_MTU_SIZE = 68 # As prescribed by RFC 791, Section 3.2 - + # Fragmentation and Reassembly. + INET_MAX_MTU_SIZE = 65496 # Size of inet header's total length field is + # 16 bits. Therefore max inet packet size is 2^16 + # or 65536, but Junos only supports max IP size + # of 65496 for the ping command in order to + # accomodate a (potentially) maximum sized IP + # header. + + # Constants for the size of headers + INET_HEADER_SIZE = 20 + ICMP_HEADER_SIZE = 8 + INET_AND_ICMP_HEADER_SIZE = INET_HEADER_SIZE + ICMP_HEADER_SIZE + + # Choices for max_size + MAX_SIZE_CHOICES = [0] + list(map(lambda x: 2**x, range(1, 17))) + + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + dest=dict( + type="str", + required=True, + aliases=[ + "dest_ip", + "dest_host", + "destination", + "destination_ip", + "destination_host", + ], + default=None, + ), + max_size=dict(type="int", required=False, default=1500), + max_range=dict( + type="int", required=False, choices=MAX_SIZE_CHOICES, default=512 + ), + source=dict( + type="str", + required=False, + aliases=["source_ip", "source_host", "src", "src_ip", "src_host"], + default=None, + ), + interface=dict(type="str", required=False, default=None), + routing_instance=dict(type="str", required=False, default=None), + ), + # Since this module doesn't change the device's configuration, there is + # no additional work required to support check mode. It's inherently + # supported. + supports_check_mode=True, + ) + + # We're going to be using params a lot + params = junos_module.params + + # max_size must be between INET_MIN_MTU_SIZE and INET_MAX_MTU_SIZE + if params["max_size"] < INET_MIN_MTU_SIZE or params["max_size"] > INET_MAX_MTU_SIZE: + junos_module.fail_json( + msg="The value of the max_size option(%d) " + "must be between %d and %d." + % (params["max_size"], INET_MIN_MTU_SIZE, INET_MAX_MTU_SIZE) + ) + + # Initialize ping parameters. + ping_params = { + "host": params.get("dest"), + "count": "3", + "rapid": True, + "inet": True, + "do_not_fragment": True, + } + + # Add optional ping parameters + o_ping_params = {} + if params["source"] is not None: + o_ping_params["source"] = params["source"] + if params["interface"] is not None: + o_ping_params["interface"] = params["interface"] + if params["routing_instance"] is not None: + o_ping_params["routing_instance"] = params["routing_instance"] + ping_params.update(o_ping_params) + + # Set initial results values. Assume failure until we know it's success. + results = { + "changed": False, + "failed": True, + "inet_mtu": 0, + "host": params.get("dest"), + } + # Results should include all the o_ping_params. + for key in o_ping_params: + results[key] = ping_params.get(key) + # Add aliases for backwards compatibility + results.update( + { + "dest": ping_params.get("host"), + "dest_ip": ping_params.get("host"), + "source_ip": ping_params.get("source"), + } + ) + + # Execute a minimally-sized ping just to verify basic connectivity. + junos_module.logger.debug("Verifying basic connectivity.") + ping_params["size"] = str(INET_MIN_MTU_SIZE - INET_AND_ICMP_HEADER_SIZE) + results_for_minimal = dict(results) + results_for_minimal = junos_module.ping( + ping_params, acceptable_percent_loss=100, results=results_for_minimal + ) + if round(float(results_for_minimal.get("packet_loss", 100))) == 100: + results["msg"] = "Basic connectivity to %s failed." % (results["host"]) + junos_module.exit_json(**results) + + # Initialize test_size and step + test_size = params["max_size"] + step = params["max_range"] + min_test_size = test_size - (params["max_range"] - 1) + if min_test_size < INET_MIN_MTU_SIZE: + min_test_size = INET_MIN_MTU_SIZE + + while True: + if test_size < INET_MIN_MTU_SIZE: + test_size = INET_MIN_MTU_SIZE + if test_size > params["max_size"]: + test_size = params["max_size"] + junos_module.logger.debug("Probing with size: %d", test_size) + step = step // 2 if step >= 2 else 0 + ping_params["size"] = str(test_size - INET_AND_ICMP_HEADER_SIZE) + current_results = dict(results) + current_results = junos_module.ping( + ping_params, acceptable_percent_loss=100, results=current_results + ) + loss = round(float(current_results.get("packet_loss", 100))) + if loss < 100 and test_size == params["max_size"]: + # ping success with max test_size, save and break + results["failed"] = False + results["inet_mtu"] = test_size + break + elif loss < 100: + # ping success, increase test_size + results["failed"] = False + results["inet_mtu"] = test_size + test_size += step + else: + # ping fail, lower size + test_size -= step + if step < 1: + break + + if results.get("inet_mtu", 0) == 0: + junos_module.fail_json( + msg="The MTU of the path to %s is less than " + "the minimum tested size(%d). Try " + "decreasing max_size(%d) or increasing " + "max_range(%d)." + % (results["host"], min_test_size, params["max_size"], params["max_range"]), + **results + ) + + # Return results. + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/rpc.py b/ansible_collections/juniper/device/plugins/modules/rpc.py new file mode 100644 index 00000000..dd32f828 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/rpc.py @@ -0,0 +1,636 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible.module_utils.six import iteritems + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: rpc +author: "Juniper Networks - Stacy Smith (@stacywsmith)" +short_description: Execute one or more NETCONF RPCs on a Junos device +description: + - Execute one or more NETCONF RPCs on a Junos device. + - Use the C(| display xml rpc) modifier to determine the equivalent RPC + name for a Junos CLI command. For example, + C(show version | display xml rpc) reveals the equivalent RPC name is + C(get-software-information). +options: + attrs: + description: + - The attributes and values to the RPCs specified by the + I(rpcs) option. The value of this option can either be a single + dictionary of keywords and values, or a list of dictionaries + containing keywords and values. + - There is a one-to-one correspondence between the elements in the + I(kwargs) list and the RPCs in the I(rpcs) list. In other words, the + two lists must always contain the same number of elements. + required: false + default: none + type: dict or list of dict + aliases: + - attr + dest: + description: + - The path to a file, on the Ansible control machine, where the output of + the RPC will be saved. + - The file must be writeable. If the file already exists, it is + overwritten. + - When tasks are executed against more than one target host, + one process is forked for each target host. (Up to the maximum + specified by the forks configuration. See + U(forks|http://docs.ansible.com/ansible/latest/intro_configuration.html#forks) + for details.) This means that the value of this option must be unique + per target host. This is usually accomplished by including + C({{ inventory_hostname }}) in the I(dest) value. It is the user's + responsibility to ensure this value is unique per target host. + - For this reason, this option is deprecated. It is maintained for + backwards compatibility. Use the I(dest_dir) option in new playbooks. + The I(dest) and I(dest_dir) options are mutually exclusive. + required: false + default: None + type: path + aliases: + - destination + dest_dir: + description: + - The path to a directory, on the Ansible control machine, where + the output of the RPC will be saved. The output will be logged + to a file named C({{ inventory_hostname }}_)I(rpc).I(format) + in the I(dest_dir) directory. + - The destination file must be writeable. If the file already exists, + it is overwritten. It is the users responsibility to ensure a unique + I(dest_dir) value is provided for each execution of this module + within a playbook. + - The I(dest_dir) and I(dest) options are mutually exclusive. The + I(dest_dir) option is recommended for all new playbooks. + required: false + default: None + type: path + aliases: + - destination_dir + - destdir + filter: + description: + - This argument only applies if the I(rpcs) option contains a single + RPC with the value C(get-config). When used, this value specifies an + XML filter used to restrict the portions of the configuration which are + retrieved. See the PyEZ + U(get_config method|http://junos-pyez.readthedocs.io/en/stable/jnpr.junos.html#jnpr.junos.rpcmeta._RpcMetaExec.get_config) + for details on the value of this option. + required: false + default: none + type: str + aliases: + - filter_xml + formats: + description: + - The format of the reply for the RPCs specified by the + I(rpcs) option. + - The specified format(s) must be supported by the + target Junos device. + - The value of this option can either be a single + format, or a list of formats. If a single format is specified, it + applies to all RPCs specified by the I(rpcs) option. If a + list of formats are specified, there must be one value in the list for + each RPC specified by the I(rpcs) option. + required: false + default: xml + type: str or list of str + choices: + - text + - xml + - json + aliases: + - format + - display + - output + ignore_warning: + description: + - A boolean, string or list of strings. If the value is C(true), + ignore all warnings regardless of the warning message. If the value + is a string, it will ignore warning(s) if the message of each warning + matches the string. If the value is a list of strings, ignore + warning(s) if the message of each warning matches at least one of the + strings in the list. The value of the I(ignore_warning) option is + applied to the load and commit operations performed by this module. + required: false + default: none + type: bool, str, or list of str + kwargs: + description: + - The keyword arguments and values to the RPCs specified by the + I(rpcs) option. The value of this option can either be a single + dictionary of keywords and values, or a list of dictionaries + containing keywords and values. + - There must be a one-to-one correspondence between the elements in the + I(kwargs) list and the RPCs in the I(rpcs) list. In other words, the + two lists must always contain the same number of elements. For RPC + arguments which do not require a value, specify the value of True as + shown in the :ref:`rpc-examples-label`. + - By default "0" and "1" will be converted to boolean values. In case + it doesn't need to be transformed to boolean pass first kwargs as + required: false + default: none + type: dict or list of dict + aliases: + - kwarg + - args + - arg + return_output: + description: + - Indicates if the output of the RPC should be returned in the + module's response. You might want to set this option to C(false), + and set the I(dest_dir) option, if the RPC output is very large + and you only need to save the output rather than using it's content in + subsequent tasks/plays of your playbook. + required: false + default: true + type: bool + rpcs: + description: + - A list of one or more NETCONF RPCs to execute on the Junos device. + required: true + default: none + type: list + aliases: + - rpc +""" + +EXAMPLES = """ +--- +- name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: false + + tasks: + - name: "Execute RPC with filters" + juniper.device.rpc: + rpcs: + - "get-config" + format: xml + filter: re0 + attr: name=re0 + register: test1 + ignore_errors: true + + - name: Check TEST 1 + ansible.builtin.debug: + var: test1 + + - name: "Execute RPC with host data and store logging" + juniper.device.rpc: + host: "10.x.x.x" + user: "user" + passwd: "user123" + port: "22" + rpcs: + - "get-software-information" + logfile: "/var/tmp/rpc.log" + ignore_warning: true + register: test1 + ignore_errors: true + + - name: "Print results - summary" + ansible.builtin.debug: + var: test1.stdout_lines + + - name: "Execute multiple RPC" + juniper.device.rpc: + rpcs: + - "get-config" + - "get-software-information" + + - name: Get Device Configuration for vlan - 1 + juniper.device.rpc: + rpc: "get-config" + filter_xml: "" + dest: "get_config_vlan.conf" + register: junos + + - name: Get interface information with kwargs + juniper.device.rpc: + rpc: get-interface-information + kwargs: + interface_name: em1 + media: True + format: json + dest: get_interface_information.conf + register: junos""" + +RETURN = """ +attrs: + description: + - The RPC attributes and values from the list of dictionaries in the + I(attrs) option. This will be none if no attributes are applied to the + RPC. + returned: always + type: dict +changed: + description: + - Indicates if the device's state has changed. Since this module doesn't + change the operational or configuration state of the device, the value + is always set to C(false). + - You could use this module to execute an RPC which + changes the operational state of the the device. For example, + C(clear-ospf-neighbor-information). Beware, this module is unable to + detect this situation, and will still return a I(changed) value of + C(false) in this case. + returned: success + type: bool +failed: + description: + - Indicates if the task failed. See the I(results) key for additional + details. + returned: always + type: bool +format: + description: + - The format of the RPC response from the list of formats in the I(formats) + option. + returned: always + type: str + choices: + - text + - xml + - json +kwargs: + description: + - The keyword arguments from the list of dictionaries in the I(kwargs) + option. This will be C(none) if no kwargs are applied to the RPC. + returned: always + type: dict +msg: + description: + - A human-readable message indicating the result. + returned: always + type: str +parsed_output: + description: + - The RPC reply from the Junos device parsed into a JSON datastructure. + For XML replies, the response is parsed into JSON using the + U(jxmlease|https://github.com/Juniper/jxmlease) + library. For JSON the response is parsed using the Python + U(json|https://docs.python.org/2/library/json.html) library. + - When Ansible converts the jxmlease or native Python data structure + into JSON, it does not guarantee that the order of dictionary/object keys + are maintained. + returned: when RPC executed successfully, I(return_output) is C(true), + and the RPC format is C(xml) or C(json). + type: dict +results: + description: + - The other keys are returned when a single RPC is specified for the + I(rpcs) option. When the value of the I(rpcs) option is a list + of RPCs, this key is returned instead. The value of this key is a + list of dictionaries. Each element in the list corresponds to the + RPCs in the I(rpcs) option. The keys for each element in the list + include all of the other keys listed. The I(failed) key indicates if the + individual RPC failed. In this case, there is also a top-level + I(failed) key. The top-level I(failed) key will have a value of C(false) + if ANY of the RPCs ran successfully. In this case, check the value + of the I(failed) key for each element in the I(results) list for the + results of individual RPCs. + returned: when the I(rpcs) option is a list value. + type: list of dict +rpc: + description: + - The RPC which was executed from the list of RPCs in the I(rpcs) option. + returned: always + type: str +stdout: + description: + - The RPC reply from the Junos device as a single multi-line string. + returned: when RPC executed successfully and I(return_output) is C(true). + type: str +stdout_lines: + description: + - The RPC reply from the Junos device as a list of single-line strings. + returned: when RPC executed successfully and I(return_output) is C(true). + type: list of str +""" + + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + rpcs=dict(required=True, type="list", aliases=["rpc"], default=None), + formats=dict( + required=False, + type="list", + aliases=["format", "display", "output"], + default=None, + ), + kwargs=dict( + required=False, + aliases=["kwarg", "args", "arg"], + type="str", + default=None, + ), + attrs=dict(required=False, type="str", aliases=["attr"], default=None), + filter=dict( + required=False, type="str", aliases=["filter_xml"], default=None + ), + dest=dict( + required=False, type="path", aliases=["destination"], default=None + ), + dest_dir=dict( + required=False, + type="path", + aliases=["destination_dir", "destdir"], + default=None, + ), + ignore_warning=dict(required=False, type="list", default=None), + return_output=dict(required=False, type="bool", default=True), + ), + # Since this module doesn't change the device's configuration, there is + # no additional work required to support check mode. It's inherently + # supported. Well, that's not completely true. It does depend on the + # RPC executed. See the I(changed) key in the RETURN documentation + # for more details. + supports_check_mode=True, + min_jxmlease_version=cfg.MIN_JXMLEASE_VERSION, + ) + + # Check over rpcs + rpcs = junos_module.params.get("rpcs") + # Ansible allows users to specify a rpcs argument with no value. + if rpcs is None: + junos_module.fail_json(msg="The rpcs option must have a value.") + + # Parse ignore_warning value + ignore_warning = junos_module.parse_ignore_warning_option() + # Check over formats + formats = junos_module.params.get("formats") + if formats is None: + # Default to xml format + formats = ["xml"] + valid_formats = juniper_junos_common.RPC_OUTPUT_FORMAT_CHOICES + # Check format values + for format in formats: + # Is it a valid format? + if format not in valid_formats: + junos_module.fail_json( + msg="The value %s in formats is invalid. " + "Must be one of: %s" % (format, ", ".join(map(str, valid_formats))) + ) + # Correct number of format values? + if len(formats) != 1 and len(formats) != len(rpcs): + junos_module.fail_json( + msg="The formats option must have a single " + "value, or one value per rpc. There " + "are %d rpcs and %d formats." % (len(rpcs), len(formats)) + ) + # Same format for all rpcs + elif len(formats) == 1 and len(rpcs) > 1: + formats = formats * len(rpcs) + + # Check over kwargs + kwstring = junos_module.params.get("kwargs") + kwargs = junos_module.parse_arg_to_list_of_dicts( + "kwargs", kwstring, allow_bool_values=True + ) + if kwargs is not None: + if len(kwargs) != len(rpcs): + junos_module.fail_json( + msg="The kwargs option must have one value " + "per rpc. There are %d rpcs and %d " + "kwargs." % (len(rpcs), len(kwargs)) + ) + else: + kwargs = [None] * len(rpcs) + + # Check over attrs + attrstring = junos_module.params.get("attrs") + attrs = junos_module.parse_arg_to_list_of_dicts("attrs", attrstring) + if attrs is not None: + if len(attrs) != len(rpcs): + junos_module.fail_json( + msg="The attrs option must have one value" + "per rpc. There are %d rpcs and %d " + "attrs." % (len(rpcs), len(attrs)) + ) + else: + attrs = [None] * len(rpcs) + + # Check filter + if junos_module.params.get("filter") is not None: + if len(rpcs) != 1 or (rpcs[0] != "get-config" and rpcs[0] != "get_config"): + junos_module.fail_json( + msg="The filter option is only valid " + "when the rpcs option value is a " + "single 'get-config' RPC." + ) + + results = list() + for rpc_string, format, kwarg, attr in zip(rpcs, formats, kwargs, attrs): + # Replace underscores with dashes in RPC name. + rpc_string = rpc_string.replace("_", "-") + # Set initial result values. Assume failure until we know it's success. + result = { + "msg": "", + "rpc": rpc_string, + "format": format, + "kwargs": kwarg, + "attrs": attr, + "changed": False, + "failed": True, + } + + # Execute the RPC + try: + # for get-config in case of exception handling it will not display + # filters and arguments. To be added in future. + rpc = junos_module.etree.Element(rpc_string, format=format) + if rpc_string == "get-config": + filter = junos_module.params.get("filter") + if attr is None: + attr = {} + if kwarg is None: + kwarg = {} + if format is not None: + attr["format"] = format + junos_module.logger.debug( + 'Executing "get-config" RPC. ' + "filter_xml=%s, options=%s, " + "kwargs=%s", + filter, + str(attr), + str(kwarg), + ) + # not adding ignore_warning as we don't expect to get rpc-error + # with severity warning during get_config + if junos_module.conn_type == "local": + resp = junos_module.dev.rpc.get_config( + filter_xml=filter, options=attr, **kwarg + ) + else: + resp = junos_module.get_config( + filter_xml=filter, options=attr, **kwarg + ) + result["msg"] = 'The "get-config" RPC executed successfully.' + junos_module.logger.debug( + "The 'get-config' RPC executed successfully." + ) + else: + if kwarg is not None: + # Add kwarg + for key, value in iteritems(kwarg): + # Replace underscores with dashes in key name. + key = key.replace("_", "-") + sub_element = junos_module.etree.SubElement(rpc, key) + if not isinstance(value, bool): + sub_element.text = value + if attr is not None: + # Add attr + for key, value in iteritems(attr): + # Replace underscores with dashes in key name. + key = key.replace("_", "-") + rpc.set(key, value) + junos_module.logger.debug( + 'Executing RPC "%s".', + junos_module.etree.tostring(rpc, pretty_print=True), + ) + if junos_module.conn_type == "local": + resp = junos_module.dev.rpc(rpc, normalize=bool(format == "xml")) + else: + try: + resp = junos_module.get_rpc( + rpc, ignore_warning=ignore_warning, format=format + ) + except Exception as ex: + if "RpcError" in (str(ex)): + raise junos_module.pyez_exception.RpcError + if "ConnectError" in (str(ex)): + raise junos_module.pyez_exception.ConnectError + result["msg"] = "The RPC executed successfully." + junos_module.logger.debug( + 'RPC "%s" executed successfully.', + junos_module.etree.tostring(rpc, pretty_print=True), + ) + except ( + junos_module.pyez_exception.ConnectError, + junos_module.pyez_exception.RpcError, + ) as ex: + junos_module.logger.debug( + 'Unable to execute RPC "%s". Error: %s', + junos_module.etree.tostring(rpc, pretty_print=True), + str(ex), + ) + result["msg"] = "Unable to execute the RPC: %s. Error: %s" % ( + junos_module.etree.tostring(rpc, pretty_print=True), + str(ex), + ) + results.append(result) + continue + + text_output = None + parsed_output = None + if resp is True: + text_output = "" + elif isinstance(resp, junos_module.etree._Element): + # Handle the output based on format + if format == "text": + text_output = resp.text + junos_module.logger.debug("Text output set.") + elif format == "xml": + text_output = junos_module.etree.tostring(resp, pretty_print=True) + parsed_output = junos_module.jxmlease.parse_etree(resp) + junos_module.logger.debug("XML output set.") + elif format == "json": + text_output = str(resp) + parsed_output = resp + junos_module.logger.debug("JSON output set.") + else: + result["msg"] = "Unexpected format %s." % (format) + results.append(result) + junos_module.logger.debug("Unexpected format %s.", format) + continue + else: + result["msg"] = "Unexpected response type %s." % (type(resp)) + results.append(result) + junos_module.logger.debug("Unexpected response type %s.", type(resp)) + continue + + # Set the output keys + if junos_module.params["return_output"] is True: + if text_output is not None: + result["stdout"] = text_output + result["stdout_lines"] = text_output.splitlines() + if parsed_output is not None: + result["parsed_output"] = parsed_output + # Save the output + junos_module.save_text_output(rpc_string, format, text_output) + # This command succeeded. + result["failed"] = False + # Append to the list of results + results.append(result) + + # Return response. + if len(results) == 1: + junos_module.exit_json(**results[0]) + else: + # Calculate the overall failed. Only failed if all commands failed. + failed = True + for result in results: + if result.get("failed") is False: + failed = False + break + junos_module.exit_json(results=results, changed=False, failed=failed) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/software.py b/ansible_collections/juniper/device/plugins/modules/software.py new file mode 100644 index 00000000..679adbc0 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/software.py @@ -0,0 +1,861 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: software +author: + - Jeremy Schulman + - "Juniper Networks - Stacy Smith (@stacywsmith)" +short_description: Install software on a Junos device +description: + - > + Install a Junos OS image, or other software package, on a Junos device. + This action is generally equivalent to the C(request system software add) + operational-mode CLI command. It performs the following + steps in order: + + + #. Compare the currently installed Junos version to the desired version + specified by the I(version) option. + + * If the current and desired versions are the same, stop and return + I(changed) with a value of C(false). + * If running in check mode, and the current and desired versions differ, + stop and return I(changed) with a value of C(true). + * Otherwise, proceed. + #. If the I(local_package) option is specified, compute the MD5 checksum + of the I(local_package) file on the local Ansible control machine. + #. Check if the file exists at the I(remote_package) location on the target + Junos device. If so, compute the MD5 checksum of the file on the target + Junos device. + #. If the I(cleanfs) option is C(true), the default, then perform the + equivalent of the C(request system storage cleanup) CLI command. + #. If the checksums computed in steps 2 and 3 differ, or if the + I(remote_package) file does not exist on the target Junos device, then + copy the package from I(local_package) on the local Ansible control + machine to I(remote_package) on the target Junos device. + #. Install the software pacakge from the I(remote_package) location on the + target Junos device using the options specified. + #. If the I(reboot) option is C(true), the default, initiate a reboot of + the target Junos device. +options: + all_re: + description: + - Whether or not to install the software on all Routing Engines of the + target Junos device. If C(true), and the device has multiple Routing + Engines, the software is installed on all Routing Engines. If C(false), + the software is only installed on the current Routing Engine. + required: false + default: true + type: bool + member_id: + description: + - install software on the specified members ids of VC. + required: false + default: none + type: list + checksum: + description: + - The pre-calculated checksum, using the I(checksum_algorithm) of the + file specified by the I(local_package) option. Specifying this option + is simply an optimization to avoid repeatedly computing the checksum of + the I(local_package) file once for each target Junos host. + required: false + default: none + type: str + checksum_algorithm: + description: + - The algorithm to use when calculating the checksum of the local and + remote software packages. + required: false + default: md5 + type: str + checksum_timeout: + description: + - The number of seconds to wait for the calculation of the checksum to + complete on the target Junos device. + required: false + default: 300 (5 minutes) + type: int + cleanfs: + description: + - Whether or not to perform a C(request system storage cleanup) prior to + copying or installing the software. + required: false + default: true (unless I(no_copy) is C(true), then C(false)) + type: bool + cleanfs_timeout: + description: + - The number of seconds to wait for the + C(request system storage cleanup) to complete on the target Junos + device. + required: false + default: 300 (5 minutes) + type: int + force_host: + description: + - Forces the upgrade of the Host Software package on QFX-series devices. + required: false + default: false + type: bool + install_timeout: + description: + - The number of seconds to wait for the software installation to + complete on the target Junos device. + required: false + default: 1800 (30 minutes) + type: int + issu: + description: + - Indicates if a unified in-service software upgrade (ISSU) should be + attempted. ISSU enables the upgrade between two different + Junos OS releases with no control plane disruption and minimal data + plane traffic disruption. + - In order for an ISSU to succeed, ISSU must be supported. This includes + support for the current to desired Junos versions, the hardware + of the target Junos device, and the current software configuration of + the target Junos device. + - The I(issu) and I(nssu) options are mutually exclusive. + required: false + default: false + type: bool + kwargs: + description: + - Additional keyword arguments and values which are passed to the + C() RPC used to install the software package. The + value of this option is a dictionary of keywords and values. + required: false + default: none + type: dict + aliases: + - kwarg + - args + - arg + local_package: + description: + - The path, on the local Ansible control machine, of a Junos software + package. This Junos software package will be installed on the target + Junos device. + - If this option is specified, and a file with the same MD5 checksum + doesn't already exist at the I(remote_package) location on the target + Junos device, then the file is copied from the local Ansible control + machine to the target Junos device. + - If this option is not specified, it is assumed that the + software package already exists on the target Junos device. In this + case, the I(remote_package) option must be specified. + required: false + default: none + type: path + aliases: + - package + no_copy: + description: + - Indicates if the file containing the software package should be copied + from the I(local_package) location on the local Ansible control + machine to the I(remote_package) location on the target Junos device. + - If the value is C(true), or if the I(local_package) option is not + specified, then the copy is skipped and the file must already exist + at the I(remote_package) location on the target Junos device. + required: false + default: false + type: bool + nssu: + description: + - Indicates if a non-stop software upgrade (NSSU) should be + attempted. NSSU enables the upgrade between two different + Junos OS releases with minimal data plane traffic disruption. + - NSSU is specific to EX-series Virtual Chassis systems or EX-series + stand-alone systems with redundant Routing Engines. + - In order for an NSSU to succeed, NSSU must be supported. This includes + support for the current to desired Junos versions, the hardware + of the target Junos device, and the current software configuration of + the target Junos device. + - The I(nssu) and I(issu) options are mutually exclusive. + required: false + default: false + type: bool + reboot: + description: + - Indicates if the target Junos device should be rebooted after + performing the software install. + required: false + default: true + type: bool + reboot_pause: + description: + - The amount of time, in seconds, to wait after the reboot is issued + before the module returns. This gives time for the reboot to begin. The + default value of 10 seconds is designed to ensure the device is no + longer reachable (because the reboot has begun) when the next task + begins. The value must be an integer greater than or equal to 0. + required: false + default: 10 + type: int + remote_package: + description: + - This option may take one of two formats. + - The first format is a URL, from the perspective of the target Junos + device, from which the device retrieves the software package to be + installed. The acceptable formats for the URL value may be found + U(here|https://www.juniper.net/documentation/en_US/junos/topics/concept/junos-software-formats-filenames-urls.html). + - When using the URL format, the I(local_package) and I(no_copy) options + must not be specified. + - The second format is a file path, on the taget Junos device, to the + software package. + - If the I(local_package) option is also specified, and the + I(no_copy) option is C(false), the software package will be copied + from I(local_package) to I(remote_package), if necessary. + - If the I(no_copy) option is C(true) or the I(local_package) option + is not specified, then the file specified by this option must already + exist on the target Junos device. + - If this option is not specified, it is assumed that the software + package will be copied into the C(/var/tmp) directory on the target + Junos device using the filename portion of the I(local_package) option. + In this case, the I(local_package) option must be specified. + - Specifying the I(remote_package) option and not specifying the + I(local_package) option is equivalent to specifying the + I(local_package) option and the I(no_copy) option. In this case, + you no longer have to explicitly specify the I(no_copy) option. + - If the I(remote_package) value is a directory (ends with /), then + the filename portion of I(local_package) will be appended to the + I(remote_package) value. + - If the I(remote_package) value is a file (does not end with /), + then the filename portion of I(remote_package) must be the same as + the filename portion of I(local_package). + required: false + default: C(/var/tmp/) + filename portion of I(local_package) + type: path + pkg_set: + description: + - install software on the members in a mixed Virtual Chassis. Currently + we are not doing target package check this option is provided. + required: false + default: false + type: list + validate: + description: + - Whether or not to have the target Junos device should validate the + current configuration against the new software package. + required: false + default: false + type: bool + version: + description: + - The version of software contained in the file specified by the + I(local_package) and/or I(remote_package) options. This value should + match the Junos version which will be reported by the device once the + new software is installed. If the device is already running a version + of software which matches the I(version) option value, the software + install is not necessary. In this case the module returns a I(changed) + value of C(false) and an I(failed) value of C(false) and does not + attempt to perform the software install. + required: false + default: Attempt to extract the version from the file name specified by + the I(local_package) or I(remote_package) option values IF the + package appears to be a Junos software package. Otherwise, C(none). + type: str + aliases: + - target_version + - new_version + - desired_version + vmhost: + description: + - Whether or not this is a vmhost software installation. + required: false + default: false + type: bool +notes: + - This module does support connecting to the console of a Junos device, but + does not support copying the software package from the local Ansible + control machine to the target Junos device while connected via the console. + In this situation, the I(remote_package) option must be specified, and the + specified software package must already exist on the target Junos device. + - This module returns after installing the software and, optionally, + initiating a reboot of the target Junos device. It does not wait for + the reboot to complete, and it does not verify that the desired version of + software specified by the I(version) option is actually activated on the + target Junos device. It is the user's responsibility to confirm the + software installation using additional follow on tasks in their playbook. +""" + +EXAMPLES = """ +--- +- name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: false + + tasks: + - name: Execute a basic Junos software upgrade. + juniper.device.software: + local_package: "./images/" + register: response + + - name: Print the complete response. + ansible.builtin.debug: + var: response + + - name: Upgrade Junos OS from package copied at device + juniper.device.software: + host: "10.x.x.x" + user: "user" + passwd: "user123" + remote_package: "/var/tmp/junos-install-mx-x86-64-20.1R1.5.tgz" + no_copy: false + cleanfs: false + validate: true + register: response +""" + + +RETURN = """ +changed: + description: + - Indicates if the device's state has changed, or if the state would have + changed when executing in check mode. This value is set to C(true) when + the version of software currently running on the target Junos device does + not match the desired version of software specified by the I(version) + option. If the current and desired software versions match, the value + of this key is set to C(false). + returned: success + type: bool +check_mode: + description: + - Indicates whether or not the module ran in check mode. + returned: success + type: bool +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +msg: + description: + - A human-readable message indicating the result of the software + installation. + returned: always + type: str +""" + +# Standard Library imports +import os.path +import re +import time + +try: + # Python 3.x + from urllib.parse import urlparse +except ImportError: + # Python 2.x + from urlparse import urlparse + + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def parse_version_from_filename(filename): + """Attempts to parse a version string from the filename of a Junos package. + + There is wide variety in the naming schemes used by Junos software + packages. This function attempts to parse the version string from the + filename, but may not be able to accurately do so. It's also not + guaranteed that the filename of a package accurately reflects the version + of software in the file. (A user may have renamed it.) + If the filename does not appear to be a Junos package (maybe some other + type of package which can be installed on Junos devices), then return None. + + Args: + filename - The filename from which to parse the version string. + + Returns: + The version string, or None if unable to parse. + """ + # Known prefixes for filenames which contain Junos software packages. + JUNOS_PACKAGE_PREFIXES = [ + "jbundle", + "jinstall", + "junos-install", + "junos-srx", + "junos-vmhost-install", + "junos-vrr", + "vmx-bundle", + "junos-arm", + ] + for prefix in JUNOS_PACKAGE_PREFIXES: + if filename.startswith(prefix): + # Assumes the version string will be prefixed by -. + # Assume major version will begin with two digits followed by dot. + # Assume the version string ends with the last digit in filename. + match = re.search(r"-(\d{2}\..*\d).*", filename) + if match is not None: + return match.group(1) + return None + + +def define_progress_callback(junos_module): + """Create callback which can be passed to SW.install(progress=progress)""" + + def myprogress(_0, report): + """A progress function which logs report at level INFO. + + Args: + _0: The PyEZ device object. Unused because the logger already knows. + report: The string to be logged. + """ + junos_module.logger.info(report) + + return myprogress + + +def main(): + CHECKSUM_ALGORITHM_CHOICES = ["md5", "sha1", "sha256"] + + # Define the argument spec. + software_argument_spec = dict( + local_package=dict( + required=False, aliases=["package"], type="path", default=None + ), + remote_package=dict( + required=False, + type="path", + # Default is '/var/tmp/' + filename from the + # local_package option, if set. + default=None, + ), + pkg_set=dict(required=False, type="list", default=None), + version=dict( + required=False, + aliases=["target_version", "new_version", "desired_version"], + type="str", + # Default is determined from filename portion of + # remote_package option. + default=None, + ), + no_copy=dict(required=False, type="bool", default=False), + reboot=dict(required=False, type="bool", default=True), + reboot_pause=dict(required=False, type="int", default=10), + issu=dict(required=False, type="bool", default=False), + nssu=dict(required=False, type="bool", default=False), + force_host=dict(required=False, type="bool", default=False), + validate=dict(required=False, type="bool", default=False), + cleanfs=dict(required=False, type="bool", default=True), + all_re=dict(required=False, type="bool", default=True), + member_id=dict(required=False, type="list", default=None), + vmhost=dict(required=False, type="bool", default=False), + checksum=dict(required=False, type="str", default=None), + checksum_algorithm=dict( + required=False, + choices=CHECKSUM_ALGORITHM_CHOICES, + type="str", + default="md5", + ), + checksum_timeout=dict(required=False, type="int", default=300), + cleanfs_timeout=dict(required=False, type="int", default=300), + install_timeout=dict(required=False, type="int", default=1800), + kwargs=dict( + required=False, aliases=["kwarg", "args", "arg"], type="dict", default=None + ), + ) + # Save keys for later. Must do because software_argument_spec gets + # modified. + option_keys = list(software_argument_spec.keys()) + + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=software_argument_spec, + # Mutually exclusive options. + mutually_exclusive=[["issu", "nssu"]], + # One of local_package and remote_package is required. + required_one_of=[["local_package", "remote_package", "pkg_set"]], + supports_check_mode=True, + ) + + # Straight from params + local_package = junos_module.params.pop("local_package") + remote_package = junos_module.params.pop("remote_package") + pkg_set = junos_module.params.pop("pkg_set") + target_version = junos_module.params.pop("version") + no_copy = junos_module.params.pop("no_copy") + reboot = junos_module.params.pop("reboot") + reboot_pause = junos_module.params.pop("reboot_pause") + install_timeout = junos_module.params.pop("install_timeout") + cleanfs = junos_module.params.pop("cleanfs") + all_re = junos_module.params.pop("all_re") + member_id = junos_module.params.pop("member_id") + kwargs = junos_module.params.pop("kwargs") + + url = None + remote_dir = None + if remote_package is not None: + # Is the remote package a URL? + parsed_url = urlparse(remote_package) + if parsed_url.scheme == "": + # A file on the remote host. + (remote_dir, remote_filename) = os.path.split(remote_package) + else: + url = remote_package + (_0, remote_filename) = os.path.split(parsed_url.path) + else: + # Default remote_dir value + remote_dir = "/var/tmp" + remote_filename = "" + + if url is not None and local_package is not None: + junos_module.fail_json( + msg="There remote_package (%s) is a URL. " + "The local_package option is not allowed." % remote_package + ) + + if url is not None and no_copy is True: + junos_module.fail_json( + msg="There remote_package (%s) is a URL. " + "The no_copy option is not allowed." % remote_package + ) + + if url is None: + local_filename = None + if local_package is not None: + # Expand out the path. + local_package = os.path.abspath(local_package) + (local_dir, local_filename) = os.path.split(local_package) + if local_filename == "": + junos_module.fail_json( + msg="There is no filename component to " + "the local_package (%s)." % local_package + ) + elif remote_package is not None: + # remote package was, so we must assume no_copy. + no_copy = True + + if no_copy is False: + if local_package is not None and not os.path.isfile(local_package): + junos_module.fail_json( + msg="The local_package (%s) is not a " + "valid file on the local Ansible " + "control machine." % local_package + ) + elif pkg_set is not None: + pkg_set = [os.path.abspath(item) for item in pkg_set] + for pkg_set_item in pkg_set: + if not os.path.isfile(pkg_set_item): + junos_module.fail_json( + msg="The pkg (%s) is not a valid file on the local" + " Ansible control machine." % pkg_set_item + ) + + if remote_filename == "": + # Use the same name as local_filename + remote_filename = local_filename + + if local_filename is not None and remote_filename != local_filename: + junos_module.fail_json( + msg="The filename of the remote_package " + "(%s) must be the same as the filename " + "of the local_package (%s)." % (remote_filename, local_filename) + ) + + # If no_copy is True, then we need to turn off cleanfs to keep from + # deleting the software package which is already present on the device. + if no_copy is True: + cleanfs = False + + if target_version is None and pkg_set is None: + target_version = parse_version_from_filename(remote_filename) + junos_module.logger.debug("New target version is: %s.", target_version) + + # Initialize the results. Assume not changed and failure until we know. + results = { + "msg": "", + "changed": False, + "check_mode": junos_module.check_mode, + "failed": True, + } + + if junos_module.conn_type == "local": + facts = dict(junos_module.dev.facts) + else: + facts = junos_module.get_facts() + # facts checking has been done as part of persitent connection itself. + + # Check version info to see if we need to do the install. + if target_version is not None: + if all_re is True: + junos_info = facts["junos_info"] + for current_re in junos_info: + current_version = junos_info[current_re]["text"] + if target_version != current_version: + junos_module.logger.debug( + "Current version on %s: %s. Target version: %s.", + current_version, + current_re, + target_version, + ) + results["changed"] = True + else: + results["msg"] += ( + "Current version on %s: %s same as Targeted " + "version: %s.\n" % (current_version, current_re, target_version) + ) + else: + current_version = facts["version"] + if junos_module.conn_type == "local": + re_name = junos_module.dev.re_name + else: + re_name = junos_module._pyez_conn.get_re_name() + if target_version != current_version: + junos_module.logger.debug( + "Current version on %s: %s. Target version: %s.", + current_version, + re_name, + target_version, + ) + results["changed"] = True + else: + results[ + "msg" + ] += "Current version on %s: %s same as Targeted " "version: %s.\n" % ( + current_version, + re_name, + target_version, + ) + else: + # A non-Junos install. Always attempt to install. + results["changed"] = True + + # Do the install if necessary + if results["changed"] is True and not junos_module.check_mode: + junos_module.logger.debug("Beginning installation of %s.", remote_filename) + # Calculate the install parameters + install_params = {} + if url is not None: + install_params["package"] = url + elif local_package is not None: + install_params["package"] = local_package + elif pkg_set is not None: + install_params["pkg_set"] = pkg_set + else: + install_params["package"] = remote_filename + if remote_dir is not None: + install_params["remote_path"] = remote_dir + if junos_module.conn_type != "local": + install_params["progress"] = True + else: + install_params["progress"] = define_progress_callback(junos_module) + install_params["cleanfs"] = cleanfs + install_params["no_copy"] = no_copy + install_params["timeout"] = install_timeout + install_params["all_re"] = all_re + install_params["member_id"] = member_id + for key in option_keys: + value = junos_module.params.get(key) + if value is not None: + install_params[key] = value + if kwargs is not None: + install_params.update(kwargs) + + junos_module.logger.debug("Install parameters are: %s", str(install_params)) + if junos_module.conn_type != "local": + try: + results["msg"] = junos_module._pyez_conn.software_api(install_params) + except Exception as err: # pylint: disable=broad-except + if "ConnectionError" in str(type(err)): + # If Exception is ConnectionError, it is excpected + # Device installation inititated succesfully + junos_module.logger.debug("Package successfully installed.") + results["msg"] += "Package successfully installed." + else: + # If exception is not ConnectionError + # we will raise the exception + raise + junos_module.logger.debug("Package successfully installed") + else: + try: + junos_module.add_sw() + ok, msg_ret = junos_module.sw.install(**install_params) + if ok is not True: + results["msg"] = "Unable to install the software %s", msg_ret + junos_module.fail_json(**results) + msg = ( + "Package %s successfully installed. Response from device is: %s" + % ( + install_params.get("package") or install_params.get("pkg_set"), + msg_ret, + ) + ) + results["msg"] = msg + junos_module.logger.debug(msg) + except ( + junos_module.pyez_exception.ConnectError, + junos_module.pyez_exception.RpcError, + ) as ex: + results["msg"] = "Installation failed. Error: %s" % str(ex) + junos_module.fail_json(**results) + if reboot is True: + junos_module.logger.debug("Initiating reboot.") + if junos_module.conn_type != "local": + try: + # Handling reboot of specific VC members + if member_id is not None: + results["msg"] += junos_module._pyez_conn.reboot_api( + all_re, install_params.get("vmhost"), member_id=member_id + ) + else: + results["msg"] += junos_module._pyez_conn.reboot_api( + all_re, install_params.get("vmhost") + ) + except Exception as err: # pylint: disable=broad-except + if "ConnectionError" in str(type(err)): + # If Exception is ConnectionError, it is excpected + # Device reboot inititated succesfully + junos_module.logger.debug("Reboot RPC executed.") + results["msg"] += " Reboot succeeded." + else: + # If exception is not ConnectionError + # we will raise the exception + raise + junos_module.logger.debug("Reboot RPC successfully initiated.") + else: + try: + # Try to deal with the fact that we might not get the closing + # and therefore might get an RpcTimeout. + # (This is a known Junos bug.) Set the timeout low so this + # happens relatively quickly. + restore_timeout = junos_module.dev.timeout + if junos_module.dev.timeout > 5: + junos_module.logger.debug( + "Decreasing device RPC timeout to 5 seconds." + ) + junos_module.dev.timeout = 5 + try: + if member_id is not None: + got = junos_module.sw.reboot( + 0, + None, + all_re, + None, + install_params.get("vmhost"), + member_id=member_id, + ) + else: + got = junos_module.sw.reboot( + 0, None, all_re, None, install_params.get("vmhost") + ) + junos_module.dev.timeout = restore_timeout + except Exception: # pylint: disable=broad-except + junos_module.dev.timeout = restore_timeout + raise + junos_module.logger.debug("Reboot RPC executed.") + + if got is not None: + results["msg"] += ( + " Reboot successfully initiated. " + "Reboot message: %s" % got + ) + else: + # This is the else clause of the for loop. + # It only gets executed if the loop finished without + # hitting the break. + results["msg"] += ( + " Did not find expected response " "from reboot RPC. " + ) + junos_module.fail_json(**results) + except junos_module.pyez_exception.RpcTimeoutError as ex: + # This might be OK. It might just indicate the device didn't + # send the closing (known Junos bug). + # Try to close the device. If it closes cleanly, then it was + # still reachable, which probably indicates a problem. + try: + junos_module.close(raise_exceptions=True) + # This means the device wasn't already disconnected. + results["msg"] += ( + " Reboot failed. It may not have been " "initiated." + ) + junos_module.fail_json(**results) + except ( + junos_module.pyez_exception.RpcError, + junos_module.pyez_exception.RpcTimeoutError, + junos_module.pyez_exception.ConnectError, + ): + # This is expected. The device has already disconnected. + results["msg"] += " Reboot succeeded." + except junos_module.ncclient_exception.TimeoutExpiredError: + # This is not really expected. Still consider reboot success as + # Looks like rpc was consumed but no response as its rebooting. + results["msg"] += " Reboot succeeded. Ignoring close error." + except ( + junos_module.pyez_exception.RpcError, + junos_module.pyez_exception.ConnectError, + ) as ex: + results["msg"] += " Reboot failed. Error: %s" % (str(ex)) + junos_module.fail_json(**results) + else: + try: + junos_module.close() + except junos_module.ncclient_exception.TimeoutExpiredError: + junos_module.logger.debug( + "Ignoring TimeoutError for close call" + ) + + junos_module.logger.debug("Reboot RPC successfully initiated.") + if reboot_pause > 0: + junos_module.logger.debug("Sleeping for %d seconds", reboot_pause) + time.sleep(reboot_pause) + + # If we made it this far, it's success. + results["failed"] = False + + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/srx_cluster.py b/ansible_collections/juniper/device/plugins/modules/srx_cluster.py new file mode 100644 index 00000000..d66e31ea --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/srx_cluster.py @@ -0,0 +1,321 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: srx_cluster +author: "Juniper Networks - Stacy Smith (@stacywsmith)" +short_description: Add or remove SRX chassis cluster configuration +description: + - Add an SRX chassis cluster configuration and reboot the device. Assuming + the device is capable of forming an SRX cluster and has the correct + cables connected, this will form an SRX cluster. + - If an SRX chassis cluster is already present, setting I(cluster_enable) to + C(false) will remove the SRX chassis cluster configuration and reboot + the device causing the SRX cluster to be broken and the device to return + to stand-alone mode. +options: + enable: + description: + - Enable or disable cluster mode. When C(true) cluster mode is enabled + and I(cluster_id) and I(node_id) must also be specified. When C(false) + cluster mode is disabled and the device returns to stand-alone mode. + required: true + default: none + type: bool + aliases: + - cluster_enable + cluster_id: + description: + - The cluster ID to configure. + - Required when I(enable) is C(true). + required: false + default: none + type: int + aliases: + - cluster + node_id: + description: + - The node ID to configure. (C(0) or C(1)) + - Required when I(enable) is C(true). + required: false + default: none + type: int + aliases: + - node +""" + +EXAMPLES = """ +--- +- name: Manipulate the SRX cluster configuration of Junos SRX devices + hosts: junos-all + connection: local + gather_facts: no + tasks: + - name: Enable an SRX cluster + juniper.device.srx_cluster: + enable: true + cluster_id: 4 + node_id: 0 + register: response + - name: Print the response. + ansible.builtin.debug: + var: response.config_lines + + - name: Disable an SRX cluster + juniper.device.srx_cluster: + enable: false + register: response + - name: Print the response. + ansible.builtin.debug: + var: response.config_lines +""" + +RETURN = """ +changed: + description: + - Indicates if the device's configuration has changed, or would have + changed when in check mode. + returned: success + type: bool +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +msg: + description: + - A human-readable message indicating the result. + returned: always + type: str +reboot: + description: + - Indicates if a reboot of the device has been initiated. + returned: success + type: bool +""" + +# Standard library imports + + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + enable=dict( + type="bool", required=True, aliases=["cluster_enable"], default=None + ), + cluster_id=dict( + type="int", required=False, aliases=["cluster"], default=None + ), + node_id=dict(type="int", required=False, aliases=["node"], default=None), + ), + # Required if options + # If enable is True, then cluster_id and node_id must be set. + required_if=[["enable", True, ["cluster_id", "node_id"]]], + # Check mode is implemented. + supports_check_mode=True, + ) + # Do additional argument verification. + + # Straight from params + enable = junos_module.params.get("enable") + cluster_id = junos_module.params.get("cluster_id") + node_id = junos_module.params.get("node_id") + + # cluster_id must be between 0 and 255 + if cluster_id is not None: + if cluster_id < 0 or cluster_id > 255: + junos_module.fail_json( + msg="The cluster_id option (%s) must have " + "an integer value between 0 and 255." % (cluster_id) + ) + + # node_id must be between 0 and 1 + if node_id is not None: + if node_id < 0 or node_id > 1: + junos_module.fail_json( + msg="The node_id option (%s) must have a " + "value of 0 or 1." % (node_id) + ) + + # Initialize the results. Assume failure until we know it's success. + results = {"msg": "", "changed": False, "reboot": False, "failed": True} + + if junos_module.conn_type == "local": + facts = dict(junos_module.dev.facts) + else: + facts = junos_module.get_facts() + # facts checking has been done as part of persitent connection itself. + junos_module.logger.debug("Check current SRX cluster operational state.") + current_cluster_state = facts["srx_cluster"] + current_cluster_id = facts["srx_cluster_id"] + if current_cluster_id is not None: + current_cluster_id = int(current_cluster_id) + if junos_module.conn_type == "local": + current_node_name = junos_module.dev.re_name + else: + current_node_name = junos_module._pyez_conn.get_re_name() + current_node_id = None + if current_node_name is not None: + (_0, _1, current_node_id) = current_node_name.partition("node") + if current_node_id: + current_node_id = int(current_node_id) + junos_module.logger.debug( + "Current SRX cluster operational state: %s, cluster_id: %s, node_id: %s", + "enabled if current_cluster_state else disabled", + str(current_cluster_id), + str(current_node_id), + ) + + # Is a state change needed? + if current_cluster_state != enable: + junos_module.logger.debug( + "SRX cluster configuration change needed. Current state: %s. " + "Desired state: %s", + "enabled" if current_cluster_state else "disabled", + "enabled" if enable else "disabled", + ) + results["changed"] = True + + # Is a cluster ID change needed? + if ( + enable is True + and current_cluster_id is not None + and current_cluster_id != cluster_id + ): + junos_module.logger.debug( + "SRX cluster ID change needed. Current cluster ID: %d. " + "Desired cluster ID: %d", + current_cluster_id, + cluster_id, + ) + results["changed"] = True + + # Is a node ID change needed? + if enable is True and current_node_id is not None and current_node_id != node_id: + junos_module.logger.debug( + "SRX node ID change needed. Current node ID: %d. Desired cluster ID: %d", + current_node_id, + node_id, + ) + results["changed"] = True + + results["msg"] = "Current state: %s, cluster_id: %s, node_id: %s" % ( + "enabled" if current_cluster_state else "disabled", + str(current_cluster_id), + str(current_node_id), + ) + + if results["changed"] is True: + results["msg"] += " Desired state: %s, cluster_id: %s, " "node_id: %s" % ( + "enabled" if enable else "disabled", + str(cluster_id), + str(node_id), + ) + + if not junos_module.check_mode: + results["msg"] += " Initiating change." + try: + output = None + if enable is True: + if junos_module.conn_type == "local": + resp = junos_module.dev.rpc.set_chassis_cluster_enable( + cluster_id=str(cluster_id), + node=str(node_id), + reboot=True, + normalize=True, + ) + else: + resp = junos_module._pyez_conn.set_chassis_cluster_enable( + cluster_id=str(cluster_id), node=str(node_id) + ) + else: + if junos_module.conn_type == "local": + resp = junos_module.dev.rpc.set_chassis_cluster_disable( + reboot=True, normalize=True + ) + else: + try: + resp = junos_module._pyez_conn.set_chassis_cluster_disable() + except Exception: + # Reboot initiated + # We got Exception ConnectionError + # so handling the exception + resp = None + output = None + if resp is not None: + output = resp.getparent().findtext(".//output") + if output is None: + output = resp.getparent().findtext(".//message") + results["msg"] += " Reboot initiated. Response: %s" % (output) + results["reboot"] = True + except ( + junos_module.pyez_exception.ConnectError, + junos_module.pyez_exception.RpcError, + ) as ex: + junos_module.logger.debug("Error: %s", str(ex)) + results["msg"] += " Error: %s" % (str(ex)) + junos_module.fail_json(**results) + + # If we made it this far, everything was successful. + results["failed"] = False + + # Return response. + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/system.py b/ansible_collections/juniper/device/plugins/modules/system.py new file mode 100644 index 00000000..32bba3b6 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/system.py @@ -0,0 +1,447 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: system +author: "Juniper Networks - Stacy Smith (@stacywsmith)" +short_description: Initiate operational actions on the Junos system +description: + - Initiate an operational action (shutdown, reboot, halt or zeroize) on a + Junos system. The particular action to execute is defined by the mandatory + I(action) option. +options: + action: + description: + - The action performed by the module. + - > + The following actions are supported: + - B(shutdown) - Power off the Junos devices. The values C(off), + C(power-off), and C(power_off) are aliases for this value. + This is the equivalent of the C(request system power-off) CLI + command. + - B(halt) - Stop the Junos OS running on the RE, but do not power off + the system. Once the system is halted, it will reboot if a + keystroke is entered on the console. This is the equivalent + of the C(request system halt) CLI command. + - B(reboot) - Reboot the system. This is the equivalent of the + C(request system reboot) CLI command. + - B(zeroize) - Restore the system (configuration, log files, etc.) to a + factory default state. This is the equivalent of the + C(request system zeroize) CLI command. + required: true + default: none + type: str + choices: + - shutdown + - halt + - reboot + - zeroize + - 'off' + - power-off + - power_off + at: + description: + - The time at which to shutdown, halt, or reboot the system. + - > + The value may be specified in one of the following ways: + - B(now) - The action takes effect immediately. + - B(+minutes) — The action takes effect in C(minutes) minutes from now. + - B(yymmddhhmm) — The action takes effect at C(yymmddhhmm) absolute + time, specified as year, month, day, hour, and minute. + - B(hh:mm) — The action takes effect at C(hh:mm) absolute time on the + current day, specified in 24-hour time. + - The I(at) option can not be used when the I(action) option has a + value of C(zeroize). The I(at) option is mutually exclusive with the + I(in_min) option. + required: false + default: none + type: str + in_min: + description: + - Specify a delay, in minutes, before the shutdown, halt, or reboot. + - The I(in_min) option can not be used when the I(action) option has a + value of C(zeroize). The I(in_min) option is mutually exclusive with + the I(at) option. + required: false + default: 0 + type: int + all_re: + description: + - If the system has multiple Routing Engines and this option is C(true), + then the action is performed on all REs in the system. If the system + does not have multiple Routing Engines, then this option has no effect. + - This option applies to all I(action) values. + - The I(all_re) option is mutually exclusive with the I(other_re) option. + required: false + default: true + type: bool + other_re: + description: + - If the system has dual Routing Engines and this option is C(true), + then the action is performed on the other REs in the system. If the + system does not have dual Routing Engines, then this option has no + effect. + - The I(other_re) option can not be used when the I(action) option has a + value of C(zeroize). + - The I(other_re) option is mutually exclusive with the I(all_re) option. + required: false + default: false + type: bool + media: + description: + - Overwrite media when performing the zeroize operation. This option is + only valid when the I(action) option has a value of C(zeroize). + required: false + default: false + type: bool + vmhost: + description: + - Whether or not this is a vmhost reboot. + required: false + default: false + type: bool +notes: + - This module only B(INITIATES) the action. It does B(NOT) wait for the + action to complete. + - Some Junos devices are effected by a Junos defect which causes this Ansible + module to hang indefinitely when connected to the Junos device via + the console. This problem is not seen when connecting to the Junos device + using the normal NETCONF over SSH transport connection. Therefore, it is + recommended to use this module only with a NETCONF over SSH transport + connection. However, this module does still permit connecting to Junos + devices via the console port and this functionality may still be used for + Junos devices running Junos versions less than 15.1. +""" + +EXAMPLES = """ +--- +- name: 'Explicit host argument' + hosts: junos + connection: local + gather_facts: false + + tasks: + - name: Reboot all REs of the device + juniper.device.system: + action: "reboot" + + - name: Power off the other RE of the device. + juniper.device.system: + action: "shutdown" + othe_re: True + + - name: Reboot this RE at 8pm today. + juniper.device.system: + action: "reboot" + all_re: False + at: "20:00" + + - name: Halt the system on 25 January 2018 at 4pm. + juniper.device.system: + action: "halt" + at: "1801251600" + + - name: Reboot the system in 30 minutes. + juniper.device.system: + action: "reboot" + in_min: 30 + + - name: Reboot the system in 30 minutes. + juniper.device.system: + action: "reboot" + at: "+30m" + + - name: Zeroize the local RE only. + juniper.device.system: + action: "zeroize" + all_re: False + + - name: Zeroize all REs and overwrite medea. + juniper.device.system: + action: "zeroize" + media: True +""" + +RETURN = """ +action: + description: + - The value of the I(action) option. + returned: always + type: str +all_re: + description: + - The value of the I(all_re) option. + returned: always + type: str +changed: + description: + - Indicates if the device's state has changed. If the action is performed + (or if it would have been performed when in check mode) then the value + will be C(true). If there was an error before the action, then the value + will be C(false). + returned: always + type: bool +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +media: + description: + - The value of the I(media) option. + returned: always + type: str +msg: + description: + - A human-readable message indicating the result. + returned: always + type: str +other_re: + description: + - The value of the I(other_re) option. + returned: always + type: str +""" + + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def main(): + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + action=dict( + type="str", + required=True, + choices=[ + "shutdown", + "off", + "power-off", + "power_off", + "halt", + "reboot", + "zeroize", + ], + default=None, + ), + at=dict(type="str", required=False, default=None), + in_min=dict(type="int", required=False, aliases=["in"], default=0), + all_re=dict(type="bool", required=False, default=True), + member_id=dict(type="list", required=False, default=None), + other_re=dict(type="bool", required=False, default=False), + vmhost=dict(required=False, type="bool", default=False), + media=dict(type="bool", required=False, default=False), + ), + mutually_exclusive=[["at", "in_min"], ["all_re", "other_re"]], + supports_check_mode=True, + ) + + # We're going to be using params a lot + params = junos_module.params + + action = params["action"] + at = params.get("at") + in_min = params.get("in_min") + all_re = params.get("all_re") + other_re = params.get("other_re") + media = params.get("media") + vmhost = params.get("vmhost") + member_id = params.get("member_id") + + # Synonymn for shutdown + if action == "off" or action == "power_off" or action == "power-off": + action = "shutdown" + + if action != "reboot" and vmhost is True: + junos_module.fail_json( + msg="The vmhost option can only be used when " + 'the action option has the value "reboot".' + ) + + # Four actions are expected - reboot, shutdown, halt and zeroize + + # if action is zeroize, at, in_min, other_re shouldn't be set + if action == "zeroize": + # at, in_min and other_re option only applies to reboot, shutdown, or halt action. + if (at is not None) or (in_min != 0) or (other_re is True): + junos_module.fail_json( + msg="The options at, in_min and other_re can only be used when " + 'the action option has the value "zeroize"' + ) + elif media is True: # media option only applies to zeroize action. + junos_module.fail_json( + msg="The media option can only be used when " + 'the action option has the value "zeroize".' + ) + + # Set initial results values. Assume failure until we know it's success. + results = { + "changed": True, + "msg": "", + "reboot": bool(action == "reboot"), + "action": action, + "all_re": all_re, + "other_re": other_re, + "media": media, + "vmhost": vmhost, + "failed": True, + } + + if not junos_module.check_mode: + if junos_module.conn_type != "local": + if member_id is not None: + for m_id in member_id: + results["msg"] = junos_module._pyez_conn.system_api( + action, + in_min, + at, + all_re, + vmhost, + other_re, + media, + member_id=m_id, + ) + else: + results["msg"] = junos_module._pyez_conn.system_api( + action, in_min, at, all_re, vmhost, other_re, media + ) + results["failed"] = False + else: + if action != "zeroize": + # If we're going to do a shutdown, reboot, or halt right away then + # try to deal with the fact that we might not get the closing + # and therefore might get an RpcTimeout. + # (This is a known Junos bug.) Set the timeout low so this happens + # relatively quickly. + if at == "now" or (in_min == 0 and at is None): + if junos_module.dev.timeout > 5: + junos_module.logger.debug( + "Decreasing device RPC timeout to 5 seconds." + ) + junos_module.dev.timeout = 5 + + # Execute the RPC. + try: + junos_module.logger.debug("Executing RPC") + junos_module.add_sw() + if action == "reboot": + if member_id is not None: + for m_id in member_id: + got = junos_module.sw.reboot( + in_min, + at, + all_re, + None, + vmhost, + other_re, + member_id=m_id, + ) + else: + got = junos_module.sw.reboot( + in_min, at, all_re, None, vmhost, other_re + ) + elif action == "shutdown": + got = junos_module.sw.poweroff( + in_min, at, None, all_re, other_re, vmhost + ) + elif action == "halt": + got = junos_module.sw.halt(in_min, at, all_re, other_re) + elif action == "zeroize": + got = junos_module.sw.zeroize(all_re, media) + else: + junos_module.fail_json(msg="Relevant action not found") + + junos_module.logger.debug("RPC executed") + if got is None: + results["msg"] = "Did not find expected RPC response." + results["changed"] = False + else: + results["msg"] = "%s successfully initiated. Response got %s" % ( + action, + got, + ) + results["failed"] = False + except junos_module.pyez_exception.RpcTimeoutError as ex: + # This might be OK. It might just indicate the device didn't + # send the closing (known Junos bug). + # Try to close the device. If it closes cleanly, then it was + # still reachable, which probably indicates there was a problem. + try: + junos_module.close(raise_exceptions=True) + # This means the device wasn't already disconnected. + results["changed"] = False + results["msg"] = "%s failed. %s may not have been " "initiated." % ( + action, + action, + ) + except ( + junos_module.pyez_exception.RpcError, + junos_module.pyez_exception.ConnectError, + ): + # This is expected. The device has already disconnected. + results["msg"] = "%s succeeded." % (action) + results["failed"] = False + except ( + junos_module.pyez_exception.RpcError, + junos_module.pyez_exception.ConnectError, + ) as ex: + results["changed"] = False + results["msg"] = "%s failed. Error: %s" % (action, str(ex)) + + # Return results. + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/plugins/modules/table.py b/ansible_collections/juniper/device/plugins/modules/table.py new file mode 100644 index 00000000..52b82169 --- /dev/null +++ b/ansible_collections/juniper/device/plugins/modules/table.py @@ -0,0 +1,493 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2016 Jason Edelman +# Network to Code, LLC +# +# Copyright (c) 2017-2020, Juniper Networks Inc. All rights reserved. +# +# License: Apache 2.0 +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the Juniper Networks nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "supported_by": "community", + "status": ["stableinterface"], +} + +DOCUMENTATION = """ +--- +extends_documentation_fragment: + - juniper_junos_common.connection_documentation + - juniper_junos_common.logging_documentation +module: table +author: + - Jason Edelman (@jedelman8) + - Updated by Juniper Networks - Stacy Smith (@stacywsmith) +short_description: Retrieve data from a Junos device using a PyEZ table/view +description: + - Retrieve data from a Junos device using PyEZ's operational table/views. + This module may be used with the tables/views which are included in the + PyEZ distribution or it may be used with user-defined tables/views. +options: + file: + description: + - Name of the YAML file, relative to the I(path) option, that contains + the table/view definition. The file name must end with the C(.yml) or + C(.yaml) extension. + required: true + default: none + type: path + kwargs: + description: + - Optional keyword arguments and values to the table's get() method. The + value of this option is a dictionary of keywords and values which are + used to refine the data return from performing a get() on the table. + The exact keywords and values which are supported are specific to the + table's definition and the underlying RPC which the table invokes. + required: false + default: none + type: dict + aliases: + - kwarg + - args + - arg + path: + description: + - The directory containing the YAML table/view definition file as + specified by the I(file) option. The default value is the C(op) + directory in C(jnpr.junos.op). This is the directory containing the + table/view definitions which are included in the PyEZ distribution. + required: false + default: C(op) directory in C(jnpr.junos.op) + type: path + aliases: + - directory + - dir + response_type: + description: + - Defines the format of data returned by the module. See RETURN. + The value of the I(resource) key in the module's response is either + a list of dictionaries C(list_of_dicts) or PyEZ's native return + format C(juniper_items). Because Ansible module's may only return JSON + data, PyEZ's native return format C(juniper_items) is translated into + a list of lists. + required: false + default: list_of_dicts + choices: + - list_of_dicts + - juniper_items + type: str + table: + description: + - Name of the PyEZ table used to retrieve data. If not specified, + defaults to the name of the table defined in the I(file) option. Any + table names in I(file) which begin with C(_) are ignored. If more than + one table is defined in I(file), the module fails with an error + message. In this case, you must manually specify the name of the table + by setting this option. + required: false + default: The name of the table defined in the I(file) option. + type: str +notes: + - This module only works with operational tables/views; it does not work with + configuration tables/views. +""" + +EXAMPLES = """ +--- +- name: Retrieve data from a Junos device using a PyEZ table/view. + hosts: junos-all + connection: local + gather_facts: false + + tasks: + - name: Retrieve LLDP Neighbor Information Using PyEZ-included Table + juniper.device.table: + file: "lldp.yml" + register: response + - name: Print response + ansible.builtin.debug: + var: response + + - name: Retrieve routes within 192.68.1/8 + juniper.device.table: + file: "routes.yml" + table: "RouteTable" + kwargs: + destination: "192.68.1.0/8" + response_type: "juniper_items" + register: response + - name: Print response + ansible.builtin.debug: + var: response + + - name: Retrieve from custom table in playbook directory + juniper.device.table: + file: "fpc.yaml" + path: "." + register: response + - name: Print response + ansible.builtin.debug: + var: response +""" + +RETURN = """ +changed: + description: + - Indicates if the device's configuration has changed. Since this + module does not change the operational or configuration state of the + device, the value is always set to C(false). + returned: success + type: bool +failed: + description: + - Indicates if the task failed. + returned: always + type: bool +msg: + description: + - A human-readable message indicating a summary of the result. + returned: always + type: str +resource: + description: + - The items retrieved by the table/view. + returned: success + type: list of dicts if I(response_type) is C(list_of_dicts) or list of + lists if I(respsonse_type) is C(juniper_items). + sample: | + # when response_type == 'list_of_dicts' + [ + { + "local_int": "ge-0/0/3", + "local_parent": "-", + "remote_chassis_id": "00:05:86:08:d4:c0", + "remote_port_desc": null, + "remote_port_id": "ge-0/0/0", + "remote_sysname": "r5", + "remote_type": "Mac address" + }, + { + "local_int": "ge-0/0/0", + "local_parent": "-", + "remote_chassis_id": "00:05:86:18:f3:c0", + "remote_port_desc": null, + "remote_port_id": "ge-0/0/2", + "remote_sysname": "r4", + "remote_type": "Mac address" + } + ] + # when response_type == 'juniper_items' + [ + [ + "ge-0/0/3", + [ + [ + "local_parent", + "-" + ], + [ + "remote_port_id", + "ge-0/0/0" + ], + [ + "remote_chassis_id", + "00:05:86:08:d4:c0" + ], + [ + "remote_port_desc", + null + ], + [ + "remote_type", + "Mac address" + ], + [ + "local_int", + "ge-0/0/3" + ], + [ + "remote_sysname", + "r5" + ] + ] + ], + [ + "ge-0/0/0", + [ + [ + "local_parent", + "-" + ], + [ + "remote_port_id", + "ge-0/0/2" + ], + [ + "remote_chassis_id", + "00:05:86:18:f3:c0" + ], + [ + "remote_port_desc", + null + ], + [ + "remote_type", + "Mac address" + ], + [ + "local_int", + "ge-0/0/0" + ], + [ + "remote_sysname", + "r4" + ] + ] + ] + ] +""" + +# Standard library imports +import os.path + +# Constants +RESPONSE_CHOICES = ["list_of_dicts", "juniper_items"] + + +"""From Ansible 2.1, Ansible uses Ansiballz framework for assembling modules +But custom module_utils directory is supported from Ansible 2.3 +Reference for the issue: https://groups.google.com/forum/#!topic/ansible-project/J8FL7Z1J1Mw """ + +# Ansiballz packages module_utils into ansible.module_utils + +from ansible_collections.juniper.device.plugins.module_utils import configuration as cfg +from ansible_collections.juniper.device.plugins.module_utils import juniper_junos_common + + +def expand_items(module, data): + """Recursively expand any table items""" + resources = [] + # data.items() is a list of tuples + for table_key, table_fields in data.items(): + # sample: + # ('fxp0', [('neighbor_interface', '1'), ('local_interface', 'fxp0'), + # ('neighbor', 'vmx2')] + # table_key - element 0 is the key from the Table - not using at all + # table_fields - element 1 is also a list of tuples + temp = [] + for key, value in table_fields: + # calling it normalized value because YOU/WE created the keys + if value and isinstance(value, module.pyez_factory_table.Table): + value = expand_items(module, value) + temp.append((key, value)) + resources.append((table_key, temp)) + return resources + + +def juniper_items_to_list_of_dicts(module, data): + """Recursively convert Juniper PyEZ Table/View items to list of dicts.""" + resources = [] + # data.items() is a list of tuples + for table_key, table_fields in data.items(): + # sample: + # ('fxp0', [('neighbor_interface', '1'), ('local_interface', 'fxp0'), + # ('neighbor', 'vmx2')] + # table_key - element 0 is the key from the Table - not using at all + # table_fields - element 1 is also a list of tuples + temp = {} + for key, value in table_fields: + if isinstance(value, module.pyez_factory_table.Table): + value = juniper_items_to_list_of_dicts(module, value) + temp[key] = value + resources.append(temp) + return resources + + +def main(): + # Create the module instance. + junos_module = juniper_junos_common.JuniperJunosModule( + argument_spec=dict( + file=dict(type="path", required=True, default=None), + table=dict(type="str", required=False, default=None), + path=dict( + type="path", required=False, aliases=["directory", "dir"], default=None + ), + kwargs=dict( + required=False, + aliases=["kwarg", "args", "arg"], + type="dict", + default=None, + ), + response_type=dict( + choices=RESPONSE_CHOICES, + type="str", + required=False, + default="list_of_dicts", + ), + ), + # Check mode is implemented. + supports_check_mode=True, + min_yaml_version=cfg.MIN_YAML_VERSION, + ) + + # Straight from params + file = junos_module.params.get("file") + table = junos_module.params.get("table") + path = junos_module.params.get("path") + kwargs = junos_module.params.get("kwargs") + response_type = junos_module.params.get("response_type") + + if not file.endswith(".yml") and not file.endswith(".yaml"): + junos_module.fail_json( + msg="The value of the file option must end " + "with the .yml or .yaml extension" + ) + + # If needed, get the default path + if path is None: + path = os.path.dirname(os.path.abspath(junos_module.pyez_op_table.__file__)) + + # file_name is path + file + file_name = os.path.join(path, file) + + junos_module.logger.debug("Attempting to open: %s.", file_name) + try: + with open(file_name, "r") as fp: + try: + junos_module.logger.debug( + "Attempting to parse YAML from : %s.", file_name + ) + table_view = junos_module.yaml.safe_load(fp) + junos_module.logger.debug( + "YAML from %s successfully parsed.", file_name + ) + except junos_module.yaml.YAMLError as ex: + junos_module.fail_json( + msg="Failed parsing YAML file %s. " + "Error: %s" % (file_name, str(ex)) + ) + except IOError: + junos_module.fail_json( + msg="The file name %s could not be opened for" "reading." % (file_name) + ) + junos_module.logger.debug("%s successfully read.", file_name) + + # Initialize the results. Assume failure until we know it's success. + results = {"msg": "", "changed": False, "failed": True} + + # Default to the table defined in file_name. + # Ignore table names which begin with an underscore. + if table is None: + for key in table_view: + if not key.startswith("_") and "Table" in key: + if table is not None: + junos_module.fail_json( + msg="The file name %s contains multiple table " + "definitions. Specify the desired table with the " + "table option." % (file_name) + ) + table = key + + if table is None: + junos_module.fail_json( + msg="No table definition was found in the %s file. Specify a " + "value for the file option which contains a valid table/view " + "definition." % (file_name) + ) + junos_module.logger.debug("Table: %s", table) + + try: + loader = junos_module.pyez_factory_loader.FactoryLoader().load(table_view) + junos_module.logger.debug("Loader created successfully.") + except Exception as ex: + junos_module.fail_json( + msg="Unable to create a table loader from the " + "%s file. Error: %s" % (file_name, str(ex)) + ) + try: + # there is a limitation of JSON for persistent connection. + # the connection passes information as JSON and can't use device object here. + # closing the persistent connection and creating a normal connection. + if junos_module.conn_type != "local": + junos_module._pyez_conn.close() + junos_module.open() + + data = loader[table](junos_module.dev) + junos_module.logger.debug("Table %s created successfully.", table) + if kwargs is None: + data.get() + else: + data.get(**kwargs) + junos_module.logger.debug("Data retrieved from %s successfully.", table) + except KeyError: + junos_module.fail_json( + msg="Unable to find table %s in the " "%s file." % (table, file_name) + ) + except ( + junos_module.pyez_exception.ConnectError, + junos_module.pyez_exception.RpcError, + ) as ex: + junos_module.fail_json( + msg="Unable to retrieve data from table %s. " "Error: %s" % (table, str(ex)) + ) + + if data is not None: + try: + len_data = len(data) + except Exception as ex: + junos_module.fail_json( + msg="Unable to parse table %s data into " + "items. Error: %s" % (table, str(ex)) + ) + junos_module.logger.debug( + "Successfully retrieved %d items from %s.", len_data, table + ) + results["msg"] = "Successfully retrieved %d items from %s." % (len_data, table) + + if response_type == "list_of_dicts": + junos_module.logger.debug("Converting data to list of dicts.") + resource = juniper_items_to_list_of_dicts(junos_module, data) + else: + resource = expand_items(junos_module, data) + + # If we made it this far, everything was successful. + results["failed"] = False + results["resource"] = resource + + # Return response. + junos_module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/juniper/device/requirements.txt b/ansible_collections/juniper/device/requirements.txt new file mode 120000 index 00000000..5f811608 --- /dev/null +++ b/ansible_collections/juniper/device/requirements.txt @@ -0,0 +1 @@ +../../../requirements.txt \ No newline at end of file diff --git a/ansible_collections/juniper/device/version.py b/ansible_collections/juniper/device/version.py new file mode 100755 index 00000000..4791496c --- /dev/null +++ b/ansible_collections/juniper/device/version.py @@ -0,0 +1,5 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +VERSION = "v1.0.7" +DATE = "2024-Dec-5" diff --git a/docs/ansible2rst.py b/docs/ansible2rst.py deleted file mode 100755 index 6330dc67..00000000 --- a/docs/ansible2rst.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python -# (c) 2012, Jan-Piet Mens -# -# This file is part of Ansible -# -# Modified to support stand-alone Galaxy documentation -# Copyright (c) 2014, Juniper Networks Inc. -# 2014, Rick Sherman -# -# 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 . -# - -import os -import re -import sys -import datetime -import cgi -from jinja2 import Environment, FileSystemLoader - -import ansible.utils -from ansible.utils import module_docs - -##################################################################################### -# constants and paths - -_ITALIC = re.compile(r"I\(([^)]+)\)") -_BOLD = re.compile(r"B\(([^)]+)\)") -_MODULE = re.compile(r"M\(([^)]+)\)") -_URL = re.compile(r"U\(([^)]+)\)") -_CONST = re.compile(r"C\(([^)]+)\)") - -MODULEDIR = "../library/" -OUTPUTDIR = "." - -##################################################################################### - - -def rst_ify(text): - ''' convert symbols like I(this is in italics) to valid restructured text ''' - - t = _ITALIC.sub(r'*' + r"\1" + r"*", text) - t = _BOLD.sub(r'**' + r"\1" + r"**", t) - t = _MODULE.sub(r'``' + r"\1" + r"``", t) - t = _URL.sub(r"\1", t) - t = _CONST.sub(r'``' + r"\1" + r"``", t) - - return t - -##################################################################################### - - -def html_ify(text): - ''' convert symbols like I(this is in italics) to valid HTML ''' - - t = cgi.escape(text) - t = _ITALIC.sub("" + r"\1" + "", t) - t = _BOLD.sub("" + r"\1" + "", t) - t = _MODULE.sub("" + r"\1" + "", t) - t = _URL.sub("" + r"\1" + "", t) - t = _CONST.sub("" + r"\1" + "", t) - - return t - - -##################################################################################### - -def rst_fmt(text, fmt): - ''' helper for Jinja2 to do format strings ''' - - return fmt % (text) - -##################################################################################### - - -def rst_xline(width, char="="): - ''' return a restructured text line of a given length ''' - - return char * width - -##################################################################################### - - -def write_data(text, outputname, module, output_dir=None): - ''' dumps module output to a file or the screen, as requested ''' - - if output_dir is not None: - f = open(os.path.join(output_dir, outputname % module), 'w') - f.write(text.encode('utf-8')) - f.close() - else: - print text - -##################################################################################### - - -def jinja2_environment(template_dir, typ): - - env = Environment(loader=FileSystemLoader(template_dir), - variable_start_string="@{", - variable_end_string="}@", - trim_blocks=True, - ) - env.globals['xline'] = rst_xline - - if typ == 'rst': - env.filters['convert_symbols_to_format'] = rst_ify - env.filters['html_ify'] = html_ify - env.filters['fmt'] = rst_fmt - env.filters['xline'] = rst_xline - template = env.get_template('rst.j2') - outputname = "%s.rst" - else: - raise Exception("unknown module format type: %s" % typ) - - return env, template, outputname - -##################################################################################### - - -def process_module(fname, template, outputname): - - print MODULEDIR + fname - doc, examples, returndocs = module_docs.get_docstring(MODULEDIR + fname) - - all_keys = [] - - if 'version_added' not in doc: - sys.stderr.write("*** ERROR: missing version_added in: %s ***\n".format(fname)) - sys.exit(1) - - added = 0 - if doc['version_added'] == 'historical': - del doc['version_added'] - else: - added = doc['version_added'] - - # don't show version added information if it's too old to be called out - if added: - added_tokens = str(added).split(".") - added = added_tokens[0] + "." + added_tokens[1] - added_float = float(added) - - for (k, v) in doc['options'].iteritems(): - all_keys.append(k) - all_keys = sorted(all_keys) - doc['option_keys'] = all_keys - - doc['filename'] = fname - doc['docuri'] = doc['module'].replace('_', '-') - doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d') - doc['plainexamples'] = examples # plain text - - # here is where we build the table of contents... - - text = template.render(doc) - write_data(text, outputname, fname, OUTPUTDIR) - -##################################################################################### - - -def main(): - - env, template, outputname = jinja2_environment('.', 'rst') - modules = [] - - for module in os.listdir(MODULEDIR): - if module.startswith("junos_"): - process_module(module, template, outputname) - modules.append(module) - - index_file_path = os.path.join(OUTPUTDIR, "index.rst") - index_file = open(index_file_path, "w") - index_file.write('Juniper.junos Ansible Modules\n') - index_file.write('=================================================\n') - index_file.write('\n') - index_file.write('Contents:\n') - index_file.write('\n') - index_file.write('.. toctree::\n') - index_file.write(' :maxdepth: 1\n') - index_file.write('\n') - - for module in modules: - index_file.write(' %s\n' % module) - -if __name__ == '__main__': - main() diff --git a/docs/rst.j2 b/docs/rst.j2 deleted file mode 100644 index 5c393144..00000000 --- a/docs/rst.j2 +++ /dev/null @@ -1,107 +0,0 @@ -.. _@{ module }@: - -{% if short_description %} -{% set title = module + ' - ' + short_description|convert_symbols_to_format %} -{% else %} -{% set title = module %} -{% endif %} -{% set title_len = title|length %} - -@{ title }@ -@{ '+' * title_len }@ - -{% if author %} -:Author: @{ author }@ -{% endif %} - -.. contents:: - :local: - :depth: 1 - -{# ------------------------------------------ - # - # Please note: this looks like a core dump - # but it isn't one. - # - --------------------------------------------#} - -Synopsis --------- - -{% if version_added is defined -%} -.. versionadded:: @{ version_added }@ -{% endif %} - -{% for desc in description -%} -@{ desc | convert_symbols_to_format }@ -{% endfor %} - -{% if options -%} -Options -------- - -.. raw:: html - - - - - - - - - - {% for k in option_keys %} - {% set v = options[k] %} - - - - - {% if v.get('type', 'not_bool') == 'bool' %} - - {% else %} - - {% endif %} - - - {% endfor %} -
parameterrequireddefaultchoicescomments
@{ k }@{% if v.get('required', False) %}yes{% else %}no{% endif %}{% if v['default'] %}@{ v['default'] }@{% endif %}
  • yes
  • no
    {% for choice in v.get('choices',[]) -%}
  • @{ choice }@
  • {% endfor -%}
{% for desc in v.description -%}@{ desc | html_ify }@{% endfor -%}{% if v['version_added'] %} (added in Ansible @{v['version_added']}@){% endif %}
-{% endif %} - -{% if requirements %} -{% for req in requirements %} - -.. note:: Requires @{ req | convert_symbols_to_format }@ - -{% endfor %} -{% endif %} - -{% if examples or plainexamples %} -Examples --------- - -.. raw:: html - -{% for example in examples %} - {% if example['description'] %}

@{ example['description'] | html_ify }@

{% endif %} -

-

-@{ example['code'] | escape | indent(4, True) }@
-    
-

-{% endfor %} -
- -{% if plainexamples %} - -:: - -@{ plainexamples | indent(4, True) }@ -{% endif %} -{% endif %} - -{% if notes %} -{% for note in notes %} -.. note:: @{ note | convert_symbols_to_format }@ -{% endfor %} -{% endif %} - diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..87be8428 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set +e + +## Functions +function apk_add { + echo "Installing additional OS packages" + while IFS= read -r pkg + do + echo "Installing ${pkg}" + apk add --no-cache -q "${pkg}" + done < "$1" +} + +function pip_install { + echo "Installing Python packages" + pip install -r "$1" +} + +function galaxy_install { + echo "Install Ansible Collections" + ansible-galaxy install -r "$1" +} + +function run_command { + echo "Executing given commands" + bash -c "$*" +} + + +if [ "$APK" ]; then APK=$APK +elif [ -f "/extras/apk.txt" ]; then APK="/extras/apk.txt" +else APK='' +fi + +if [ "$REQ" ]; then REQ=$REQ +elif [ -f "/extras/requirements.txt" ];then REQ="/extras/requirements.txt" +else REQ='' +fi + +if [ "$COLLECTIONS" ]; then COLLECTIONS=$COLLECTIONS +elif [ -f "/extras/requirements.yml" ]; then COLLECTIONS="/extras/requirements.yml" +else COLLECTIONS='' +fi + + +[[ -z "$APK" ]] || apk_add "$APK" + +[[ -z "$REQ" ]] || pip_install "$REQ" + +[[ -z "$COLLECTIONS" ]] || galaxy_install "$COLLECTIONS" + +if [ -z "$1" ] +then + echo "Starting an interactive Bash session" + /bin/bash +else run_command "$*" +fi diff --git a/env-setup b/env-setup index e87c55fd..67c4772a 100755 --- a/env-setup +++ b/env-setup @@ -1,5 +1,5 @@ #!/bin/bash -# usage: source env-setup +# usage: source env-setup # When run using source as directed, $0 gets set to bash, so we must use $BASH_SOURCE if [ -n "$BASH_SOURCE" ] ; then diff --git a/library/junos_commit b/library/junos_commit deleted file mode 100644 index 2630a16d..00000000 --- a/library/junos_commit +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 1999-2015, Juniper Networks Inc. -# 2014, Jeremy Schulman -# 2015, Rick Sherman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_commit -author: Rick Sherman, Juniper Networks -version_added: "1.2.0" -short_description: Execute commit on device -description: - - Execute a Commit on a device running Junos independently of loading a configuration -requirements: - - junos-eznc >= 1.2.2 -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes - required: false - default: None - comment: - description: - - Provide a comment to the commit of the configuration - required: false - default: None - confirm: - description: - - Provide a confirm in minutes to the commit of the configuration - required: false - default: None -''' - -EXAMPLES = ''' -- junos_commit: - host: "{{ inventory_hostname }}" - logfile=changes.log - comment="Non load commit" -''' -from distutils.version import LooseVersion -import logging - -try: - from jnpr.junos import Device - from jnpr.junos.version import VERSION - from jnpr.junos.utils.config import Config - from jnpr.junos.exception import CommitError, LockError, UnlockError - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - HAS_PYEZ = False - else: - HAS_PYEZ = True -except ImportError: - HAS_PYEZ = False - - -def main(): - - module = AnsibleModule( - argument_spec=dict(host=dict(required=True, default=None), # host or ipaddr - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - port=dict(required=False, default=830), - logfile=dict(required=False, default=None), - comment=dict(required=False, default=None), - confirm=dict(required=False, default=None) - ), - supports_check_mode=True) - - if not HAS_PYEZ: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - args = module.params - in_check_mode = module.check_mode - - results = dict(changed=False) - - logfile = args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'CONFIG:' + args['host'] - - logging.info("connecting to host: {0}@{1}:{2}".format(args['user'], args['host'], args['port'])) - - try: - dev = Device(args['host'], user=args['user'], password=args['passwd'], - port=args['port'], gather_facts=False).open() - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(args['host'], str(err)) - logging.error(msg) - module.fail_json(msg=msg) - # --- UNREACHABLE --- - - try: - cu = Config(dev) - - logging.info("taking lock") - cu.lock() - - if (in_check_mode): - logging.info("doing a commit-check, please be patient") - cu.commit_check() - else: - logging.info("committing change, please be patient") - opts = {} - if args['comment'] is not None: - opts['comment'] = args['comment'] - if args['confirm'] is not None: - opts['confirm'] = args['confirm'] - - cu.commit(**opts) - results['changed'] = True - logging.info("change completed") - - logging.info("unlocking") - cu.unlock() - - except LockError as err: - msg = 'Unable to lock configuration - will not commit: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except UnlockError as err: - msg = 'Unable to unlock configuration - commit should succeed: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except CommitError as err: - msg = 'Unable to commit: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except Exception as err: - msg = 'Uncaught exception - please report: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - dev.close() - - module.exit_json(**results) - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_get_config b/library/junos_get_config deleted file mode 100755 index a99b96e7..00000000 --- a/library/junos_get_config +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 1999-2015, Juniper Networks Inc. -# 2014, Jeremy Schulman -# 2015, Rick Sherman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_get_config -author: Rick Sherman, Juniper Networks -version_added: "1.2.0" -short_description: Retrieve configuration of device -description: - - Retrieve the configuration of a device running Junos and save it to a file. - B(Note) unicode chars will be converted to '??' as also done in PyEZ -requirements: - - junos-eznc >= 1.2.2 -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes - required: false - default: None - dest: - description: - - Path to the local server directory where configuration will - be saved. - required: true - default: None - format: - description: - - text - configuration saved as text (curly-brace) format - - xml - configuration saved as XML - required: false - choices: ['text','xml'] - default: 'text' - options: - description: - - Additional options to pass to get_config. Refer - to B(jnpr.junos.rpcmeta.get_config) for details. - required: false - default: None - filter: - description: - - Defines heircachy of configuration to retrieve. If omitted - entire configuration is retrieved. Format is slash notation - ex I(groups/routeinst/routing-instances/ISP-1) - required: false - default: None -''' - -EXAMPLES = ''' -- junos_get_config: - host: "{{ inventory_hostname }}" - logfile: get_config.log - dest: "{{ inventory_hostname }}.xml" - format: xml - filter: "interfaces" - options: {inherit: inherit, groups: groups} -''' -from distutils.version import LooseVersion -import logging -from lxml import etree -from lxml.builder import E - -try: - from jnpr.junos import Device - from jnpr.junos.version import VERSION - from jnpr.junos.exception import RpcError - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - HAS_PYEZ = False - else: - HAS_PYEZ = True -except ImportError: - HAS_PYEZ = False - - -def main(): - - module = AnsibleModule( - argument_spec=dict(host=dict(required=True, default=None), # host or ipaddr - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - port=dict(required=False, default=830), - logfile=dict(required=False, default=None), - dest=dict(required=False, default=None), - format=dict(required=False, choices=['text', 'xml'], default='text'), - options=dict(required=False, default=None), - filter=dict(required=False, default=None) - ), - supports_check_mode=True) - - if not HAS_PYEZ: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - args = module.params - - logfile = args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'CONFIG:' + args['host'] - - logging.info("connecting to host: {0}@{1}:{2}".format(args['user'], args['host'], args['port'])) - - try: - dev = Device(args['host'], user=args['user'], password=args['passwd'], - port=args['port'], gather_facts=False).open() - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(args['host'], str(err)) - logging.error(msg) - module.fail_json(msg=msg) - # --- UNREACHABLE --- - - try: - options = args['options'] or {} - options['format'] = args['format'] - - filter_xml = None - if args['filter']: - flist = args['filter'].split('/') - if flist[0] == 'configuration': - filter_xml = E(flist.pop(0)) - else: - filter_xml = E('configuration') - - for f in flist: - filter_xml.append(E(f)) - - logging.info("Getting config with filter={0}".format(etree.tostring(filter_xml))) - - logging.info("Getting config with options={0}".format(options)) - - config = dev.rpc.get_config(options=options, filter_xml=filter_xml) - - with open(args['dest'], 'w') as confile: - if args['format'] == 'text': - confile.write(config.text.encode('ascii', 'replace')) - elif args['format'] == 'xml': - confile.write(etree.tostring(config)) - - except (ValueError, RpcError) as err: - msg = 'Unable to get config: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except Exception as err: - msg = 'Uncaught exception - please report: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - dev.close() - - module.exit_json() - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_get_facts b/library/junos_get_facts deleted file mode 100644 index 70d93050..00000000 --- a/library/junos_get_facts +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 1999-2014, Juniper Networks Inc. -# 2014, Jeremy Schulman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_get_facts -author: Jeremy Schulman, Juniper Networks -version_added: "1.0.0" -short_description: Retrieve facts for a device running Junos OS. -description: - - Retrieve facts for a device running Junos OS, which includes information - such as the serial number, product model, and Junos OS version. - The module supports using both NETCONF and CONSOLE-based retrieval - and returns the information as a JSON dictionary. - The information is similar to facts gathered by other IT frameworks. -requirements: - - junos-eznc >= 1.2.2 - - junos-netconify >= 1.0.1, when using the I(console) option -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - console: - description: - - CONSOLE port, per the B(netconify) utility - required: false - default: None - savedir: - description: - - Path to the local server directory where device fact - files will be stored. Resulting file will be - I(savedir/hostname-facts.json) - required: false - default: $CWD - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes. This option is used only with the - I(console) option. - required: false - default: None - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 -''' - -EXAMPLES = ''' -# retrieve facts using NETCONF, assumes ssh-keys - -- junos_get_facts: host={{ inventory_hostname }} - register: junos - -# retrieve facts using CONSOLE, assumes Amnesiac system -# root login, no password - -- junos_get_facts: - host={{ inventory_hostname }} - user=root - console="--telnet={{TERMSERV}},{{TERMSERVPORT}}" - savedir=/usr/local/junos/inventory - register: junos - -# access the facts - -- name: version - debug: msg="{{ junos.facts.version }}" -''' - -import os -import json - - -def main(): - from distutils.version import LooseVersion - module = AnsibleModule( - argument_spec=dict( - host=dict(required=True), - console=dict(required=False, default=None), - logfile=dict(required=False, default=None), - savedir=dict(required=False, default=None), - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - port=dict(required=False, default=830)), - supports_check_mode=True) - - m_args = module.params - m_results = dict(changed=False) - - if m_args['console'] is None: - try: - from jnpr.junos import Device - from jnpr.junos.version import VERSION - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - except ImportError: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - # ----------- - # via NETCONF - # ----------- - dev = Device(m_args['host'], user=m_args['user'], passwd=m_args['passwd'], port=m_args['port']) - try: - dev.open() - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(m_args['host'], str(err)) - module.fail_json(msg=msg) - return - else: - dev.close() - dev.facts['has_2RE'] = dev.facts['2RE'] - del dev.facts['2RE'] # Ansible doesn't allow variables starting with numbers - m_results['facts'] = dev.facts - if m_args['savedir'] is not None: - fname = "{0}/{1}-facts.json".format(m_args['savedir'], dev.facts['hostname']) - with open(fname, 'w') as factfile: - json.dump(dev.facts, factfile) - else: - # ----------- - # via CONSOLE - # ----------- - try: - from netconify.cmdo import netconifyCmdo - from netconify.constants import version - if not LooseVersion(version) >= LooseVersion('1.0.1'): - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - except ImportError: - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - import logging - - c_args = [] - c_args.append(m_args['console']) - c_args.append('--facts') - if m_args['savedir'] is not None: - c_args.append('--savedir=' + m_args['savedir']) - c_args.append('--user=' + m_args['user']) - if m_args['passwd'] is not None: - c_args.append('--passwd=' + m_args['passwd']) - - c_args.append(m_args['host']) - - logfile = m_args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'NETCONIFY:' + module.params['host'] - - def log_notify(self, event, message): - logging.info("%s:%s" % (event, message)) - use_notifier = log_notify - else: - def silent_notify(self, event, message): - pass - use_notifier = silent_notify - - try: - nc = netconifyCmdo(notify=use_notifier) - c_results = nc.run(c_args) - except Exception as err: - module.fail_json(msg=str(err)) - m_results['args'] = m_args # for debug - m_results['facts'] = c_results['facts'] - - module.exit_json(**m_results) - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_install_config b/library/junos_install_config deleted file mode 100644 index 29946dba..00000000 --- a/library/junos_install_config +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 1999-2014, Juniper Networks Inc. -# 2014, Jeremy Schulman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_install_config -author: Jeremy Schulman, Juniper Networks -version_added: "1.0.0" -short_description: Load a configuration file or snippet onto a device running Junos OS. -description: - - Load a complete Junos OS configuration (overwrite) or merge a configuration - snippet onto a device running Junos OS and commit it. The default behavior - is to perform a B(load merge) operation (overwrite='no'). This module - performs an atomic lock/edit/unlock. If the process fails at any step, then - all configuration changes are discarded. You can load the configuration using - either NETCONF or the CONSOLE port. Specify the I(console) option to use the - CONSOLE port. - - You provide the configuration data in a file. Supported formats when using - NETCONF include ASCII text, Junos XML elements, and Junos OS B(set) commands. - Configurations performed through the console must only use ASCII text formatting. -requirements: - - junos-eznc >= 1.2.2 - - junos-netconify >= 1.0.1, when using the I(console) option -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - file: - description: - - Path to the file containing the Junos OS configuration data. - If the file has a C(*.conf) extension, the content is treated - as text format. If the file has a C(*.xml) extension, the - content is treated as XML format. If the file has a C(*.set) - extension, the content is treated as Junos OS B(set) - commands. - required: true - overwrite: - description: - - Specify whether the configuration I(file) completely replaces - the existing configuration. - required: false - default: no - choices: ['true','false','yes','no'] - replace: - description: - - Specify whether the configuration I(file) uses "replace:" statements. - (NETCONF only) B(NOT) compatible with B(set) format - required: false - default: no - choices: ['true','false','yes','no'] - timeout: - description: - - Extend the NETCONF RPC timeout beyond the default value of - 30 seconds. Set this value to accommodate configuration - changes (commits) that might take longer than the default - timeout interval. - required: false - default: "0" - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes - required: false - default: None - diffs_file: - description: - - Path to the file where any diffs will be written - required: false - default: None - console: - description: - - Port configuration, per the B(netconify) utility - required: false - default: None - savedir: - description: - - Path to the local server directory where device facts and - inventory files will be stored. This option is used only - with the I(console) option. - Refer to the B(netconify) utility for details. - required: false - default: None - comment: - description: - - Provide a comment to the commit of the configuration - required: false - default: None - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 - confirm: - description: - - Provide a confirm in minutes to the commit of the configuration - required: false - default: None -''' - -EXAMPLES = ''' -# load merge a change to the Junos OS configuration using NETCONF - -- junos_install_config: - host={{ inventory_hostname }} - file=banner.conf - -# load overwrite a new Junos OS configuration using the CONSOLE port - -- junos_install_config: - host={{ inventory_hostname }} - console="--telnet={{TERMSERV}},{{TERMSERV_PORT}}" - file=default_new_switch.conf - overwrite=yes - -# load merge a change to the Junos OS configuration using NETCONF and supplying a commit log message -- junos_install_config: - host={{ inventory_hostname }} - file=banner.conf - comment="configured by ansible" - -# load replace a change to the Junos OS configuration using NETCONF -- junos_install_config: - host={{ inventory_hostname }} - file=snmp.conf - replace=yes -''' - -import logging -from os.path import isfile -import os -from distutils.version import LooseVersion - -try: - from jnpr.junos import Device - from jnpr.junos.exception import * - from jnpr.junos.utils.config import Config - from jnpr.junos.version import VERSION - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - HAS_PYEZ = False - else: - HAS_PYEZ = True -except ImportError: - HAS_PYEZ = False - - -def _load_via_netconf(module): - args = module.params - - logfile = args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'CONFIG:' + args['host'] - - in_check_mode = module.check_mode - overwrite = module.boolean(module.params['overwrite']) - replace = module.boolean(module.params['replace']) - - if all([overwrite, replace]): - msg = "Overwrite and Replace cannot both be True!" - logging.error(msg) - module.fail_json(msg=msg) - - logging.info("connecting to host: {0}@{1}:{2}".format(args['user'], args['host'], args['port'])) - - try: - dev = Device(args['host'], user=args['user'], password=args['passwd'], port=args['port']) - dev.open(gather_facts=False) - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(args['host'], str(err)) - logging.error(msg) - module.fail_json(msg=msg) - return - - timeout = int(args['timeout']) - if timeout > 0: - dev.timeout = timeout - - cu = Config(dev) - - results = {} - - file_path = module.params['file'] - file_path = os.path.abspath(file_path) - - results['file'] = file_path - results['changed'] = False - - logging.info("pushing file: {0}".format(file_path)) - try: - logging.info("taking lock") - cu.lock() - - except LockError: - msg = "Unable to lock configuration" - logging.error(msg) - module.fail_json(msg=msg) - - try: - # load the config. the cu.load will raise - # an exception if there is even a warning. - # so we want to avoid that condition. - logging.info("loading config") - load_args = {'path': file_path} - if replace is True: - load_args['merge'] = False - elif overwrite is True: - load_args['overwrite'] = True - elif overwrite is False: - load_args['merge'] = True - cu.load(**load_args) - except (ValueError, ConfigLoadError, Exception) as err: - msg = "Unable to load config: {0}".format(err) - logging.error(msg) - try: - logging.info("unlocking") - cu.unlock() - except UnlockError as err: - logging.error("Unable to unlock config: {0}".format(err)) - dev.close() - module.fail_json(msg=msg) - - diff = cu.diff() - - if diff is not None: - diffs_file = args['diffs_file'] - if diffs_file is not None: - try: - f = open(diffs_file, 'w') - f.write(diff) - f.close() - except IOError as (errno, strerror): - msg = "Problem with diffs_file {0}: ".format(diffs_file) - msg += "I/O Error: ({0}): {1}".format(errno, strerror) - logging.error(msg) - module.fail_json(msg=msg) - except: - msg = "Problem with diffs_file {0}: ".format(diffs_file) - msg += "Unexpected error:", sys.exc_info()[0] - logging.error(msg) - module.fail_json(msg=msg) - - try: - logging.info("doing a commit-check, please be patient") - cu.commit_check() - if (in_check_mode): - logging.info("Ansible check mode complete") - module.exit_json(**results) - else: - logging.info("committing change, please be patient") - opts = {} - if args['comment'] is not None: - opts['comment'] = args['comment'] - if args['confirm'] is not None: - opts['confirm'] = args['confirm'] - - cu.commit(**opts) - results['changed'] = True - - except CommitError as err: - msg = "Unable to commit configuration: {0}".format(err) - logging.error(msg) - try: - logging.info("unlocking") - cu.unlock() - except UnlockError as err: - logging.error("Unable to unlock config: {0}".format(err)) - dev.close() - module.fail_json(msg=msg) - - try: - logging.info("unlocking") - cu.unlock() - except UnlockError as err: - logging.error("Unable to unlock config: {0}".format(err)) - - logging.info("change completed") - - dev.close() - module.exit_json(**results) - - -def _load_via_console(module): - try: - from netconify.cmdo import netconifyCmdo - from netconify.constants import version - if not LooseVersion(version) >= LooseVersion('1.0.1'): - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - except ImportError: - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - - m_args = module.params - - c_args = [] - c_args.append(m_args['console']) - c_args.append('--file=' + m_args['file']) - if m_args['savedir'] is not None: - c_args.append('--savedir=' + m_args['savedir']) - c_args.append('--user=' + m_args['user']) - if m_args['passwd'] is not None: - c_args.append('--passwd=' + m_args['passwd']) - - # the default mode for loading a config via the console - # is to load-overwrite. So we need to check the module - # option and set the "--merge" option if overwrite is False - - overwrite = module.boolean(module.params['overwrite']) - if overwrite is False: - c_args.append('--merge') - - c_args.append(m_args['host']) - - logfile = m_args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'NETCONIFY:' + m_args['host'] - - def log_notify(self, event, message): - logging.info("%s:%s" % (event, message)) - use_notifier = log_notify - else: - def silent_notify(self, event, message): - pass - use_notifier = silent_notify - - try: - nc = netconifyCmdo(notify=use_notifier) - c_results = nc.run(c_args) - except Exception as err: - module.fail_json(msg=str(err)) - m_results = dict(changed=c_results['changed']) - if c_results['failed'] is True: - module.fail_json(msg=c_results['errmsg']) - else: - module.exit_json(**m_results) - -# --------------------------------------------------------------------------- -# MAIN -# --------------------------------------------------------------------------- - - -def main(): - module = AnsibleModule( - argument_spec=dict( - host=dict(required=True), - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - console=dict(required=False, default=None), - file=dict(required=True), - overwrite=dict(required=False, choices=BOOLEANS, default=False), - replace=dict(required=False, choices=BOOLEANS, default=False), - logfile=dict(required=False, default=None), - diffs_file=dict(required=False, default=None), - savedir=dict(required=False), - timeout=dict(required=False, default=0), - comment=dict(required=False, default=None), - port=dict(required=False, default=830), - confirm=dict(required=False, default=None) - ), - supports_check_mode=True) - - if not HAS_PYEZ: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - args = module.params - - # ------------------------------ - # make sure file actually exists - # ------------------------------ - - if not isfile(args['file']): - module.fail_json(msg="file not found: {0}".format(args['file'])) - return - - _ldr = _load_via_netconf if args['console'] is None else _load_via_console - _ldr(module) - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_install_os b/library/junos_install_os deleted file mode 100644 index 406dafdb..00000000 --- a/library/junos_install_os +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 1999-2014, Juniper Networks Inc. -# 2014, Jeremy Schulman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_install_os -author: Jeremy Schulman, Juniper Networks -version_added: "1.0.0" -short_description: Install a Junos OS image. -description: - - Install a Junos OS image on one or more Routing Engines. - This module supports installations on single Routing Engine devices, - MX Series routers with dual Routing Engines, and EX Series switches in - a non-mixed Virtual Chassis. This action is equivalent to performing the - Junos OS B(request system software add) operational command. - - If the existing Junos OS version matches the desired version, - no action is performed, and the "changed" attribute reports False. - If the existing version does not match, then the module performs the - following actions - - (1) Computes the MD5 checksum of the package located on the server. - (2) Copies the Junos OS software package to the device running Junos OS. - (3) Computes the MD5 checksum on the device running Junos OS and compares the two. - (4) Installs the Junos OS software package. - (5) Reboots the device (default). - - Running the module in check mode reports whether the current Junos OS - version matches the desired version. - -requirements: - - py-junos-eznc >= 1.2.2 -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - no_copy: - description: - - Installer need to be copied or not on the device. - required: false - default: false - choices: ['true','false'] - reboot: - description: - - If set to B(yes), the device reboots - after the installation completes. - required: false - default: yes - choices: ['yes','no'] - reboot_pause: - description: - - Amount of time in seconds to wait after the reboot is issued - required: false - default: "10" - version: - description: - - Junos OS version string as it would be reported by the - B(show version) command - required: true - package: - description: - - Absolute path on the local server to the Junos OS software package - required: true - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes - required: false - default: None - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 -''' - -EXAMPLES = ''' -- junos_install_os: - host={{ inventory_hostname }} - version=12.1X46-D10.2 - package=/usr/local/junos/images/junos-vsrx-12.1X46-D10.2-domestic.tgz - logfile=/usr/local/junos/log/software.log -''' - -import logging -import re -import os -from distutils.version import LooseVersion -try: - from jnpr.junos import Device - from jnpr.junos.version import VERSION - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - HAS_PYEZ = False - else: - HAS_PYEZ = True -except ImportError: - HAS_PYEZ = False - - -def junos_install_os(module, dev): - args = module.params - - # ------------------------------------------------------------------------- - # logging - # ------------------------------------------------------------------------- - - logfile = args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = args['host'] - - def do_log(msg, level='info'): - getattr(logging, level)(msg) - else: - def do_log(msg): - pass - - # ------------------------------------------------------------------------- - # check installed version against desired version - # ------------------------------------------------------------------------- - - if args['version'] is None: - # extract version string from package file - m = re.search('-([^\\-]*)-domestic.*', args['package']) - args['version'] = m.group(1) - - has_ver = dev.facts['version'] - should_ver = args['version'] - need_upgrade = bool(has_ver != should_ver) - - results = dict(changed=need_upgrade, ver=dict(has=has_ver, should=should_ver)) - - if need_upgrade is False: - do_log("No need to perform upgrade: {0}".format(has_ver)) - return results - if module.check_mode is True: - do_log("upgrade REQUIRED has: {0}, shoud: {1}".format(has_ver, should_ver)) - return results - - # ------------------------------------------------------------------------- - # proceeed with software install - # ------------------------------------------------------------------------- - - from jnpr.junos.utils.sw import SW - sw = SW(dev) - - package = args['package'] - do_log("Starting the software upgrade process: {0}".format(package)) - - def update_my_progress(dev, report): - # log the progress of the installing process - do_log(report) - - sw_args = dict(progress=update_my_progress) - sw_args['no_copy'] = module.boolean(args['no_copy']) - ok = sw.install(package, **sw_args) - - if ok is not True: - results['failed'] = True - results['msg'] = "Unable to install the software" - do_log(results['msg'], level='error') - return results - - # ------------------------------------------------------------------------- - # reboot the box if desired - # ------------------------------------------------------------------------- - - if module.boolean(args['reboot']) is True: - do_log("") - rsp = sw.reboot() - - # ------------------------------------------------------------------------- - # all done. - # ------------------------------------------------------------------------- - do_log("upgrade pending reboot cycle, please be patient.") - return results - - -def main(): - # - module = AnsibleModule( - argument_spec=dict( - host=dict(required=True), - package=dict(required=True), - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - version=dict(required=False, default=None), - logfile=dict(required=False, default=None), - no_copy=dict(required=False, choices=BOOLEANS, default=False), - reboot=dict(required=False, choices=BOOLEANS, default=True), - reboot_pause=dict(required=False, type='int', default=10), - port=dict(required=False, default=830) - ), - supports_check_mode=True - ) - - if not HAS_PYEZ: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - args = module.params - - # @@@ need to verify that the package file actually exists - # @@@ before proceeding. - - dev = Device(args['host'], user=args['user'], password=args['passwd'], port=args['port']) - try: - dev.open() - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(args['host'], str(err)) - module.fail_json(msg=msg) - return - - results = junos_install_os(module, dev) - results['check_mode'] = module.check_mode - dev.close() - - if not module.check_mode and results['changed'] is True: - logging.info('pausing: {0}'.format(args['reboot_pause'])) - import time - time.sleep(args['reboot_pause']) - logging.info('process completed OK.') - - module.exit_json(**results) - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_rollback b/library/junos_rollback deleted file mode 100755 index 881e0cb0..00000000 --- a/library/junos_rollback +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 1999-2015, Juniper Networks Inc. -# 2014, Jeremy Schulman -# 2015, Rick Sherman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_rollback -author: Rick Sherman, Juniper Networks -version_added: "1.2.0" -short_description: Rollback configuration of device -description: - - Rollback the configuration of a device running Junos -requirements: - - junos-eznc >= 1.2.2 -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes - required: false - default: None - rollback: - description: - - The rollback id value [0-49] - required: true - default: None - comment: - description: - - Provide a comment to the commit of the configuration - required: false - default: None - confirm: - description: - - Provide a confirm in minutes to the commit of the configuration - required: false - default: None - diffs_file: - description: - - Path to the file where any diffs will be written - required: false - default: None -''' - -EXAMPLES = ''' -- junos_rollback: - host: "{{ inventory_hostname }}" - logfile=rollback.log - diffs_file=rollback.diff - rollback=1 - comment="Rolled back by Ansible" - confirm=5 -''' -from distutils.version import LooseVersion -import logging - -try: - from jnpr.junos import Device - from jnpr.junos.version import VERSION - from jnpr.junos.utils.config import Config - from jnpr.junos.exception import CommitError, LockError, UnlockError, RpcError - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - HAS_PYEZ = False - else: - HAS_PYEZ = True -except ImportError: - HAS_PYEZ = False - - -def main(): - - module = AnsibleModule( - argument_spec=dict(host=dict(required=True, default=None), # host or ipaddr - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - port=dict(required=False, default=830), - logfile=dict(required=False, default=None), - rollback=dict(required=True, default=None), - comment=dict(required=False, default=None), - confirm=dict(required=False, default=None), - diffs_file=dict(required=False, default=None) - ), - supports_check_mode=True) - - if not HAS_PYEZ: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - args = module.params - in_check_mode = module.check_mode - - results = dict(changed=False) - - logfile = args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'CONFIG:' + args['host'] - - rb_id = int(args['rollback']) - if not 0 <= rb_id <= 49: - msg = "Rollback must be an integer [0-49]" - logging.error(msg) - module.fail_json(msg=msg) - - logging.info("connecting to host: {0}@{1}:{2}".format(args['user'], args['host'], args['port'])) - - try: - dev = Device(args['host'], user=args['user'], password=args['passwd'], - port=args['port'], gather_facts=False).open() - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(args['host'], str(err)) - logging.error(msg) - module.fail_json(msg=msg) - # --- UNREACHABLE --- - - try: - cu = Config(dev) - - logging.info("taking lock") - cu.lock() - - logging.info("Rolling back to: {0}".format(rb_id)) - cu.rollback(rb_id=rb_id) - - diff = cu.diff() - if diff is not None: - diffs_file = args['diffs_file'] - if diffs_file is not None: - try: - f = open(diffs_file, 'w') - f.write(diff) - f.close() - except IOError as (errno, strerror): - msg = "Problem with diffs_file {0}: ".format(diffs_file) - msg += "I/O Error: ({0}): {1}".format(errno, strerror) - logging.error(msg) - except: - msg = "Problem with diffs_file {0}: ".format(diffs_file) - msg += "Unexpected error:", sys.exc_info()[0] - logging.error(msg) - - if (in_check_mode): - logging.info("doing a commit-check, please be patient") - cu.commit_check() - else: - logging.info("committing change, please be patient") - opts = {} - if args['comment'] is not None: - opts['comment'] = args['comment'] - if args['confirm'] is not None: - opts['confirm'] = args['confirm'] - - cu.commit(**opts) - results['changed'] = True - logging.info("change completed") - - logging.info("unlocking") - cu.unlock() - - except LockError as err: - msg = 'Unable to lock configuration - will not rollback: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except UnlockError as err: - msg = 'Unable to unlock configuration - commit should succeed: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except CommitError as err: - msg = 'Unable to commit: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except (ValueError, RpcError) as err: - msg = 'Unable to rollback: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - except Exception as err: - msg = 'Uncaught exception - please report: {0}'.format(str(err)) - logging.error(msg) - dev.close() - module.fail_json(msg=msg) - - dev.close() - - module.exit_json(**results) - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_shutdown b/library/junos_shutdown deleted file mode 100644 index 6150e88a..00000000 --- a/library/junos_shutdown +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 1999-2014, Juniper Networks Inc. -# 2014, Jeremy Schulman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_shutdown -author: Jeremy Schulman, Juniper Networks -version_added: "1.0.0" -short_description: Shut down or reboot a device running Junos OS. -description: - - Shut down (power off) or reboot a device running Junos OS. This includes - all Routing Engines in a Virtual Chassis or a dual Routing Engine system. - This is equivalent to executing either the Junos OS B(request system power-off) - or B(request system reboot) operational command. -requirements: - - junos-eznc >= 1.2.2 -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - reboot: - description: - - If set to B(yes), then the device is rebooted rather than powered off. - required: false - default: no - choices: ['yes','no'] - shutdown: - description: - - Safety mechanism. You B(MUST) set this to 'shutdown'. - required: yes - default: None - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 -''' - -EXAMPLES = ''' -- junos_shutdown: - host={{ inventory_hostname }} - shutdown="shutdown" - reboot=yes -''' -from distutils.version import LooseVersion - -try: - from jnpr.junos import Device - from jnpr.junos.utils.sw import SW - from jnpr.junos.version import VERSION - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - HAS_PYEZ = False - else: - HAS_PYEZ = True -except ImportError: - HAS_PYEZ = False - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - shutdown=dict(required=True, default=None), # must be set to 'shutdown' - host=dict(required=True, default=None), # host or ipaddr - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - reboot=dict(required=False, choices=BOOLEANS, default=False), - port=dict(required=False, default=830) - ), - supports_check_mode=False) - - if not HAS_PYEZ: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - args = module.params - if args['shutdown'] != 'shutdown': - module.fail_json(msg='Say "shutdown" to proceed') - - restart_mode = module.boolean(args['reboot']) - - try: - dev = Device(args['host'], user=args['user'], password=args['passwd'], port=args['port']).open() - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(args['host'], str(err)) - module.fail_json(msg=msg) - # --- UNREACHABLE --- - - results = {'changed': True, 'reboot': restart_mode} - - sw = SW(dev) - do_it = sw.reboot if restart_mode is True else sw.poweroff - do_it(0) - - # dev.close isn't performed since the device will - # be in the process of shutting down and we'll - # lose connectivity anyways ... - - module.exit_json(**results) - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_srx_cluster b/library/junos_srx_cluster deleted file mode 100644 index 4a9f02ad..00000000 --- a/library/junos_srx_cluster +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 1999-2014, Juniper Networks Inc. -# 2014, Patrik Bok -# 2015, Rick Sherman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_srx_cluster -author: Patrik Bok, Ashley Burston, Rick Sherman, Juniper Networks -version_added: "1.2.0" -short_description: Create an srx chassis cluster for cluster capable srx running Junos OS. -description: - - Create an srx chassis cluster and reboot the device. - The device must be capable of forming an srx cluster and have the correct - cables installed. -requirements: - - junos-eznc >= 1.2.2 -options: - host: - description: - - Set to {{ inventory_hostname }} - required: true - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 - console: - description: - - SERIAL or TERMINAL-SERVER port setting, per use with - the B(netconify) utility - required: false - default: None - notes: - - ex: "--telnet=termserv101,2000" - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - cluster_enable: - description: - - yes/true - set device to cluster mode (specify cluster_id and node) - - no/false - set device to stand alone mode (disable cluster mode) - required: true - choices: ['true','false','yes','no'] - cluster_id: - description: - - set to the cluster id , required for cluster_enable=YES - required: false - default: None - node: - description: - - set to the node required (0 or 1) - required: false - default: None - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes - required: false - default: None -''' - -EXAMPLES = ''' --junos_srx_cluster: - host={{ inventory_hostname }} - console="--port={{ serial }}" - user=rick - passwd=password123 - cluster_enable=true - logfile=cluster.log - cluster_id={{ cluster_id }} - node={{ node_id }} - --junos_srx_cluster: - host={{ inventory_hostname }} - user=rick - passwd=password123 - cluster_enable=false - logfile=cluster.log -''' -import os -import logging -from distutils.version import LooseVersion - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - host=dict(required=True, default=None), # host or ipaddr - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - port=dict(required=False, default=830), - console=dict(required=False, default=None), # param to netconify - cluster_enable=dict(required=True, choices=BOOLEANS, default=None), - cluster_id=dict(required=False, default=None), - node=dict(required=False, default=None), - logfile=dict(required=False, default=None)), - supports_check_mode=False) - - args = module.params - - logfile = args['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'SRX_Cluster:' + args['host'] - - def log_notify(self, event, message): - logging.info("%s:%s" % (event, message)) - use_notifier = log_notify - else: - def silent_notify(self, event, message): - pass - use_notifier = silent_notify - - cluster = module.boolean(module.params['cluster_enable']) - - if args.get('console') is None: - # via NETCONF - logging.info('CONSOLE: no console parameter, using NETCONF') - - try: - from jnpr.junos import Device - from jnpr.junos.version import VERSION - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - except ImportError: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - dev = Device(args['host'], user=args['user'], password=args['passwd'], port=args['port'], gather_facts=('false')) - try: - logging.info('LOGIN: host={0} port={1}'.format(args['host'], args['port'])) - dev.open() - logging.info('LOGIN: OK') - except Exception as err: - msg = 'unable to connect to {0}:{1}: {2}'.format(args['host'], args['port'], str(err)) - logging.error(msg) - module.fail_json(msg=msg) - return - # --- UNREACHABLE --- - - logging.info('SRX cluster: invoking command') - if cluster is True: - if (args['cluster_id'] and args['node']) is not None: - do_it = dev.rpc.set_chassis_cluster_enable(cluster_id=args['cluster_id'], node=args['node'], reboot=True) - logging.info('Device message: {0}'.format(do_it.findtext('.//message').strip())) - else: - msg = 'FAIL: Cluster and Node ID required with enable_cluster=YES' - logging.error(msg) - dev.close() - results = {'changed': False, 'msg': msg} - module.fail_json(**results) - else: - do_it = dev.rpc.set_chassis_cluster_disable(reboot=True, normalize=True) - if do_it.tag == 'output': - msg = do_it.text - else: - msg = do_it.findtext('.//message') - logging.info('Device message: {0}'.format(msg)) - results = {'changed': True, 'reboot': True} - else: - # connection using NETCONIFY - try: - from netconify.cmdo import netconifyCmdo - from netconify.constants import version - if not LooseVersion(version) >= LooseVersion('1.0.1'): - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - except ImportError: - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - nc_args = [] - nc_args.append(args['console']) - if cluster is True: - if (args['cluster_id'] and args['node']) is not None: - nc_args.append('--srx_cluster=' + args['cluster_id'] + ':' + args['node']) - else: - msg = 'FAIL: Cluster and Node ID required with enable_cluster=YES' - logging.error(msg) - results = {'changed': False, 'msg': msg} - module.fail_json(**results) - else: - nc_args.append('--srx_cluster_disable') - if args['user'] is not None: - nc_args.append('--user=' + args['user']) - if args['passwd'] is not None: - nc_args.append('--passwd=' + args['passwd']) - logging.info('Status: passing argument to netconify: {}'.format(nc_args)) - nc = netconifyCmdo(notify=use_notifier) - nc.run(nc_args) - results = {'changed': True, 'reboot': True} - - if cluster is True: - logging.info('Cluster set...device rebooting... DONE') - else: - logging.info('Cluster disabled...device rebooting... DONE') - module.exit_json(**results) - -from ansible.module_utils.basic import * -main() diff --git a/library/junos_zeroize b/library/junos_zeroize deleted file mode 100644 index 22f9e818..00000000 --- a/library/junos_zeroize +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 1999-2014, Juniper Networks Inc. -# 2014, Jeremy Schulman -# -# All rights reserved. -# -# License: Apache 2.0 -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the Juniper Networks nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY Juniper Networks, Inc. ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL Juniper Networks, Inc. BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -DOCUMENTATION = ''' ---- -module: junos_zeroize -author: Jeremy Schulman, Juniper Networks -version_added: "1.0.0" -short_description: Erase all data, including configuration and log files, on a device running Junos OS. -description: - - Execute the Junos OS B(request system zeroize) command to remove all - configuration information on the Routing Engines and reset all key - values on a device running Junos OS. The command removes all data files, - including customized configuration and log files, by unlinking the files - from their directories. The command also removes all user-created files - from the system including all plain-text passwords, secrets, and - private keys for SSH, local encryption, local authentication, IPsec, - RADIUS, TACACS+, and SNMP. - - This command reboots the device and sets it to the factory default - configuration. After the reboot, you must log in through the console - as root in order to access the device. -requirements: - - junos-eznc >= 1.2.2 - - junos-netconify >= 1.0.1, when using the I(console) option -options: - host: - description: - - Set to {{ inventory_hostname }} - required: false - user: - description: - - Login username - required: false - default: $USER - passwd: - description: - - Login password - required: false - default: assumes ssh-key active - console: - description: - - SERIAL or TERMINAL-SERVER port setting, per use with - the B(netconify) utility - required: false - default: None - notes: - - ex: "--telnet=termserv101,2000" - zeroize: - description: - - Safety mechanism. You B(MUST) set this to 'zeroize'. - required: yes - default: None - logfile: - description: - - Path on the local server where the progress status is logged - for debugging purposes - required: false - default: None - port: - description: - - TCP port number to use when connecting to the device - required: false - default: 830 -notes: - - You B(MUST) either use the I(host) option or the I(console) option to designate - how the device is accessed. -''' - -EXAMPLES = ''' -- junos_zeroize: - host={{ inventory_hostname }} - zeroize="zeroize" -''' - -import os -import logging -from distutils.version import LooseVersion - - -def main(): - module = AnsibleModule( - argument_spec=dict( - zeroize=dict(required=True, default=None), # must be set to 'zeroize' - host=dict(required=False, default=None), # host or ipaddr - console=dict(required=False, default=None), # param to netconify - user=dict(required=False, default=os.getenv('USER')), - passwd=dict(required=False, default=None), - logfile=dict(required=False, default=None), - port=dict(required=False, default=830) - ), - supports_check_mode=False) - - args = module.params - - if args['zeroize'] != 'zeroize': - results = dict(changed=False, failed=True) - results['msg'] = "You must set 'zeroize=zeroize' ({0})".format(args['zeroize']) - module.exit_json(**results) - # - # ! UNREACHABLE - - results = {} - - logfile = module.params['logfile'] - if logfile is not None: - logging.basicConfig(filename=logfile, level=logging.INFO, - format='%(asctime)s:%(name)s:%(message)s') - logging.getLogger().name = 'NETCONIFY:' + module.params['host'] - - def log_notify(self, event, message): - logging.info("%s:%s" % (event, message)) - use_notifier = log_notify - else: - def silent_notify(self, event, message): - pass - use_notifier = silent_notify - - if args.get('console') is None: - # via NETCONF - try: - from jnpr.junos import Device - from jnpr.junos.version import VERSION - if not LooseVersion(VERSION) >= LooseVersion('1.2.2'): - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - except ImportError: - module.fail_json(msg='junos-eznc >= 1.2.2 is required for this module') - - dev = Device(args['host'], user=args['user'], password=args['passwd'], port=args['port']) - try: - use_notifier(None, 'LOGIN', 'host={0}'.format(args['host'])) - dev.open() - use_notifier(None, 'LOGIN', 'OK') - except Exception as err: - msg = 'unable to connect to {0}: {1}'.format(args['host'], str(err)) - module.fail_json(msg=msg) - # --- UNREACHABLE --- - - use_notifier(None, 'ZEROIZE', 'invoking command') - dev.cli('request system zeroize') - results['changed'] = True - # no close, we're done after this point. - else: - try: - from netconify.cmdo import netconifyCmdo - from netconify.constants import version - if not LooseVersion(version) >= LooseVersion('1.0.1'): - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - except ImportError: - module.fail_json(msg='junos-netconify >= 1.0.1 is required for this module') - nc_args = [] - nc_args.append(args['console']) - nc_args.append('--zeroize') - if args['user'] is not None: - nc_args.append('--user=' + args['user']) - if args['passwd'] is not None: - nc_args.append('--passwd=' + args['passwd']) - - try: - nc = netconifyCmdo(notify=use_notifier) - nc.run(nc_args) - except Exception as err: - module.fail_json(msg=str(err)) - results['changed'] = True - - # indicate done in the logfile and return results - use_notifier(None, 'DONE', 'OK') - module.exit_json(**results) - -from ansible.module_utils.basic import * -main() diff --git a/meta/main.yml b/meta/main.yml index 015cc58b..068cffaa 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,124 +1,17 @@ --- galaxy_info: - author: Juniper #Jeremy Schulman - description: Network build automation of Junos devices. + author: Juniper #Stacy W. Smith @stacywsmith + role_name: junos + description: Network build automation of Junos devices. company: Juniper Networks, Inc. - # Some suggested licenses: - # - BSD (default) - # - MIT - # - GPLv2 - # - GPLv3 - # - Apache - # - CC-BY license: Apache 2.0 - min_ansible_version: 1.5 - # - # Below are all platforms currently available. Just uncomment - # the ones that apply to your role. If you don't see your - # platform on this list, let us know and we'll get it added! - # - #platforms: - #- name: EL - # versions: - # - all - # - 5 - # - 6 - # - 7 - #- name: GenericUNIX - # versions: - # - all - # - any - #- name: Fedora - # versions: - # - all - # - 16 - # - 17 - # - 18 - # - 19 - # - 20 - #- name: opensuse - # versions: - # - all - # - 12.1 - # - 12.2 - # - 12.3 - # - 13.1 - # - 13.2 - #- name: Amazon - # versions: - # - all - # - 2013.03 - # - 2013.09 - #- name: GenericBSD - # versions: - # - all - # - any - #- name: FreeBSD - # versions: - # - all - # - 8.0 - # - 8.1 - # - 8.2 - # - 8.3 - # - 8.4 - # - 9.0 - # - 9.1 - # - 9.1 - # - 9.2 - #- name: Ubuntu - # versions: - # - all - # - lucid - # - maverick - # - natty - # - oneiric - # - precise - # - quantal - # - raring - # - saucy - # - trusty - #- name: SLES - # versions: - # - all - # - 10SP3 - # - 10SP4 - # - 11 - # - 11SP1 - # - 11SP2 - # - 11SP3 - #- name: GenericLinux - # versions: - # - all - # - any - #- name: Debian - # versions: - # - all - # - etch - # - lenny - # - squeeze - # - wheezy - # - # Below are all categories currently available. Just as with - # the platforms above, uncomment those that apply to your role. - # - categories: - #- cloud - #- cloud:ec2 - #- cloud:gce - #- cloud:rax - #- clustering - #- database - #- database:nosql - #- database:sql - #- development - #- monitoring + min_ansible_version: 2.4 + platforms: + - name: junos + versions: + - all + galaxy_tags: - networking - #- packaging - #- system - #- web + - junos + - juniper dependencies: [] - # List your role dependencies here, one per line. Only - # dependencies available via galaxy should be listed here. - # Be sure to remove the '[]' above if you add dependencies - # to this list. - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..314f7eb2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +ansible >= 2.10 +junos-eznc >= 2.6.0 +jsnapy>=1.3.6 +jxmlease +xmltodict +looseversion diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 5a14e204..a0cc7e6f --- a/setup.py +++ b/setup.py @@ -1,33 +1,37 @@ +#!/usr/bin/env python from setuptools import setup + from version import VERSION setup( - name="ansible-junos-stdlib", + name="ansible-junos-stdlib", version=VERSION, - author="Jeremy Schulman", + author="Jeremy Schulman, Nitin Kumar, Rick Sherman, Stacy Smith", author_email="jnpr-community-netdev@juniper.net", description=("Ansible Network build automation of Junos devices."), license="Apache 2.0", keywords="Ansible Junos NETCONF networking automation", url="http://www.github.com/Juniper/ansible-junos-stdlib", - packages=['library'], - #scripts = ['library/junos_get_facts', 'library/junos_install_config'], + packages=[ + "ansible_collections/juniper/device/plugins/modules", + "ansible_collections/juniper/device/plugins/module_utils", + ], classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Intended Audience :: System Administrators', - 'Intended Audience :: Telecommunications Industry', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Application Frameworks', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: System :: Networking', - 'Topic :: Text Processing :: Markup :: XML' + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Intended Audience :: Telecommunications Industry", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Networking", + "Topic :: Text Processing :: Markup :: XML", ], ) diff --git a/tests/.travis.yml b/tests/.travis.yml new file mode 100644 index 00000000..482d79d0 --- /dev/null +++ b/tests/.travis.yml @@ -0,0 +1,30 @@ +language: python +python: + - 2.7 + - 3.6 + +sudo: required +dist: trusty + +env: + - ANSIBLE_VERSION=2.4.0.0 + - ANSIBLE_VERSION=2.3.0.0 + +install: +## Create Docker with Ansible modules and all dependancies + - docker build --build-arg ver_ansible=$ANSIBLE_VERSION -t juniper/pyez-ansible:travis . + - docker pull juniper/ravello-ansible:v0.1 +## Install Ansible locally for Ravello and install Roles + - cd tests + +script: +## Start Virtual topology on Ravello with 2 VQFX and collect IP addresses +## Anyone can connect here to see the list of applications running and see the VMs +## https://cloud.ravellosystems.com/#/GtHFbCOuKgD1pcfkvCCIgenj6DOtn3VgRLjaYipdideCsiPC1NxJitt1UHfhF0Bf/apps + - docker run -t -i -v $(pwd):/project -e "ANSIBLE_VERSION=$(echo $ANSIBLE_VERSION)" -e "TRAVIS_JOB_ID=$(echo $TRAVIS_JOB_ID)" -e "TRAVIS_COMMIT=$(echo $TRAVIS_COMMIT)" juniper/ravello-ansible:v0.1 ansible-playbook -i ravello.ini pb.rav.token.create-deploy.yaml + - docker run -t -i -v $(pwd):/project -e "ANSIBLE_VERSION=$(echo $ANSIBLE_VERSION)" -e "TRAVIS_JOB_ID=$(echo $TRAVIS_JOB_ID)" -e "TRAVIS_COMMIT=$(echo $TRAVIS_COMMIT)" juniper/ravello-ansible:v0.1 ansible-playbook -i ravello.ini pb.rav.token.fqdn_get.yaml + +## Execute Tests with Docker + - docker run -t -i -v $(pwd):/project juniper/pyez-ansible:travis ansible-playbook -i ravello.ini pb.junos_ping.yaml + - docker run -t -i -v $(pwd):/project juniper/pyez-ansible:travis ansible-playbook -i ravello.ini pb.junos_pmtud.yaml + - docker run -t -i -v $(pwd):/project juniper/pyez-ansible:travis ansible-playbook -i ravello.ini pb.junos_jsnapy.yaml diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..babb0b13 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,61 @@ +# Functional Tests for Juniper Ansible collection for Junos + +Following are the steps to execute Ansible Functional test playbooks + +## Steps to execute Ansible Functional test playbooks + +1. Git clone the functional tests +``` +git clone https://github.com/Juniper/ansible-junos-stdlib.git +``` +2. Change directory to ansible-junos-stdlib/tests +``` +cd ansible-junos-stdlib/tests/ +``` +3. Update the ansible.cfg under tests directory +``` +[defaults] +hash_behaviour=merge +inventory = inventory +host_key_checking = False +log_path = ./ansible.log + +[persistent_connection] +command_timeout = 300 +``` +4. To execute test playbook with Local connection, you need to update the inventory file for local connection test cases by setting ansible_connection=local +``` +[junos] +local_connection_testcases ansible_host=xx.xx.xx.xx ansible_user=xyz ansible_pass=xyz ansible_port=22 ansible_connection=local ansible_command_timeout=300 + +[all:vars] +ansible_python_interpreter= +``` +5. To execute test playbook with PyEZ persistent connection, you need to update the inventory file for persistent connection test cases by setting ansible_connection=juniper.device.pyez +``` +[junos] +pyez_connection_testcases ansible_host=xx.xx.xx.xx ansible_user=xyz ansible_ssh_pass=xyz ansible_port=22 ansible_connection=juniper.device.pyez ansible_command_timeout=300 + +[all:vars] +ansible_python_interpreter= +``` +6. To execute Functional test playbooks +ansible-playbook +``` +ansible-playbook pb.juniper_junos_system.yml +``` +### NOTE: + +To execute pb.juniper_junos_software_member.yml playbook, you have to use the following devices + +Virtual Chassis - EX-VC, MX-VC + +To execute pb.juniper_junos_srx_cluster.yml playbook, you have to use the following devices + +SRX HA Cluster enabled devices + +### Ansible Fixes : +For the PyEZ persistent connection support, you need to apply the following fix for Ansible issue +``` +https://github.com/Juniper/ansible-junos-stdlib/issues/606 +``` diff --git a/tests/ansible.cfg b/tests/ansible.cfg new file mode 100644 index 00000000..c2c908e2 --- /dev/null +++ b/tests/ansible.cfg @@ -0,0 +1,7 @@ + +[defaults] +hash_behaviour=merge +roles_path = ~/.ansible/roles + +[persistent_connection] +command_timeout = 300 diff --git a/tests/inventory b/tests/inventory new file mode 100644 index 00000000..eb53d4ed --- /dev/null +++ b/tests/inventory @@ -0,0 +1,7 @@ +[junos] +local_connection_testcases ansible_host=x.x.x.x ansible_user=xxxx ansible_pass=xxxx ansible_port=22 ansible_connection=local ansible_command_timeout=300 + +pyez_connection_testcases ansible_host=x.x.x.x ansible_user=xxx ansible_ssh_pass=xxxx ansible_port=22 ansible_connection=juniper.device.pyez ansible_command_timeout=300 + +[all:vars] +ansible_python_interpreter= diff --git a/tests/junos_jsnapy/add_loopback.set b/tests/junos_jsnapy/add_loopback.set new file mode 100644 index 00000000..6019602d --- /dev/null +++ b/tests/junos_jsnapy/add_loopback.set @@ -0,0 +1,3 @@ +set routing-instances LO10 interface lo0.10 +set routing-instances LO10 instance-type virtual-router +set interfaces lo0 unit 10 family inet diff --git a/tests/junos_jsnapy/delete_loopback.set b/tests/junos_jsnapy/delete_loopback.set new file mode 100644 index 00000000..818ff206 --- /dev/null +++ b/tests/junos_jsnapy/delete_loopback.set @@ -0,0 +1,2 @@ +delete routing-instances LO10 +delete interfaces lo0 unit 10 diff --git a/tests/junos_jsnapy/test_junos_storage.yml b/tests/junos_jsnapy/test_junos_storage.yml new file mode 100644 index 00000000..1be2c82b --- /dev/null +++ b/tests/junos_jsnapy/test_junos_storage.yml @@ -0,0 +1,11 @@ +tests_include: + - check_storage + +check_storage: + - command: show system storage + - iterate: + xpath: //system-storage-information/filesystem[normalize-space(mounted-on)='/.mount'] + tests: + - is-lt: used-percent, 95 + info: "File system {{post['mounted-on']}} use less than 95%" + err: "File system {{post['mounted-on']}} use {{post['used-percent']}} %" diff --git a/tests/junos_jsnapy/test_loopback.yml b/tests/junos_jsnapy/test_loopback.yml new file mode 100644 index 00000000..cd27467d --- /dev/null +++ b/tests/junos_jsnapy/test_loopback.yml @@ -0,0 +1,16 @@ +tests_include: + - test_command_version + +test_command_version: + - command: show interfaces terse lo* + - iterate: + xpath: //logical-interface + id: './name' + tests: + - list-not-more: admin-status + err: "Test Failed!! name list changed, <{{pre['admin-status']}}> with name <{{id_0}}> is not found in post snapshot" + info: "Test successful!! name list is same, with name <{{id_0}}>" + + - list-not-less: admin-status + err: "Test Failed!! name list changed, <{{pre['admin-status']}}> with name <{{id_0}}> was not present in PRE snapshot" + info: "Test successful!! name list is same, with name <{{id_0}}>" diff --git a/tests/junos_jsnapy/test_snmp.yml b/tests/junos_jsnapy/test_snmp.yml new file mode 100644 index 00000000..6967c959 --- /dev/null +++ b/tests/junos_jsnapy/test_snmp.yml @@ -0,0 +1,11 @@ +tests_include: + - test_snmp_config + +test_snmp_config: + - rpc: get-configuration + - iterate: + xpath: /configuration/snmp + tests: + - exists: community[name='mycommunity'] + info: SNMP community ''mycommunity' is configured. + err: SNMP community ''mycommunity' is not configured! diff --git a/tests/junos_jsnapy/test_version.yml b/tests/junos_jsnapy/test_version.yml new file mode 100644 index 00000000..ed2571c8 --- /dev/null +++ b/tests/junos_jsnapy/test_version.yml @@ -0,0 +1,12 @@ +tests_include: + - test_version_check + +test_version_check: + - command: show version + - iterate: + id: host-name + xpath: //software-information + tests: + - exists: //package-information/name + info: "Test Succeeded!! node //package-information/name exists with name <{{pre['//package-information/name']}}> and hostname: <{{id_0}} > " + err: "Test Failed!!! node //package-information/name does not exists in hostname: <{{id_0}}> !! " diff --git a/tests/pb.juniper_junos_command.yml b/tests/pb.juniper_junos_command.yml new file mode 100644 index 00000000..d51c1725 --- /dev/null +++ b/tests/pb.juniper_junos_command.yml @@ -0,0 +1,142 @@ +--- +- name: Test juniper.device.command module + hosts: all + gather_facts: false + + tasks: + - name: TEST 1 - Execute single "show version" command. + juniper.device.command: + commands: "show version" + register: test1 + + - name: Check TEST 1 + ansible.builtin.assert: + that: + test1.msg == "The command executed successfully." + + - name: Creates directory + ansible.builtin.file: + path: out + state: directory + mode: '0644' + + - name: TEST 2 - Execute three commands. + juniper.device.command: + commands: + - "show version" + - "show system uptime" + - "show interface terse" + register: test2 + + - name: Check TEST 2 + ansible.builtin.assert: + that: + - test2.results[0].command == "show version" + - test2.results[0].msg == "The command executed successfully." + - test2.results[1].command == "show system uptime" + - test2.results[1].msg == "The command executed successfully." + - test2.results[2].command == "show interface terse" + - test2.results[2].msg == "The command executed successfully." + + - name: Print the command output of each. + ansible.builtin.debug: + var: item.stdout + with_items: "{{ test2.results }}" + + - name: TEST 3 - Two commands with XML output. + juniper.device.command: + commands: + - "show route" + - "show version" + format: xml + register: test3 + + - name: Check TEST 3 + ansible.builtin.assert: + that: + - test3.results[0].command == "show route" + - test3.results[0].msg == "The command executed successfully." + - test3.results[1].command == "show version" + - test3.results[1].msg == "The command executed successfully." + + - name: TEST 4 - show route with XML output - show version with JSON output + juniper.device.command: + commands: + - "show route" + - "show version" + formats: + - "xml" + - "json" + register: test4 + + - name: Check TEST 4 + ansible.builtin.assert: + that: + - test4.results[0].command == "show route" + - test4.results[0].msg == "The command executed successfully." + - test4.results[1].command == "show version" + - test4.results[1].msg == "The command executed successfully." + + - name: TEST 5 - save outputs in dest_dir + juniper.device.command: + commands: + - "show route" + - "show version" + dest_dir: "./out" + register: test5 + + - name: Check TEST 5 + ansible.builtin.assert: + that: + - test5.results[0].command == "show route" + - test5.results[0].msg == "The command executed successfully." + - test5.results[1].command == "show version" + - test5.results[1].msg == "The command executed successfully." + + - name: TEST 6 - save output to dest + juniper.device.command: + command: "show system uptime" + dest: "./out/{{ inventory_hostname }}.uptime.output" + register: test6 + + - name: Check TEST 6 + ansible.builtin.assert: + that: + - test6.command == "show system uptime" + - test6.msg == "The command executed successfully." + + - name: TEST 7 - save output to dest + juniper.device.command: + command: + - "show route" + - "show version" + dest: "./out/{{ inventory_hostname }}.commands.output" + register: test7 + + - name: Check TEST 7 + ansible.builtin.assert: + that: + - test7.results[0].command == "show route" + - test7.results[0].msg == "The command executed successfully." + - test7.results[1].command == "show version" + - test7.results[1].msg == "The command executed successfully." + + - name: TEST 8 - Multiple commands, save outputs, but don't return them + juniper.device.command: + commands: + - "show route" + - "show version" + formats: + - "xml" + - "json" + dest_dir: "./out/" + return_output: false + register: test8 + + - name: Check TEST 8 + ansible.builtin.assert: + that: + - test8.results[0].command == "show route" + - test8.results[0].msg == "The command executed successfully." + - test8.results[1].command == "show version" + - test8.results[1].msg == "The command executed successfully." diff --git a/tests/pb.juniper_junos_config.yml b/tests/pb.juniper_junos_config.yml new file mode 100644 index 00000000..a7021f54 --- /dev/null +++ b/tests/pb.juniper_junos_config.yml @@ -0,0 +1,160 @@ +--- +- name: Test juniper.device.config module + hosts: all + gather_facts: false + + tasks: +################ + - name: Retrieve the committed configuration + juniper.device.config: + retrieve: 'committed' + diff: false + check: false + commit: false + register: test1 + ignore_errors: true + tags: [test1] + + - name: Check TEST 1 + ansible.builtin.assert: + that: + - test1.config +################ + - name: Append .foo to the hostname using private config mode. + juniper.device.config: + config_mode: 'private' + load: 'merge' + lines: + - "set system host-name {{ inventory_hostname }}.foo" + comment: "Append .foo to the hostname" + register: test2 + ignore_errors: true + tags: [test2] + + - name: Check TEST 2 + ansible.builtin.assert: + that: + - test2.diff_lines +################ + - name: Rollback to the previous config. + juniper.device.config: + config_mode: 'private' + rollback: 1 + register: test3 + ignore_errors: true + tags: [test3] + + - name: Check TEST 3 + ansible.builtin.assert: + that: + - test3.diff_lines +################# + - name: Save rescue configuration + juniper.device.command: + commands: "request system configuration rescue save" + formats: + - "xml" + + - name: Configure syslog configuration + juniper.device.config: + load: 'merge' + lines: + - "set system syslog file TEST any any" + comment: "Configured system services" + + - name: Rollback to the rescue config. + juniper.device.config: + rollback: 'rescue' + register: test4 + - name: Check TEST 4 + ansible.builtin.assert: + that: + - test4.diff_lines + - name: Clean up TEST 4 + ansible.builtin.file: + path: out + state: absent +############### + - name: Configure system services. + juniper.device.config: + config_mode: 'private' + load: 'merge' + lines: + - "set system services netconf ssh" + comment: "Configured system services" + + - name: Retrieve [edit system services] of current committed config. + juniper.device.config: + retrieve: 'committed' + filter: 'system/services' + diff: true + check: false + commit: false + register: test5 + ignore_errors: true + tags: [test5] + + - name: Check TEST 5 + ansible.builtin.assert: + that: + - test5.failed == False + - "'system {' in test5.config_lines" +################# + + - name: Confirm the previous commit with a commit check (but no commit) + juniper.device.config: + check: true + diff: false + commit: false + register: test6 + + - name: Check TEST 6 + ansible.builtin.assert: + that: + test6.changed == False + +################# + + - name: Confirm the commit with a commit sync + juniper.device.config: + check: true + diff: false + comment: "Juniper Networks" + commit_sync: true + register: test7 + + - name: Check TEST 7 + ansible.builtin.assert: + that: + - test7.changed == False + +################# + + - name: Confirm the commit with a commit sync force + juniper.device.config: + check: true + diff: false + comment: "Juniper Networks" + commit_force_sync: true + register: test8 + + - name: Check TEST 8 + ansible.builtin.assert: + that: + test8.changed == False +################ + - name: Test commit timeout . + juniper.device.config: + load: 'merge' + lines: + - "set system host-name {{ inventory_hostname }}.foo" + comment: "Append .foo to the hostname" + timeout: 300 + register: test2 + ignore_errors: true + tags: [test2] + + - name: Check TEST 2 + ansible.builtin.assert: + that: + - test2.diff_lines diff --git a/tests/pb.juniper_junos_facts.yml b/tests/pb.juniper_junos_facts.yml new file mode 100644 index 00000000..7d2fb042 --- /dev/null +++ b/tests/pb.juniper_junos_facts.yml @@ -0,0 +1,28 @@ +--- +- name: Test juniper.device.facts module + hosts: all + gather_facts: false + tasks: + - name: "TEST 1 - Gather Facts" + juniper.device.facts: + ignore_errors: true + register: test1 + + + - name: Check TEST 1 + ansible.builtin.assert: + that: + - test1.facts.hostname + - test1.facts.serialnumber + - test1.facts.model + - test1.facts.fqdn + + - name: TEST 2 - get facts in xml format + juniper.device.facts: + config_format: xml + register: test2 + + - name: Check TEST 2 + ansible.builtin.assert: + that: + - "'" + register: test7 + ignore_errors: true + tags: [test7] + + - name: Check TEST 7 + ansible.builtin.assert: + that: + - test7.msg == "The \"get-config\" RPC executed successfully." + tags: [test7] + +################# + - name: "Execute wrong RPC to generate RPC error" + juniper.device.rpc: + rpcs: + - "wrong-rpc" + register: test8 + ignore_errors: true + tags: [test8] + + - name: Check TEST 8 + ansible.builtin.assert: + that: + - '"Unable to execute the RPC" or "MODULE FAILURE" in test8.msg' + tags: [test8] + +################# + - name: "Check configuration for errors" + juniper.device.rpc: + rpcs: + - "load-configuration" + attrs: + - action: 'set' + format: 'text' + kwargs: + - configuration_set: 'set system syslog file test1 any any' + register: test9 + tags: [test9] + + - name: Check TEST 9 + ansible.builtin.debug: + var: test9 + tags: [test9] + + - name: Check TEST 9 + ansible.builtin.assert: + that: + - test9.msg == "The RPC executed successfully." + tags: [test9] + +################# + - name: "Check huge xml/text data" + juniper.device.rpc: + rpcs: + - get-support-information + - file-archive + kwargs: + - {} + - destination: "support_info" + source: /var/log/* + compress: true + formats: text + timeout: 1200 + huge_tree: true + register: test10 + tags: [test10] + + - name: Check TEST 10 + ansible.builtin.assert: + that: + test10.results[0].msg == "The RPC executed successfully." + tags: [test10] + +##################### + - name: Check rollback info + juniper.device.rpc: + rpc: get-rollback-information + kwargs: + rollback: "3" + compare: "2" + register: test11 + tags: [test11] + + - name: Check TEST 11 + ansible.builtin.assert: + that: + test11.msg == "The RPC executed successfully." + tags: [test11] + +#################### + - name: Check rollback info with boolean values + juniper.device.rpc: + rpc: get-rollback-information + kwargs: + allow_bool_values: "0" + rollback: "1" + compare: "0" + register: test12 + tags: [test12] + + - name: Check TEST 12 + ansible.builtin.assert: + that: + test12.msg == "The RPC executed successfully." + tags: [test12] diff --git a/tests/pb.juniper_junos_software.yml b/tests/pb.juniper_junos_software.yml new file mode 100644 index 00000000..96e53e05 --- /dev/null +++ b/tests/pb.juniper_junos_software.yml @@ -0,0 +1,45 @@ +--- +- name: Test juniper.device.software module + hosts: all + gather_facts: false + vars: + wait_time: 3600 + pkg_dir: /var/tmp/ + os_version: 14.1R1.10 + os_package: jinstall-14.1R1.10-domestic-signed.tgz + log_dir: /var/log/ + + tasks: + - name: Checking NETCONF connectivity + ansible.builtin.wait_for: + host: "{{ ansible_ssh_host }}" + port: 830 + timeout: 5 + - name: Install Junos OS package + juniper.device.software: + reboot: true + no_copy: true + all_re: false + version: "{{ os_version }}" + package: "{{ pkg_dir }}/{{ os_package }}" + logfile: "{{ log_dir }}/software.log" + register: test1 + notify: + - Wait_reboot + + - name: Print response + ansible.builtin.debug: + var: test1 + + - name: Check TEST - 1 + ansible.builtin.assert: + that: + - test1.failed == false + + handlers: + - name: Wait_reboot + ansible.builtin.wait_for: + host: "{{ ansible_ssh_host }}" + port: 830 + timeout: "{{ wait_time }}" + when: not test1.check_mode diff --git a/tests/pb.juniper_junos_software_member.yml b/tests/pb.juniper_junos_software_member.yml new file mode 100644 index 00000000..89429af5 --- /dev/null +++ b/tests/pb.juniper_junos_software_member.yml @@ -0,0 +1,46 @@ +--- +- name: Test juniper.device.software module + hosts: all + gather_facts: false + vars: + wait_time: 3600 + pkg_dir: /var/tmp/ + OS_version: 22.4 + OS_package: junos-install-ex-x86-64-22.4I62500TB237700_cd-builder.tgz + log_dir: /var/log/ + + tasks: + - name: Checking NETCONF connectivity + ansible.builtin.wait_for: + host: "{{ ansible_ssh_host }}" + port: 830 + timeout: 5 + - name: Install Junos OS package on VC member + juniper.device.software: + reboot: false + no_copy: true + version: "{{ OS_version }}" + package: "{{ pkg_dir }}/{{ OS_package }}" + logfile: "{{ log_dir }}/software.log" + all_re: false + member_id: ['1', '2'] + register: test1 + notify: + - Wait_reboot + + - name: Print response + ansible.builtin.debug: + var: test1 + + - name: Check TEST - 1 + ansible.builtin.assert: + that: + - test1.failed == false + + handlers: + - name: Wait_reboot + ansible.builtin.wait_for: + host: "{{ ansible_ssh_host }}" + port: 830 + timeout: "{{ wait_time }}" + when: not test1.check_mode diff --git a/tests/pb.juniper_junos_srx_cluster.yml b/tests/pb.juniper_junos_srx_cluster.yml new file mode 100644 index 00000000..441eafba --- /dev/null +++ b/tests/pb.juniper_junos_srx_cluster.yml @@ -0,0 +1,50 @@ +--- +- name: Test juniper.device.srx_cluster module + hosts: all + gather_facts: false + + tasks: + - name: Enable an SRX cluster + juniper.device.srx_cluster: + enable: true + cluster_id: 4 + node_id: 0 + register: test1 + tags: [test1] + + - name: Print the response. + ansible.builtin.debug: + var: test1 + + - name: Check TEST 1 + ansible.builtin.assert: + that: + - test1.failed == false + tags: [test1] + + - name: Sleep for 300 seconds and continue with play + ansible.builtin.wait_for: + timeout: 300 + delegate_to: localhost + + - name: Checking NETCONF connectivity + ansible.builtin.wait_for: + host: "{{ ansible_ssh_host }}" + port: 830 + timeout: 5 + + - name: Disable an SRX cluster + juniper.device.srx_cluster: + enable: false + register: test2 + tags: [test2] + + - name: Print the response. + ansible.builtin.debug: + var: test2 + + - name: Check TEST 2 + ansible.builtin.assert: + that: + - test2.failed == false + tags: [test2] diff --git a/tests/pb.juniper_junos_system.yml b/tests/pb.juniper_junos_system.yml new file mode 100644 index 00000000..50b225d1 --- /dev/null +++ b/tests/pb.juniper_junos_system.yml @@ -0,0 +1,24 @@ +--- +- name: Test juniper.device.system module + hosts: all + gather_facts: false + + tasks: + - name: Reboot all REs of the device + juniper.device.system: + action: "reboot" + register: test1 + ignore_errors: true + tags: [test1] + + - name: CHECK TEST 1 + ansible.builtin.assert: + that: + - test1.reboot == true + tags: [test1] + + - name: Checking NETCONF connectivity + ansible.builtin.wait_for: + host: "{{ ansible_ssh_host }}" + port: 830 + timeout: 360 diff --git a/tests/pb.juniper_junos_table.yml b/tests/pb.juniper_junos_table.yml new file mode 100644 index 00000000..aba778a5 --- /dev/null +++ b/tests/pb.juniper_junos_table.yml @@ -0,0 +1,43 @@ +--- +- name: Test juniper.device.table PyEZ table/view module. + hosts: all + gather_facts: false + + tasks: + - name: Retrieve LLDP Neighbor Information Using PyEZ-included Table + juniper.device.table: + file: "lldp.yml" + register: test1 + ignore_errors: true + tags: [test1] + + - name: Check TEST 1 + ansible.builtin.assert: + that: + - test1.msg == "Successfully retrieved 0 items from LLDPNeighborTable." + + - name: Retrieve routes within 192.68.1/8 + juniper.device.table: + file: "routes.yml" + table: "RouteTable" + kwargs: + destination: "192.68.1.0/8" + response_type: "juniper_items" + register: test2 + + - name: Check TEST 2 + ansible.builtin.assert: + that: + - test2.msg == "Successfully retrieved 1 items from RouteTable." + + - name: Retrieve ethernet devices + juniper.device.table: + file: "ethport.yml" + table: "EthPortTable" + kwargs: + interface_name: ge-0/0/0 + register: test3 + - name: Check TEST 3 + ansible.builtin.assert: + that: + - test3.msg == "Successfully retrieved 1 items from EthPortTable." diff --git a/tools/sw_upgrade b/tools/sw_upgrade index ec5cb8b4..bdb813ee 100755 --- a/tools/sw_upgrade +++ b/tools/sw_upgrade @@ -1,9 +1,12 @@ #!/usr/bin/env python2.7 import argparse -import os, sys, re import logging +import os +import re +import sys from getpass import getpass + from jnpr.junos import Device JUNOSDIR = '/usr/local/junos' diff --git a/version.py b/version.py old mode 100644 new mode 100755 index d3360e46..71d704f6 --- a/version.py +++ b/version.py @@ -1,2 +1,2 @@ -VERSION = "1.2.0" -DATE = "2015-July-31" +VERSION = "v1.0.7" +DATE = "2024-Dec-18"