From c995ed02e095757959b4d6b1c3c235974c009460 Mon Sep 17 00:00:00 2001 From: Paul <> Date: Fri, 31 Mar 2023 16:56:13 -0400 Subject: [PATCH 1/5] adding node linking feature --- plugins/module_utils/cml_utils.py | 6 + plugins/modules/cml_link_node.py | 218 ++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 plugins/modules/cml_link_node.py diff --git a/plugins/module_utils/cml_utils.py b/plugins/module_utils/cml_utils.py index 816a0c2..87acef6 100644 --- a/plugins/module_utils/cml_utils.py +++ b/plugins/module_utils/cml_utils.py @@ -67,6 +67,12 @@ def get_node_by_name(self, lab, name): return node return None + def get_link_by_nodes(self, lab, node1, node2): + for link in lab.links(): + if link.node_a.label == node1.label and link.node_b.label == node2.label or link.node_b.label == node1.label and link.node_a.label == node2: + return link + return None + def exit_json(self, **kwargs): self.result.update(**kwargs) diff --git a/plugins/modules/cml_link_node.py b/plugins/modules/cml_link_node.py new file mode 100644 index 0000000..4468705 --- /dev/null +++ b/plugins/modules/cml_link_node.py @@ -0,0 +1,218 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} + +DOCUMENTATION = r""" +--- +module: cml_link_node +short_description: Create, update or delete a link between two nodes in a CML Lab +description: + - Create, update or delete a link between two nodes in a CML Lab + - Establishes a link between two nodes + - Node names need to be specified to create the link + - Node links can be updated to point to different nodes: node1->node2 is changed to node1->node3 + - Node links can be deleted +author: + - Paul Pajerski (@ppajersk) +requirements: + - virl2_client +version_added: '0.1.0' +options: + action: + description: The desired action to take with the link + required: false + type: str + choices: ['create', 'update', 'delete'] + default: create + + lab: + description: The name of the CML lab (CML_LAB) + required: true + type: str + + source_node: + description: The name of the first node + required: true + type: str + + destination_node_1: + description: The name of the second node + required: true + type: str + + destination_node_2: + description: The name of the third node, this node is only used in update commands + where if provided alongside the update action, will update the link between source_node and destination_node_1 + to link between source_node and destination_node_2 + required: false + type: str + + x: + description: X coordinate on topology canvas + required: false + type: int + + y: + description: Y coordinate on topology canvas + required: false + type: int + + tags: + description: List of tags + required: false + type: list + elements: str + + wait: + description: Wait for lab virtual machines to boot before continuing + required: false + type: bool + default: False +""" + +EXAMPLES = r""" +- name: Link two CML nodes + hosts: cml_hosts + connection: local + gather_facts: no + tasks: + - name: Link nodes + cisco.cml.cml_link_node: + host: "{{ cml_host }}" + user: "{{ cml_username }}" + password: "{{ cml_password }}" + lab: "{{ cml_lab }}" + source_node: "{{ source_node }}" + destination_node_1: "{{ destination_node_1 }}" + action: create + +- name: Update a link between two CML nodes + hosts: cml_hosts + connection: local + gather_facts: no + tasks: + - name: Link nodes + cisco.cml.cml_link_node: + host: "{{ cml_host }}" + user: "{{ cml_username }}" + password: "{{ cml_password }}" + lab: "{{ cml_lab }}" + source_node: "{{ source_node }}" + destination_node_1: "{{ destination_node_1 }}" + destination_node_2: "{{ destination_node_2 }}" + action: update + +- name: Delete a link between two CML nodes + hosts: cml_hosts + connection: local + gather_facts: no + tasks: + - name: Link nodes + cisco.cml.cml_link_node: + host: "{{ cml_host }}" + user: "{{ cml_username }}" + password: "{{ cml_password }}" + lab: "{{ cml_lab }}" + source_node: "{{ source_node }}" + destination_node_1: "{{ destination_node_1 }}" + action: delete +""" + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible_collections.cisco.cml.plugins.module_utils.cml_utils import cmlModule, cml_argument_spec + +def run_module(): + # define available arguments/parameters a user can pass to the module + argument_spec = cml_argument_spec() + argument_spec.update( + lab=dict(type='str', required=True, fallback=(env_fallback, ['CML_LAB'])), + source_node=dict(type='str'), + destination_node_1=dict(type='str'), + destination_node_2=dict(type='str'), + tags=dict(type='list', elements='str'), + x=dict(type='int'), + y=dict(type='int'), + wait=dict(type='bool', default=False), + ) + + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + cml = cmlModule(module) + + labs = cml.client.find_labs_by_title(cml.params['lab']) + if len(labs) > 0: + lab = labs[0] + else: + cml.fail_json("Cannot find lab {0}".format(cml.params['lab'])) + + # get both nodes by name + source_node = cml.get_node_by_name(lab, cml.params['source_node']) + destination_node_1 = cml.get_node_by_name(lab, cml.params['destination_node_1']) + destination_node_2 = cml.get_node_by_name(lab, cml.params['destination_node_2']) + + if source_node == None or destination_node_1 == None: + cml.fail_json("One or more nodes cannot be found. Nodes need to be created before a link can be established") + cml.exit_json(**cml.result) + return + + link = get_link_by_nodes(source_node, destination_node_1) + + if cml.params['action'] == 'create': + if link == None: # if the link does not exist + link = lab.connect_two_nodes(source_node, destination_node_1) + cml.result['changed'] = True + else: + cml.fail_json("Link between nodes already exists") + elif cml.params['action'] == 'update': + if link is not None: + if destination_node_2 is not None: # only need to check if destination_node_2 is none here + lab.remove_link(link) # remove current link + link = lab.connect_two_nodes(source_node, destination_node_2) # create new link + cml.result['changed'] = True + else: + cml.fail_json("destination_node_2 cannot be found or does not exist") + else: + cml.fail_json("Link between nodes does not exists") + elif cml.params['action'] == 'delete': + if link is not None: + lab.remove_link(link) # remove current link + cml.result['changed'] = True + else: + cml.fail_json("Link between nodes does not exists") + cml.exit_json(**cml.result) + +def main(): + run_module() + + +if __name__ == '__main__': + main() From 64c78cf87448d4957261e6a3dd4b628158e6890b Mon Sep 17 00:00:00 2001 From: Paul <> Date: Fri, 31 Mar 2023 21:16:44 -0400 Subject: [PATCH 2/5] updating code to have cml. prefix for get_link_by_nodes --- plugins/modules/cml_link_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/cml_link_node.py b/plugins/modules/cml_link_node.py index 4468705..18d2bee 100644 --- a/plugins/modules/cml_link_node.py +++ b/plugins/modules/cml_link_node.py @@ -184,7 +184,7 @@ def run_module(): cml.exit_json(**cml.result) return - link = get_link_by_nodes(source_node, destination_node_1) + link = cml.get_link_by_nodes(source_node, destination_node_1) if cml.params['action'] == 'create': if link == None: # if the link does not exist From ddb53cc1283d57aef501291bea9a05b1776bb0f6 Mon Sep 17 00:00:00 2001 From: Paul <> Date: Fri, 31 Mar 2023 21:31:53 -0400 Subject: [PATCH 3/5] addressing conditional logic --- plugins/module_utils/cml_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/cml_utils.py b/plugins/module_utils/cml_utils.py index 87acef6..c5417df 100644 --- a/plugins/module_utils/cml_utils.py +++ b/plugins/module_utils/cml_utils.py @@ -69,7 +69,7 @@ def get_node_by_name(self, lab, name): def get_link_by_nodes(self, lab, node1, node2): for link in lab.links(): - if link.node_a.label == node1.label and link.node_b.label == node2.label or link.node_b.label == node1.label and link.node_a.label == node2: + if (link.node_a.label == node1.label and link.node_b.label == node2.label) or (link.node_b.label == node1.label and link.node_a.label == node2): return link return None From 44cc75abb87686c13e9e056e6b176487a7b117b7 Mon Sep 17 00:00:00 2001 From: Paul <> Date: Fri, 31 Mar 2023 22:34:26 -0400 Subject: [PATCH 4/5] testing module and fixing bugs --- plugins/module_utils/cml_utils.py | 3 ++- plugins/modules/cml_link_node.py | 43 ++++++++++++++++--------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/plugins/module_utils/cml_utils.py b/plugins/module_utils/cml_utils.py index c5417df..1bb2684 100644 --- a/plugins/module_utils/cml_utils.py +++ b/plugins/module_utils/cml_utils.py @@ -69,7 +69,8 @@ def get_node_by_name(self, lab, name): def get_link_by_nodes(self, lab, node1, node2): for link in lab.links(): - if (link.node_a.label == node1.label and link.node_b.label == node2.label) or (link.node_b.label == node1.label and link.node_a.label == node2): + if ((link.node_a.label == node1.label and link.node_b.label == node2.label) + or (link.node_b.label == node1.label and link.node_a.label == node2.label)): return link return None diff --git a/plugins/modules/cml_link_node.py b/plugins/modules/cml_link_node.py index 18d2bee..4eaf3ba 100644 --- a/plugins/modules/cml_link_node.py +++ b/plugins/modules/cml_link_node.py @@ -35,6 +35,7 @@ - Node names need to be specified to create the link - Node links can be updated to point to different nodes: node1->node2 is changed to node1->node3 - Node links can be deleted + - Note: when updating, all three nodes must be specified author: - Paul Pajerski (@ppajersk) requirements: @@ -58,15 +59,15 @@ required: true type: str - destination_node_1: + destination_node: description: The name of the second node required: true type: str - destination_node_2: + update_node: description: The name of the third node, this node is only used in update commands - where if provided alongside the update action, will update the link between source_node and destination_node_1 - to link between source_node and destination_node_2 + where if provided alongside the update action, will update the link between source_node and destination_node + to link between source_node and update_node required: false type: str @@ -106,7 +107,7 @@ password: "{{ cml_password }}" lab: "{{ cml_lab }}" source_node: "{{ source_node }}" - destination_node_1: "{{ destination_node_1 }}" + destination_node: "{{ destination_node }}" action: create - name: Update a link between two CML nodes @@ -121,8 +122,8 @@ password: "{{ cml_password }}" lab: "{{ cml_lab }}" source_node: "{{ source_node }}" - destination_node_1: "{{ destination_node_1 }}" - destination_node_2: "{{ destination_node_2 }}" + destination_node: "{{ destination_node }}" + update_node: "{{ update_node }}" action: update - name: Delete a link between two CML nodes @@ -137,7 +138,7 @@ password: "{{ cml_password }}" lab: "{{ cml_lab }}" source_node: "{{ source_node }}" - destination_node_1: "{{ destination_node_1 }}" + destination_node: "{{ destination_node }}" action: delete """ @@ -149,9 +150,10 @@ def run_module(): argument_spec = cml_argument_spec() argument_spec.update( lab=dict(type='str', required=True, fallback=(env_fallback, ['CML_LAB'])), + action=dict(type='str'), source_node=dict(type='str'), - destination_node_1=dict(type='str'), - destination_node_2=dict(type='str'), + destination_node=dict(type='str'), + update_node=dict(type='str'), tags=dict(type='list', elements='str'), x=dict(type='int'), y=dict(type='int'), @@ -176,43 +178,42 @@ def run_module(): # get both nodes by name source_node = cml.get_node_by_name(lab, cml.params['source_node']) - destination_node_1 = cml.get_node_by_name(lab, cml.params['destination_node_1']) - destination_node_2 = cml.get_node_by_name(lab, cml.params['destination_node_2']) + destination_node = cml.get_node_by_name(lab, cml.params['destination_node']) + update_node = cml.get_node_by_name(lab, cml.params['update_node']) - if source_node == None or destination_node_1 == None: + if source_node == None or destination_node == None: cml.fail_json("One or more nodes cannot be found. Nodes need to be created before a link can be established") cml.exit_json(**cml.result) return - link = cml.get_link_by_nodes(source_node, destination_node_1) + link = cml.get_link_by_nodes(lab, source_node, destination_node) if cml.params['action'] == 'create': if link == None: # if the link does not exist - link = lab.connect_two_nodes(source_node, destination_node_1) + link = lab.connect_two_nodes(source_node, destination_node) cml.result['changed'] = True else: cml.fail_json("Link between nodes already exists") elif cml.params['action'] == 'update': if link is not None: - if destination_node_2 is not None: # only need to check if destination_node_2 is none here + if update_node is not None: # only need to check if update_node is none here lab.remove_link(link) # remove current link - link = lab.connect_two_nodes(source_node, destination_node_2) # create new link + link = lab.connect_two_nodes(source_node, update_node) # create new link cml.result['changed'] = True else: - cml.fail_json("destination_node_2 cannot be found or does not exist") + cml.fail_json("update_node cannot be found or does not exist") else: - cml.fail_json("Link between nodes does not exists") + cml.fail_json("Link between nodes does not exist") elif cml.params['action'] == 'delete': if link is not None: lab.remove_link(link) # remove current link cml.result['changed'] = True else: - cml.fail_json("Link between nodes does not exists") + cml.fail_json("Link between nodes does not exist") cml.exit_json(**cml.result) def main(): run_module() - if __name__ == '__main__': main() From bd2abb8f8ce91fc5de7885205d93bc427e5502bb Mon Sep 17 00:00:00 2001 From: Paul <> Date: Tue, 4 Apr 2023 12:30:55 -0400 Subject: [PATCH 5/5] adding required flags for the source and dest nodes, as well as action --- plugins/modules/cml_link_node.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/cml_link_node.py b/plugins/modules/cml_link_node.py index 4eaf3ba..0376237 100644 --- a/plugins/modules/cml_link_node.py +++ b/plugins/modules/cml_link_node.py @@ -150,10 +150,10 @@ def run_module(): argument_spec = cml_argument_spec() argument_spec.update( lab=dict(type='str', required=True, fallback=(env_fallback, ['CML_LAB'])), - action=dict(type='str'), - source_node=dict(type='str'), - destination_node=dict(type='str'), - update_node=dict(type='str'), + action=dict(type='str', required=True), + source_node=dict(type='str', required=True), + destination_node=dict(type='str', required=True), + update_node=dict(type='str', required=False), tags=dict(type='list', elements='str'), x=dict(type='int'), y=dict(type='int'),