mirror of
https://github.com/GNS3/gns3-server
synced 2025-01-12 17:10:55 +00:00
Reload a topology work
This commit is contained in:
parent
524f8991bc
commit
6d36429870
@ -92,6 +92,7 @@ class Controller:
|
|||||||
# Preload the list of projects from disk
|
# Preload the list of projects from disk
|
||||||
server_config = Config.instance().get_section_config("Server")
|
server_config = Config.instance().get_section_config("Server")
|
||||||
projects_path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects"))
|
projects_path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects"))
|
||||||
|
os.makedirs(projects_path, exist_ok=True)
|
||||||
try:
|
try:
|
||||||
for project_path in os.listdir(projects_path):
|
for project_path in os.listdir(projects_path):
|
||||||
project_dir = os.path.join(projects_path, project_path)
|
project_dir = os.path.join(projects_path, project_path)
|
||||||
@ -105,7 +106,6 @@ class Controller:
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.error(str(e))
|
log.error(str(e))
|
||||||
|
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
"""
|
"""
|
||||||
:returns: whether the current instance is the controller
|
:returns: whether the current instance is the controller
|
||||||
@ -219,7 +219,7 @@ class Controller:
|
|||||||
|
|
||||||
project = yield from self.add_project(path=os.path.dirname(path), status="closed", **topo_data)
|
project = yield from self.add_project(path=os.path.dirname(path), status="closed", **topo_data)
|
||||||
if load:
|
if load:
|
||||||
yield from project.load()
|
yield from project.open()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def projects(self):
|
def projects(self):
|
||||||
|
@ -228,7 +228,19 @@ class Compute:
|
|||||||
def password(self, value):
|
def password(self, value):
|
||||||
self._set_auth(self._user, value)
|
self._set_auth(self._user, value)
|
||||||
|
|
||||||
def __json__(self):
|
def __json__(self, topology_dump=False):
|
||||||
|
"""
|
||||||
|
:param topology_dump: Filter to keep only properties require for saving on disk
|
||||||
|
"""
|
||||||
|
if topology_dump:
|
||||||
|
return {
|
||||||
|
"compute_id": self._id,
|
||||||
|
"name": self._name,
|
||||||
|
"protocol": self._protocol,
|
||||||
|
"host": self._host,
|
||||||
|
"port": self._port,
|
||||||
|
"user": self._user
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"compute_id": self._id,
|
"compute_id": self._id,
|
||||||
"name": self._name,
|
"name": self._name,
|
||||||
|
@ -55,6 +55,7 @@ class Link:
|
|||||||
|
|
||||||
if len(self._nodes) == 2:
|
if len(self._nodes) == 2:
|
||||||
self._project.controller.notification.emit("link.created", self.__json__())
|
self._project.controller.notification.emit("link.created", self.__json__())
|
||||||
|
self._project.dump()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def create(self):
|
def create(self):
|
||||||
@ -156,7 +157,10 @@ class Link:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __json__(self):
|
def __json__(self, topology_dump=False):
|
||||||
|
"""
|
||||||
|
:param topology_dump: Filter to keep only properties require for saving on disk
|
||||||
|
"""
|
||||||
res = []
|
res = []
|
||||||
for side in self._nodes:
|
for side in self._nodes:
|
||||||
res.append({
|
res.append({
|
||||||
@ -164,6 +168,11 @@ class Link:
|
|||||||
"adapter_number": side["adapter_number"],
|
"adapter_number": side["adapter_number"],
|
||||||
"port_number": side["port_number"]
|
"port_number": side["port_number"]
|
||||||
})
|
})
|
||||||
|
if topology_dump:
|
||||||
|
return {
|
||||||
|
"nodes": res,
|
||||||
|
"link_id": self._id
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"nodes": res,
|
"nodes": res,
|
||||||
"link_id": self._id,
|
"link_id": self._id,
|
||||||
|
@ -25,6 +25,10 @@ from .compute import ComputeConflict
|
|||||||
from ..utils.images import images_directories
|
from ..utils.images import images_directories
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
class Node:
|
||||||
# This properties are used only on controller and are not forwarded to the compute
|
# This properties are used only on controller and are not forwarded to the compute
|
||||||
CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "symbol", "label", "console_host"]
|
CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "symbol", "label", "console_host"]
|
||||||
@ -68,7 +72,11 @@ class Node:
|
|||||||
}
|
}
|
||||||
# Update node properties with additional elements
|
# Update node properties with additional elements
|
||||||
for prop in kwargs:
|
for prop in kwargs:
|
||||||
|
try:
|
||||||
setattr(self, prop, kwargs[prop])
|
setattr(self, prop, kwargs[prop])
|
||||||
|
except AttributeError as e:
|
||||||
|
log.critical("Can't set attribute %s", prop)
|
||||||
|
raise e
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
@ -370,7 +378,25 @@ class Node:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<gns3server.controller.Node {} {}>".format(self._node_type, self._name)
|
return "<gns3server.controller.Node {} {}>".format(self._node_type, self._name)
|
||||||
|
|
||||||
def __json__(self):
|
def __json__(self, topology_dump=False):
|
||||||
|
"""
|
||||||
|
:param topology_dump: Filter to keep only properties require for saving on disk
|
||||||
|
"""
|
||||||
|
if topology_dump:
|
||||||
|
return {
|
||||||
|
"compute_id": str(self._compute.id),
|
||||||
|
"node_id": self._id,
|
||||||
|
"node_type": self._node_type,
|
||||||
|
"name": self._name,
|
||||||
|
"console": self._console,
|
||||||
|
"console_type": self._console_type,
|
||||||
|
"properties": self._properties,
|
||||||
|
"label": self._label,
|
||||||
|
"x": self._x,
|
||||||
|
"y": self._y,
|
||||||
|
"z": self._z,
|
||||||
|
"symbol": self._symbol
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"compute_id": str(self._compute.id),
|
"compute_id": str(self._compute.id),
|
||||||
"project_id": self._project.id,
|
"project_id": self._project.id,
|
||||||
|
@ -60,7 +60,12 @@ class Project:
|
|||||||
if path is None:
|
if path is None:
|
||||||
path = os.path.join(get_default_project_directory(), self._id)
|
path = os.path.join(get_default_project_directory(), self._id)
|
||||||
self.path = path
|
self.path = path
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""
|
||||||
|
Called when open/close a project. Cleanup internal stuff
|
||||||
|
"""
|
||||||
self._allocated_node_names = set()
|
self._allocated_node_names = set()
|
||||||
self._nodes = {}
|
self._nodes = {}
|
||||||
self._links = {}
|
self._links = {}
|
||||||
@ -291,7 +296,8 @@ class Project:
|
|||||||
def close(self):
|
def close(self):
|
||||||
for compute in self._project_created_on_compute:
|
for compute in self._project_created_on_compute:
|
||||||
yield from compute.post("/projects/{}/close".format(self._id))
|
yield from compute.post("/projects/{}/close".format(self._id))
|
||||||
self._allocated_node_names.clear()
|
self.reset()
|
||||||
|
self._status = "closed"
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def delete(self):
|
def delete(self):
|
||||||
@ -324,11 +330,13 @@ class Project:
|
|||||||
return os.path.join(self.path, filename)
|
return os.path.join(self.path, filename)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def load(self):
|
def open(self):
|
||||||
"""
|
"""
|
||||||
Load topology elements
|
Load topology elements
|
||||||
"""
|
"""
|
||||||
|
self.reset()
|
||||||
path = self._topology_file()
|
path = self._topology_file()
|
||||||
|
if os.path.exists(path):
|
||||||
topology = load_topology(path)["topology"]
|
topology = load_topology(path)["topology"]
|
||||||
for compute in topology["computes"]:
|
for compute in topology["computes"]:
|
||||||
yield from self.controller.add_compute(**compute)
|
yield from self.controller.add_compute(**compute)
|
||||||
@ -342,6 +350,7 @@ class Project:
|
|||||||
for node_link in link_data["nodes"]:
|
for node_link in link_data["nodes"]:
|
||||||
node = self.get_node(node_link["node_id"])
|
node = self.get_node(node_link["node_id"])
|
||||||
yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"])
|
yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"])
|
||||||
|
self._status = "opened"
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
"""
|
"""
|
||||||
@ -362,5 +371,5 @@ class Project:
|
|||||||
"name": self._name,
|
"name": self._name,
|
||||||
"project_id": self._id,
|
"project_id": self._id,
|
||||||
"path": self._path,
|
"path": self._path,
|
||||||
"status": "opened"
|
"status": self._status
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,12 @@ def project_to_topology(project):
|
|||||||
computes = set()
|
computes = set()
|
||||||
for node in project.nodes.values():
|
for node in project.nodes.values():
|
||||||
computes.add(node.compute)
|
computes.add(node.compute)
|
||||||
data["topology"]["nodes"].append(node.__json__())
|
data["topology"]["nodes"].append(node.__json__(topology_dump=True))
|
||||||
for link in project.links.values():
|
for link in project.links.values():
|
||||||
data["topology"]["links"].append(link.__json__())
|
data["topology"]["links"].append(link.__json__(topology_dump=True))
|
||||||
for compute in computes:
|
for compute in computes:
|
||||||
if hasattr(compute, "__json__"):
|
if hasattr(compute, "__json__"):
|
||||||
data["topology"]["computes"].append(compute.__json__())
|
data["topology"]["computes"].append(compute.__json__(topology_dump=True))
|
||||||
#TODO: check JSON schema
|
#TODO: check JSON schema
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -91,8 +91,26 @@ class ProjectHandler:
|
|||||||
controller = Controller.instance()
|
controller = Controller.instance()
|
||||||
project = controller.get_project(request.match_info["project_id"])
|
project = controller.get_project(request.match_info["project_id"])
|
||||||
yield from project.close()
|
yield from project.close()
|
||||||
controller.remove_project(project)
|
response.set_status(201)
|
||||||
response.set_status(204)
|
response.json(project)
|
||||||
|
|
||||||
|
@Route.post(
|
||||||
|
r"/projects/{project_id}/open",
|
||||||
|
description="Open a project",
|
||||||
|
parameters={
|
||||||
|
"project_id": "Project UUID",
|
||||||
|
},
|
||||||
|
status_codes={
|
||||||
|
201: "The project has been opened",
|
||||||
|
404: "The project doesn't exist"
|
||||||
|
})
|
||||||
|
def open(request, response):
|
||||||
|
|
||||||
|
controller = Controller.instance()
|
||||||
|
project = controller.get_project(request.match_info["project_id"])
|
||||||
|
yield from project.open()
|
||||||
|
response.set_status(201)
|
||||||
|
response.json(project)
|
||||||
|
|
||||||
@Route.delete(
|
@Route.delete(
|
||||||
r"/projects/{project_id}",
|
r"/projects/{project_id}",
|
||||||
|
@ -50,6 +50,10 @@ VPCS_CREATE_SCHEMA = {
|
|||||||
"description": "Content of the VPCS startup script",
|
"description": "Content of the VPCS startup script",
|
||||||
"type": ["string", "null"]
|
"type": ["string", "null"]
|
||||||
},
|
},
|
||||||
|
"startup_script_path": {
|
||||||
|
"description": "Path of the VPCS startup script relative to project directory (IGNORED)",
|
||||||
|
"type": ["string", "null"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
"required": ["name"]
|
"required": ["name"]
|
||||||
|
@ -205,6 +205,14 @@ def test_json(compute):
|
|||||||
"user": "test",
|
"user": "test",
|
||||||
"connected": True
|
"connected": True
|
||||||
}
|
}
|
||||||
|
assert compute.__json__(topology_dump=True) == {
|
||||||
|
"compute_id": "my_compute_id",
|
||||||
|
"name": compute.name,
|
||||||
|
"protocol": "https",
|
||||||
|
"host": "example.com",
|
||||||
|
"port": 84,
|
||||||
|
"user": "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_streamFile(project, async_run, compute):
|
def test_streamFile(project, async_run, compute):
|
||||||
|
@ -26,7 +26,7 @@ from gns3server.controller.node import Node
|
|||||||
from gns3server.controller.compute import Compute
|
from gns3server.controller.compute import Compute
|
||||||
from gns3server.controller.project import Project
|
from gns3server.controller.project import Project
|
||||||
|
|
||||||
from tests.utils import AsyncioBytesIO
|
from tests.utils import AsyncioBytesIO, AsyncioMagicMock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -54,6 +54,7 @@ def test_addNode(async_run, project, compute):
|
|||||||
node1 = Node(project, compute, "node1")
|
node1 = Node(project, compute, "node1")
|
||||||
|
|
||||||
link = Link(project)
|
link = Link(project)
|
||||||
|
project.dump = AsyncioMagicMock()
|
||||||
async_run(link.add_node(node1, 0, 4))
|
async_run(link.add_node(node1, 0, 4))
|
||||||
assert link._nodes == [
|
assert link._nodes == [
|
||||||
{
|
{
|
||||||
@ -62,6 +63,7 @@ def test_addNode(async_run, project, compute):
|
|||||||
"port_number": 4
|
"port_number": 4
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
assert project.dump.called
|
||||||
|
|
||||||
|
|
||||||
def test_json(async_run, project, compute):
|
def test_json(async_run, project, compute):
|
||||||
@ -90,6 +92,21 @@ def test_json(async_run, project, compute):
|
|||||||
"capture_file_name": None,
|
"capture_file_name": None,
|
||||||
"capture_file_path": None
|
"capture_file_path": None
|
||||||
}
|
}
|
||||||
|
assert link.__json__(topology_dump=True) == {
|
||||||
|
"link_id": link.id,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"node_id": node1.id,
|
||||||
|
"adapter_number": 0,
|
||||||
|
"port_number": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_id": node2.id,
|
||||||
|
"adapter_number": 1,
|
||||||
|
"port_number": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_start_streaming_pcap(link, async_run, tmpdir, project):
|
def test_start_streaming_pcap(link, async_run, tmpdir, project):
|
||||||
|
@ -70,6 +70,20 @@ def test_json(node, compute):
|
|||||||
"symbol": node.symbol,
|
"symbol": node.symbol,
|
||||||
"label": node.label
|
"label": node.label
|
||||||
}
|
}
|
||||||
|
assert node.__json__(topology_dump=True) == {
|
||||||
|
"compute_id": str(compute.id),
|
||||||
|
"node_id": node.id,
|
||||||
|
"node_type": node.node_type,
|
||||||
|
"name": "demo",
|
||||||
|
"console": node.console,
|
||||||
|
"console_type": node.console_type,
|
||||||
|
"properties": node.properties,
|
||||||
|
"x": node.x,
|
||||||
|
"y": node.y,
|
||||||
|
"z": node.z,
|
||||||
|
"symbol": node.symbol,
|
||||||
|
"label": node.label
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_init_without_uuid(project, compute):
|
def test_init_without_uuid(project, compute):
|
||||||
|
@ -228,3 +228,12 @@ def test_dump():
|
|||||||
with open(os.path.join(directory, p.id, "Test.gns3")) as f:
|
with open(os.path.join(directory, p.id, "Test.gns3")) as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
assert "00010203-0405-0607-0809-0a0b0c0d0e0f" in content
|
assert "00010203-0405-0607-0809-0a0b0c0d0e0f" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_close(async_run, controller):
|
||||||
|
project = Project(controller=controller, status="closed")
|
||||||
|
assert project.status == "closed"
|
||||||
|
async_run(project.open())
|
||||||
|
assert project.status == "opened"
|
||||||
|
async_run(project.close())
|
||||||
|
assert project.status == "closed"
|
||||||
|
@ -59,9 +59,9 @@ def test_basic_topology(tmpdir, async_run, controller):
|
|||||||
|
|
||||||
topo = project_to_topology(project)
|
topo = project_to_topology(project)
|
||||||
assert len(topo["topology"]["nodes"]) == 2
|
assert len(topo["topology"]["nodes"]) == 2
|
||||||
assert node1.__json__() in topo["topology"]["nodes"]
|
assert node1.__json__(topology_dump=True) in topo["topology"]["nodes"]
|
||||||
assert topo["topology"]["links"][0] == link.__json__()
|
assert topo["topology"]["links"][0] == link.__json__(topology_dump=True)
|
||||||
assert topo["topology"]["computes"][0] == compute.__json__()
|
assert topo["topology"]["computes"][0] == compute.__json__(topology_dump=True)
|
||||||
|
|
||||||
|
|
||||||
def test_load_topology(tmpdir):
|
def test_load_topology(tmpdir):
|
||||||
|
@ -47,6 +47,7 @@ def test_create_project_with_path(http_controller, tmpdir):
|
|||||||
assert response.status == 201
|
assert response.status == 201
|
||||||
assert response.json["name"] == "test"
|
assert response.json["name"] == "test"
|
||||||
assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f"
|
assert response.json["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f"
|
||||||
|
assert response.json["status"] == "opened"
|
||||||
|
|
||||||
|
|
||||||
def test_create_project_without_dir(http_controller):
|
def test_create_project_without_dir(http_controller):
|
||||||
@ -95,9 +96,15 @@ def test_delete_project_invalid_uuid(http_controller):
|
|||||||
def test_close_project(http_controller, project):
|
def test_close_project(http_controller, project):
|
||||||
with asyncio_patch("gns3server.controller.project.Project.close", return_value=True) as mock:
|
with asyncio_patch("gns3server.controller.project.Project.close", return_value=True) as mock:
|
||||||
response = http_controller.post("/projects/{project_id}/close".format(project_id=project.id), example=True)
|
response = http_controller.post("/projects/{project_id}/close".format(project_id=project.id), example=True)
|
||||||
assert response.status == 204
|
assert response.status == 201
|
||||||
|
assert mock.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_close_project(http_controller, project):
|
||||||
|
with asyncio_patch("gns3server.controller.project.Project.open", return_value=True) as mock:
|
||||||
|
response = http_controller.post("/projects/{project_id}/open".format(project_id=project.id), example=True)
|
||||||
|
assert response.status == 201
|
||||||
assert mock.called
|
assert mock.called
|
||||||
assert project not in Controller.instance().projects
|
|
||||||
|
|
||||||
|
|
||||||
def test_notification(http_controller, project, controller, loop):
|
def test_notification(http_controller, project, controller, loop):
|
||||||
|
Loading…
Reference in New Issue
Block a user