#!/usr/bin/env python ANSIBLE_METADATA = { "metadata_version": "1.4", "status": ["preview"], "supported_by": "community", } DOCUMENTATION = """ --- module: gns3_project short_description: Module to interact with GNS3 server projects version_added: '2.8' description: - 'Module to interact with GNS3 server projects. - It is using the L(gns3fy library,https://davidban77.github.io/gns3fy/)' - It opens/closes projects and performs basic turnup/teradown operations on nodes. - It creates/updates or deletes projects. requirements: [ gns3fy ] author: - David Flores (@davidban77) options: url: description: - URL target of the GNS3 server required: true type: str port: description: - TCP port to connect to server REST API type: int default: 3080 user: description: - User to connect to GNS3 server type: str password: description: - Password to connect to GNS3 server type: str state: description: - State of the project to be on the GNS3 server - '- C(opened): Opens a project and turns up nodes' - '- C(closed): Closes a project and turns down nodes' - '- C(present): Creates/update a project on the server' - '- C(absent): Deletes a project on the server' type: str choices: ['opened', 'closed', 'present', 'absent'] project_name: description: - Project name type: str project_id: description: - Project ID type: str nodes_state: description: - Starts/stops nodes on the project. - Used when I(state) is C(opened)/C(closed) type: str choices: ['started', 'stopped'] nodes_strategy: description: - Start/stop strategy of the devices defined on the project. - '- C(all): It starts/stops all nodes at once' - '- C(one_by_one): It starts/stops nodes serialy using I(nodes_delay) time between each action' - Used when I(state) is C(opened)/C(closed) type: str choices: ['all', 'one_by_one'] default: 'all' nodes_delay: description: - Delay time in seconds to wait between nodes start/stop - Used when I(nodes_strategy) is C(one_by_one) type: int default: 10 poll_wait_time: description: - Delay in seconds to wait to poll nodes when they are started/stopped. - Used when I(nodes_state) is C(started)/C(stopped) type: int default: 5 nodes_spec: description: - List of dictionaries specifying the nodes properties. - '- Mandatory attributes: C(name), C(node_type) and C(template).' - '- Optional attributes: C(compute_id). It defaults to C(local)' type: list links_spec: description: - 'List of lists specifying the links endpoints. Example: C(- ["alpine-1", "eth0", "alpine-2", "eth0"])' - 'Mandatory attributes: C(node_a), C(port_a), C(node_b) and C(port_b)' type: list """ EXAMPLES = """ # Open a GNS3 project - name: Start lab gns3_project: url: http://localhost state: opened project_name: lab_example # Stop all nodes inside an open project - name: Stop nodes gns3_project: url: http://localhost state: opened project_name: lab_example nodes_state: stopped nodes_strategy: all poll_wait_time: 5 # Open a GNS3 project and start nodes one by one with a delay of 10sec between them - name: Start nodes one by one gns3_project: url: http://localhost state: opened project_name: lab_example nodes_state: started nodes_strategy: one_by_one nodes_delay: 10 # Close a GNS3 project - name: Stop lab gns3_project: url: http://localhost state: closed project_id: 'UUID-SOMETHING-1234567' # Create a GNS3 project - name: Create a project given nodes and links specs gns3_project: url: http://localhost state: present project_name: new_lab nodes_spec: - name: alpine-1 node_type: docker template: alpine - name: alpine-2 node_type: docker template: alpine links_spec: - ('alpine-1', 'eth0', 'alpine-2', 'eth1') # Delete a GNS3 project - name: Delete project gns3_project: url: http://localhost state: absent project_name: new_lab """ RETURN = """ name: description: Project name type: str project_id: description: Project UUID type: str status: description: Project status. Possible values: opened, closed type: str path: description: Path of the project on the server (works only with compute=local) type: str auto_close: description: Project auto close when client cut off the notifications feed type: bool auto_open: description: Project open when GNS3 start type: bool auto_start: description: Project start when opened type: bool filename: description: Project filename type: str """ import time import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib GNS3FY_IMP_ERR = None try: from gns3fy import Gns3Connector, Project HAS_GNS3FY = True except Exception: HAS_GNS3FY = False GNS3FY_IMP_ERR = traceback.format_exc() def return_project_data(project): "Returns the project main attributes" return dict( name=project.name, project_id=project.project_id, status=project.status, path=project.path, auto_close=project.auto_close, auto_open=project.auto_open, auto_start=project.auto_start, filename=project.filename, ) # def nodes_state_verification(module, project, result): def nodes_state_verification( expected_nodes_state, nodes_strategy, nodes_delay, poll_wait_time, project ): "Verifies each node state and returns a changed attribute" nodes_statuses = [node.status for node in project.nodes] # Verify if nodes do not match expected state if expected_nodes_state == "started" and any( status == "stopped" for status in nodes_statuses ): # Turnup the nodes based on strategy if nodes_strategy == "all": project.start_nodes(poll_wait_time=poll_wait_time) elif nodes_strategy == "one_by_one": for node in project.nodes: if node.status != "started": node.start() time.sleep(nodes_delay) return True elif expected_nodes_state == "stopped" and any( status == "started" for status in nodes_statuses ): # Shutdown nodes based on strategy if nodes_strategy == "all": project.stop_nodes(poll_wait_time=poll_wait_time) elif nodes_strategy == "one_by_one": for node in project.nodes: if node.status != "stopped": node.stop() time.sleep(nodes_delay) return True return False def create_node(node_spec, project, module): "Creates the node specified in nodes_spec" # If exceptions occur then print them out in ansible format try: project.create_node(**node_spec) except Exception as err: module.fail_json(msg=str(err), exception=traceback.format_exc()) def create_link(link_spec, project, module): "Creates the node specified in nodes_spec" # If exceptions occur then print them out in ansible format try: project.create_link(*link_spec) except ValueError as err: if "At least one port is used" in str(err): return False else: module.fail_json(msg=str(err), exception=traceback.format_exc()) except Exception as err: module.fail_json(msg=str(err), exception=traceback.format_exc()) return True def main(): module = AnsibleModule( argument_spec=dict( url=dict(type="str", required=True), port=dict(type="int", default=3080), user=dict(type="str", default=None), password=dict(type="str", default=None, no_log=True), state=dict( type="str", required=True, choices=["opened", "closed", "present", "absent"], ), project_name=dict(type="str", default=None), project_id=dict(type="str", default=None), nodes_state=dict(type="str", choices=["started", "stopped"]), nodes_strategy=dict( type="str", choices=["all", "one_by_one"], default="all" ), nodes_delay=dict(type="int", default=10), poll_wait_time=dict(type="int", default=5), nodes_spec=dict(type="list"), links_spec=dict(type="list"), ), supports_check_mode=True, required_one_of=[["project_name", "project_id"]], required_if=[["nodes_strategy", "one_by_one", ["nodes_delay"]]], ) result = dict(changed=False) if not HAS_GNS3FY: module.fail_json(msg=missing_required_lib("gns3fy"), exception=GNS3FY_IMP_ERR) if module.check_mode: module.exit_json(**result) server_url = module.params["url"] server_port = module.params["port"] server_user = module.params["user"] server_password = module.params["password"] state = module.params["state"] project_name = module.params["project_name"] project_id = module.params["project_id"] nodes_state = module.params["nodes_state"] nodes_strategy = module.params["nodes_strategy"] nodes_delay = module.params["nodes_delay"] poll_wait_time = module.params["poll_wait_time"] nodes_spec = module.params["nodes_spec"] links_spec = module.params["links_spec"] try: # Create server session server = Gns3Connector( url=f"{server_url}:{server_port}", user=server_user, cred=server_password ) # Define the project if project_name is not None: project = Project(name=project_name, connector=server) elif project_id is not None: project = Project(project_id=project_id, connector=server) except Exception as err: module.fail_json(msg=str(err), **result) #  Retrieve project information try: project.get() pr_exists = True except Exception as err: pr_exists = False reason = str(err) if state == "opened": if pr_exists: if project.status != "opened": # Open project project.open() # Now verify nodes if nodes_state is not None: # Change flag based on the nodes state result["changed"] = nodes_state_verification( expected_nodes_state=nodes_state, nodes_strategy=nodes_strategy, nodes_delay=nodes_delay, poll_wait_time=poll_wait_time, project=project, ) else: # Means that nodes are not taken into account for idempotency result["changed"] = True # Even if the project is open if nodes_state has been set, check it else: if nodes_state is not None: result["changed"] = nodes_state_verification( expected_nodes_state=nodes_state, nodes_strategy=nodes_strategy, nodes_delay=nodes_delay, poll_wait_time=poll_wait_time, project=project, ) else: module.fail_json(msg=reason, **result) elif state == "closed": if pr_exists: if project.status != "closed": # Close project project.close() result["changed"] = True else: module.fail_json(msg=reason, **result) elif state == "present": if pr_exists: if nodes_spec is not None: # Need to verify if nodes exist _nodes_already_created = [node.name for node in project.nodes] for node_spec in nodes_spec: if node_spec["name"] not in _nodes_already_created: # Open the project in case it was closed project.open() create_node(node_spec, project, module) result["changed"] = True if links_spec is not None: for link_spec in links_spec: project.open() # Trigger another get to refresh nodes attributes project.get() # Link verification is already built in the library created = create_link(link_spec, project, module) if created: result["changed"] = True else: # Create project project.create() # Nodes section if nodes_spec is not None: for node_spec in nodes_spec: create_node(node_spec, project, module) # Links section if links_spec is not None: for link_spec in links_spec: create_link(link_spec, project, module) result["changed"] = True elif state == "absent": if pr_exists: # Stop nodes and close project to perform delete gracefully if project.status != "opened": # Project needs to be opened in order to be deleted... project.open() project.stop_nodes(poll_wait_time=0) project.delete() result["changed"] = True else: module.exit_json(**result) # Return the project data result["project"] = return_project_data(project) module.exit_json(**result) if __name__ == "__main__": main()