435 lines
14 KiB
Python
435 lines
14 KiB
Python
#!/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()
|