diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9b8c974 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +## v1.2.1 (2023-12-13) + +### BUG FIXING + - added CHANGELOG.md + +## v1.2.0 (2023-12-13) + +### FEATURE ENHANCEMENT + - added PATty functionality \ No newline at end of file diff --git a/README.md b/README.md index 473866e..cbbaa08 100644 --- a/README.md +++ b/README.md @@ -32,38 +32,98 @@ Cisco Modeling Labs is an on-premise network simulation tool that runs on workst ## Inventory -To use the dynamic inventory plugin, the environmental variables must be set and a file (e.g. `cml.yml`) placed in the inventory specifying the plugin information: - -``` -plugin: cisco.cml.cml_inventory -``` - The dynamic inventory script will then return information about the nodes in the lab: ``` -ok: [hq-host1] => { +ok: [hq-rtr1] => { "cml_facts": { - "config": "#cloud-config\npassword: admin\nchpasswd: { expire: False }\nssh_pwauth: True\nhostname: hq-host1\nruncmd:\n - sudo ip address add 10.0.1.10/24 dev enp0s2\n - sudo ip link set dev enp0s2 up\n - sudo ip route add default via 10.0.1.1\n", - "cpus": null, + "config": "hostname hq-rtr1\nvrf definition Mgmt-intf\n!\naddress-family ipv4\nexit-address-family\n!\naddress-family ipv6\nexit-address-family\n!\nusername admin privilege 15 secret 0 admin\ncdp run\nno aaa new-model\nip domain-name mdd.cisco.com\n!\ninterface GigabitEthernet1\nvrf forwarding Mgmt-intf\nip address dhcp\nnegotiation auto\nno cdp enable\nno shutdown\n!\ninterface GigabitEthernet2\ncdp enable\n!\ninterface GigabitEthernet3\ncdp enable\n!\ninterface GigabitEthernet4\ncdp enable\n!\nip http server\nip http secure-server\nip http max-connections 2\n!\nip ssh time-out 60\nip ssh version 2\nip ssh server algorithm encryption aes128-ctr aes192-ctr aes256-ctr\nip ssh client algorithm encryption aes128-ctr aes192-ctr aes256-ctr\n!\nline vty 0 4\nexec-timeout 30 0\nabsolute-timeout 60\nsession-limit 16\nlogin local\ntransport input ssh\n!\nend", + "cpus": 1, "data_volume": null, - "image_definition": "ubuntu-18-04", + "image_definition": null, "interfaces": [ { - "ipv4_addresses": [], + "ipv4_addresses": null, + "ipv6_addresses": null, + "mac_address": null, + "name": "Loopback0", + "state": "STARTED" + }, + { + "ipv4_addresses": [ + "192.168.255.199" + ], "ipv6_addresses": [], - "mac_address": "52:54:00:13:1b:fb", - "name": "enp0s2", + "mac_address": "52:54:00:13:51:66", + "name": "GigabitEthernet1", "state": "STARTED" } ], - "node_definition": "ubuntu", - "ram": null, + "node_definition": "csr1000v", + "ram": 3072, "state": "BOOTED" } } ``` +The first IPv4 address found (in order of the interfaces) is used as `ansible_host` to enable the playbook to connect to the device. + +To use the CML dynamic inventory plugin, the environmental variables must be set and a file (e.g. `cml.yml`) placed in the inventory specifying the plugin information: + +``` +plugin: cisco.cml.cml_inventory +group_tags: network, ios, nxos, router +``` + +Options: + +`plugin:` Specfies the name of the inventory plugin +`group_tags:` The group tags that, if one or more are found in a CML device tags, will create an Ansible group of the same name + +To create an Ansible group, specify a device tag in CML: + +![CML Tag Example](cml_group_tag.png?raw=true "CML Tag Example") + +When the CML dynamic inventory plugin runs, it will create a router group with all of the devices that have that tag: +``` +mdd % ansible-playbook cisco.cml.inventory --limit=router + +PLAY [cml_hosts] ****************************************************************************************************************************** + +TASK [debug] ********************************************************************************************************************************** +ok: [hq-rtr1] => { + "msg": "Node: hq-rtr1(csr1000v), State: BOOTED, Address: 192.168.255.199:22" +} +ok: [hq-rtr2] => { + "msg": "Node: hq-rtr2(csr1000v), State: BOOTED, Address: 192.168.255.53:22" +} +ok: [site1-rtr1] => { + "msg": "Node: site1-rtr1(csr1000v), State: BOOTED, Address: 192.168.255.63:22" +} +ok: [site2-rtr1] => { + "msg": "Node: site2-rtr1(csr1000v), State: BOOTED, Address: 192.168.255.7:22" +} + +PLAY RECAP ************************************************************************************************************************************ +hq-rtr1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +hq-rtr2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +site1-rtr1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +site2-rtr1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +In addition to group tags, the CML dynamic inventory plugin will also parse tags to pass information from PATty and to create +generic inventory facts. + +![PAT Tag Example](cml_pat_tags.png?raw=true "PAT Tag Example") + +If a CML tag is specified that matches `^pat:(?:tcp|udp)?:?(\d+):(\d+)`, the CML server address (as opposed to the first IPv4 address found) +will be used for `ansible_host`. To change `ansible_port` to point to the translated SSH port, the tag `ansible:ansible_port=2020` can +be set. These two tags tell the Ansible playbook to connect to port 2020 of the CML server to automate the specified host in the topology. +The `ansible:` tag can also be used to specify other host facts. For example, the tag `ansible:nso_api_port=2021` can be used to tell the +playbook the port to use to reach the Cisco NSO API. Any arbitrary fact can be set in this way. + + ## Collection Playbooks ### `cisco.cml.build` diff --git a/cml_group_tag.png b/cml_group_tag.png new file mode 100644 index 0000000..6dded7a Binary files /dev/null and b/cml_group_tag.png differ diff --git a/cml_pat_tags.png b/cml_pat_tags.png new file mode 100644 index 0000000..41d492b Binary files /dev/null and b/cml_pat_tags.png differ diff --git a/galaxy.yml b/galaxy.yml index b7be43e..51d6e68 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -8,7 +8,7 @@ namespace: cisco name: cml # The version of the collection. Must be compatible with semantic versioning -version: 1.1.0 +version: 1.2.1 # The path to the Markdown (.md) readme file. This path is relative to the root of the collection readme: README.md diff --git a/playbooks/clean.yml b/playbooks/clean.yml index 8ae2153..f88cb04 100644 --- a/playbooks/clean.yml +++ b/playbooks/clean.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://json.schemastore.org/ansible-playbook.json - hosts: localhost gather_facts: no connection: local diff --git a/playbooks/inventory.yml b/playbooks/inventory.yml index 41313ab..2b0e6b2 100644 --- a/playbooks/inventory.yml +++ b/playbooks/inventory.yml @@ -1,9 +1,10 @@ +# yaml-language-server: $schema=https://json.schemastore.org/ansible-playbook.json - hosts: cml_hosts connection: local gather_facts: no tasks: - debug: - msg: "Node: {{ inventory_hostname }}({{ cml_facts.node_definition }}), State: {{ cml_facts.state }}, Address: {{ ansible_host }}" + msg: "Node: {{ inventory_hostname }}({{ cml_facts.node_definition }}), State: {{ cml_facts.state }}, Address: {{ ansible_host }}:{{ ansible_port | default('22') }}" when: ansible_host is defined # - debug: # var: cml_facts diff --git a/plugins/inventory/cml_inventory.py b/plugins/inventory/cml_inventory.py index 41e3c34..cbf1a6e 100644 --- a/plugins/inventory/cml_inventory.py +++ b/plugins/inventory/cml_inventory.py @@ -60,6 +60,7 @@ import os import traceback +import re from ansible.plugins.inventory import BaseInventoryPlugin from ansible.errors import AnsibleError, AnsibleParserError from ansible.module_utils._text import to_text @@ -185,6 +186,25 @@ def parse(self, inventory, loader, path, cache=True): } interface_list = [] ansible_host = None + ansible_port = None + # pat_regex_list = [r"^pat:tcp:(\d+):22", r"^pat:(\d+):22"] + for tag in node.tags(): + fact_match = re.search(r"^ansible:([^=]+)=(\d+)$", tag) + pat_match = re.search(r"^pat:(?:tcp|udp)?:?(\d+):(\d+)", tag) + if fact_match: + self.display.vvv("Add fact to node {0}: {1}={2}".format(node.label, fact_match.group(1), fact_match.group(2))) + self.inventory.set_variable(node.label, fact_match.group(1), fact_match.group(2)) + # for regex_pattern in pat_regex_list: + # # Use re.search to find a match + elif pat_match: + self.display.vvv("Found PAT: outside_port={0}, inside_port={1}".format(pat_match.group(1), pat_match.group(2))) + # Extract values from capture groups + # outside_port = match.group(1) + ansible_host = self.host + # ansible_port = match.group(1) + # break # Exit the inner loop once a match is found + else: + continue # Continue with the next string if no match was found for interface in node.interfaces(): if node.state == 'BOOTED': # Fill out the oper data if the node is not fully booted @@ -211,6 +231,8 @@ def parse(self, inventory, loader, path, cache=True): cml.update({'interfaces': interface_list}) if ansible_host: self.inventory.set_variable(node.label, 'ansible_host', ansible_host) + if ansible_port: + self.inventory.set_variable(node.label, 'ansible_port', ansible_port) self.inventory.set_variable(node.label, 'cml_facts', cml) self.display.vvv("Adding {0}({1}) to group {2}, state: {3}, ansible_host: {4}".format( node.label, node.node_definition, self.group, node.state, ansible_host))