diff --git a/.gitignore b/.gitignore index 180c65e..bb51700 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,6 @@ pip-selfcheck.json group_vars/vmware *.ova *.iso +*.vmdk +*.mf +*.ovf diff --git a/dhcpd_config.yaml b/dhcpd-config.yml similarity index 100% rename from dhcpd_config.yaml rename to dhcpd-config.yml diff --git a/dhcpd_server.yaml b/dhcpd-server.yml similarity index 100% rename from dhcpd_server.yaml rename to dhcpd-server.yml diff --git a/files/nodes.yml b/files/nodes.yml index b7c177a..1f25b71 100644 --- a/files/nodes.yml +++ b/files/nodes.yml @@ -13,7 +13,9 @@ nodes: srlab-vmx1: mgmt: 10.39.0.101 rid: 192.168.0.1 - sid: 10 + rid6: fec0:0:0:1111::1 + sid: 401 + sid6: 601 iso: 49.0001.0010.0100.1001.00 lags: switches: @@ -31,12 +33,15 @@ nodes: - 192.168.0.7 af: - inet + - inet6 - inet-vpn srlab-vmx2: mgmt: 10.39.0.102 rid: 192.168.0.2 - sid: 20 + rid6: fec0:0:0:1111::2 + sid: 402 + sid6: 602 iso: 49.0001.0010.0100.1002.00 lags: switches: @@ -52,12 +57,15 @@ nodes: - 192.168.0.7 af: - inet + - inet6 - inet-vpn srlab-vmx3: mgmt: 10.39.0.103 rid: 192.168.0.3 - sid: 30 + rid6: fec0:0:0:1111::3 + sid: 403 + sid6: 603 iso: 49.0001.0010.0100.1003.00 lags: switches: @@ -73,12 +81,15 @@ nodes: - 192.168.0.7 af: - inet + - inet6 - inet-vpn srlab-vmx4: mgmt: 10.39.0.104 rid: 192.168.0.4 - sid: 40 + rid6: fec0:0:0:1111::4 + sid: 404 + sid6: 604 iso: 49.0001.0010.0100.1004.00 lags: switches: @@ -101,12 +112,15 @@ nodes: - 192.168.0.9 af: - inet + - inet6 - inet-vpn srlab-vmx5: mgmt: 10.39.0.105 rid: 192.168.0.5 - sid: 50 + rid6: fec0:0:0:1111::5 + sid: 405 + sid6: 605 iso: 49.0001.0010.0100.1005.00 lags: ge-0/0/2: ae0 @@ -127,12 +141,15 @@ nodes: - 192.168.0.7 af: - inet + - inet6 - inet-vpn srlab-vmx6: mgmt: 10.39.0.106 rid: 192.168.0.6 - sid: 60 + rid6: fec0:0:0:1111::6 + sid: 406 + sid6: 606 iso: 49.0001.0010.0100.1006.00 lags: switches: @@ -154,12 +171,15 @@ nodes: - 192.168.0.7 af: - inet + - inet6 - inet-vpn srlab-vmx7: mgmt: 10.39.0.107 rid: 192.168.0.7 - sid: 70 + rid6: fec0:0:0:1111::7 + sid: 407 + sid6: 607 iso: 49.0001.0010.0100.1007.00 lags: ge-0/0/2: ae0 @@ -183,12 +203,15 @@ nodes: - 192.168.0.9 af: - inet + - inet6 - inet-vpn srlab-vmx8: mgmt: 10.39.0.108 rid: 192.168.0.8 - sid: 80 + rid6: fec0:0:0:1111::8 + sid: 408 + sid6: 608 iso: 49.0001.0010.0100.1008.00 lags: switches: @@ -204,12 +227,15 @@ nodes: - 192.168.0.7 af: - inet + - inet6 - inet-vpn srlab-vmx9: mgmt: 10.39.0.109 rid: 192.168.0.9 - sid: 90 + rid6: fec0:0:0:1111::9 + sid: 409 + sid6: 609 iso: 49.0001.0010.0100.1009.00 lags: switches: @@ -225,4 +251,5 @@ nodes: - 192.168.0.7 af: - inet + - inet6 - inet-vpn diff --git a/files/srlab-config/srlab-vmx1-config.txt b/files/srlab-config/srlab-vmx1-config.txt index 6805593..0d07e8b 100644 --- a/files/srlab-config/srlab-vmx1-config.txt +++ b/files/srlab-config/srlab-vmx1-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -39,6 +41,9 @@ system { } } } +chassis { + network-services enhanced-ip; +} interfaces { ge-0/0/0 { description "srlab-vmx2" @@ -46,6 +51,7 @@ interfaces { family inet { address 10.0.0.0/31 } + family inet6; family mpls; family iso; } @@ -57,6 +63,7 @@ interfaces { family inet { address 10.0.0.2/31 } + family inet6; family mpls; family iso; } @@ -68,6 +75,7 @@ interfaces { family inet { address 10.0.0.22/31 } + family inet6; family mpls; family iso; } @@ -86,6 +94,10 @@ interfaces { family inet { address 192.168.0.1/32; } + family inet6 { + address fec0:0:0:1111::1/128; + } + family iso { address 49.0001.0010.0100.1001.00; } @@ -113,6 +125,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -121,10 +136,10 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { node-segment { - ipv4-index 10; + ipv4-index 401; + ipv6-index 601; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx2-config.txt b/files/srlab-config/srlab-vmx2-config.txt index c5668c1..9f0d098 100644 --- a/files/srlab-config/srlab-vmx2-config.txt +++ b/files/srlab-config/srlab-vmx2-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -39,6 +41,9 @@ system { } } } +chassis { + network-services enhanced-ip; +} interfaces { ge-0/0/0 { description "srlab-vmx1" @@ -46,6 +51,7 @@ interfaces { family inet { address 10.0.0.1/31 } + family inet6; family mpls; family iso; } @@ -57,6 +63,7 @@ interfaces { family inet { address 10.0.0.4/31 } + family inet6; family mpls; family iso; } @@ -75,6 +82,10 @@ interfaces { family inet { address 192.168.0.2/32; } + family inet6 { + address fec0:0:0:1111::2/128; + } + family iso { address 49.0001.0010.0100.1002.00; } @@ -102,6 +113,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -110,10 +124,10 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { node-segment { - ipv4-index 20; + ipv4-index 402; + ipv6-index 602; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx3-config.txt b/files/srlab-config/srlab-vmx3-config.txt index cee8bab..a33f589 100644 --- a/files/srlab-config/srlab-vmx3-config.txt +++ b/files/srlab-config/srlab-vmx3-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -39,6 +41,9 @@ system { } } } +chassis { + network-services enhanced-ip; +} interfaces { ge-0/0/0 { description "srlab-vmx1" @@ -46,6 +51,7 @@ interfaces { family inet { address 10.0.0.23/31 } + family inet6; family mpls; family iso; } @@ -57,6 +63,7 @@ interfaces { family inet { address 10.0.0.24/31 } + family inet6; family mpls; family iso; } @@ -75,6 +82,10 @@ interfaces { family inet { address 192.168.0.3/32; } + family inet6 { + address fec0:0:0:1111::3/128; + } + family iso { address 49.0001.0010.0100.1003.00; } @@ -102,6 +113,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -110,10 +124,10 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { node-segment { - ipv4-index 30; + ipv4-index 403; + ipv6-index 603; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx4-config.txt b/files/srlab-config/srlab-vmx4-config.txt index 5759f2c..8080e6c 100644 --- a/files/srlab-config/srlab-vmx4-config.txt +++ b/files/srlab-config/srlab-vmx4-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -39,6 +41,9 @@ system { } } } +chassis { + network-services enhanced-ip; +} interfaces { ge-0/0/0 { description "srlab-vmx5" @@ -46,6 +51,7 @@ interfaces { family inet { address 10.0.0.6/31 } + family inet6; family mpls; family iso; } @@ -57,6 +63,7 @@ interfaces { family inet { address 10.0.0.3/31 } + family inet6; family mpls; family iso; } @@ -68,6 +75,7 @@ interfaces { family inet { address 10.0.0.8/31 } + family inet6; family mpls; family iso; } @@ -79,6 +87,7 @@ interfaces { family inet { address 10.0.0.10/31 } + family inet6; family mpls; family iso; } @@ -97,6 +106,10 @@ interfaces { family inet { address 192.168.0.4/32; } + family inet6 { + address fec0:0:0:1111::4/128; + } + family iso { address 49.0001.0010.0100.1004.00; } @@ -124,6 +137,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -134,10 +150,10 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { node-segment { - ipv4-index 40; + ipv4-index 404; + ipv6-index 604; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx5-config.txt b/files/srlab-config/srlab-vmx5-config.txt index d31ec16..9d67a92 100644 --- a/files/srlab-config/srlab-vmx5-config.txt +++ b/files/srlab-config/srlab-vmx5-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -40,6 +42,7 @@ system { } } chassis { + network-services enhanced-ip; aggregated-devices { ethernet { device-count 10; @@ -53,6 +56,7 @@ interfaces { family inet { address 10.0.0.12/31 } + family inet6; family mpls; family iso; } @@ -64,6 +68,7 @@ interfaces { family inet { address 10.0.0.7/31 } + family inet6; family mpls; family iso; } @@ -75,6 +80,7 @@ interfaces { family inet { address 10.0.0.5/31 } + family inet6; family mpls; family iso; } @@ -105,6 +111,10 @@ interfaces { family inet { address 192.168.0.5/32; } + family inet6 { + address fec0:0:0:1111::5/128; + } + family iso { address 49.0001.0010.0100.1005.00; } @@ -132,6 +142,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -140,10 +153,11 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { + srgb start-label 800000 index-range 100000; node-segment { - ipv4-index 50; + ipv4-index 405; + ipv6-index 605; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx6-config.txt b/files/srlab-config/srlab-vmx6-config.txt index de7a918..9751202 100644 --- a/files/srlab-config/srlab-vmx6-config.txt +++ b/files/srlab-config/srlab-vmx6-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -39,6 +41,9 @@ system { } } } +chassis { + network-services enhanced-ip; +} interfaces { ge-0/0/0 { description "srlab-vmx7" @@ -46,6 +51,7 @@ interfaces { family inet { address 10.0.0.14/31 } + family inet6; family mpls; family iso; } @@ -57,6 +63,7 @@ interfaces { family inet { address 10.0.0.16/31 } + family inet6; family mpls; family iso; } @@ -68,6 +75,7 @@ interfaces { family inet { address 10.0.0.9/31 } + family inet6; family mpls; family iso; } @@ -79,6 +87,7 @@ interfaces { family inet { address 10.0.0.11/31 } + family inet6; family mpls; family iso; } @@ -90,6 +99,7 @@ interfaces { family inet { address 10.0.0.25/31 } + family inet6; family mpls; family iso; } @@ -108,6 +118,10 @@ interfaces { family inet { address 192.168.0.6/32; } + family inet6 { + address fec0:0:0:1111::6/128; + } + family iso { address 49.0001.0010.0100.1006.00; } @@ -135,6 +149,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -143,10 +160,10 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { node-segment { - ipv4-index 60; + ipv4-index 406; + ipv6-index 606; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx7-config.txt b/files/srlab-config/srlab-vmx7-config.txt index d456b91..348ab8a 100644 --- a/files/srlab-config/srlab-vmx7-config.txt +++ b/files/srlab-config/srlab-vmx7-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -40,6 +42,7 @@ system { } } chassis { + network-services enhanced-ip; aggregated-devices { ethernet { device-count 10; @@ -53,6 +56,7 @@ interfaces { family inet { address 10.0.0.13/31 } + family inet6; family mpls; family iso; } @@ -64,6 +68,7 @@ interfaces { family inet { address 10.0.0.15/31 } + family inet6; family mpls; family iso; } @@ -75,6 +80,7 @@ interfaces { family inet { address 10.0.0.18/31 } + family inet6; family mpls; family iso; } @@ -105,6 +111,10 @@ interfaces { family inet { address 192.168.0.7/32; } + family inet6 { + address fec0:0:0:1111::7/128; + } + family iso { address 49.0001.0010.0100.1007.00; } @@ -132,6 +142,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -142,10 +155,11 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { + srgb start-label 800000 index-range 100000; node-segment { - ipv4-index 70; + ipv4-index 407; + ipv6-index 607; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx8-config.txt b/files/srlab-config/srlab-vmx8-config.txt index bf83302..fc9f83a 100644 --- a/files/srlab-config/srlab-vmx8-config.txt +++ b/files/srlab-config/srlab-vmx8-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -39,6 +41,9 @@ system { } } } +chassis { + network-services enhanced-ip; +} interfaces { ge-0/0/0 { description "srlab-vmx9" @@ -46,6 +51,7 @@ interfaces { family inet { address 10.0.0.20/31 } + family inet6; family mpls; family iso; } @@ -57,6 +63,7 @@ interfaces { family inet { address 10.0.0.17/31 } + family inet6; family mpls; family iso; } @@ -75,6 +82,10 @@ interfaces { family inet { address 192.168.0.8/32; } + family inet6 { + address fec0:0:0:1111::8/128; + } + family iso { address 49.0001.0010.0100.1008.00; } @@ -102,6 +113,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -110,10 +124,10 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { node-segment { - ipv4-index 80; + ipv4-index 408; + ipv6-index 608; } } level 1 disable; diff --git a/files/srlab-config/srlab-vmx9-config.txt b/files/srlab-config/srlab-vmx9-config.txt index 7a9e6dc..4273ee7 100644 --- a/files/srlab-config/srlab-vmx9-config.txt +++ b/files/srlab-config/srlab-vmx9-config.txt @@ -17,7 +17,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -39,6 +41,9 @@ system { } } } +chassis { + network-services enhanced-ip; +} interfaces { ge-0/0/0 { description "srlab-vmx8" @@ -46,6 +51,7 @@ interfaces { family inet { address 10.0.0.21/31 } + family inet6; family mpls; family iso; } @@ -57,6 +63,7 @@ interfaces { family inet { address 10.0.0.19/31 } + family inet6; family mpls; family iso; } @@ -75,6 +82,10 @@ interfaces { family inet { address 192.168.0.9/32; } + family inet6 { + address fec0:0:0:1111::9/128; + } + family iso { address 49.0001.0010.0100.1009.00; } @@ -102,6 +113,9 @@ protocols { family inet { unicast; } + family inet6 { + unicast; + } family inet-vpn { unicast; } @@ -110,10 +124,10 @@ protocols { } } isis { - no-ipv6-routing; source-packet-routing { node-segment { - ipv4-index 90; + ipv4-index 409; + ipv6-index 609; } } level 1 disable; diff --git a/group_vars/all b/group_vars/all index ab10388..63ccf43 100644 --- a/group_vars/all +++ b/group_vars/all @@ -1,3 +1,4 @@ --- ansible_user: salt ansible_ssh_private_key_file: /srv/salt/ssh/id_rsa +remote_user: "{{ ansible_user }}" diff --git a/group_vars/ksr b/group_vars/ksr new file mode 100644 index 0000000..060a012 --- /dev/null +++ b/group_vars/ksr @@ -0,0 +1,6 @@ +--- +ansible_connection: local +model: core-model-ksr.yml +config_dir: files/srlab-config +tasks_dir: files/srlab-tasks +dryrun: True diff --git a/hosts.ini b/hosts.ini index 43ba81d..faf5718 100644 --- a/hosts.ini +++ b/hosts.ini @@ -9,6 +9,17 @@ srlab-vmx7 ansible_host=10.39.0.107 srlab-vmx8 ansible_host=10.39.0.108 srlab-vmx9 ansible_host=10.39.0.109 +[ksr] +ksr-vmx1 ansible_host=10.39.0.201 +ksr-vmx2 ansible_host=10.39.0.202 +ksr-vmx3 ansible_host=10.39.0.203 +ksr-vmx4 ansible_host=10.39.0.204 +ksr-vmx5 ansible_host=10.39.0.205 +ksr-vmx6 ansible_host=10.39.0.206 +ksr-vmx7 ansible_host=10.39.0.207 +ksr-vmx8 ansible_host=10.39.0.208 +ksr-vmx9 ansible_host=10.39.0.209 + [vmxlab] vmx1 ansible_host=10.39.8.10 vmx2 ansible_host=10.39.8.11 @@ -19,6 +30,7 @@ vmx6 ansible_host=10.39.8.15 [vmx:children] srlab +ksr vmxlab [mx5] diff --git a/lab-config.yaml b/lab-config.yml similarity index 96% rename from lab-config.yaml rename to lab-config.yml index 5140ef9..50574d5 100644 --- a/lab-config.yaml +++ b/lab-config.yml @@ -1,6 +1,5 @@ --- -- hosts: srlab - name: Create per node data model +- name: Create per node data model gather_facts: no tags: [ model ] vars_files: diff --git a/lab-deploy.yaml b/lab-deploy.yml similarity index 98% rename from lab-deploy.yaml rename to lab-deploy.yml index ea5bf27..ffb0a45 100644 --- a/lab-deploy.yaml +++ b/lab-deploy.yml @@ -81,7 +81,7 @@ allow_duplicates: no name: "{{ inventory_hostname }}-vcp" datastore: "host 2 - datastore 2" - ova: files/ova/vcp_17.3R3.10.ova + ova: files/ova/vcp_17.4R2.4.ova disk_provisioning: thin power_on: no networks: @@ -97,7 +97,7 @@ allow_duplicates: no name: "{{ inventory_hostname }}-vfpc" datastore: "host 2 - datastore 2" - ova: files/ova/vfpc_17.3R3.10.ova + ova: files/ova/vfpc_17.4R2.4.ova disk_provisioning: thin power_on: no networks: diff --git a/lab-poweron.yaml b/lab-poweron.yml similarity index 100% rename from lab-poweron.yaml rename to lab-poweron.yml diff --git a/lab-routes.yml b/lab-routes.yml new file mode 100644 index 0000000..b2bf0af --- /dev/null +++ b/lab-routes.yml @@ -0,0 +1,32 @@ +--- +- name: Retrieve PyEZ tables + hosts: srlab + roles: + - juniper.junos + gather_facts: no + vars: + ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + tasks: + - name: Retrieve LLDP Neighbor Information Using PyEZ-included Table + juniper_junos_table: + file: "lldp.yml" + host: "{{ ansible_host }}" + register: response + delegate_to: localhost + - name: Print response + debug: + var: response + + - name: Retrieve routes within 10.0.0/8 + juniper_junos_table: + file: "routes.yml" + table: "RouteTable" + kwargs: + destination: "10.0.0.0/8" + host: "{{ ansible_host }}" + register: response + delegate_to: localhost + - name: Print response + debug: + var: response + diff --git a/lab-snapshot.yaml b/lab-snapshot.yml similarity index 100% rename from lab-snapshot.yaml rename to lab-snapshot.yml diff --git a/lab-teardown.yml b/lab-teardown.yml new file mode 100644 index 0000000..576b711 --- /dev/null +++ b/lab-teardown.yml @@ -0,0 +1,59 @@ +--- +- name: Delete routers + hosts: srlab + tags: delete + gather_facts: no + vars_files: group_vars/vmware + tasks: + - name: power on vcp and vfpc + vmware_guest: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" +# esxi_hostname: "{{ esxi_hostname }}" + validate_certs: False + name: "{{ item }}" + folder: / + datastore: "host 2 - datastore 2" + state: absent + force: yes + loop: + - "{{ inventory_hostname }}-vcp" + - "{{ inventory_hostname }}-vfpc" + delegate_to: localhost + +- name: Delete internode vswitches + hosts: srlab + tags: delet_node_switches + gather_facts: no + vars_files: group_vars/vmware + tasks: + - include_vars: "files/nodes.yml" + - name: Delete the vswitch + vmware_vswitch: + hostname: "{{ esxi_hostname }}" + username: "{{ esxi_username }}" + password: "{{ esxi_password }}" + esxi_hostname: "{{ esxi_hostname }}" + validate_certs: False + switch_name: "{{ item }}" + state: absent + loop: "{{ nodes[inventory_hostname]['switches'] }}" + delegate_to: localhost + +- name: Delete node bridge vswitches + hosts: srlab + tags: delete_bridge_switches + gather_facts: no + vars_files: group_vars/vmware + tasks: + - name: Create the vswitch + vmware_vswitch: + hostname: "{{ esxi_hostname }}" + username: "{{ esxi_username }}" + password: "{{ esxi_password }}" + esxi_hostname: "{{ esxi_hostname }}" + validate_certs: False + switch_name: "{{ inventory_hostname }}-br-int" + state: absent + delegate_to: localhost diff --git a/lab-tilfa.yml b/lab-tilfa.yml new file mode 100644 index 0000000..2968ebd --- /dev/null +++ b/lab-tilfa.yml @@ -0,0 +1,19 @@ +--- +- name: Push TI-LFA SR configs + hosts: srlab + roles: + - juniper.junos + gather_facts: no + tasks: + - name: Merge config + juniper_junos_config: + config_mode: "private" + load: "replace" + format: "text" + timeout: 60 + commit: true + diff: true + src: templates/sr-tilfa.conf + host: "{{ ansible_host }}" + ssh_private_key_file: "{{ ansible_ssh_private_key_file }}" + delegate_to: localhost diff --git a/myTables/ConfigTables.py b/myTables/ConfigTables.py new file mode 100644 index 0000000..f1c581b --- /dev/null +++ b/myTables/ConfigTables.py @@ -0,0 +1,4 @@ +from jnpr.junos.factory import loadyaml +from os.path import splitext +_YAML_ = splitext(__file__)[0] + '.yml' +globals().update(loadyaml(_YAML_)) diff --git a/myTables/ConfigTables.yml b/myTables/ConfigTables.yml new file mode 100644 index 0000000..26a936f --- /dev/null +++ b/myTables/ConfigTables.yml @@ -0,0 +1,20 @@ +UserTable: + get: system/login/user + view: UserView + +UserView: + fields: + username: name + userclass: class + +InterfaceTable: + get: interfaces/interface + view: InterfaceView + +InterfaceView: + fields: + name: name + description: description + unit: unit/name + rpf_check: unit/family/*/rpf-check + fail_filter: unit/family/*/rpf-check/fail-filter diff --git a/myTables/__init__.py b/myTables/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index fc23366..eee9540 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ junos-netconify napalm-ansible PyVmomi jinja2-cli +jxmlease diff --git a/roles/juniper.junos/.gitignore b/roles/juniper.junos/.gitignore new file mode 100644 index 0000000..7f6e6ab --- /dev/null +++ b/roles/juniper.junos/.gitignore @@ -0,0 +1,55 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +.backup + +# Documentation +docs/build +docs/*.rst +docs/_build/ +docs/sphinx.log + +tests/.vagrant/* + +# Window File Explorer +desktop.ini + +# Mac OS X Finder +.DS_Store + +# PyCharm +.idea diff --git a/roles/juniper.junos/.travis.yml b/roles/juniper.junos/.travis.yml new file mode 100644 index 0000000..482d79d --- /dev/null +++ b/roles/juniper.junos/.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/roles/juniper.junos/COPYRIGHT b/roles/juniper.junos/COPYRIGHT new file mode 100644 index 0000000..63dd879 --- /dev/null +++ b/roles/juniper.junos/COPYRIGHT @@ -0,0 +1,30 @@ + + Copyright (c) 1999-2018, Juniper Networks Inc. + 2014, Jeremy Schulman + + All rights reserved. + + 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. diff --git a/roles/juniper.junos/Dockerfile b/roles/juniper.junos/Dockerfile new file mode 100644 index 0000000..8a399cc --- /dev/null +++ b/roles/juniper.junos/Dockerfile @@ -0,0 +1,38 @@ +FROM juniper/pyez:latest +MAINTAINER Stephen Steiner + +ARG ver_ansible=2.4.0.0 +ARG ver_jsnapy=1.2.1 + +WORKDIR /tmp +RUN mkdir /tmp/ansible-junos-stdlib &&\ + mkdir /tmp/ansible-junos-stdlib/library &&\ + mkdir /tmp/ansible-junos-stdlib/meta &&\ + mkdir /project + +ADD action_plugins /tmp/ansible-junos-stdlib/action_plugins +ADD callback_plugins /tmp/ansible-junos-stdlib/callback_plugins +ADD library /tmp/ansible-junos-stdlib/library +ADD LICENSE /tmp/ansible-junos-stdlib/LICENSE +ADD meta /tmp/ansible-junos-stdlib/meta +ADD module_utils /tmp/ansible-junos-stdlib/module_utils +ADD version.py /tmp/ansible-junos-stdlib/version.py + + + +RUN tar -czf Juniper.junos ansible-junos-stdlib &&\ + apk update && apk add ca-certificates &&\ + apk add openssh-client &&\ + apk add build-base gcc g++ make python-dev &&\ + apk update && apk add py-pip &&\ + pip install --upgrade pip setuptools &&\ + pip install jxmlease &&\ + pip install ansible==$ver_ansible &&\ + pip install jsnapy==$ver_jsnapy &&\ + ansible-galaxy install --roles-path=/etc/ansible/roles Juniper.junos &&\ + apk del -r --purge gcc make g++ &&\ + rm -rf /source/* &&\ + rm -rf /var/cache/apk/* &&\ + rm -rf /tmp/* + +WORKDIR /project diff --git a/roles/juniper.junos/ISSUE_TEMPLATE.md b/roles/juniper.junos/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..255b3d9 --- /dev/null +++ b/roles/juniper.junos/ISSUE_TEMPLATE.md @@ -0,0 +1,55 @@ + + +Issue Type +------ + + - Bug Report + - Feature Idea + - Documentation Report + +Module Name +------ + + +Juniper.Junos role and Python libraries version + +``` + +``` + +OS / Environment +------ + + +Summary +------ + + +Steps to reproduce +------ + + + +```yaml + +``` + + + +Expected results + +``` + +``` +Actual results +------ + + + +``` + +``` diff --git a/roles/juniper.junos/LICENSE b/roles/juniper.junos/LICENSE new file mode 100644 index 0000000..e06d208 --- /dev/null +++ b/roles/juniper.junos/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + 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/roles/juniper.junos/README.md b/roles/juniper.junos/README.md new file mode 100644 index 0000000..a6ba521 --- /dev/null +++ b/roles/juniper.junos/README.md @@ -0,0 +1,214 @@ +## About + +Juniper Networks supports Ansible for managing devices running +the Junos operating system (Junos OS). This role is hosted on the Ansible Galaxy website under +the role [Juniper.junos](https://galaxy.ansible.com/Juniper/junos/). The Juniper.junos role 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 role. + +## Two Sets of Ansible Modules for Junos devices +Since Ansible version >= 2.1, Ansible also natively includes +[core modules for Junos](http://docs.ansible.com/ansible/list_of_network_modules.html#junos). The Junos modules included +in Ansible core have names which begin with the prefix `junos_`. The Junos modules included in this Juniper.junos role +have names which begin with the prefix `juniper_junos_`. These two sets of Junos modules can coexist on the same +Ansible control machine, and an Ansible play may invoke a module from either (or both) sets. Juniper Networks recommends +using the modules in this role when writing new playbooks that manage Junos devices. + +## Overview of Modules +This Juniper.junos role includes the following modules: +- **juniper_junos_command** — Execute one or more CLI commands on a Junos device. +- **juniper_junos_config** — Manipulate the configuration of a Junos device. +- **juniper_junos_facts** — Retrieve facts from a Junos device. +- **juniper_junos_jsnapy** — Execute JSNAPy tests on a Junos device. +- **juniper_junos_ping** — Execute ping from a Junos device. +- **juniper_junos_pmtud** — Perform path MTU discovery from a Junos device to a destination. +- **juniper_junos_rpc** — Execute one or more NETCONF RPCs on a Junos device. +- **juniper_junos_software** — Install software on a Junos device. +- **juniper_junos_srx_cluster** — Add or remove SRX chassis cluster configuration. +- **juniper_junos_system** — Initiate operational actions on the Junos system. +- **juniper_junos_table** — Retrieve data from a Junos device using a PyEZ table/view. + +### Important Changes +Significant changes to the modules in the Juniper.junos role were made between versions 1.4.3 and 2.0.0. +In versions <= 1.4.3 of the Juniper.junos role, the modules used different module and argument names. Versions >= 2.0.0 +of the Juniper.junos role provide backwards compatibility with playbooks written to prior versions of the Juniper.junos +role. If a playbook worked with a prior version of the Juniper.junos role, it should +continue to work on the current version without requiring modifications to the playbook. However, these older module and +argument names are no longer present in the current documentation. You may reference previous module and argument names +by referring directly to the +[1.4.3 version of the Juniper.junos role documentation](http://junos-ansible-modules.readthedocs.io/en/1.4.3/). + +### Overview of Plugins + +In addition to the modules listed above, a callback_plugin `jsnapy` is available for the module `juniper_junos_jsnapy`. + +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: + +``` +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"} +``` + +The `jsnapy` plugin is currently in **Experimental** stage, please provide feedback. + +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](http://junos-ansible-modules.readthedocs.org) + + +## INSTALLATION +You must have the [DEPENDENCIES](#dependencies) installed on your system. + +### Ansible Galaxy Role +To download the latest released version of the junos role to the Ansible +server, execute the ansible-galaxy install command, and specify **Juniper.junos**. + +``` +[root@ansible-cm]# ansible-galaxy install Juniper.junos +- downloading role 'junos', owned by Juniper +- downloading role from https://github.com/Juniper/ansible-junos-stdlib/archive/1.3.1.tar.gz +- extracting Juniper.junos to /usr/local/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 role directly from GitHub. +``` +sudo ansible-galaxy install git+https://github.com/Juniper/ansible-junos-stdlib.git,,Juniper.junos +``` + +### 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 +``` +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 +``` + +### Docker +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 ash +``` +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 ash +``` +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: +``` +$ docker run -it --rm -v $PWD:/project juniper/pyez-ansible ansible-playbook -i hosts playbook.yml +``` +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 +``` + +## 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 + vars: + wait_time: 3600 + pkg_dir: /var/tmp/junos-install + 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 + - name: Install Junos OS package + juniper_junos_software: + reboot: yes + version: "{{ OS_version }}" + package: "{{ pkg_dir }}/{{ OS_package }}" + logfile: "{{ log_dir }}/software.log" + register: sw + notify: + - wait_reboot + + handlers: + - name: wait_reboot + wait_for: host={{ inventory_hostname }} port=830 timeout={{ wait_time }} + when: not sw.check_mode +``` + +## DEPENDENCIES +This modules requires the following to be installed on the Ansible control machine: +* Python >= 2.7 +* [Ansible](http://www.ansible.com) 2.3 or later +* Junos [py-junos-eznc](https://github.com/Juniper/py-junos-eznc) 2.1.7 or later +* [jxmlease](https://github.com/Juniper/jxmlease) 1.0.1 or later + +## LICENSE +Apache 2.0 + +## SUPPORT +Support for this Juniper.junos role is provided by the community and Juniper Networks. If you have an +issue with a module in the Juniper.junos role, 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 +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:* +[Nitin Kumar](https://github.com/vnitinv), [Stacy W Smith](https://github.com/stacywsmith), +[David Gethings](https://github.com/dgjnpr) + +* v2.1.0: [Raja Shekar](https://github.com/rsmekala), [Stacy W Smith](https://github.com/stacywsmith) + +*Former Contributors:* +[Jeremy Schulman](https://github.com/jeremyschulman), [Rick Sherman](https://github.com/shermdog), +[Damien Garros](https://github.com/dgarros) diff --git a/roles/juniper.junos/action_plugins/_junos_cli.py b/roles/juniper.junos/action_plugins/_junos_cli.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_cli.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_commit.py b/roles/juniper.junos/action_plugins/_junos_commit.py new file mode 100755 index 0000000..347912d --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_commit.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 + +# Standard library imports +import os.path +import sys + +# The module_utils path must be added to sys.path in order to import +# juniper_junos_common. The module_utils path is relative to the path of this +# file. +module_utils_path = os.path.normpath(os.path.dirname(__file__) + + '/../module_utils') +if module_utils_path is not None: + sys.path.insert(0, module_utils_path) + import juniper_junos_common + del sys.path[0] + + +# Use the custom behavior of JuniperJunosActionModule as the superclass of +# our ActionModule. +class ActionModule(juniper_junos_common.JuniperJunosActionModule): + """Translates junos_commit args to juniper_junos_config args. + + This class is a subclass of JuniperJunosActionModule. It exists solely + for backwards compatibility. It translates the arguments from the old + junos_commit module into the arguments on the new + juniper_junos_config module. + """ + def run(self, tmp=None, task_vars=None): + check = self._task.args.get('check') + if check is True: + # In the old module, check and commit were mutually exclusive. + # In the new module, you can potentially do both. + # If check is set on the old module, then we need to not commit. + self._task.args['commit'] = False + + # Remaining arguments can be passed through transparently. + + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/roles/juniper.junos/action_plugins/_junos_get_config.py b/roles/juniper.junos/action_plugins/_junos_get_config.py new file mode 100755 index 0000000..f874063 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_get_config.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 + +# Standard library imports +import os.path +import sys + +# The module_utils path must be added to sys.path in order to import +# juniper_junos_common. The module_utils path is relative to the path of this +# file. +module_utils_path = os.path.normpath(os.path.dirname(__file__) + + '/../module_utils') +if module_utils_path is not None: + sys.path.insert(0, module_utils_path) + import juniper_junos_common + del sys.path[0] + + +# Use the custom behavior of JuniperJunosActionModule as the superclass of +# our ActionModule. +class ActionModule(juniper_junos_common.JuniperJunosActionModule): + """Translates junos_get_config args to juniper_junos_config args. + + This class is a subclass of JuniperJunosActionModule. It exists solely + for backwards compatibility. It translates the arguments from the old + junos_get_config module into the arguments on the new + juniper_junos_config module. + """ + def run(self, tmp=None, task_vars=None): + # No diff, check, or commit + self._task.args['diff'] = False + self._task.args['check'] = False + self._task.args['commit'] = False + # Retrieve candidate + self._task.args['retrieve'] = 'candidate' + + # Remaining arguments can be passed through transparently. + + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/roles/juniper.junos/action_plugins/_junos_get_facts.py b/roles/juniper.junos/action_plugins/_junos_get_facts.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_get_facts.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_get_table.py b/roles/juniper.junos/action_plugins/_junos_get_table.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_get_table.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_install_config.py b/roles/juniper.junos/action_plugins/_junos_install_config.py new file mode 100755 index 0000000..55fe8ef --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_install_config.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 + +# Standard library imports +import os.path +import sys + +# The module_utils path must be added to sys.path in order to import +# juniper_junos_common. The module_utils path is relative to the path of this +# file. +module_utils_path = os.path.normpath(os.path.dirname(__file__) + + '/../module_utils') +if module_utils_path is not None: + sys.path.insert(0, module_utils_path) + import juniper_junos_common + del sys.path[0] + + +# Use the custom behavior of JuniperJunosActionModule as the superclass of +# our ActionModule. +class ActionModule(juniper_junos_common.JuniperJunosActionModule): + """Translates junos_install_config args to juniper_junos_config args. + + This class is a subclass of JuniperJunosActionModule. It exists solely + for backwards compatibility. It translates the arguments from the old + junos_install_config module into the arguments on the new + juniper_junos_config module. + """ + def run(self, tmp=None, task_vars=None): + # Pop the action arguments + update = self._task.args.pop('update', False) + overwrite = self._task.args.pop('overwrite', False) + replace = self._task.args.pop('replace', False) + action = '' + if update is True: + action += 'update' + if overwrite is True: + action += 'overwrite' + if replace is True: + action += 'replace' + if not action: + action = 'merge' + # Set the load argument based on action + self._task.args['load'] = action + + # Always commit changes to mimic the previous behavior + self._task.args['commit_empty_changes'] = True + + # If check_commit is False, then also bypass the commit. + check = True + # Check for the 'check_commit' option which was an optional boolean + # argument for the junos_install_config module. + if 'check_commit' in self._task.args: + check = self._task.args.pop('check_commit') + if check is not None and self.convert_to_bool(check) is False: + # Translate to check_commit = False, commit = False, and + # commit_empty_changes = False + self._task.args['check_commit'] = False + self._task.args['commit'] = False + self._task.args['commit_empty_changes'] = False + + # Remaining arguments can be passed through transparently. + + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/roles/juniper.junos/action_plugins/_junos_install_os.py b/roles/juniper.junos/action_plugins/_junos_install_os.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_install_os.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_jsnapy.py b/roles/juniper.junos/action_plugins/_junos_jsnapy.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_jsnapy.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_ping.py b/roles/juniper.junos/action_plugins/_junos_ping.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_ping.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_pmtud.py b/roles/juniper.junos/action_plugins/_junos_pmtud.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_pmtud.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_rollback.py b/roles/juniper.junos/action_plugins/_junos_rollback.py new file mode 100755 index 0000000..cd12f48 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_rollback.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 + +# Standard library imports +import os.path +import sys + +# The module_utils path must be added to sys.path in order to import +# juniper_junos_common. The module_utils path is relative to the path of this +# file. +module_utils_path = os.path.normpath(os.path.dirname(__file__) + + '/../module_utils') +if module_utils_path is not None: + sys.path.insert(0, module_utils_path) + import juniper_junos_common + del sys.path[0] + + +# Use the custom behavior of JuniperJunosActionModule as the superclass of +# our ActionModule. +class ActionModule(juniper_junos_common.JuniperJunosActionModule): + """Translates junos_rollback args to juniper_junos_config args. + + This class is a subclass of JuniperJunosActionModule. It exists solely + for backwards compatibility. It translates the arguments from the old + junos_rollback module into the arguments on the new + juniper_junos_config module. + """ + def run(self, tmp=None, task_vars=None): + rollback = self._task.args.get('rollback') + if rollback is None: + # rollback is mandatory when called as junos_rollback. + # Mimic this behavior be setting rollback to 'value not specified'. + self._task.args['rollback'] = 'value not specified' + # Always commit changes to mimic the previous behavior + self._task.args['commit_empty_changes'] = True + + # Remaining arguments can be passed through transparently. + + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/roles/juniper.junos/action_plugins/_junos_rpc.py b/roles/juniper.junos/action_plugins/_junos_rpc.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_rpc.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_shutdown.py b/roles/juniper.junos/action_plugins/_junos_shutdown.py new file mode 100755 index 0000000..3610639 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_shutdown.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 + +# Standard library imports +import os.path +import sys + +# The module_utils path must be added to sys.path in order to import +# juniper_junos_common. The module_utils path is relative to the path of this +# file. +module_utils_path = os.path.normpath(os.path.dirname(__file__) + + '/../module_utils') +if module_utils_path is not None: + sys.path.insert(0, module_utils_path) + import juniper_junos_common + del sys.path[0] + + +# Use the custom behavior of JuniperJunosActionModule as the superclass of +# our ActionModule. +class ActionModule(juniper_junos_common.JuniperJunosActionModule): + """Translates junos_shutdown args to juniper_junos_system args. + + This class is a subclass of JuniperJunosActionModule. It exists solely + for backwards compatibility. It translates the arguments from the old + junos_shutdown module into the arguments on the new juniper_junos_system + module. + """ + def run(self, tmp=None, task_vars=None): + # Check for the 'shutdown' option which was mandatory for + # the junos_shutdown module. + if 'shutdown' in self._task.args: + shutdown = self._task.args.pop('shutdown') + # 'shutdown' was the only valid value for the 'shutdown' option. + if shutdown == 'shutdown': + reboot = False + # Check for the 'reboot' option which was an optional boolean + # argument for the junos_shutdown module. + if 'reboot' in self._task.args: + reboot = self._task.args.pop('reboot') + if reboot is not None and self.convert_to_bool(reboot) is True: + # Translate to action="reboot" + self._task.args['action'] = 'reboot' + elif reboot is None or self.convert_to_bool(reboot) is False: + # Translate to action="shutdown" + self._task.args['action'] = 'shutdown' + else: + # This isn't a valid value for action/reboot + # We'll pass it through and the module will complain + # appropriately. + self._task.args['action'] = reboot + else: + # This isn't a valid value for action/shutdown + # We'll pass it through and the module will complain + # appropriately. + self._task.args['action'] = shutdown + + # Remaining arguments can be passed through transparently. + + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/roles/juniper.junos/action_plugins/_junos_srx_cluster.py b/roles/juniper.junos/action_plugins/_junos_srx_cluster.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_srx_cluster.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/_junos_zeroize.py b/roles/juniper.junos/action_plugins/_junos_zeroize.py new file mode 100755 index 0000000..bfcfcea --- /dev/null +++ b/roles/juniper.junos/action_plugins/_junos_zeroize.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 + +# Standard library imports +import os.path +import sys + +# The module_utils path must be added to sys.path in order to import +# juniper_junos_common. The module_utils path is relative to the path of this +# file. +module_utils_path = os.path.normpath(os.path.dirname(__file__) + + '/../module_utils') +if module_utils_path is not None: + sys.path.insert(0, module_utils_path) + import juniper_junos_common + del sys.path[0] + + +# Use the custom behavior of JuniperJunosActionModule as the superclass of +# our ActionModule. +class ActionModule(juniper_junos_common.JuniperJunosActionModule): + """Translates junos_zeroize args to juniper_junos_system args. + + This class is a subclass of JuniperJunosActionModule. It exists solely + for backwards compatibility. It translates the arguments from the old + junos_zeroize module into the arguments on the new juniper_junos_system + module. + """ + def run(self, tmp=None, task_vars=None): + # Check for the 'zeroize' option which was mandatory for + # the junos_zeroize module. + if 'zeroize' in self._task.args: + # Delete the zeroize option. + zeroize = self._task.args.pop('zeroize') + # Add the action option with the value from the zeroize option. + # This should normally be the value 'zeroize'. If it's not, then + # the juniper_junos_system module will throw an appropriate error. + self._task.args['action'] = zeroize + + # Remaining arguments can be passed through transparently. + + # Call the parent action module. + return super(ActionModule, self).run(tmp, task_vars) diff --git a/roles/juniper.junos/action_plugins/juniper_junos_command.py b/roles/juniper.junos/action_plugins/juniper_junos_command.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_command.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_common_action.py b/roles/juniper.junos/action_plugins/juniper_junos_common_action.py new file mode 100755 index 0000000..37e2fbd --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_common_action.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 + +# Standard library imports +import os.path +import sys + +# The module_utils path must be added to sys.path in order to import +# juniper_junos_common. The module_utils path is relative to the path of this +# file. +module_utils_path = os.path.normpath(os.path.dirname(__file__) + + '/../module_utils') +if module_utils_path is not None: + sys.path.insert(0, module_utils_path) + import juniper_junos_common + del sys.path[0] + + +# Use the custom behavior of JuniperJunosActionModule as our ActionModule. +# The Ansible core engine will call ActionModule.run() +from juniper_junos_common import JuniperJunosActionModule as ActionModule diff --git a/roles/juniper.junos/action_plugins/juniper_junos_config.py b/roles/juniper.junos/action_plugins/juniper_junos_config.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_config.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_facts.py b/roles/juniper.junos/action_plugins/juniper_junos_facts.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_facts.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_jsnapy.py b/roles/juniper.junos/action_plugins/juniper_junos_jsnapy.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_jsnapy.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_ping.py b/roles/juniper.junos/action_plugins/juniper_junos_ping.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_ping.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_pmtud.py b/roles/juniper.junos/action_plugins/juniper_junos_pmtud.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_pmtud.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_rpc.py b/roles/juniper.junos/action_plugins/juniper_junos_rpc.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_rpc.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_software.py b/roles/juniper.junos/action_plugins/juniper_junos_software.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_software.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_srx_cluster.py b/roles/juniper.junos/action_plugins/juniper_junos_srx_cluster.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_srx_cluster.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_system.py b/roles/juniper.junos/action_plugins/juniper_junos_system.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_system.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/action_plugins/juniper_junos_table.py b/roles/juniper.junos/action_plugins/juniper_junos_table.py new file mode 120000 index 0000000..7470277 --- /dev/null +++ b/roles/juniper.junos/action_plugins/juniper_junos_table.py @@ -0,0 +1 @@ +juniper_junos_common_action.py \ No newline at end of file diff --git a/roles/juniper.junos/callback_plugins/jsnapy.py b/roles/juniper.junos/callback_plugins/jsnapy.py new file mode 100644 index 0000000..826192c --- /dev/null +++ b/roles/juniper.junos/callback_plugins/jsnapy.py @@ -0,0 +1,88 @@ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import collections +import os +import time +import pprint +import json +from six import iteritems + +from ansible.plugins.callback import CallbackBase +from ansible import constants as C + +class CallbackModule(CallbackBase): + """ + This callback add extra logging for the module junos_jsnapy . + """ + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'aggregate' + CALLBACK_NAME = 'jsnapy' + +## 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 not host 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['final_result'] == "Failed": + for test_name, test_results in iteritems(res['test_results']): + for testlet in test_results: + if 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) diff --git a/roles/juniper.junos/docs/Makefile b/roles/juniper.junos/docs/Makefile new file mode 100644 index 0000000..7b3aac7 --- /dev/null +++ b/roles/juniper.junos/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/roles/juniper.junos/docs/_static/juniper-junos-modules.css b/roles/juniper.junos/docs/_static/juniper-junos-modules.css new file mode 100644 index 0000000..2723c30 --- /dev/null +++ b/roles/juniper.junos/docs/_static/juniper-junos-modules.css @@ -0,0 +1,7 @@ +td, th { + padding: 20px; +} +code { + color: #3a87ad; + background-color: #d9edf7; +} \ No newline at end of file diff --git a/roles/juniper.junos/docs/_static/juniper.png b/roles/juniper.junos/docs/_static/juniper.png new file mode 100755 index 0000000..e3db7d8 Binary files /dev/null and b/roles/juniper.junos/docs/_static/juniper.png differ diff --git a/roles/juniper.junos/docs/ansible2rst.py b/roles/juniper.junos/docs/ansible2rst.py new file mode 100755 index 0000000..a580981 --- /dev/null +++ b/roles/juniper.junos/docs/ansible2rst.py @@ -0,0 +1,426 @@ +#!/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 os +import re +import sys +import datetime +import cgi +from distutils.version import LooseVersion +from jinja2 import Environment, FileSystemLoader +import yaml +from six import print_ + +from collections import MutableMapping, MutableSet, MutableSequence + +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.module_utils._text import to_bytes + +try: + from html import escape as html_escape +except ImportError: + # Python-3.2 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 = "juniper_junos_" +MODULEDIR = "../library/" +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('../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)) + + # 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)) + + # 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)) + + # 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)) + + # 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)) + + # 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)) + + # 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... + text = 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.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_name in module_names: + index_file.write(' %s\n' % module_name) + +if __name__ == '__main__': + main() diff --git a/roles/juniper.junos/docs/conf.py b/roles/juniper.junos/docs/conf.py new file mode 100644 index 0000000..3371c88 --- /dev/null +++ b/roles/juniper.junos/docs/conf.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +# +# Junos Ansible Modules documentation build configuration file, created by +# sphinx-quickstart on Fri Jul 11 17:28:14 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import sphinx_bootstrap_theme + + +def setup(app): + app.add_stylesheet("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('..')) + +# 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' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +#templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Junos Ansible Modules' +copyright = u'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. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#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 + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#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' + +# 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': [ + ("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 + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +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 + +# 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'] + +# 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 = [] + +# 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' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + '**': [ + 'globaltoc.html', + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is 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 = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'JunosAnsibleModulesdoc' + + +# -- 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': '', +} + +# 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'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 + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# 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'Juniper Networks, Inc.'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'JunosAnsibleModules', u'Junos Ansible Modules Documentation', + u'Juniper Networks, Inc.', 'JunosAnsibleModules', 'Ansible Modules for ' + 'Junos', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/roles/juniper.junos/docs/docreq.txt b/roles/juniper.junos/docs/docreq.txt new file mode 100644 index 0000000..bc96b3b --- /dev/null +++ b/roles/juniper.junos/docs/docreq.txt @@ -0,0 +1,2 @@ +git+https://github.com/ryan-roemer/sphinx-bootstrap-theme.git#egg=sphinx-bootstrap-theme +ansible diff --git a/roles/juniper.junos/docs/rst.j2 b/roles/juniper.junos/docs/rst.j2 new file mode 100644 index 0000000..3b7338f --- /dev/null +++ b/roles/juniper.junos/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/roles/juniper.junos/env-setup b/roles/juniper.junos/env-setup new file mode 100755 index 0000000..e87c55f --- /dev/null +++ b/roles/juniper.junos/env-setup @@ -0,0 +1,25 @@ +#!/bin/bash +# 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 + HACKING_DIR=`dirname $BASH_SOURCE` +elif [ $(basename $0) = "env-setup" ]; then + HACKING_DIR=`dirname $0` +else + HACKING_DIR="$PWD" +fi + +# The below is an alternative to readlink -fn which doesn't exist on OS X +# Source: http://stackoverflow.com/a/1678636 +FULL_PATH=`python -c "import os; print(os.path.realpath('$HACKING_DIR'))"` +PRJ_LIBRARY="$FULL_PATH/library" + +# we are going to set the ANSIBLE_LIBRARY path, which also needs to include +# the installed library; so check to see if that is setup or not. + +if [ -z "$ANSIBLE_LIBRARY" ]; then + export ANSIBLE_LIBRARY=$(grep library /etc/ansible/ansible.cfg | awk '{ /(.*) = (.*)/; print $3 }') +fi + +[[ $ANSIBLE_LIBRARY != ${PRJ_LIBRARY}* ]] && export ANSIBLE_LIBRARY=$PRJ_LIBRARY:$ANSIBLE_LIBRARY diff --git a/roles/juniper.junos/library/_junos_cli.py b/roles/juniper.junos/library/_junos_cli.py new file mode 120000 index 0000000..cc8aaf8 --- /dev/null +++ b/roles/juniper.junos/library/_junos_cli.py @@ -0,0 +1 @@ +juniper_junos_command.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_commit.py b/roles/juniper.junos/library/_junos_commit.py new file mode 120000 index 0000000..5383605 --- /dev/null +++ b/roles/juniper.junos/library/_junos_commit.py @@ -0,0 +1 @@ +juniper_junos_config.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_get_config.py b/roles/juniper.junos/library/_junos_get_config.py new file mode 120000 index 0000000..5383605 --- /dev/null +++ b/roles/juniper.junos/library/_junos_get_config.py @@ -0,0 +1 @@ +juniper_junos_config.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_get_facts.py b/roles/juniper.junos/library/_junos_get_facts.py new file mode 120000 index 0000000..cbdd3b9 --- /dev/null +++ b/roles/juniper.junos/library/_junos_get_facts.py @@ -0,0 +1 @@ +juniper_junos_facts.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_get_table.py b/roles/juniper.junos/library/_junos_get_table.py new file mode 120000 index 0000000..32df1a4 --- /dev/null +++ b/roles/juniper.junos/library/_junos_get_table.py @@ -0,0 +1 @@ +juniper_junos_table.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_install_config.py b/roles/juniper.junos/library/_junos_install_config.py new file mode 120000 index 0000000..5383605 --- /dev/null +++ b/roles/juniper.junos/library/_junos_install_config.py @@ -0,0 +1 @@ +juniper_junos_config.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_install_os.py b/roles/juniper.junos/library/_junos_install_os.py new file mode 120000 index 0000000..0a63f35 --- /dev/null +++ b/roles/juniper.junos/library/_junos_install_os.py @@ -0,0 +1 @@ +juniper_junos_software.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_jsnapy.py b/roles/juniper.junos/library/_junos_jsnapy.py new file mode 120000 index 0000000..e14ff8d --- /dev/null +++ b/roles/juniper.junos/library/_junos_jsnapy.py @@ -0,0 +1 @@ +juniper_junos_jsnapy.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_ping.py b/roles/juniper.junos/library/_junos_ping.py new file mode 120000 index 0000000..3ca2aa7 --- /dev/null +++ b/roles/juniper.junos/library/_junos_ping.py @@ -0,0 +1 @@ +juniper_junos_ping.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_pmtud.py b/roles/juniper.junos/library/_junos_pmtud.py new file mode 120000 index 0000000..b7bdfe9 --- /dev/null +++ b/roles/juniper.junos/library/_junos_pmtud.py @@ -0,0 +1 @@ +juniper_junos_pmtud.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_rollback.py b/roles/juniper.junos/library/_junos_rollback.py new file mode 120000 index 0000000..5383605 --- /dev/null +++ b/roles/juniper.junos/library/_junos_rollback.py @@ -0,0 +1 @@ +juniper_junos_config.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_rpc.py b/roles/juniper.junos/library/_junos_rpc.py new file mode 120000 index 0000000..9e5b8b3 --- /dev/null +++ b/roles/juniper.junos/library/_junos_rpc.py @@ -0,0 +1 @@ +juniper_junos_rpc.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_shutdown.py b/roles/juniper.junos/library/_junos_shutdown.py new file mode 120000 index 0000000..95f3297 --- /dev/null +++ b/roles/juniper.junos/library/_junos_shutdown.py @@ -0,0 +1 @@ +juniper_junos_system.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_srx_cluster.py b/roles/juniper.junos/library/_junos_srx_cluster.py new file mode 120000 index 0000000..511a3de --- /dev/null +++ b/roles/juniper.junos/library/_junos_srx_cluster.py @@ -0,0 +1 @@ +juniper_junos_srx_cluster.py \ No newline at end of file diff --git a/roles/juniper.junos/library/_junos_zeroize.py b/roles/juniper.junos/library/_junos_zeroize.py new file mode 120000 index 0000000..95f3297 --- /dev/null +++ b/roles/juniper.junos/library/_junos_zeroize.py @@ -0,0 +1 @@ +juniper_junos_system.py \ No newline at end of file diff --git a/roles/juniper.junos/library/juniper_junos_command.py b/roles/juniper.junos/library/juniper_junos_command.py new file mode 100644 index 0000000..9a2b66a --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_command.py @@ -0,0 +1,502 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, 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. +# + +from __future__ import absolute_import, division, print_function + +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: juniper_junos_command +version_added: "2.0.0" # of Juniper.junos role +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. + - 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: Examples of juniper_junos_command + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + + tasks: + - name: Execute single "show version" command. + juniper_junos_command: + commands: "show version" + register: response + + - name: Print the command output + debug: + var: response.stdout + + - name: Execute three commands. + juniper_junos_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: Two commands with XML output. + juniper_junos_command: + commands: + - "show route" + - "show lldp neighbors" + format: xml + + - name: show route with XML output - show version with JSON output + juniper_junos_command: + commands: + - "show route" + - "show version" + formats: + - "xml" + - "json" + + - name: save outputs in dest_dir + juniper_junos_command: + commands: + - "show route" + - "show version" + dest_dir: "./output" + + - name: save output to dest + juniper_junos_command: + command: "show system uptime" + dest: "/tmp/{{ inventory_hostname }}.uptime.output" + + - name: save output to dest + juniper_junos_command: + command: + - "show route" + - "show lldp neighbors" + dest: "/tmp/{{ inventory_hostname }}.commands.output" + + - name: Multiple commands, save outputs, but don't return them + juniper_junos_command: + commands: + - "show route" + - "show version" + formats: + - "xml" + - "json" + dest_dir: "/tmp/outputs/" + return_output: false +''' + +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.module_utils.basic import AnsibleModule +from ansible.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), + 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=juniper_junos_common.MIN_JXMLEASE_VERSION, + ) + + # 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 + resp = junos_module.dev.rpc(rpc, normalize=bool(format == 'xml')) + 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 (resp, junos_module.etree._Element): + # 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/roles/juniper.junos/library/juniper_junos_config.py b/roles/juniper.junos/library/juniper_junos_config.py new file mode 100644 index 0000000..c7f1386 --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_config.py @@ -0,0 +1,1123 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, 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. +# + +from __future__ import absolute_import, division, print_function + +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: juniper_junos_config +version_added: "2.0.0" # of Juniper.junos role +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. + #. 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 + config_mode: + description: + - The mode used to access the candidate configuration database. + required: false + default: exclusive + type: str + choices: + - exclusive + - private + aliases: + - config_access + - edit_mode + - edit_access + 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 + 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: Manipulate the configuration of Junos devices + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + tasks: + - name: Retrieve the committed configuration + juniper_junos_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. + juniper_junos_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. + juniper_junos_config: + config_mode: 'private' + rollback: 1 + register: response + - name: Print the config changes. + debug: + var: response.diff_lines + + - name: Rollback to the rescue config. + juniper_junos_config: + rollback: 'rescue' + register: response + - name: Print the complete response. + debug: + var: response + + - name: Load override from a file. + juniper_junos_config: + load: 'override' + src: "{{ inventory_hostname }}.conf" + register: response + - name: Print the complete response. + debug: + var: response + + - name: Load from a Jinja2 template. + juniper_junos_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. + juniper_junos_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 + juniper_junos_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. + juniper_junos_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. + juniper_junos_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. + juniper_junos_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 + juniper_junos_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) + juniper_junos_config: + check: true + diff: false + commit: false + register: response + - name: Print the complete response + debug: + var: response +''' + +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 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: str +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.module_utils.basic import AnsibleModule +from ansible.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 + + # 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'), + 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), + 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), + confirmed=dict(required=False, + type='int', + aliases=['confirm'], + default=None), + 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=juniper_junos_common.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') + + # 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') + confirmed = junos_module.params.get('confirmed') + comment = junos_module.params.get('comment') + check_commit_wait = junos_module.params.get('check_commit_wait') + + # 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) + 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: + diff = junos_module.diff_configuration() + if diff is not None: + results['changed'] = True + if return_output is True: + results['diff'] = 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) + 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) + 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/roles/juniper.junos/library/juniper_junos_facts.py b/roles/juniper.junos/library/juniper_junos_facts.py new file mode 100644 index 0000000..ec1e820 --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_facts.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, 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. +# + +from __future__ import absolute_import, division, print_function + +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: juniper_junos_facts +version_added: "2.0.0" # of Juniper.junos role +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: Gather facts from Junos devices + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + tasks: + - name: Gather Junos facts with no configuration + juniper_junos_facts: + +# 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 """ + +# Ansiballz packages module_utils into ansible.module_utils +from ansible.module_utils.basic import AnsibleModule +from ansible.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. + """ + # Retrieve all PyEZ-supported facts and copy to a standard dict. + facts = dict(junos_module.dev.facts) + # Add two useful facts that are implement as PyEZ Device attributes. + facts['re_name'] = junos_module.dev.re_name + facts['master_state'] = junos_module.dev.master + # 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: + 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.params.get('savedir') is not None: + save_dir = junos_module.params.get('savedir') + file_name = '%s-inventory.xml' % (junos_module.dev.facts['hostname']) + 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(inventory.encode(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=juniper_junos_common.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.") + inventory = junos_module.dev.rpc.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/roles/juniper.junos/library/juniper_junos_jsnapy.py b/roles/juniper.junos/library/juniper_junos_jsnapy.py new file mode 100644 index 0000000..f5669c3 --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_jsnapy.py @@ -0,0 +1,358 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, Juniper Networks Inc. +# 2016, Roslan Zaki +# +# 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 + +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: juniper_junos_jsnapy +version_added: "2.0.0" # of Juniper.junos role +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/uk/en/training/jnbooks/day-one/automation-series/jsnapy/) + - 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:`juniper_junos_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 juniper_junos_jsnapy + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + + tasks: + - name: JUNOS Post Checklist + juniper_junos_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 + juniper_junos_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" + juniper_junos_jsnapy: + action: "snap_pre" + test_files: "tests/test_loopback.yml" + + - name: "Collect Post Snapshot" + juniper_junos_jsnapy: + action: "snap_post" + test_files: "tests/test_loopback.yml" + + - name: "Check after Pre and Post Snapshots" + juniper_junos_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 = ''' +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.module_utils.basic import AnsibleModule +from ansible.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), + 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=juniper_junos_common.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') + + # 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': + responses = jsa.check(data=data, + dev=junos_module.dev, + pre_file='PRE', + post_file='POST') + elif action == 'snapcheck': + responses = jsa.snapcheck(data=data, + dev=junos_module.dev) + elif action == 'snap_pre': + responses = jsa.snap(data=data, + dev=junos_module.dev, + file_name='PRE') + elif action == 'snap_post': + 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) + 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 + + junos_module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/roles/juniper.junos/library/juniper_junos_ping.py b/roles/juniper.junos/library/juniper_junos_ping.py new file mode 100644 index 0000000..19f6e80 --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_ping.py @@ -0,0 +1,504 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, Juniper Networks Inc. +# 2016, Damien Garros +# +# 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 + +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: juniper_junos_ping +version_added: "2.0.0" # of Juniper.junos role +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 juniper_junos_ping + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + + tasks: + - name: Ping 10.0.0.1 with default parameters. Fails if any packets lost. + juniper_junos_ping: + dest: "10.0.0.1" + + - name: Ping 10.0.0.1. Allow 50% packet loss. Register response. + juniper_junos_ping: + dest: "10.0.0.1" + acceptable_percent_loss: 50 + register: response + - name: Print all keys in the response. + debug: + var: response + + - name: Ping 10.0.0.1. Send 20 packets. Register response. + juniper_junos_ping: + dest: "10.0.0.1" + count: 20 + register: response + - name: Print packet sent from the response. + debug: + var: response.packets_sent + + - name: Ping 10.0.0.1. Send 10 packets wihtout rapid. Register response. + juniper_junos_ping: + dest: "10.0.0.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. + juniper_junos_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 10.0.0.1 with IP packet size of 1500. Register response. + juniper_junos_ping: + dest: "10.0.0.1" + size: 1472 + register: response + - name: Print the packets_received from the response. + debug: + var: response.packets_received + + - name: Ping 10.0.0.1 with do-not-fragment bit set. Register response. + juniper_junos_ping: + dest: "10.0.0.1" + do_not_fragment: true + register: response + - name: Print the maximum round-trip-time from the response. + debug: + var: response.rtt_maximum + + - name: Ping 10.0.0.1 with source set to 10.0.0.2. Register response. + juniper_junos_ping: + dest: "10.0.0.1" + source: "10.0.0.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. + juniper_junos_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_junos_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.module_utils.basic import AnsibleModule +from ansible.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/roles/juniper.junos/library/juniper_junos_pmtud.py b/roles/juniper.junos/library/juniper_junos_pmtud.py new file mode 100644 index 0000000..d0e7eda --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_pmtud.py @@ -0,0 +1,410 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, Juniper Networks Inc. +# 2017, Martin Komon +# +# 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 + +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: juniper_junos_pmtud +version_added: "2.0.0" # of Juniper.junos role +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 juniper_junos_mtud + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + + tasks: + - name: Perform PMTUD to 10.0.0.1 with default parameters. + juniper_junos_pmtud: + dest: "10.0.0.1" + + - name: Perform PMTUD to 10.0.0.1. Register response. + juniper_junos_pmtud: + dest: "10.0.0.1" + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 10.0.0.1. Search all possible MTU values. + juniper_junos_pmtud: + dest: "10.0.0.1" + max_size: 65496 + max_range: 65536 + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 10.0.0.1. Source from ge-0/0/0.0 interface. + juniper_junos_pmtud: + dest: "10.0.0.1" + interface: "ge-0/0/0.0" + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 10.0.0.1. Source from 192.168.1.1. + juniper_junos_pmtud: + dest: "10.0.0.1" + source: "192.168.1.1" + register: response + - name: Print the discovered MTU. + debug: + var: response.inet_mtu + + - name: Perform PMTUD to 10.0.0.1. Source from the red routing-instance. + juniper_junos_pmtud: + dest: "10.0.0.1" + routing_instance: "red" + register: response + - name: Print the discovered MTU. + 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.module_utils.basic import AnsibleModule +from ansible.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 int(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 = int(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/roles/juniper.junos/library/juniper_junos_rpc.py b/roles/juniper.junos/library/juniper_junos_rpc.py new file mode 100644 index 0000000..a8f738b --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_rpc.py @@ -0,0 +1,622 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, Juniper Networks Inc. +# 2016, Nitin Kumar +# +# 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 +from 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: juniper_junos_rpc +version_added: "2.0.0" # of Juniper.junos role +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 + 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:`juniper_junos_rpc-examples-label`. + 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: Examples of juniper_junos_rpc + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + + tasks: + - name: Execute single get-software-information RPC. + juniper_junos_rpc: + rpcs: "get-software-information" + register: response + - name: Print the RPC's output as a single multi-line string. + debug: + var: response.stdout + +###### OLD EXAMPLES ########## +- junos_rpc: + host={{ inventory_hostname }} + rpc=get-interface-information + dest=get_interface_information.conf + register=junos + +- junos_rpc: + host={{ inventory_hostname }} + rpc=get-interface-information + kwargs="interface_name=em0" + format=xml/text/json + dest=get_interface_information.conf + register=junos + +# Example to fetch device configuration +- name: Get Device Configuration + junos_rpc: + host={{ inventory_hostname }} + rpc=get-config + dest=get_config.conf + +# Fetch configuration over console server connection using PyEZ >= 2.0 +- name: Get Device Configuration + junos_rpc: + host={{ inventory_hostname }} + port=7005 + mode='telnet' + rpc=get-config + dest=get_config.conf + +# Example to fetch device configuration +- name: Get Device Configuration for interface + junos_rpc: + host={{ inventory_hostname }} + rpc=get-config + filter_xml="" + dest=get_config.conf + register: junos + +# Example to fetch configuration in json for >=14.2 +# and use it with rpc_reply +- name: Get Device Configuration + hosts: all + roles: + - Juniper.junos + connection: local + gather_facts: no + tasks: + - name: Get interface information + junos_rpc: + host: "{{ inventory_hostname }}" + rpc: get-interface-information + kwargs: + interface_name: em0 + media: True + format: json + dest: get_interface_information.conf + register: junos + + - name: Print configuration + debug: msg="{{ junos.rpc_reply }}" +###### OLD EXAMPLES ########## +''' + +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 +''' + +import os.path + + +try: + # Python 2 + basestring +except NameError: + # Python 3 + basestring = 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.module_utils.basic import AnsibleModule +from ansible.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), + 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=juniper_junos_common.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.") + + # 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: + if rpc_string == 'get-config': + filter = junos_module.params.get('filter') + if attr is None: + attr = {} + if kwarg is None: + kwarg = {} + junos_module.logger.debug('Executing "get-config" RPC. ' + 'filter_xml=%s, options=%s, ' + 'kwargs=%s', + filter, str(attr), str(kwarg)) + resp = junos_module.dev.rpc.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: + rpc = junos_module.etree.Element(rpc_string, format=format) + 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)) + resp = junos_module.dev.rpc(rpc, + normalize=bool(format == 'xml')) + 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 (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/roles/juniper.junos/library/juniper_junos_software.py b/roles/juniper.junos/library/juniper_junos_software.py new file mode 100644 index 0000000..97df8bf --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_software.py @@ -0,0 +1,747 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, 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. +# + +from __future__ import absolute_import, division, print_function + +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: juniper_junos_software +version_added: "2.0.0" # of Juniper.junos role +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 + 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 + 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: Examples of juniper_junos_software + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + + tasks: + - name: Execute a basic Junos software upgrade. + juniper_junos_software: + local_package: "./images/" + register: response + - name: Print the complete response. + debug: + var: response + +###### OLD 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 +###### OLD EXAMPLES ########## +''' + + +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.module_utils.basic import AnsibleModule +from ansible.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'] + 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('-(\d{2}\..*\d).*', filename) + if match is not None: + return match.group(1) + # Not a known Junos package name. + else: + return None + + +def define_progress_callback(junos_module): + """Create callback which can be passed to SW.install(progress=progress) + """ + def myprogress(_, report): + """A progress function which logs report at level INFO. + + Args: + _: 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), + 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), + 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']], + supports_check_mode=True + ) + + # Straight from params + local_package = junos_module.params.pop('local_package') + remote_package = junos_module.params.pop('remote_package') + 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') + 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 + (_, 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: + 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)) + else: + # Local package was not specified, so we must assume no_copy. + no_copy = True + local_filename = None + + if no_copy is False 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)) + + 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: + 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} + + # 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 = junos_module.dev.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: + current_version = junos_module.dev.facts['version'] + if target_version != current_version: + re_name = junos_module.dev.re_name + junos_module.logger.debug("Current version on %s: %s. " + "Target version: %s.", + current_version, re_name, + target_version) + results['changed'] = True + 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 + else: + install_params['package'] = remote_filename + if remote_dir is not None: + install_params['remote_path'] = remote_dir + 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 + 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) + try: + junos_module.logger.debug("Install parameters are: %s", + str(install_params)) + junos_module.add_sw() + ok = junos_module.sw.install(**install_params) + if ok is not True: + results['msg'] = 'Unable to install the software' + junos_module.fail_json(**results) + results['msg'] = 'Package %s successfully installed.' % \ + (install_params['package']) + junos_module.logger.debug('Package %s successfully installed.', + install_params['package']) + 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: + 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. + if junos_module.dev.timeout > 5: + junos_module.logger.debug("Decreasing device RPC timeout " + "to 5 seconds.") + junos_module.dev.timeout = 5 + junos_module.logger.debug('Initiating reboot.') + rpc = junos_module.etree.Element('request-reboot') + xpath_list = ['.//request-reboot-status'] + if all_re is True: + if (junos_module.sw._multi_RE is True and + junos_module.sw._multi_VC is False): + junos_module.etree.SubElement(rpc, + 'both-routing-engines') + # At least on some platforms stopping/rebooting both + # REs produces messages and + # messages. + xpath_list.append('..//output') + elif junos_module.sw._mixed_VC is True: + junos_module.etree.SubElement(rpc, 'all-members') + resp = junos_module.dev.rpc(rpc, + ignore_warning=True, + normalize=True) + junos_module.logger.debug("Reboot RPC executed cleanly.") + if len(xpath_list) > 0: + for xpath in xpath_list: + got = resp.findtext(xpath) + if got is not None: + results['msg'] += ' Reboot successfully initiated.' + break + 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) + else: + results['msg'] += ' Reboot successfully initiated.' + 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.ConnectError): + # This is expected. The device has already disconnected. + results['msg'] += ' Reboot succeeded.' + 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) + 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/roles/juniper.junos/library/juniper_junos_srx_cluster.py b/roles/juniper.junos/library/juniper_junos_srx_cluster.py new file mode 100644 index 0000000..4fb7b71 --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_srx_cluster.py @@ -0,0 +1,295 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, 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. +# + +from __future__ import absolute_import, division, print_function + +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: juniper_junos_srx_cluster +version_added: "2.0.0" # of Juniper.junos role +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 + roles: + - Juniper.junos + tasks: + - name: Enable an SRX cluster + juniper_junos_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 + juniper_junos_srx_cluster: + enable: false + register: response + - name: Print the response. + 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.module_utils.basic import AnsibleModule +from ansible.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} + + junos_module.logger.debug("Check current SRX cluster operational state.") + current_cluster_state = junos_module.dev.facts['srx_cluster'] + current_cluster_id = junos_module.dev.facts['srx_cluster_id'] + if current_cluster_id is not None: + current_cluster_id = int(current_cluster_id) + current_node_name = junos_module.dev.re_name + current_node_id = None + if current_node_name is not None: + (_, _, 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: + 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.dev.rpc.set_chassis_cluster_disable( + reboot=True, normalize=True + ) + 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/roles/juniper.junos/library/juniper_junos_system.py b/roles/juniper.junos/library/juniper_junos_system.py new file mode 100644 index 0000000..d05c731 --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_system.py @@ -0,0 +1,464 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 1999-2018, 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. +# + +from __future__ import absolute_import, division, print_function + +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: juniper_junos_system +version_added: "2.0.0" # of Juniper.junos role +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: none + 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 +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: Examples of juniper_junos_system + hosts: junos-all + connection: local + gather_facts: no + roles: + - Juniper.junos + + tasks: + - name: Reboot all REs of the device + juniper_junos_system: + action: "reboot" + + - name: Power off the other RE of the device. + juniper_junos_system: + action: "shutdown" + othe_re: True + + - name: Reboot this RE at 8pm today. + juniper_junos_system: + action: "reboot" + all_re: False + at: "20:00" + + - name: Halt the system on 25 January 2018 at 4pm. + juniper_junos_system: + action: "halt" + at: "1801251600" + + - name: Reboot the system in 30 minutes. + juniper_junos_system: + action: "reboot" + in_min: 30 + + - name: Reboot the system in 30 minutes. + juniper_junos_system: + action: "reboot" + at: "+30m" + + - name: Zeroize the local RE only. + juniper_junos_system: + action: "zeroize" + all_re: False + + - name: Zeroize all REs and overwrite medea. + juniper_junos_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.module_utils.basic import AnsibleModule +from ansible.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=None), + all_re=dict(type='bool', + required=False, + default=True), + other_re=dict(type='bool', + required=False, + 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'] + # Synonymns for shutdown + if action == 'off' or action == 'power_off' or action == 'power-off': + action = 'shutdown' + + # at option only applies to reboot, shutdown, or halt actions. + if (params.get('at') is not None and + action != 'reboot' and + action != 'shutdown' and + action != 'halt'): + junos_module.fail_json(msg='The at option can only be used when ' + 'the action option has the value "reboot", ' + '"shutdown", or "halt".') + + # in_min option only applies to reboot, shutdown, or halt actions. + if (params.get('in_min') is not None and + action != 'reboot' and + action != 'shutdown' and + action != 'halt'): + junos_module.fail_json(msg='The in_min option can only be used when ' + 'the action option has the value "reboot", ' + '"shutdown", or "halt".') + + # other_re option only applies to reboot, shutdown, or halt actions. + if (params.get('other_re') is True and + action != 'reboot' and + action != 'shutdown' and + action != 'halt'): + junos_module.fail_json(msg='The other_re option can only be used when ' + 'the action option has the value "reboot", ' + '"shutdown", or "halt".') + + # media option only applies to zeroize action. + if params['media'] is True and action != 'zeroize': + junos_module.fail_json(msg='The media option can only be used when ' + 'the action option has the value ' + '"zeroize".') + + # If other_re, then we should turn off all_re + if params['other_re'] is True: + params['all_re'] = False + + # Set initial results values. Assume failure until we know it's success. + # Assume we haven't changed the state until we do. + results = {'changed': False, + 'msg': '', + 'reboot': bool(action == 'reboot'), + 'action': action, + 'all_re': params.get('all_re'), + 'other_re': params.get('other_re'), + 'media': params.get('media'), + 'failed': True} + + # Map the action to an RPC. + rpc = None + xpath_list = [] + if action == 'reboot': + rpc = junos_module.etree.Element('request-reboot') + xpath_list.append('.//request-reboot-status') + elif action == 'shutdown': + rpc = junos_module.etree.Element('request-power-off') + xpath_list.append('.//request-reboot-status') + elif action == 'halt': + rpc = junos_module.etree.Element('request-halt') + xpath_list.append('.//request-reboot-status') + elif action == 'zeroize': + rpc = junos_module.etree.Element('request-system-zeroize') + else: + results['msg'] = 'No RPC found for the %s action.' % (action) + junos_module.fail_json(**results) + + # Add the arguments + if action == 'zeroize': + if params['all_re'] is False: + if junos_module.dev.facts['2RE']: + junos_module.etree.SubElement(rpc, 'local') + if params['media'] is True: + junos_module.etree.SubElement(rpc, 'media') + else: + if params['in_min'] is not None: + junos_module.etree.SubElement(rpc, + 'in').text = str(params['in_min']) + elif params['at'] is not None: + junos_module.etree.SubElement(rpc, + 'at').text = params['at'] + if params['other_re'] is True: + if junos_module.dev.facts['2RE']: + junos_module.etree.SubElement(rpc, 'other-routing-engine') + # At least on some platforms stopping/rebooting the other RE + # just produces messages. + xpath_list.append('..//output') + elif params['all_re'] is True: + junos_module.add_sw() + if (junos_module.sw._multi_RE is True and + junos_module.sw._multi_VC is False): + junos_module.etree.SubElement(rpc, 'both-routing-engines') + # At least on some platforms stopping/rebooting both REs + # produces messages and + # messages. + xpath_list.append('..//output') + elif junos_module.sw._mixed_VC is True: + junos_module.etree.SubElement(rpc, 'all-members') + + # OK, we're ready to do something. Set changed and log the RPC. + results['changed'] = True + junos_module.logger.debug("Ready to execute RPC: %s", + junos_module.etree.tostring(rpc, + pretty_print=True)) + + if not junos_module.check_mode: + 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 (params['at'] == 'now' or params['in_min'] == 0 or + (params['at'] is None and params['in_min'] 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: %s", + junos_module.etree.tostring(rpc, pretty_print=True)) + resp = junos_module.dev.rpc(rpc, + ignore_warning=True, + normalize=True) + junos_module.logger.debug("RPC executed cleanly.") + if len(xpath_list) > 0: + for xpath in xpath_list: + got = resp.findtext(xpath) + if got is not None: + results['msg'] = '%s successfully initiated.' % \ + (action) + results['failed'] = False + break + 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 RPC response.' + results['changed'] = False + else: + results['msg'] = '%s successfully initiated.' % (action) + 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/roles/juniper.junos/library/juniper_junos_table.py b/roles/juniper.junos/library/juniper_junos_table.py new file mode 100644 index 0000000..008c5a8 --- /dev/null +++ b/roles/juniper.junos/library/juniper_junos_table.py @@ -0,0 +1,477 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2016 Jason Edelman +# Network to Code, LLC +# +# Copyright (c) 2017-2018, 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 + +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: juniper_junos_table +version_added: "2.0.0" # of Juniper.junos role +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: no + roles: + - Juniper.junos + + tasks: + - name: Retrieve LLDP Neighbor Information Using PyEZ-included Table + juniper_junos_table: + file: "lldp.yml" + register: response + - name: Print response + debug: + var: response + + - name: Retrieve routes within 10.0.0/8 + juniper_junos_table: + file: "routes.yml" + table: "RouteTable" + kwargs: + destination: "10.0.0.0/8" + response_type: "juniper_items" + register: response + - name: Print response + debug: + var: response + + - name: Retrieve from custom table in playbook directory + juniper_junos_table: + file: "fpc.yaml" + path: "." + register: response + - name: Print response + 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.module_utils.basic import AnsibleModule +from ansible.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 (value and 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=juniper_junos_common.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.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: + 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/roles/juniper.junos/meta/.galaxy_install_info b/roles/juniper.junos/meta/.galaxy_install_info new file mode 100644 index 0000000..9cefadb --- /dev/null +++ b/roles/juniper.junos/meta/.galaxy_install_info @@ -0,0 +1 @@ +{install_date: 'Tue Feb 19 16:43:28 2019', version: 2.1.0} diff --git a/roles/juniper.junos/meta/main.yml b/roles/juniper.junos/meta/main.yml new file mode 100644 index 0000000..3b4daf4 --- /dev/null +++ b/roles/juniper.junos/meta/main.yml @@ -0,0 +1,18 @@ +--- +galaxy_info: + author: Juniper #Stacy W. Smith @stacywsmith + description: Network build automation of Junos devices. + company: Juniper Networks, Inc. + license: Apache 2.0 + min_ansible_version: 2.1 + platforms: + - name: junos + versions: + - all + categories: + - networking + galaxy_tags: + - networking + - junos + - juniper +dependencies: [] diff --git a/roles/juniper.junos/module_utils/__init__.py b/roles/juniper.junos/module_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/roles/juniper.junos/module_utils/juniper_junos_common.py b/roles/juniper.junos/module_utils/juniper_junos_common.py new file mode 100644 index 0000000..4051394 --- /dev/null +++ b/roles/juniper.junos/module_utils/juniper_junos_common.py @@ -0,0 +1,1959 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2017-2018, 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 +from six import iteritems + +# Ansible imports +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.plugins.action.normal import ActionModule as ActionNormal + +# Standard library imports +from argparse import ArgumentParser +from distutils.version import LooseVersion +import json +import logging +import os + +# 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: + import jnpr.junos.utils.config + HAS_PYEZ_CONFIG = True +except ImportError: + HAS_PYEZ_CONFIG = False + +try: + import jnpr.junos.op + import jnpr.junos.factory.factory_loader + import jnpr.junos.factory.table + 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: + 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 + +try: + # Python 2 + basestring +except NameError: + # Python 3 + basestring = str + + +# Constants +# Minimum PyEZ version required by shared code. +MIN_PYEZ_VERSION = "2.1.7" +# 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.2.1" +# 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 convert_to_bool_func(arg): + """Try converting arg to a bool value using Ansible's aliases for bool. + + Args: + arg: The value to convert. + + Returns: + A boolean value if successfully converted, or None if not. + """ + if arg is None or type(arg) == bool: + return arg + if isinstance(arg, basestring): + arg = arg.lower() + if arg in BOOLEANS_TRUE: + return True + elif arg in BOOLEANS_FALSE: + return False + else: + return None + + +class ModuleDocFragment(object): + """Documentation fragment for connection-related parameters. + + All juniper_junos_* 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 + juniper_junos_* modules. + LOGGING_DOCUMENTATION: The documentation string defining the + logging-related parameters for the + juniper_junos_* 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. + 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 +''' + + 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 + ''' + provider: + description: + - An alternative syntax for specifying the connection options. Rather + than specifying each connection-related top-level option, the + connection-related options may be specified as a dictionary of + suboptions to the I(provider) option. All connection-related options + must either be specified as top-level options or as suboptions of + the I(provider) option. You can not combine the two methods of + specifying connection-related options. + required: false + default: none + type: dict + suboptions:''' + _SUB_CONNECT_DOCUMENTATION + ''' + requirements: + - U(junos-eznc|https://github.com/Juniper/py-junos-eznc) >= ''' + MIN_PYEZ_VERSION + ''' + - Python >= 2.7 + 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 either in provider or 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 either in provider or 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), + '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), +} +# Connection arguments which are mutually exclusive. +connection_spec_mutually_exclusive = [['mode', 'console'], + ['port', 'console'], + ['baud', 'console'], + ['attempts','console']] +# Keys are connection options. Values are a list of task_vars to use as the +# default value. +connection_spec_fallbacks = { + 'host': ['inventory_hostname'], + 'user': ['ansible_connection_user', 'ansible_ssh_user', 'ansible_user'], + 'passwd': ['ansible_ssh_pass', 'ansible_pass'], + 'ssh_private_key_file': ['ansible_ssh_private_key_file', + 'ansible_private_key_file'] +} + +# Specify the provider spec with options matching connection_spec. +provider_spec = { + 'provider': dict(type='dict', + options=connection_spec) +} + +# The provider option is mutually exclusive with all top-level connection +# options. +provider_spec_mutually_exclusive = [] +for key in connection_spec: + provider_spec_mutually_exclusive.append(['provider', key]) + +# 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 + provider_spec + logging_spec +top_spec = connection_spec +top_spec.update(provider_spec) +top_spec.update(logging_spec) +top_spec_mutually_exclusive = connection_spec_mutually_exclusive +top_spec_mutually_exclusive += provider_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', + required=True, + default=None), + '_module_name': dict(type='str', + required=True, + 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'] +# Supported configuration modes +CONFIG_MODE_CHOICES = ['exclusive', 'private'] + + +def convert_to_bool_func(arg): + """Try converting arg to a bool value using Ansible's aliases for bool. + + Args: + arg: The value to convert. + + Returns: + A boolean value if successfully converted, or None if not. + """ + if arg is None or type(arg) == bool: + return arg + if isinstance(arg, basestring): + arg = arg.lower() + if arg in BOOLEANS_TRUE: + return True + elif arg in BOOLEANS_FALSE: + return False + else: + return None + + +class JuniperJunosModule(AnsibleModule): + """A subclass of AnsibleModule used by all juniper_junos_* modules. + + All juniper_junos_* 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. + convert_to_bool: Try converting to bool using aliases for bool. + 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 or private 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={}, + mutually_exclusive=[], + min_pyez_version=MIN_PYEZ_VERSION, + min_lxml_etree_version=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 juniper_junos_* modules. Performs additional checks on options. + Collapses any provider options to be top-level 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. + """ + # 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) + self.module_name = self.params.get('_module_name') + # Remove any arguments in internal_spec + for arg_name in internal_spec: + self.params.pop(arg_name) + # Promote any provider arg_name into params + if 'provider' in self.params and self.params['provider'] is not None: + for arg_name, arg_value in self.params['provider'].items(): + if arg_name in self.aliases: + arg_name = self.aliases[arg_name] + self.params[arg_name] = arg_value + self.params.pop('provider') + # Parse the console option + self._parse_console_options() + # 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'])) + # Default baud if serial or telnet mode + if self.params.get('baud') is None: + if (self.params.get('mode') == 'telnet' or + self.params.get('mode') == 'serial'): + self.params['baud'] = 9600 + # Default attempts if serial or telnet mode + if self.params.get('attemps') is None: + if (self.params.get('mode') == 'telnet' or + self.params.get('mode') == 'serial'): + self.params['attempts'] = 10 + # baud and attempts are only valid if mode != None + if (self.params.get('baud') is not None and + self.params.get('mode') is 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 and + self.params.get('mode') is None): + self.fail_json(msg="The attempts option (%s) is not valid when " + "mode == none." % (self.params.get('attempts'))) + # 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") + # Check PyEZ version and add attributes to reach PyEZ components. + self.check_pyez(min_pyez_version, + check_device=True, + check_sw=True, + check_config=True, + check_op_table=True, + check_exception=True) + 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 + # Check LXML Etree. + self.check_lxml_etree(min_lxml_etree_version) + self.etree = etree + # Check jsnapy if needed. + if min_jsnapy_version is not None: + self.check_jsnapy(min_jsnapy_version) + if hasattr(jnpr, 'jsnapy'): + self.jsnapy = jnpr.jsnapy + else: + self.fail_json("JSNAPy not available.") + # Check jxmlease if needed. + if min_jxmlease_version is not None: + self.check_jxmlease(min_jxmlease_version) + self.jxmlease = jxmlease + # Check yaml if needed. + if min_yaml_version is not None: + self.check_yaml(min_yaml_version) + self.yaml = yaml + # Setup logging. + self.logger = self._setup_logging() + # Open the PyEZ connection + self.open() + + 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. + self.close() + 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. + self.close() + 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) + self.params['mode'] = 'serial' + if port is not None: + self.params['port'] = port + if baud is not None: + self.params['baud'] = baud + + # 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 as ex: + 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 _check_library(self, + 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: + self.fail_json(msg='%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: + self.fail_json(msg='%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): + self.fail_json( + msg='%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)) + + def check_pyez(self, minimum=None, + check_device=False, + check_sw=False, + check_config=False, + check_op_table=False, + check_exception=False): + """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. + - check_device and PyEZ Device object can't be imported + - check_exception and PyEZ excepetions can't be imported + """ + self._check_library('junos-eznc', HAS_PYEZ_VERSION, + PYEZ_INSTALLATION_URL, minimum=minimum, + library_nickname='junos-eznc (aka PyEZ)') + if check_device is True: + if HAS_PYEZ_DEVICE is False: + self.fail_json(msg='junos-eznc (aka PyEZ) is installed, but ' + 'the jnpr.junos.device.Device class could ' + 'not be imported.') + if check_sw is True: + if HAS_PYEZ_SW is False: + self.fail_json(msg='junos-eznc (aka PyEZ) is installed, but ' + 'the jnpr.junos.utils.sw class could ' + 'not be imported.') + if check_config is True: + if HAS_PYEZ_CONFIG is False: + self.fail_json(msg='junos-eznc (aka PyEZ) is installed, but ' + 'the jnpr.junos.utils.config class could ' + 'not be imported.') + if check_op_table is True: + if HAS_PYEZ_OP_TABLE is False: + self.fail_json(msg='junos-eznc (aka PyEZ) is installed, but ' + 'the jnpr.junos.op class could not be ' + 'imported.') + if check_exception is True: + if HAS_PYEZ_EXCEPTIONS is False: + self.fail_json(msg='junos-eznc (aka PyEZ) is installed, but ' + 'the jnpr.junos.exception module could not ' + 'be imported.') + + def check_jsnapy(self, 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. + """ + self._check_library('jsnapy', HAS_JSNAPY_VERSION, + JSNAPY_INSTALLATION_URL, minimum=minimum) + + def check_jxmlease(self, 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. + """ + self._check_library('jxmlease', HAS_JXMLEASE_VERSION, + JXMLEASE_INSTALLATION_URL, minimum=minimum) + + def check_lxml_etree(self, 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. + """ + self._check_library('lxml Etree', HAS_LXML_ETREE_VERSION, + LXML_ETREE_INSTALLATION_URL, minimum=minimum) + + def check_yaml(self, 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. + """ + self._check_library('yaml', HAS_YAML_VERSION, + YAML_INSTALLATION_URL, minimum=minimum) + + def convert_to_bool(self, arg): + """Try converting arg to a bool value using Ansible's aliases for bool. + + Args: + arg: The value to convert. + + Returns: + A boolean value if successfully converted, or None if not. + """ + return convert_to_bool_func(arg) + + 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, basestring): + # 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 = self._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." % + (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, basestring): + # This might be a keyword1=value1 keyword2=value2 type string. + # The _check_type_dict method will parse this into a dict. + try: + kwarg = self._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." % + (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 " + "invalid. Unable to translate into a list " + "of dicts." % + (option_name, string_val)) + # 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, basestring): + 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. + bool_val = self.convert_to_bool(v) + 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: + bool_val = self.convert_to_bool(ignore_warn_list[0]) + if bool_val is not None: + return bool_val + elif isinstance(ignore_warn_list[0], basestring): + return ignore_warn_list[0] + else: + 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, basestring): + 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, basestring): + 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' + 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 jnp.junos.utils.sw.SW() to self. + """ + self.sw = jnpr.junos.utils.sw.SW(self.dev) + + def open_configuration(self, mode): + """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. + """ + # Already have an open configuration? + if self.config is None: + if mode not in CONFIG_MODE_CHOICES: + self.fail_json(msg='Invalid configuration mode: %s' % (mode)) + 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='uncommitted changes will be ' + 'discarded on exit') + 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.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 == 'private': + 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={}, filter=None): + """Return the device configuration in the specified format. + + Return the datbase device configuration datbase 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. + 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 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.dev is None: + self.open() + + self.logger.debug("Retrieving device configuration. Options: %s " + "Filter %s", str(options), str(filter)) + config = None + try: + config = self.dev.rpc.get_config(options=options, + filter_xml=filter) + 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 config.tag != 'configuration-text': + self.fail_json(msg='Unexpected XML tag returned. ' + 'Configuration is: %s' % + (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 config.tag != 'configuration-set': + self.fail_json(msg='Unexpected XML tag returned. ' + 'Configuration is: %s' % + (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 config.tag != 'configuration': + self.fail_json(msg='Unexpected XML tag returned. ' + 'Configuration is: %s' % + (etree.tostring(config, pretty_print=True))) + return_val = (etree.tostring(config, pretty_print=True), + 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.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. + """ + if self.dev is None or self.config is None: + self.fail_json(msg='The device or configuration is not open.') + + self.logger.debug("Checking the configuration.") + 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): + """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. + """ + if self.dev is None or self.config is None: + self.fail_json(msg='The device or configuration is not open.') + + self.logger.debug("Diffing candidate and committed configurations.") + try: + diff = self.config.diff(rb_id=0) + 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' + 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.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 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: + load_args['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) + + 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): + """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. + + Failures: + - An error returned from committing the configuration. + """ + 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) + 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={}): + """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. + """ + # Assume failure until we know success. + results['failed'] = True + + # Execute the ping. + try: + self.logger.debug("Executing ping with parameters: %s", + str(params)) + resp = self.dev.rpc.ping(normalize=True, **params) + 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 = int(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') + hostname = self.params.get('host') + file_name = '%s.diff' % (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 = 'a' + elif self.params.get('dest_dir') is not None: + dest_dir = self.params.get('dest_dir') + hostname = self.params.get('host') + # 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' % (hostname, name, format) + file_path = os.path.normpath(os.path.join(dest_dir, file_name)) + if file_path is not None: + try: + with open(file_path, mode) as save_file: + save_file.write(text.encode(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)) + + +class JuniperJunosActionModule(ActionNormal): + """A subclass of ActionNormal used by all juniper_junos_* modules. + + All juniper_junos_* 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. + + Public Methods: + convert_to_bool: Try converting to bool using aliases for bool. + """ + def run(self, tmp=None, task_vars=None): + # The new connection arguments based on fallback/defaults. + new_connection_args = dict() + + # Get the current connection args from either provider or the top-level + if 'provider' in self._task.args: + connection_args = self._task.args['provider'] + else: + connection_args = self._task.args + + # 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] + break + + # 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 + + # Copy the new connection arguments back into either top-level or + # the provider dictionary. + if 'provider' in self._task.args: + self._task.args['provider'].update(new_connection_args) + else: + 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 + + # Call the parent action module. + return super(JuniperJunosActionModule, self).run(tmp, task_vars) + + def convert_to_bool(self, arg): + """Try converting arg to a bool value using Ansible's aliases for bool. + + Args: + arg: The value to convert. + + Returns: + A boolean value if successfully converted, or None if not. + """ + return convert_to_bool_func(arg) diff --git a/roles/juniper.junos/requirements.txt b/roles/juniper.junos/requirements.txt new file mode 100644 index 0000000..fc1f4b0 --- /dev/null +++ b/roles/juniper.junos/requirements.txt @@ -0,0 +1,5 @@ +junos-eznc>=2.1.7 +jsnapy>=1.2.1 +jxmlease>=1.0.1 +lxml>3.2.4 +yaml>=3.08 \ No newline at end of file diff --git a/roles/juniper.junos/setup.py b/roles/juniper.junos/setup.py new file mode 100755 index 0000000..62585c2 --- /dev/null +++ b/roles/juniper.junos/setup.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +from setuptools import setup +from version import VERSION + +setup( + name="ansible-junos-stdlib", + version=VERSION, + 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'], + 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.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' + ], +) diff --git a/roles/juniper.junos/tests/ansible.cfg b/roles/juniper.junos/tests/ansible.cfg new file mode 100644 index 0000000..7435948 --- /dev/null +++ b/roles/juniper.junos/tests/ansible.cfg @@ -0,0 +1,4 @@ + +[defaults] +hash_behaviour=merge +roles_path = /etc/ansible/roles diff --git a/roles/juniper.junos/tests/junos_jsnapy/add_loopback.set b/roles/juniper.junos/tests/junos_jsnapy/add_loopback.set new file mode 100644 index 0000000..6019602 --- /dev/null +++ b/roles/juniper.junos/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/roles/juniper.junos/tests/junos_jsnapy/delete_loopback.set b/roles/juniper.junos/tests/junos_jsnapy/delete_loopback.set new file mode 100644 index 0000000..818ff20 --- /dev/null +++ b/roles/juniper.junos/tests/junos_jsnapy/delete_loopback.set @@ -0,0 +1,2 @@ +delete routing-instances LO10 +delete interfaces lo0 unit 10 diff --git a/roles/juniper.junos/tests/junos_jsnapy/test_junos_storage.yml b/roles/juniper.junos/tests/junos_jsnapy/test_junos_storage.yml new file mode 100644 index 0000000..3473c27 --- /dev/null +++ b/roles/juniper.junos/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)='/'] + 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/roles/juniper.junos/tests/junos_jsnapy/test_loopback.yml b/roles/juniper.junos/tests/junos_jsnapy/test_loopback.yml new file mode 100644 index 0000000..cd27467 --- /dev/null +++ b/roles/juniper.junos/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/roles/juniper.junos/tests/junos_jsnapy/test_version.yml b/roles/juniper.junos/tests/junos_jsnapy/test_version.yml new file mode 100644 index 0000000..ed2571c --- /dev/null +++ b/roles/juniper.junos/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/roles/juniper.junos/tests/pb.junos_get_facts.yaml b/roles/juniper.junos/tests/pb.junos_get_facts.yaml new file mode 100644 index 0000000..a94f6d9 --- /dev/null +++ b/roles/juniper.junos/tests/pb.junos_get_facts.yaml @@ -0,0 +1,26 @@ +--- +- name: Test junos_get_facts module + hosts: all + connection: local + gather_facts: no + roles: + - Juniper.junos + tasks: + - name: "TEST 1 - Gather Facts" + junos_get_facts: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + ignore_errors: True + register: test1 + + # - debug: var=test1 + + - name: Check TEST 1 + assert: + that: + - test1.facts.hostname + - test1.facts.serialnumber + - test1.facts.model + - test1.facts.fqdn diff --git a/roles/juniper.junos/tests/pb.junos_jsnapy.yaml b/roles/juniper.junos/tests/pb.junos_jsnapy.yaml new file mode 100644 index 0000000..874ccd8 --- /dev/null +++ b/roles/juniper.junos/tests/pb.junos_jsnapy.yaml @@ -0,0 +1,217 @@ +--- +- name: Test junos_ping module + hosts: all + connection: local + gather_facts: no + roles: + - Juniper.junos + tasks: +################################################## +#### TEST 1 ## +################################################## + - name: "TEST 1 - Execute SNAPCHECK with 1 test file / no dir" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: junos_jsnapy/test_junos_storage.yml + action: snapcheck + register: test1 + ignore_errors: True + tags: [ test1 ] + # - debug: var=test1 + + - name: Check TEST 1 + assert: + that: + - test1|succeeded + - test1.passPercentage == 100 + - test1.total_tests == 1 + tags: [ test1 ] + +################################################## +#### TEST 2 ## +################################################## + - name: "TEST 2 - Execute SNAPCHECK with 2 test file & dir" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: + - test_junos_storage.yml + - test_version.yml + dir: junos_jsnapy + action: snapcheck + register: test2 + ignore_errors: True + tags: [ test2 ] + # - debug: var=test2 + + - name: Check TEST 2 + assert: + that: + - test2|succeeded + - test2.passPercentage == 100 + - test2.total_tests == 2 + tags: [ test2 ] + +################################################## +#### TEST 3 ## +################################################## + - name: "TEST 3 - Wrong test file" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: file_that_doesnt_exist.yml + action: snapcheck + register: test3 + ignore_errors: True + tags: [ test3 ] + # - debug: var=test3 + + - name: Check TEST 3 + assert: + that: + - test3|failed + tags: [ test3 ] + +################################################## +#### TEST 4 ## +################################################## + - name: "TEST 4 - SNAP_PRE" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: junos_jsnapy/test_loopback.yml + action: snap_pre + register: test4 + ignore_errors: True + tags: [ test4 ] + + # - debug: var=test4 + + - name: Check TEST 4 + assert: + that: + - test4|succeeded + tags: [ test4 ] + +################################################## +#### TEST 5 ## +################################################## + - name: "TEST 5 - SNAP_POST" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: junos_jsnapy/test_loopback.yml + action: snap_post + register: test5 + ignore_errors: True + tags: [ test5 ] + + # - debug: var=test5 + + - name: Check TEST 5 + assert: + that: + - test5|succeeded + tags: [ test5 ] + +################################################## +#### TEST 6 ## +################################################## + - name: "TEST 6 - CHECK" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: junos_jsnapy/test_loopback.yml + action: check + register: test6 + ignore_errors: True + tags: [ test6 ] + + - debug: var=test6 + + - name: Check TEST 6 + assert: + that: + - test6|succeeded + - test6.passPercentage == 100 + tags: [ test6 ] + +################################################## +#### TEST 7 ## +################################################## + - name: "PRE-TEST 7 - Add loopback address" + junos_install_config: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + file: junos_jsnapy/add_loopback.set + overwrite: no + register: test7_1 + ignore_errors: True + tags: [ test7 ] + + - name: Wait for loopback to come up + pause: seconds=15 + + - name: "TEST 7 - SNAP_POST with additional loopback" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: junos_jsnapy/test_loopback.yml + action: snap_post + register: test7_2 + ignore_errors: True + tags: [ test7 ] + # - debug: var=pretest7 + + - name: "TEST 7 - CHECK" + junos_jsnapy: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + test_files: junos_jsnapy/test_loopback.yml + action: check + register: test7 + ignore_errors: True + tags: [ test7 ] + - debug: var=test7 + + - name: "TEST 7 - Cleanup" + junos_install_config: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + file: junos_jsnapy/delete_loopback.set + overwrite: no + register: test7_3 + ignore_errors: True + tags: [ test7 ] + + - name: Check TEST 7 + assert: + that: + - test7_1|succeeded + - test7_2|succeeded + - test7_3|succeeded + - test7|succeeded + - test7.passPercentage == 50 + - test7.total_tests == 2 + tags: [ test7 ] diff --git a/roles/juniper.junos/tests/pb.junos_ping.yaml b/roles/juniper.junos/tests/pb.junos_ping.yaml new file mode 100644 index 0000000..a12e672 --- /dev/null +++ b/roles/juniper.junos/tests/pb.junos_ping.yaml @@ -0,0 +1,102 @@ +--- +- name: Test junos_ping module + hosts: all + connection: local + gather_facts: no + roles: + - Juniper.junos + tasks: + - name: "TEST 1 - Ping Google DNS" + junos_ping: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + dest_ip: 8.8.8.8 + register: test1 + ignore_errors: True +# - debug: var=test1 + + - name: Check TEST 1 + assert: + that: + - test1.packet_loss == '0' + +############ + + - name: "TEST 2 - Ping Wrong IP" + junos_ping: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + dest_ip: 8.8.1.1 + register: test2 + ignore_errors: True +# - debug: var=test2 + + - name: Check TEST 2 + assert: + that: + - test2.packet_loss == '100' +################# + + - name: "TEST 3 - Change nbr packets" + junos_ping: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + dest_ip: 8.8.8.8 + count: 3 + register: test3 + ignore_errors: True +# - debug: var=test3 + + - name: Check TEST 3 + assert: + that: + - test3.packets_sent == '3' + +################# + + - name: "TEST 4 - Ping with DF-bit set" + junos_ping: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + dest_ip: 8.8.8.8 + count: 3 + do_not_fragment: True + size: 64 + register: test4 + ignore_errors: True +# - debug: var=test4 + + - name: Check TEST 4 + assert: + that: + - test4.packets_received == '3' + +################# + + - name: "TEST 5 - Ping with DF-bit set and size that well exceeds jumbo sizes" + junos_ping: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + dest_ip: 8.8.8.8 + count: 3 + do_not_fragment: True + size: 9999 + register: test5 + ignore_errors: True +# - debug: var=test5 + + - name: Check TEST 5 + assert: + that: + - test5.packets_received == '0' + diff --git a/roles/juniper.junos/tests/pb.junos_pmtud.yaml b/roles/juniper.junos/tests/pb.junos_pmtud.yaml new file mode 100644 index 0000000..7b39039 --- /dev/null +++ b/roles/juniper.junos/tests/pb.junos_pmtud.yaml @@ -0,0 +1,23 @@ +--- +- name: Test junos_pmtud module + hosts: all + connection: local + gather_facts: no + roles: + - Juniper.junos + tasks: + - name: "TEST 1 - Check path MTU to Google DNS" + junos_pmtud: + host: "{{ ansible_ssh_host }}" + port: "{{ ansible_ssh_port }}" + user: "{{ ansible_ssh_user }}" + passwd: "{{ ansible_ssh_pass }}" + dest_ip: 8.8.8.8 + register: test1 + ignore_errors: True + - debug: var=test1 + + - name: Check TEST 1 + assert: + that: + - 768 <= test1.inet_mtu <= 1500 diff --git a/roles/juniper.junos/tests/pb.rav.token.app_stop.yaml b/roles/juniper.junos/tests/pb.rav.token.app_stop.yaml new file mode 100644 index 0000000..e1d8fad --- /dev/null +++ b/roles/juniper.junos/tests/pb.rav.token.app_stop.yaml @@ -0,0 +1,34 @@ +--- +- name: Get FQDN for all VM on Ravello + connection: local + hosts: all + gather_facts: no + roles: + - ravello.lib + + tasks: +############################### +## Get VM ID ### +############################### + - name: Get App ID from Ravello + ravello_get_id: + resource_name: "{{ ravello_ci_app_name }}" + resource_type: applications + token: "{{ ravello_ci_token }}" + register: app + run_once: true + + +############################### +## Delete the application ## +############################### + - name: Stop Application on Ravello + uri: + url: "https://cloud.ravellosystems.com/api/v1/applications/{{ app.json.id }}/stop" + method: POST + status_code: 202 + HEADER_Content-Type: 'application/json' + HEADER_Accept: 'application/json' + HEADER_X-Ephemeral-Token-Authorization: "{{ ravello_ci_token }}" + run_once: true + changed_when: true diff --git a/roles/juniper.junos/tests/pb.rav.token.create-deploy.yaml b/roles/juniper.junos/tests/pb.rav.token.create-deploy.yaml new file mode 100644 index 0000000..4f789e3 --- /dev/null +++ b/roles/juniper.junos/tests/pb.rav.token.create-deploy.yaml @@ -0,0 +1,85 @@ +--- +- name: Create Application on Ravello for CI + connection: local + hosts: all + gather_facts: no + roles: + - ravello.lib + vars: + ravello_deploy_topology_cloud: AMAZON + ravello_deploy_topology_region: Oregon + ravello_deploy_topology_optimization: PERFORMANCE_OPTIMIZED + ravello_deploy_topology_start_all: true + + tasks: + - name: Create Application from Blueprint for CI + uri: + url: "https://cloud.ravellosystems.com/api/v1/applications/" + method: POST + status_code: 201 + HEADER_Content-Type: 'application/json' + HEADER_Accept: 'application/json' + HEADER_X-Ephemeral-Token-Authorization: "{{ ravello_ci_token }}" + body: + name: "{{ ravello_ci_app_name }}" + description: "App created by Travis CI" + baseBlueprintId: "{{ ravello_ci_blueprint }}" + body_format: json + run_once: true + changed_when: true + + - name: Get App ID from Ravello + ravello_get_id: + resource_name: "{{ ravello_ci_app_name }}" + resource_type: applications + token: "{{ ravello_ci_token }}" + register: app + run_once: true + + - debug: var=app + run_once: true + +####################################################### +## Deploy Application(s) ## +####################################################### + - name: Deploy Application On Ravello + uri: + url: "https://cloud.ravellosystems.com/api/v1/applications/{{ app.json.id }}/publish" + method: POST + status_code: 202 + HEADER_Content-Type: 'application/json' + HEADER_Accept: 'application/json' + HEADER_X-Ephemeral-Token-Authorization: "{{ ravello_ci_token }}" + body: > + { + "preferredCloud": "{{ ravello_deploy_topology_cloud }}", + "preferredRegion": "{{ ravello_deploy_topology_region }}", + "optimizationLevel": "{{ ravello_deploy_topology_optimization }}", + "startAllVms": "{{ ravello_deploy_topology_start_all }}" + } + body_format: json + run_once: true + +######################################## +## Set application Expiration time ## +######################################## + - name: Set Application Expiration time + uri: + url: "https://cloud.ravellosystems.com/api/v1/applications/{{ app.json.id }}/setExpiration" + method: POST + status_code: 200 + HEADER_Content-Type: 'application/json' + HEADER_Accept: 'application/json' + HEADER_X-Ephemeral-Token-Authorization: "{{ ravello_ci_token }}" + body: '{ "expirationFromNowSeconds": {{ ravello_ci_expiration_time_min * 60 }} }' + body_format: json + run_once: true + + - name: Wait for devices to come up + pause: minutes=5 + + - name: Wait for devices to come up + pause: minutes=5 + + - name: Wait for devices to come up + pause: minutes=2 diff --git a/roles/juniper.junos/tests/pb.rav.token.fqdn_get.yaml b/roles/juniper.junos/tests/pb.rav.token.fqdn_get.yaml new file mode 100644 index 0000000..6304c2c --- /dev/null +++ b/roles/juniper.junos/tests/pb.rav.token.fqdn_get.yaml @@ -0,0 +1,54 @@ +--- +- name: Get FQDN for all VM on Ravello + connection: local + hosts: junos + gather_facts: no + roles: + - ravello.lib + + tasks: + +############################### +## Get VM ID ### +############################### + - name: Get App ID from Ravello + ravello_get_id: + resource_name: "{{ ravello_ci_app_name }}" + resource_type: applications + token: "{{ ravello_ci_token }}" + register: app + run_once: true + + # - debug: var=app + + - name: Get VM ID from Ravello + ravello_get_id: + application_id: "{{ app.json.id }}" + resource_type: vms + resource_name: "{{ inventory_hostname }}" + token: "{{ ravello_ci_token }}" + failed_if_not_found: true + register: vm + + # - debug: var=vm + + - name: Get VM public FQDN + uri: + url: "https://cloud.ravellosystems.com/api/v1/applications/{{ app.json.id }}/vms/{{ vm.json.id }}/fqdn;deployment" + method: GET + status_code: 200 + HEADER_Content-Type: 'application/json' + HEADER_Accept: 'application/json' + HEADER_X-Ephemeral-Token-Authorization: "{{ ravello_ci_token }}" + register: ravello_public_ip + + - name: Delete previous file + file: + path: "host_vars/{{ inventory_hostname}}/fqdn.yaml" + state: absent + + - name: Populate ansible_ssh_host Variable based on FQDN + lineinfile: + create: yes + dest: "host_vars/{{ inventory_hostname}}/fqdn.yaml" + line: "ansible_ssh_host: {{ ravello_public_ip.json.value }}" diff --git a/roles/juniper.junos/tests/ravello.ini b/roles/juniper.junos/tests/ravello.ini new file mode 100644 index 0000000..051bbef --- /dev/null +++ b/roles/juniper.junos/tests/ravello.ini @@ -0,0 +1,18 @@ +[all:children] +junos + +[junos] +vqfx-01 +vqfx-02 + +################################### +### Define variables per groups ### +################################### +[all:vars] +ansible_ssh_user=root +ansible_ssh_pass=Juniper +ansible_ssh_port=22 +ravello_ci_app_name="Ansible-junos-stdlib Ansible_{{ lookup('env','ANSIBLE_VERSION') }} Travis_{{ lookup('env','TRAVIS_JOB_ID') }} {{ lookup('env','TRAVIS_COMMIT') }}" +ravello_ci_blueprint="75695295" +ravello_ci_token="GtHFbCOuKgD1pcfkvCCIgenj6DOtn3VgRLjaYipdideCsiPC1NxJitt1UHfhF0Bf" +ravello_ci_expiration_time_min=50 diff --git a/roles/juniper.junos/tools/sw_upgrade b/roles/juniper.junos/tools/sw_upgrade new file mode 100755 index 0000000..ec5cb8b --- /dev/null +++ b/roles/juniper.junos/tools/sw_upgrade @@ -0,0 +1,129 @@ +#!/usr/bin/env python2.7 + +import argparse +import os, sys, re +import logging +from getpass import getpass +from jnpr.junos import Device + +JUNOSDIR = '/usr/local/junos' +PACKAGEDIR = JUNOSDIR + '/packages' +LOGDIR = JUNOSDIR + '/log' + + +def die(message, errno=1): + sys.stderr.write("ERROR:{0}\n".format(message)) + sys.exit(errno) + +# ------------------------------------------------------------------------- +# CLI args processing +# ------------------------------------------------------------------------- + + +def cli_args(): + p = argparse.ArgumentParser(add_help=True) + + # ------------------------------------------------------------------------- + # login + # ------------------------------------------------------------------------- + + p.add_argument('hostname', nargs='?', + help='hostname or ipaddr') + + p.add_argument('-u', '--user', default=os.getenv('USER'), + help='login user name, defaults to $USER') + + p.add_argument('-P', '--passwd', default='', + help='login user password, defaults assumes ssh-keys') + + p.add_argument('-k', action='store_true', default=False, + dest='passwd_prompt', + help='prompt for user password') + + # ------------------------------------------------------------------------- + # softawre + # ------------------------------------------------------------------------- + + p.add_argument('-v', '--version', + help="Junos version string for checking device facts") + + p.add_argument('-p', '--package', + help='Junos package file') + + # ------------------------------------------------------------------------- + # modes/flags/etc. + # ------------------------------------------------------------------------- + + p.add_argument('--dry-run', dest='dry_run_mode', action='store_true', + help='Check for need to upgrade, but do not do it') + + args = p.parse_args() + if args.passwd_prompt is True: + args.passwd = getpass() + + if args.version is None: + # extract from package file + m = re.search('-([^\\-]*)-domestic.*', args.package) + args.version = m.group(1) + + if args.version is None: + die("No version-string") + + return args + +# ------------------------------------------------------------------------- +# software upgrade process +# ------------------------------------------------------------------------- + + +def update_my_progress(dev, report): + logging.info(report) + + +def do_sw_upgrade(dev): + from jnpr.junos.utils.sw import SW + sw = SW(dev) + + logfile = LOGDIR + '/' + args.hostname + '.log' + + logging.basicConfig(filename=logfile, level=logging.INFO, + format='%(asctime)s:%(name)s:%(message)s') + logging.getLogger().name = args.hostname + + print "logging to file: {0}".format(logfile) + + logging.info(" Starting the software upgrade process: %s", args.package) + ok = sw.install(args.package, progress=update_my_progress) + if ok is not True: + die("Unable to install software") + logging.info("") + rsp = sw.reboot() + +# ------------------------------------------------------------------------- +# MAIN +# ------------------------------------------------------------------------- + +args = cli_args() +dev = Device(args.hostname, user=args.user, password=args.passwd) +try: + print "{0}@{1} connecting ...".format(args.user, args.hostname) + dev.open() +except: + die("Unable to connect to device: {0}".format(args.hostname)) + +has_ver = dev.facts['version'] +should_ver = args.version +need_upgrade = bool(has_ver != should_ver) +y_n = ('no', 'yes')[need_upgrade].upper() +print "UPGRADE={0}::HAS:{1} == SHOULD:{2}".format(y_n, has_ver, should_ver) + +if args.dry_run_mode is True: + dev.close() + sys.exit(0) + +if need_upgrade is False: + dev.close() + sys.exit(0) + +do_sw_upgrade(dev) +dev.close() diff --git a/roles/juniper.junos/version.py b/roles/juniper.junos/version.py new file mode 100755 index 0000000..13f6726 --- /dev/null +++ b/roles/juniper.junos/version.py @@ -0,0 +1,2 @@ +VERSION = "2.1.0" +DATE = "2018-June-1" diff --git a/rpf.yml b/rpf.yml new file mode 100644 index 0000000..dcda1b4 --- /dev/null +++ b/rpf.yml @@ -0,0 +1,22 @@ +--- +- name: Get rpf info + hosts: srlab + roles: + - juniper.junos + connection: local + vars: + ansible_python_interpreter: "{{ playbook_dir }}/venv/bin/python" + gather_facts: no + + tasks: + - name: rpf + juniper_junos_table: + file: "ConfigTables.yml" + path: "{{ playbook_dir }}/myTables" + table: "InterfaceTable" + host: "{{ ansible_host }}" + register: result + + - name: Print result + debug: + var: result diff --git a/single-deploy.yml b/single-deploy.yml new file mode 100644 index 0000000..74fd400 --- /dev/null +++ b/single-deploy.yml @@ -0,0 +1,24 @@ +--- +- name: Deploy an ova + hosts: localhost + tags: deploy_ova + gather_facts: no + vars_files: group_vars/vmware + tasks: + - name: Deploy the vcp ova + vmware_deploy_ovf: + hostname: "{{ vcenter_hostname }}" + username: "{{ vcenter_username }}" + password: "{{ vcenter_password }}" + datacenter: "NS Lab" + validate_certs: False + allow_duplicates: no + name: "test-vcp" + datastore: "host 1 - datastore 2" + ova: files/ova/vcp_17.4R2.4.ova + disk_provisioning: thin + power_on: no + networks: + br-ext: NS-DEV-NAT + br-int: DUMMY + delegate_to: localhost diff --git a/templates/core-to-nodes.j2 b/templates/core-to-nodes.j2 index 52ba18f..fd080c1 100644 --- a/templates/core-to-nodes.j2 +++ b/templates/core-to-nodes.j2 @@ -18,9 +18,13 @@ nodes: {{ node.name }}: mgmt: {{ node.mgmt }} rid: {{ node.rid }} + rid6: {{ node.rid6 }} {% if node.sid is defined %} sid: {{ node.sid }} {% endif %} +{% if node.sid6 is defined %} + sid6: {{ node.sid6 }} +{% endif %} {% if node.iso is defined %} iso: {{ node.iso }} {% endif %} diff --git a/templates/junos.j2 b/templates/junos.j2 index 07a1cba..92dfed1 100644 --- a/templates/junos.j2 +++ b/templates/junos.j2 @@ -23,7 +23,9 @@ system { } } services { - ssh; + ssh { + root-login allow; + } netconf { ssh; traceoptions { @@ -45,15 +47,16 @@ system { } } } -{% if node.lags %} chassis { + network-services enhanced-ip; +{% if node.lags %} aggregated-devices { ethernet { device-count 10; } } -} {% endif %} +} {# ************************************************** ### Interfaces configuration ###### @@ -71,6 +74,9 @@ interfaces { family inet { address {{intf_attr.ip}}/{{intf_attr.mask|default('24')}} } +{% if node.rid6 is defined %} + family inet6; +{% endif %} {# if intf_attr.mpls is defined #} family mpls; {# endif #} @@ -81,6 +87,9 @@ interfaces { family inet { address {{intf_attr.ip}}/{{intf_attr.mask|default('24')}} } +{% if node.rid6 is defined %} + family inet6; +{% endif %} {% if intf_attr.mpls is defined %} family mpls; {% endif %} @@ -114,6 +123,11 @@ interfaces { family inet { address {{node.rid}}/32; } +{% if node.rid6 is defined %} + family inet6 { + address {{node.rid6}}/128; + } +{% endif %} {% if node.iso is defined %} family iso { address {{node.iso}}; @@ -162,6 +176,11 @@ protocols { unicast; } {% endif %} +{% if af == 'inet6' %} + family inet6 { + unicast; + } +{% endif %} {% if af == 'inet-vpn' %} family inet-vpn { unicast; @@ -209,10 +228,15 @@ protocols { #} {% if node.iso is defined %} isis { - no-ipv6-routing; source-packet-routing { +{% if node.lags %} + srgb start-label 800000 index-range 100000; +{% endif %} node-segment { ipv4-index {{ node.sid }}; +{% if node.sid6 is defined %} + ipv6-index {{ node.sid6 }}; +{% endif %} } } level 1 disable; diff --git a/templates/sr-tilfa.conf b/templates/sr-tilfa.conf new file mode 100644 index 0000000..bf28e68 --- /dev/null +++ b/templates/sr-tilfa.conf @@ -0,0 +1,19 @@ +groups { + replace: GR-ISIS { + protocols isis interface <*e*> { +# node-link-protection; + level 2 post-convergence-lfa node-protection; + } + } +} +protocols { + isis { + apply-groups GR-ISIS; + replace: backup-spf-options { +# remote-backup-calculation; +# node-link-degradation; + use-post-convergence-lfa maximum-backup-paths 2; + use-source-packet-routing; + } + } +} diff --git a/validate.yaml b/validate.yml similarity index 100% rename from validate.yaml rename to validate.yml diff --git a/vars_files/core-model-ksr.yml b/vars_files/core-model-ksr.yml new file mode 100644 index 0000000..8398aa3 --- /dev/null +++ b/vars_files/core-model-ksr.yml @@ -0,0 +1,158 @@ +--- + +common: + bgp_asn: 65000 + +nodes: + - name: srlab-vmx1 + mgmt: 10.39.0.101 + rid: 192.168.0.1 + rid6: fec0:0:0:1111::1 + sid: 401 + sid6: 601 + iso: 49.0001.0010.0100.1001.00 + + - name: srlab-vmx2 + mgmt: 10.39.0.102 + rid: 192.168.0.2 + rid6: fec0:0:0:1111::2 + sid: 402 + sid6: 602 + iso: 49.0001.0010.0100.1002.00 + + - name: srlab-vmx3 + mgmt: 10.39.0.103 + rid: 192.168.0.3 + rid6: fec0:0:0:1111::3 + sid: 403 + sid6: 603 + iso: 49.0001.0010.0100.1003.00 + + - name: srlab-vmx4 + mgmt: 10.39.0.104 + rid: 192.168.0.4 + rid6: fec0:0:0:1111::4 + sid: 404 + sid6: 604 + iso: 49.0001.0010.0100.1004.00 + + - name: srlab-vmx5 + mgmt: 10.39.0.105 + rid: 192.168.0.5 + rid6: fec0:0:0:1111::5 + sid: 405 + sid6: 605 + iso: 49.0001.0010.0100.1005.00 + + - name: srlab-vmx6 + mgmt: 10.39.0.106 + rid: 192.168.0.6 + rid6: fec0:0:0:1111::6 + sid: 406 + sid6: 606 + iso: 49.0001.0010.0100.1006.00 + + - name: srlab-vmx7 + mgmt: 10.39.0.107 + rid: 192.168.0.7 + rid6: fec0:0:0:1111::7 + sid: 407 + sid6: 607 + iso: 49.0001.0010.0100.1007.00 + + - name: srlab-vmx8 + mgmt: 10.39.0.108 + rid: 192.168.0.8 + rid6: fec0:0:0:1111::8 + sid: 408 + sid6: 608 + iso: 49.0001.0010.0100.1008.00 + + - name: srlab-vmx9 + mgmt: 10.39.0.109 + rid: 192.168.0.9 + rid6: fec0:0:0:1111::9 + sid: 409 + sid6: 609 + iso: 49.0001.0010.0100.1009.00 + +lags: + - node: srlab-vmx5 + intf: ae0 + ports: [ge-0/0/2, ge-0/0/3] + - node: srlab-vmx7 + intf: ae0 + ports: [ge-0/0/2, ge-0/0/3] + +# using the following for the lag ports. can't extract correct information +# from the links ae interfaces for lags +switches: + - {left: srlab-vmx5, left_port: ge-0/0/2, + right: srlab-vmx7, right_port: ge-0/0/2 } + - {left: srlab-vmx5, left_port: ge-0/0/3, + right: srlab-vmx7, right_port: ge-0/0/3 } + +links: + - {left: srlab-vmx1, left_port: ge-0/0/0, left_ip: 10.0.0.0, + right: srlab-vmx2, right_port: ge-0/0/0, right_ip: 10.0.0.1, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx1, left_port: ge-0/0/1, left_ip: 10.0.0.2, + right: srlab-vmx4, right_port: ge-0/0/1, right_ip: 10.0.0.3, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx1, left_port: ge-0/0/2, left_ip: 10.0.0.22, + right: srlab-vmx3, right_port: ge-0/0/0, right_ip: 10.0.0.23, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx2, left_port: ge-0/0/1, left_ip: 10.0.0.4, + right: srlab-vmx5, right_port: ge-0/0/1, right_ip: 10.0.0.5, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx3, left_port: ge-0/0/1, left_ip: 10.0.0.24, + right: srlab-vmx6, right_port: ge-0/0/4, right_ip: 10.0.0.25, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/0, left_ip: 10.0.0.6, + right: srlab-vmx5, right_port: ge-0/0/0, right_ip: 10.0.0.7, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/2, left_ip: 10.0.0.8, + right: srlab-vmx6, right_port: ge-0/0/2, right_ip: 10.0.0.9, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/3, left_ip: 10.0.0.10, + right: srlab-vmx6, right_port: ge-0/0/3, right_ip: 10.0.0.11, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx5, left_port: ae0, left_ip: 10.0.0.12, + right: srlab-vmx7, right_port: ae0, right_ip: 10.0.0.13, + mask: 31, cost: 500, mpls: True, iso: True } + + - {left: srlab-vmx6, left_port: ge-0/0/0, left_ip: 10.0.0.14, + right: srlab-vmx7, right_port: ge-0/0/0, right_ip: 10.0.0.15, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx6, left_port: ge-0/0/1, left_ip: 10.0.0.16, + right: srlab-vmx8, right_port: ge-0/0/1, right_ip: 10.0.0.17, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx7, left_port: ge-0/0/1, left_ip: 10.0.0.18, + right: srlab-vmx9, right_port: ge-0/0/1, right_ip: 10.0.0.19, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx8, left_port: ge-0/0/0, left_ip: 10.0.0.20, + right: srlab-vmx9, right_port: ge-0/0/0, right_ip: 10.0.0.21, + mask: 31, cost: 1000, mpls: True, iso: True } + + +bgp: + rr: [192.168.0.4, 192.168.0.7] + clients: [192.168.0.1, 192.168.0.2, 192.168.0.8, 192.168.0.9] + af: + - inet + - inet6 + - inet-vpn + +#isis: +# network: p2p diff --git a/vars_files/core-model-sr-tilfa-pq.yml b/vars_files/core-model-sr-tilfa-pq.yml new file mode 100644 index 0000000..003c007 --- /dev/null +++ b/vars_files/core-model-sr-tilfa-pq.yml @@ -0,0 +1,158 @@ +--- + +common: + bgp_asn: 65000 + +nodes: + - name: srlab-vmx1 + mgmt: 10.39.0.101 + rid: 192.168.0.1 + rid6: fec0:0:0:1111::1 + sid: 401 + sid6: 601 + iso: 49.0001.0010.0100.1001.00 + + - name: srlab-vmx2 + mgmt: 10.39.0.102 + rid: 192.168.0.2 + rid6: fec0:0:0:1111::2 + sid: 402 + sid6: 602 + iso: 49.0001.0010.0100.1002.00 + + - name: srlab-vmx3 + mgmt: 10.39.0.103 + rid: 192.168.0.3 + rid6: fec0:0:0:1111::3 + sid: 403 + sid6: 603 + iso: 49.0001.0010.0100.1003.00 + + - name: srlab-vmx4 + mgmt: 10.39.0.104 + rid: 192.168.0.4 + rid6: fec0:0:0:1111::4 + sid: 404 + sid6: 604 + iso: 49.0001.0010.0100.1004.00 + + - name: srlab-vmx5 + mgmt: 10.39.0.105 + rid: 192.168.0.5 + rid6: fec0:0:0:1111::5 + sid: 405 + sid6: 605 + iso: 49.0001.0010.0100.1005.00 + + - name: srlab-vmx6 + mgmt: 10.39.0.106 + rid: 192.168.0.6 + rid6: fec0:0:0:1111::6 + sid: 406 + sid6: 606 + iso: 49.0001.0010.0100.1006.00 + + - name: srlab-vmx7 + mgmt: 10.39.0.107 + rid: 192.168.0.7 + rid6: fec0:0:0:1111::7 + sid: 407 + sid6: 607 + iso: 49.0001.0010.0100.1007.00 + + - name: srlab-vmx8 + mgmt: 10.39.0.108 + rid: 192.168.0.8 + rid6: fec0:0:0:1111::8 + sid: 408 + sid6: 608 + iso: 49.0001.0010.0100.1008.00 + + - name: srlab-vmx9 + mgmt: 10.39.0.109 + rid: 192.168.0.9 + rid6: fec0:0:0:1111::9 + sid: 409 + sid6: 609 + iso: 49.0001.0010.0100.1009.00 + +lags: + - node: srlab-vmx5 + intf: ae0 + ports: [ge-0/0/2, ge-0/0/3] + - node: srlab-vmx7 + intf: ae0 + ports: [ge-0/0/2, ge-0/0/3] + +# using the following for the lag ports. can't extract correct information +# from the links ae interfaces for lags +switches: + - {left: srlab-vmx5, left_port: ge-0/0/2, + right: srlab-vmx7, right_port: ge-0/0/2 } + - {left: srlab-vmx5, left_port: ge-0/0/3, + right: srlab-vmx7, right_port: ge-0/0/3 } + +links: + - {left: srlab-vmx1, left_port: ge-0/0/0, left_ip: 10.0.0.0, + right: srlab-vmx2, right_port: ge-0/0/0, right_ip: 10.0.0.1, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx1, left_port: ge-0/0/1, left_ip: 10.0.0.2, + right: srlab-vmx4, right_port: ge-0/0/1, right_ip: 10.0.0.3, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx1, left_port: ge-0/0/2, left_ip: 10.0.0.22, + right: srlab-vmx3, right_port: ge-0/0/0, right_ip: 10.0.0.23, + mask: 31, cost: 4000, mpls: True, iso: True } + + - {left: srlab-vmx2, left_port: ge-0/0/1, left_ip: 10.0.0.4, + right: srlab-vmx5, right_port: ge-0/0/1, right_ip: 10.0.0.5, + mask: 31, cost: 2000, mpls: True, iso: True } + + - {left: srlab-vmx3, left_port: ge-0/0/1, left_ip: 10.0.0.24, + right: srlab-vmx6, right_port: ge-0/0/4, right_ip: 10.0.0.25, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/0, left_ip: 10.0.0.6, + right: srlab-vmx5, right_port: ge-0/0/0, right_ip: 10.0.0.7, + mask: 31, cost: 2000, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/2, left_ip: 10.0.0.8, + right: srlab-vmx6, right_port: ge-0/0/2, right_ip: 10.0.0.9, + mask: 31, cost: 1100, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/3, left_ip: 10.0.0.10, + right: srlab-vmx6, right_port: ge-0/0/3, right_ip: 10.0.0.11, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx5, left_port: ae0, left_ip: 10.0.0.12, + right: srlab-vmx7, right_port: ae0, right_ip: 10.0.0.13, + mask: 31, cost: 4000, mpls: True, iso: True } + + - {left: srlab-vmx6, left_port: ge-0/0/0, left_ip: 10.0.0.14, + right: srlab-vmx7, right_port: ge-0/0/0, right_ip: 10.0.0.15, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx6, left_port: ge-0/0/1, left_ip: 10.0.0.16, + right: srlab-vmx8, right_port: ge-0/0/1, right_ip: 10.0.0.17, + mask: 31, cost: 1, mpls: True, iso: True } + + - {left: srlab-vmx7, left_port: ge-0/0/1, left_ip: 10.0.0.18, + right: srlab-vmx9, right_port: ge-0/0/1, right_ip: 10.0.0.19, + mask: 31, cost: 1, mpls: True, iso: True } + + - {left: srlab-vmx8, left_port: ge-0/0/0, left_ip: 10.0.0.20, + right: srlab-vmx9, right_port: ge-0/0/0, right_ip: 10.0.0.21, + mask: 31, cost: 1, mpls: True, iso: True } + + +bgp: + rr: [192.168.0.4, 192.168.0.7] + clients: [192.168.0.1, 192.168.0.2, 192.168.0.8, 192.168.0.9] + af: + - inet + - inet6 + - inet-vpn + +#isis: +# network: p2p diff --git a/vars_files/core-model-sr-tilfa.yml b/vars_files/core-model-sr-tilfa.yml new file mode 100644 index 0000000..8bd0a6f --- /dev/null +++ b/vars_files/core-model-sr-tilfa.yml @@ -0,0 +1,158 @@ +--- + +common: + bgp_asn: 65000 + +nodes: + - name: srlab-vmx1 + mgmt: 10.39.0.101 + rid: 192.168.0.1 + rid6: fec0:0:0:1111::1 + sid: 401 + sid6: 601 + iso: 49.0001.0010.0100.1001.00 + + - name: srlab-vmx2 + mgmt: 10.39.0.102 + rid: 192.168.0.2 + rid6: fec0:0:0:1111::2 + sid: 402 + sid6: 602 + iso: 49.0001.0010.0100.1002.00 + + - name: srlab-vmx3 + mgmt: 10.39.0.103 + rid: 192.168.0.3 + rid6: fec0:0:0:1111::3 + sid: 403 + sid6: 603 + iso: 49.0001.0010.0100.1003.00 + + - name: srlab-vmx4 + mgmt: 10.39.0.104 + rid: 192.168.0.4 + rid6: fec0:0:0:1111::4 + sid: 404 + sid6: 604 + iso: 49.0001.0010.0100.1004.00 + + - name: srlab-vmx5 + mgmt: 10.39.0.105 + rid: 192.168.0.5 + rid6: fec0:0:0:1111::5 + sid: 405 + sid6: 605 + iso: 49.0001.0010.0100.1005.00 + + - name: srlab-vmx6 + mgmt: 10.39.0.106 + rid: 192.168.0.6 + rid6: fec0:0:0:1111::6 + sid: 406 + sid6: 606 + iso: 49.0001.0010.0100.1006.00 + + - name: srlab-vmx7 + mgmt: 10.39.0.107 + rid: 192.168.0.7 + rid6: fec0:0:0:1111::7 + sid: 407 + sid6: 607 + iso: 49.0001.0010.0100.1007.00 + + - name: srlab-vmx8 + mgmt: 10.39.0.108 + rid: 192.168.0.8 + rid6: fec0:0:0:1111::8 + sid: 408 + sid6: 608 + iso: 49.0001.0010.0100.1008.00 + + - name: srlab-vmx9 + mgmt: 10.39.0.109 + rid: 192.168.0.9 + rid6: fec0:0:0:1111::9 + sid: 409 + sid6: 609 + iso: 49.0001.0010.0100.1009.00 + +lags: + - node: srlab-vmx5 + intf: ae0 + ports: [ge-0/0/2, ge-0/0/3] + - node: srlab-vmx7 + intf: ae0 + ports: [ge-0/0/2, ge-0/0/3] + +# using the following for the lag ports. can't extract correct information +# from the links ae interfaces for lags +switches: + - {left: srlab-vmx5, left_port: ge-0/0/2, + right: srlab-vmx7, right_port: ge-0/0/2 } + - {left: srlab-vmx5, left_port: ge-0/0/3, + right: srlab-vmx7, right_port: ge-0/0/3 } + +links: + - {left: srlab-vmx1, left_port: ge-0/0/0, left_ip: 10.0.0.0, + right: srlab-vmx2, right_port: ge-0/0/0, right_ip: 10.0.0.1, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx1, left_port: ge-0/0/1, left_ip: 10.0.0.2, + right: srlab-vmx4, right_port: ge-0/0/1, right_ip: 10.0.0.3, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx1, left_port: ge-0/0/2, left_ip: 10.0.0.22, + right: srlab-vmx3, right_port: ge-0/0/0, right_ip: 10.0.0.23, + mask: 31, cost: 2000, mpls: True, iso: True } + + - {left: srlab-vmx2, left_port: ge-0/0/1, left_ip: 10.0.0.4, + right: srlab-vmx5, right_port: ge-0/0/1, right_ip: 10.0.0.5, + mask: 31, cost: 2000, mpls: True, iso: True } + + - {left: srlab-vmx3, left_port: ge-0/0/1, left_ip: 10.0.0.24, + right: srlab-vmx6, right_port: ge-0/0/4, right_ip: 10.0.0.25, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/0, left_ip: 10.0.0.6, + right: srlab-vmx5, right_port: ge-0/0/0, right_ip: 10.0.0.7, + mask: 31, cost: 2000, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/2, left_ip: 10.0.0.8, + right: srlab-vmx6, right_port: ge-0/0/2, right_ip: 10.0.0.9, + mask: 31, cost: 1100, mpls: True, iso: True } + + - {left: srlab-vmx4, left_port: ge-0/0/3, left_ip: 10.0.0.10, + right: srlab-vmx6, right_port: ge-0/0/3, right_ip: 10.0.0.11, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx5, left_port: ae0, left_ip: 10.0.0.12, + right: srlab-vmx7, right_port: ae0, right_ip: 10.0.0.13, + mask: 31, cost: 500, mpls: True, iso: True } + + - {left: srlab-vmx6, left_port: ge-0/0/0, left_ip: 10.0.0.14, + right: srlab-vmx7, right_port: ge-0/0/0, right_ip: 10.0.0.15, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx6, left_port: ge-0/0/1, left_ip: 10.0.0.16, + right: srlab-vmx8, right_port: ge-0/0/1, right_ip: 10.0.0.17, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx7, left_port: ge-0/0/1, left_ip: 10.0.0.18, + right: srlab-vmx9, right_port: ge-0/0/1, right_ip: 10.0.0.19, + mask: 31, cost: 1000, mpls: True, iso: True } + + - {left: srlab-vmx8, left_port: ge-0/0/0, left_ip: 10.0.0.20, + right: srlab-vmx9, right_port: ge-0/0/0, right_ip: 10.0.0.21, + mask: 31, cost: 1000, mpls: True, iso: True } + + +bgp: + rr: [192.168.0.4, 192.168.0.7] + clients: [192.168.0.1, 192.168.0.2, 192.168.0.8, 192.168.0.9] + af: + - inet + - inet6 + - inet-vpn + +#isis: +# network: p2p diff --git a/vars_files/core-model-sr.yml b/vars_files/core-model-sr.yml index e76f1d1..8398aa3 100644 --- a/vars_files/core-model-sr.yml +++ b/vars_files/core-model-sr.yml @@ -7,55 +7,73 @@ nodes: - name: srlab-vmx1 mgmt: 10.39.0.101 rid: 192.168.0.1 - sid: 10 + rid6: fec0:0:0:1111::1 + sid: 401 + sid6: 601 iso: 49.0001.0010.0100.1001.00 - name: srlab-vmx2 mgmt: 10.39.0.102 rid: 192.168.0.2 - sid: 20 + rid6: fec0:0:0:1111::2 + sid: 402 + sid6: 602 iso: 49.0001.0010.0100.1002.00 - name: srlab-vmx3 mgmt: 10.39.0.103 rid: 192.168.0.3 - sid: 30 + rid6: fec0:0:0:1111::3 + sid: 403 + sid6: 603 iso: 49.0001.0010.0100.1003.00 - name: srlab-vmx4 mgmt: 10.39.0.104 rid: 192.168.0.4 - sid: 40 + rid6: fec0:0:0:1111::4 + sid: 404 + sid6: 604 iso: 49.0001.0010.0100.1004.00 - name: srlab-vmx5 mgmt: 10.39.0.105 rid: 192.168.0.5 - sid: 50 + rid6: fec0:0:0:1111::5 + sid: 405 + sid6: 605 iso: 49.0001.0010.0100.1005.00 - name: srlab-vmx6 mgmt: 10.39.0.106 rid: 192.168.0.6 - sid: 60 + rid6: fec0:0:0:1111::6 + sid: 406 + sid6: 606 iso: 49.0001.0010.0100.1006.00 - name: srlab-vmx7 mgmt: 10.39.0.107 rid: 192.168.0.7 - sid: 70 + rid6: fec0:0:0:1111::7 + sid: 407 + sid6: 607 iso: 49.0001.0010.0100.1007.00 - name: srlab-vmx8 mgmt: 10.39.0.108 rid: 192.168.0.8 - sid: 80 + rid6: fec0:0:0:1111::8 + sid: 408 + sid6: 608 iso: 49.0001.0010.0100.1008.00 - name: srlab-vmx9 mgmt: 10.39.0.109 rid: 192.168.0.9 - sid: 90 + rid6: fec0:0:0:1111::9 + sid: 409 + sid6: 609 iso: 49.0001.0010.0100.1009.00 lags: @@ -133,6 +151,7 @@ bgp: clients: [192.168.0.1, 192.168.0.2, 192.168.0.8, 192.168.0.9] af: - inet + - inet6 - inet-vpn #isis: diff --git a/vmware_clone_template.yaml b/vmware-clone-template.yml similarity index 100% rename from vmware_clone_template.yaml rename to vmware-clone-template.yml