1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-24 17:28:08 +00:00

Merge pull request #1332 from GNS3/global-vars

Docker `ExtraHosts`, global variables for project and supplier support, Fixes: #2482
This commit is contained in:
ziajka 2018-05-15 09:23:54 +02:00 committed by GitHub
commit c52342907a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 507 additions and 32 deletions

1
.gitignore vendored
View File

@ -39,6 +39,7 @@ nosetests.xml
.project .project
.pydevproject .pydevproject
.settings .settings
.vscode
# Pycharm # Pycharm
.idea .idea

View File

@ -61,11 +61,12 @@ class DockerVM(BaseNode):
:param console_resolution: Resolution of the VNC display :param console_resolution: Resolution of the VNC display
:param console_http_port: Port to redirect HTTP queries :param console_http_port: Port to redirect HTTP queries
:param console_http_path: Url part with the path of the web interface :param console_http_path: Url part with the path of the web interface
:param extra_hosts: Hosts which will be written into /etc/hosts into docker conainer
""" """
def __init__(self, name, node_id, project, manager, image, console=None, aux=None, start_command=None, def __init__(self, name, node_id, project, manager, image, console=None, aux=None, start_command=None,
adapters=None, environment=None, console_type="telnet", console_resolution="1024x768", adapters=None, environment=None, console_type="telnet", console_resolution="1024x768",
console_http_port=80, console_http_path="/"): console_http_port=80, console_http_path="/", extra_hosts=None):
super().__init__(name, node_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type) super().__init__(name, node_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type)
@ -84,6 +85,8 @@ class DockerVM(BaseNode):
self._console_http_path = console_http_path self._console_http_path = console_http_path
self._console_http_port = console_http_port self._console_http_port = console_http_port
self._console_websocket = None self._console_websocket = None
self._extra_hosts = extra_hosts
self._volumes = [] self._volumes = []
# Keep a list of created bridge # Keep a list of created bridge
self._bridges = set() self._bridges = set()
@ -114,7 +117,8 @@ class DockerVM(BaseNode):
"start_command": self.start_command, "start_command": self.start_command,
"status": self.status, "status": self.status,
"environment": self.environment, "environment": self.environment,
"node_directory": self.working_path "node_directory": self.working_path,
"extra_hosts": self.extra_hosts
} }
def _get_free_display_port(self): def _get_free_display_port(self):
@ -178,6 +182,14 @@ class DockerVM(BaseNode):
def environment(self, command): def environment(self, command):
self._environment = command self._environment = command
@property
def extra_hosts(self):
return self._extra_hosts
@extra_hosts.setter
def extra_hosts(self, extra_hosts):
self._extra_hosts = extra_hosts
@asyncio.coroutine @asyncio.coroutine
def _get_container_state(self): def _get_container_state(self):
"""Returns the container state (e.g. running, paused etc.) """Returns the container state (e.g. running, paused etc.)
@ -288,7 +300,7 @@ class DockerVM(BaseNode):
"HostConfig": { "HostConfig": {
"CapAdd": ["ALL"], "CapAdd": ["ALL"],
"Privileged": True, "Privileged": True,
"Binds": self._mount_binds(image_infos) "Binds": self._mount_binds(image_infos),
}, },
"Volumes": {}, "Volumes": {},
"Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573 "Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573
@ -313,11 +325,20 @@ class DockerVM(BaseNode):
# Give the information to the container the list of volume path mounted # Give the information to the container the list of volume path mounted
params["Env"].append("GNS3_VOLUMES={}".format(":".join(self._volumes))) params["Env"].append("GNS3_VOLUMES={}".format(":".join(self._volumes)))
variables = self.project.variables
if not variables:
variables = []
for var in variables:
formatted = self._format_env(variables, var.get('value', ''))
params["Env"].append("{}={}".format(var["name"], formatted))
if self._environment: if self._environment:
for e in self._environment.strip().split("\n"): for e in self._environment.strip().split("\n"):
e = e.strip() e = e.strip()
if not e.startswith("GNS3_"): if not e.startswith("GNS3_"):
params["Env"].append(e) formatted = self._format_env(variables, e)
params["Env"].append(formatted)
if self._console_type == "vnc": if self._console_type == "vnc":
yield from self._start_vnc() yield from self._start_vnc()
@ -325,12 +346,36 @@ class DockerVM(BaseNode):
params["Env"].append("DISPLAY=:{}".format(self._display)) params["Env"].append("DISPLAY=:{}".format(self._display))
params["HostConfig"]["Binds"].append("/tmp/.X11-unix/:/tmp/.X11-unix/") params["HostConfig"]["Binds"].append("/tmp/.X11-unix/:/tmp/.X11-unix/")
if self._extra_hosts:
extra_hosts = self._format_extra_hosts(self._extra_hosts)
if extra_hosts:
params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts))
result = yield from self.manager.query("POST", "containers/create", data=params) result = yield from self.manager.query("POST", "containers/create", data=params)
self._cid = result['Id'] self._cid = result['Id']
log.info("Docker container '{name}' [{id}] created".format( log.info("Docker container '{name}' [{id}] created".format(
name=self._name, id=self._id)) name=self._name, id=self._id))
return True return True
def _format_env(self, variables, env):
for variable in variables:
env = env.replace('${' + variable["name"] + '}', variable.get("value", ""))
return env
def _format_extra_hosts(self, extra_hosts):
lines = [h.strip() for h in self._extra_hosts.split("\n") if h.strip() != ""]
hosts = []
try:
for host in lines:
hostname, ip = host.split(":")
hostname = hostname.strip()
ip = ip.strip()
if hostname and ip:
hosts.append((hostname, ip))
except ValueError:
raise DockerError("Can't apply `ExtraHosts`, wrong format: {}".format(extra_hosts))
return "\n".join(["{}\t{}".format(h[1], h[0]) for h in hosts])
@asyncio.coroutine @asyncio.coroutine
def update(self): def update(self):
""" """

View File

@ -60,6 +60,14 @@ ff02::1 ip6-allnodes
ff02::2 ip6-allrouters ff02::2 ip6-allrouters
__EOF__ __EOF__
# imitate docker's `ExtraHosts` behaviour
sed -i '/GNS3_EXTRA_HOSTS_START/,/GNS3_EXTRA_HOSTS_END/d' /etc/hosts
[ -n "$GNS3_EXTRA_HOSTS" ] && cat >> /etc/hosts << __EOF__
# GNS3_EXTRA_HOSTS_START
$GNS3_EXTRA_HOSTS
# GNS3_EXTRA_HOSTS_END
__EOF__
# configure loopback interface # configure loopback interface
ip link set dev lo up ip link set dev lo up

View File

@ -25,13 +25,13 @@ import zipfile
import json import json
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from .port_manager import PortManager from .port_manager import PortManager
from .notification_manager import NotificationManager from .notification_manager import NotificationManager
from ..config import Config from ..config import Config
from ..utils.asyncio import wait_run_in_executor from ..utils.asyncio import wait_run_in_executor
from ..utils.path import check_path_allowed, get_default_project_directory from ..utils.path import check_path_allowed, get_default_project_directory
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -46,7 +46,7 @@ class Project:
:param path: path of the project. (None use the standard directory) :param path: path of the project. (None use the standard directory)
""" """
def __init__(self, name=None, project_id=None, path=None): def __init__(self, name=None, project_id=None, path=None, variables=None):
self._name = name self._name = name
if project_id: if project_id:
@ -61,6 +61,7 @@ class Project:
self._nodes = set() self._nodes = set()
self._used_tcp_ports = set() self._used_tcp_ports = set()
self._used_udp_ports = set() self._used_udp_ports = set()
self._variables = variables
if path is None: if path is None:
location = get_default_project_directory() location = get_default_project_directory()
@ -83,7 +84,8 @@ class Project:
return { return {
"name": self._name, "name": self._name,
"project_id": self._id "project_id": self._id,
"variables": self._variables
} }
def _config(self): def _config(self):
@ -131,6 +133,14 @@ class Project:
return self._nodes return self._nodes
@property
def variables(self):
return self._variables
@variables.setter
def variables(self, variables):
self._variables = variables
def record_tcp_port(self, port): def record_tcp_port(self, port):
""" """
Associate a reserved TCP port number with this project. Associate a reserved TCP port number with this project.
@ -287,6 +297,17 @@ class Project:
yield from node.delete() yield from node.delete()
self._nodes.remove(node) self._nodes.remove(node)
@asyncio.coroutine
def update(self, variables=None, **kwargs):
original_variables = self.variables
self.variables = variables
# we need to update docker nodes when variables changes
if original_variables != variables:
for node in self.nodes:
if hasattr(node, 'update'):
yield from node.update()
@asyncio.coroutine @asyncio.coroutine
def close(self): def close(self):
""" """

View File

@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import aiohttp import aiohttp
import asyncio
import psutil import psutil
import platform import platform
from .project import Project from .project import Project
@ -95,16 +96,16 @@ class ProjectManager:
log.warning(message) log.warning(message)
project.emit("log.warning", {"message": message}) project.emit("log.warning", {"message": message})
def create_project(self, name=None, project_id=None, path=None): def create_project(self, name=None, project_id=None, path=None, variables=None):
""" """
Create a project and keep a references to it in project manager. Create a project and keep a references to it in project manager.
See documentation of Project for arguments See documentation of Project for arguments
""" """
if project_id is not None and project_id in self._projects: if project_id is not None and project_id in self._projects:
return self._projects[project_id] return self._projects[project_id]
project = Project(name=name, project_id=project_id, path=path) project = Project(name=name, project_id=project_id,
path=path, variables=variables)
self._check_available_disk_space(project) self._check_available_disk_space(project)
self._projects[project.id] = project self._projects[project.id] = project
return project return project

View File

@ -460,7 +460,6 @@ class Compute:
msg = json.loads(response.data) msg = json.loads(response.data)
action = msg.pop("action") action = msg.pop("action")
event = msg.pop("event") event = msg.pop("event")
if action == "ping": if action == "ping":
self._cpu_usage_percent = event["cpu_usage_percent"] self._cpu_usage_percent = event["cpu_usage_percent"]
self._memory_usage_percent = event["memory_usage_percent"] self._memory_usage_percent = event["memory_usage_percent"]

View File

@ -69,7 +69,7 @@ class Project:
def __init__(self, name=None, project_id=None, path=None, controller=None, status="opened", def __init__(self, name=None, project_id=None, path=None, controller=None, status="opened",
filename=None, auto_start=False, auto_open=False, auto_close=True, filename=None, auto_start=False, auto_open=False, auto_close=True,
scene_height=1000, scene_width=2000, zoom=100, show_layers=False, snap_to_grid=False, show_grid=False, scene_height=1000, scene_width=2000, zoom=100, show_layers=False, snap_to_grid=False, show_grid=False,
grid_size=0, show_interface_labels=False): grid_size=0, show_interface_labels=False, variables=None, supplier=None):
self._controller = controller self._controller = controller
assert name is not None assert name is not None
@ -86,6 +86,9 @@ class Project:
self._show_grid = show_grid self._show_grid = show_grid
self._grid_size = grid_size self._grid_size = grid_size
self._show_interface_labels = show_interface_labels self._show_interface_labels = show_interface_labels
self._variables = variables
self._supplier = supplier
self._loading = False self._loading = False
# Disallow overwrite of existing project # Disallow overwrite of existing project
@ -134,6 +137,15 @@ class Project:
self.controller.notification.emit("project.updated", self.__json__()) self.controller.notification.emit("project.updated", self.__json__())
self.dump() self.dump()
# update on computes
for compute in list(self._project_created_on_compute):
yield from compute.put(
"/projects/{}".format(self._id), {
"variables": self.variables
}
)
def reset(self): def reset(self):
""" """
Called when open/close a project. Cleanup internal stuff Called when open/close a project. Cleanup internal stuff
@ -267,6 +279,36 @@ class Project:
""" """
self._show_interface_labels = show_interface_labels self._show_interface_labels = show_interface_labels
@property
def variables(self):
"""
Variables applied to the project
:return: list
"""
return self._variables
@variables.setter
def variables(self, variables):
"""
Setter for variables applied to the project
"""
self._variables = variables
@property
def supplier(self):
"""
Supplier of the project
:return: dict
"""
return self._supplier
@supplier.setter
def supplier(self, supplier):
"""
Setter for supplier of the project
"""
self._supplier = supplier
@property @property
def auto_start(self): def auto_start(self):
""" """
@ -461,12 +503,14 @@ class Project:
yield from compute.post("/projects", data={ yield from compute.post("/projects", data={
"name": self._name, "name": self._name,
"project_id": self._id, "project_id": self._id,
"path": self._path "path": self._path,
"variables": self._variables
}) })
else: else:
yield from compute.post("/projects", data={ yield from compute.post("/projects", data={
"name": self._name, "name": self._name,
"project_id": self._id, "project_id": self._id,
"variables": self._variables
}) })
self._project_created_on_compute.add(compute) self._project_created_on_compute.add(compute)
@ -676,6 +720,15 @@ class Project:
except KeyError: except KeyError:
pass pass
# don't remove supplier's logo
if self.supplier:
try:
logo = self.supplier['logo']
pictures.remove(logo)
except KeyError:
pass
for pict in pictures: for pict in pictures:
os.remove(os.path.join(self.pictures_directory, pict)) os.remove(os.path.join(self.pictures_directory, pict))
except OSError as e: except OSError as e:
@ -1004,7 +1057,9 @@ class Project:
"snap_to_grid": self._snap_to_grid, "snap_to_grid": self._snap_to_grid,
"show_grid": self._show_grid, "show_grid": self._show_grid,
"grid_size": self._grid_size, "grid_size": self._grid_size,
"show_interface_labels": self._show_interface_labels "show_interface_labels": self._show_interface_labels,
"supplier": self._supplier,
"variables": self._variables
} }
def __repr__(self): def __repr__(self):

View File

@ -85,6 +85,8 @@ def project_to_topology(project):
"show_grid": project.show_grid, "show_grid": project.show_grid,
"grid_size": project.grid_size, "grid_size": project.grid_size,
"show_interface_labels": project.show_interface_labels, "show_interface_labels": project.show_interface_labels,
"variables": project.variables,
"supplier": project.supplier,
"topology": { "topology": {
"nodes": [], "nodes": [],
"links": [], "links": [],

View File

@ -60,7 +60,8 @@ class DockerHandler:
console_resolution=request.json.get("console_resolution", "1024x768"), console_resolution=request.json.get("console_resolution", "1024x768"),
console_http_port=request.json.get("console_http_port", 80), console_http_port=request.json.get("console_http_port", 80),
console_http_path=request.json.get("console_http_path", "/"), console_http_path=request.json.get("console_http_path", "/"),
aux=request.json.get("aux")) aux=request.json.get("aux"),
extra_hosts=request.json.get("extra_hosts"))
for name, value in request.json.items(): for name, value in request.json.items():
if name != "node_id": if name != "node_id":
if hasattr(container, name) and getattr(container, name) != value: if hasattr(container, name) and getattr(container, name) != value:
@ -312,7 +313,7 @@ class DockerHandler:
props = [ props = [
"name", "console", "aux", "console_type", "console_resolution", "name", "console", "aux", "console_type", "console_resolution",
"console_http_port", "console_http_path", "start_command", "console_http_port", "console_http_path", "start_command",
"environment", "adapters" "environment", "adapters", "extra_hosts"
] ]
changed = False changed = False

View File

@ -73,11 +73,31 @@ class ProjectHandler:
p = pm.create_project( p = pm.create_project(
name=request.json.get("name"), name=request.json.get("name"),
path=request.json.get("path"), path=request.json.get("path"),
project_id=request.json.get("project_id") project_id=request.json.get("project_id"),
variables=request.json.get("variables", None)
) )
response.set_status(201) response.set_status(201)
response.json(p) response.json(p)
@Route.put(
r"/projects/{project_id}",
description="Update the project on the server",
status_codes={
201: "Project updated",
403: "Forbidden to update a project"
},
output=PROJECT_OBJECT_SCHEMA,
input=PROJECT_UPDATE_SCHEMA)
def update_project(request, response):
pm = ProjectManager.instance()
project = pm.get_project(request.match_info["project_id"])
yield from project.update(
variables=request.json.get("variables", None)
)
response.set_status(200)
response.json(project)
@Route.get( @Route.get(
r"/projects/{project_id}", r"/projects/{project_id}",
description="Get project information", description="Get project information",

View File

@ -87,6 +87,11 @@ DOCKER_CREATE_SCHEMA = {
"type": ["string", "null"], "type": ["string", "null"],
"minLength": 0, "minLength": 0,
}, },
"extra_hosts": {
"description": "Docker extra hosts (added to /etc/hosts)",
"type": ["string", "null"],
"minLength": 0,
},
"container_id": { "container_id": {
"description": "Docker container ID Read only", "description": "Docker container ID Read only",
"type": "string", "type": "string",
@ -184,6 +189,11 @@ DOCKER_OBJECT_SCHEMA = {
"type": ["string", "null"], "type": ["string", "null"],
"minLength": 0, "minLength": 0,
}, },
"extra_hosts": {
"description": "Docker extra hosts (added to /etc/hosts)",
"type": ["string", "null"],
"minLength": 0,
},
"node_directory": { "node_directory": {
"description": "Path to the node working directory Read only", "description": "Path to the node working directory Read only",
"type": "string" "type": "string"

View File

@ -15,6 +15,40 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
SUPPLIER_OBJECT_SCHEMA = {
"type": ["object", "null"],
"description": "Supplier of the project",
"properties": {
"logo": {
"type": "string",
"description": "Path to the project supplier logo"
},
"url": {
"type": "string",
"description": "URL to the project supplier site"
}
}
}
VARIABLES_OBJECT_SCHEMA = {
"type": ["array", "null"],
"description": "Variables required to run the project",
"items": {
"properties": {
"name": {
"type": "string",
"description": "Variable name"
},
"value": {
"type": "string",
"description": "Variable value"
}
},
"required": ["name"]
}
}
PROJECT_CREATE_SCHEMA = { PROJECT_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
@ -73,7 +107,9 @@ PROJECT_CREATE_SCHEMA = {
"show_interface_labels": { "show_interface_labels": {
"type": "boolean", "type": "boolean",
"description": "Show interface labels on the drawing area" "description": "Show interface labels on the drawing area"
} },
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
}, },
"additionalProperties": False, "additionalProperties": False,
"required": ["name"] "required": ["name"]
@ -136,7 +172,9 @@ PROJECT_UPDATE_SCHEMA = {
"show_interface_labels": { "show_interface_labels": {
"type": "boolean", "type": "boolean",
"description": "Show interface labels on the drawing area" "description": "Show interface labels on the drawing area"
} },
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
}, },
"additionalProperties": False, "additionalProperties": False,
} }
@ -215,7 +253,9 @@ PROJECT_OBJECT_SCHEMA = {
"show_interface_labels": { "show_interface_labels": {
"type": "boolean", "type": "boolean",
"description": "Show interface labels on the drawing area" "description": "Show interface labels on the drawing area"
} },
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA
}, },
"additionalProperties": False, "additionalProperties": False,
"required": ["project_id"] "required": ["project_id"]

View File

@ -23,6 +23,8 @@ from gns3server.schemas.compute import COMPUTE_OBJECT_SCHEMA
from gns3server.schemas.drawing import DRAWING_OBJECT_SCHEMA from gns3server.schemas.drawing import DRAWING_OBJECT_SCHEMA
from gns3server.schemas.link import LINK_OBJECT_SCHEMA from gns3server.schemas.link import LINK_OBJECT_SCHEMA
from gns3server.schemas.node import NODE_OBJECT_SCHEMA from gns3server.schemas.node import NODE_OBJECT_SCHEMA
from gns3server.schemas.project import VARIABLES_OBJECT_SCHEMA
from gns3server.schemas.project import SUPPLIER_OBJECT_SCHEMA
TOPOLOGY_SCHEMA = { TOPOLOGY_SCHEMA = {
@ -97,6 +99,8 @@ TOPOLOGY_SCHEMA = {
"type": "boolean", "type": "boolean",
"description": "Show interface labels on the drawing area" "description": "Show interface labels on the drawing area"
}, },
"supplier": SUPPLIER_OBJECT_SCHEMA,
"variables": VARIABLES_OBJECT_SCHEMA,
"topology": { "topology": {
"description": "The topology content", "description": "The topology content",
"type": "object", "type": "object",

View File

@ -61,6 +61,7 @@ def test_json(vm, project):
'console_resolution': '1024x768', 'console_resolution': '1024x768',
'console_http_port': 80, 'console_http_port': 80,
'console_http_path': '/', 'console_http_path': '/',
'extra_hosts': None,
'aux': vm.aux, 'aux': vm.aux,
'start_command': vm.start_command, 'start_command': vm.start_command,
'environment': vm.environment, 'environment': vm.environment,
@ -202,6 +203,77 @@ def test_create_vnc(loop, project, manager):
assert vm._console_type == "vnc" assert vm._console_type == "vnc"
def test_create_with_extra_hosts(loop, project, manager):
extra_hosts = "test:199.199.199.1\ntest2:199.199.199.1"
response = {
"Id": "e90e34656806",
"Warnings": []
}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", extra_hosts=extra_hosts)
loop.run_until_complete(asyncio.async(vm.create()))
called_kwargs = mock.call_args[1]
assert "GNS3_EXTRA_HOSTS=199.199.199.1\ttest\n199.199.199.1\ttest2" in called_kwargs["data"]["Env"]
assert vm._extra_hosts == extra_hosts
def test_create_with_extra_hosts_wrong_format(loop, project, manager):
extra_hosts = "test"
response = {
"Id": "e90e34656806",
"Warnings": []
}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response):
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", extra_hosts=extra_hosts)
with pytest.raises(DockerError):
loop.run_until_complete(asyncio.async(vm.create()))
def test_create_with_empty_extra_hosts(loop, project, manager):
extra_hosts = "test:\n"
response = {
"Id": "e90e34656806",
"Warnings": []
}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", extra_hosts=extra_hosts)
loop.run_until_complete(asyncio.async(vm.create()))
called_kwargs = mock.call_args[1]
assert len([ e for e in called_kwargs["data"]["Env"] if "GNS3_EXTRA_HOSTS" in e]) == 0
def test_create_with_project_variables(loop, project, manager):
response = {
"Id": "e90e34656806",
"Warnings": []
}
project.variables = [
{"name": "VAR1"},
{"name": "VAR2", "value": "VAL1"},
{"name": "VAR3", "value": "2x${VAR2}"}
]
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu")
loop.run_until_complete(asyncio.async(vm.create()))
called_kwargs = mock.call_args[1]
assert "VAR1=" in called_kwargs["data"]["Env"]
assert "VAR2=VAL1" in called_kwargs["data"]["Env"]
assert "VAR3=2xVAL1" in called_kwargs["data"]["Env"]
project.variables = None
def test_create_start_cmd(loop, project, manager): def test_create_start_cmd(loop, project, manager):
response = { response = {

View File

@ -92,9 +92,29 @@ def test_changing_path_not_allowed(tmpdir):
p.path = str(tmpdir) p.path = str(tmpdir)
def test_variables(tmpdir):
variables = [{"name": "VAR1", "value": "VAL1"}]
p = Project(project_id=str(uuid4()), variables=variables)
assert p.variables == variables
def test_json(tmpdir): def test_json(tmpdir):
p = Project(project_id=str(uuid4())) p = Project(project_id=str(uuid4()))
assert p.__json__() == {"name": p.name, "project_id": p.id} assert p.__json__() == {
"name": p.name,
"project_id": p.id,
"variables": None
}
def test_json_with_variables(tmpdir):
variables = [{"name": "VAR1", "value": "VAL1"}]
p = Project(project_id=str(uuid4()), variables=variables)
assert p.__json__() == {
"name": p.name,
"project_id": p.id,
"variables": variables
}
def test_node_working_directory(tmpdir, node): def test_node_working_directory(tmpdir, node):
@ -185,3 +205,10 @@ def test_emit(async_run):
(action, event, context) = async_run(queue.get(0.5)) (action, event, context) = async_run(queue.get(0.5))
assert action == "test" assert action == "test"
assert context["project_id"] == project.id assert context["project_id"] == project.id
def test_update_project(loop):
variables = [{"name": "TEST", "value": "VAL"}]
project = Project(project_id=str(uuid.uuid4()))
loop.run_until_complete(asyncio.async(project.update(variables=variables)))
assert project.variables == variables

View File

@ -77,19 +77,35 @@ def test_json(tmpdir):
"show_layers": False, "show_layers": False,
"snap_to_grid": False, "snap_to_grid": False,
"grid_size": 0, "grid_size": 0,
"supplier": None,
"variables": None
} }
def test_update(controller, async_run): def test_update(controller, async_run):
project = Project(controller=controller, name="Hello") project = Project(controller=controller, name="Hello")
controller._notification = MagicMock() controller._notification = MagicMock()
assert project.name == "Hello" assert project.name == "Hello"
async_run(project.update(name="World")) async_run(project.update(name="World"))
assert project.name == "World" assert project.name == "World"
controller.notification.emit.assert_any_call("project.updated", project.__json__()) controller.notification.emit.assert_any_call("project.updated", project.__json__())
def test_update_on_compute(controller, async_run):
variables = [{"name": "TEST", "value": "VAL1"}]
compute = MagicMock()
compute.id = "local"
project = Project(controller=controller, name="Test")
project._project_created_on_compute = [compute]
controller._notification = MagicMock()
async_run(project.update(variables=variables))
compute.put.assert_any_call('/projects/{}'.format(project.id), {
"variables": variables
})
def test_path(tmpdir): def test_path(tmpdir):
directory = Config.instance().get_section_config("Server").get("projects_path") directory = Config.instance().get_section_config("Server").get("projects_path")
@ -148,7 +164,8 @@ def test_add_node_local(async_run, controller):
compute.post.assert_any_call('/projects', data={ compute.post.assert_any_call('/projects', data={
"name": project._name, "name": project._name,
"project_id": project._id, "project_id": project._id,
"path": project._path "path": project._path,
"variables": None
}) })
compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id), compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id),
data={'node_id': node.id, data={'node_id': node.id,
@ -176,7 +193,8 @@ def test_add_node_non_local(async_run, controller):
compute.post.assert_any_call('/projects', data={ compute.post.assert_any_call('/projects', data={
"name": project._name, "name": project._name,
"project_id": project._id "project_id": project._id,
"variables": None
}) })
compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id), compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id),
data={'node_id': node.id, data={'node_id': node.id,
@ -216,7 +234,8 @@ def test_add_node_from_appliance(async_run, controller):
compute.post.assert_any_call('/projects', data={ compute.post.assert_any_call('/projects', data={
"name": project._name, "name": project._name,
"project_id": project._id, "project_id": project._id,
"path": project._path "path": project._path,
"variables": None
}) })
compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id), compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id),
data={'node_id': node.id, data={'node_id': node.id,
@ -395,6 +414,26 @@ def test_clean_pictures(async_run, project, controller):
assert not os.path.exists(os.path.join(project.pictures_directory, "test2.png")) assert not os.path.exists(os.path.join(project.pictures_directory, "test2.png"))
def test_clean_pictures_and_keep_supplier_logo(async_run, project, controller):
"""
When a project is close old pictures should be removed
"""
project.supplier = {
'logo': 'logo.png'
}
drawing = async_run(project.add_drawing())
drawing._svg = "test.png"
open(os.path.join(project.pictures_directory, "test.png"), "w+").close()
open(os.path.join(project.pictures_directory, "test2.png"), "w+").close()
open(os.path.join(project.pictures_directory, "logo.png"), "w+").close()
async_run(project.close())
assert os.path.exists(os.path.join(project.pictures_directory, "test.png"))
assert not os.path.exists(os.path.join(project.pictures_directory, "test2.png"))
assert os.path.exists(os.path.join(project.pictures_directory, "logo.png"))
def test_delete(async_run, project, controller): def test_delete(async_run, project, controller):
assert os.path.exists(project.path) assert os.path.exists(project.path)
async_run(project.delete()) async_run(project.delete())

View File

@ -53,6 +53,8 @@ def test_project_to_topology_empty(tmpdir):
"drawings": [] "drawings": []
}, },
"type": "topology", "type": "topology",
"supplier": None,
"variables": None,
"version": __version__ "version": __version__
} }
@ -81,6 +83,26 @@ def test_basic_topology(tmpdir, async_run, controller):
assert topo["topology"]["drawings"][0] == drawing.__json__(topology_dump=True) assert topo["topology"]["drawings"][0] == drawing.__json__(topology_dump=True)
def test_project_to_topology(tmpdir, controller):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
supplier = {
'logo': 'logo.png',
'url': 'http://example.com'
}
project = Project(name="Test", controller=controller)
compute = Compute("my_compute", controller)
compute.http_query = MagicMock()
project.variables = variables
project.supplier = supplier
topo = project_to_topology(project)
assert topo["variables"] == variables
assert topo["supplier"] == supplier
def test_load_topology(tmpdir): def test_load_topology(tmpdir):
data = { data = {
"project_id": "69f26504-7aa3-48aa-9f29-798d44841211", "project_id": "69f26504-7aa3-48aa-9f29-798d44841211",
@ -137,3 +159,55 @@ def test_load_newer_topology(tmpdir):
json.dump(data, f) json.dump(data, f)
with pytest.raises(aiohttp.web.HTTPConflict): with pytest.raises(aiohttp.web.HTTPConflict):
topo = load_topology(path) topo = load_topology(path)
def test_load_topology_with_variables(tmpdir):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
data = {
"project_id": "69f26504-7aa3-48aa-9f29-798d44841211",
"name": "Test",
"revision": GNS3_FILE_FORMAT_REVISION,
"topology": {
"nodes": [],
"links": [],
"computes": [],
"drawings": []
},
"variables": variables,
"type": "topology",
"version": __version__}
path = str(tmpdir / "test.gns3")
with open(path, "w+") as f:
json.dump(data, f)
topo = load_topology(path)
assert topo == data
def test_load_topology_with_supplier(tmpdir):
supplier = {
'logo': 'logo.png',
'url': 'http://example.com'
}
data = {
"project_id": "69f26504-7aa3-48aa-9f29-798d44841211",
"name": "Test",
"revision": GNS3_FILE_FORMAT_REVISION,
"topology": {
"nodes": [],
"links": [],
"computes": [],
"drawings": []
},
"supplier": supplier,
"type": "topology",
"version": __version__}
path = str(tmpdir / "test.gns3")
with open(path, "w+") as f:
json.dump(data, f)
topo = load_topology(path)
assert topo == data

View File

@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supp
@pytest.fixture @pytest.fixture
def base_params(): def base_params():
"""Return standard parameters""" """Return standard parameters"""
return {"name": "PC TEST 1", "image": "nginx", "start_command": "nginx-daemon", "adapters": 2, "environment": "YES=1\nNO=0", "console_type": "telnet", "console_resolution": "1280x1024"} return {"name": "PC TEST 1", "image": "nginx", "start_command": "nginx-daemon", "adapters": 2, "environment": "YES=1\nNO=0", "console_type": "telnet", "console_resolution": "1280x1024", "extra_hosts": "test:127.0.0.1"}
@pytest.yield_fixture(autouse=True) @pytest.yield_fixture(autouse=True)
@ -69,7 +69,7 @@ def test_docker_create(http_compute, project, base_params):
assert response.json["adapters"] == 2 assert response.json["adapters"] == 2
assert response.json["environment"] == "YES=1\nNO=0" assert response.json["environment"] == "YES=1\nNO=0"
assert response.json["console_resolution"] == "1280x1024" assert response.json["console_resolution"] == "1280x1024"
assert response.json["extra_hosts"] == "test:127.0.0.1"
def test_docker_start(http_compute, vm): def test_docker_start(http_compute, vm):
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.start", return_value=True) as mock: with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.start", return_value=True) as mock:
@ -150,7 +150,8 @@ def test_docker_update(http_compute, vm, tmpdir, free_console_port):
response = http_compute.put("/projects/{project_id}/docker/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"name": "test", response = http_compute.put("/projects/{project_id}/docker/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"name": "test",
"console": free_console_port, "console": free_console_port,
"start_command": "yes", "start_command": "yes",
"environment": "GNS3=1\nGNS4=0"}, "environment": "GNS3=1\nGNS4=0",
"extra_hosts": "test:127.0.0.1"},
example=True) example=True)
assert mock.called assert mock.called
assert response.status == 200 assert response.status == 200
@ -158,6 +159,7 @@ def test_docker_update(http_compute, vm, tmpdir, free_console_port):
assert response.json["console"] == free_console_port assert response.json["console"] == free_console_port
assert response.json["start_command"] == "yes" assert response.json["start_command"] == "yes"
assert response.json["environment"] == "GNS3=1\nGNS4=0" assert response.json["environment"] == "GNS3=1\nGNS4=0"
assert response.json["extra_hosts"] == "test:127.0.0.1"
def test_docker_start_capture(http_compute, vm, tmpdir, project): def test_docker_start_capture(http_compute, vm, tmpdir, project):

View File

@ -21,9 +21,6 @@ This test suite check /project endpoint
import uuid import uuid
import os import os
import asyncio
import aiohttp
import zipfile
from unittest.mock import patch from unittest.mock import patch
from tests.utils import asyncio_patch from tests.utils import asyncio_patch
@ -60,9 +57,10 @@ def test_show_project(http_compute):
response = http_compute.post("/projects", query) response = http_compute.post("/projects", query)
assert response.status == 201 assert response.status == 201
response = http_compute.get("/projects/40010203-0405-0607-0809-0a0b0c0d0e02", example=True) response = http_compute.get("/projects/40010203-0405-0607-0809-0a0b0c0d0e02", example=True)
assert len(response.json.keys()) == 2 assert len(response.json.keys()) == 3
assert response.json["project_id"] == "40010203-0405-0607-0809-0a0b0c0d0e02" assert response.json["project_id"] == "40010203-0405-0607-0809-0a0b0c0d0e02"
assert response.json["name"] == "test" assert response.json["name"] == "test"
assert response.json["variables"] is None
def test_show_project_invalid_uuid(http_compute): def test_show_project_invalid_uuid(http_compute):
@ -93,6 +91,23 @@ def test_delete_project(http_compute, project):
assert mock.called assert mock.called
def test_update_project(http_compute):
query = {"name": "test", "project_id": "51010203-0405-0607-0809-0a0b0c0d0e0f"}
response = http_compute.post("/projects", query)
assert response.status == 201
query = {
"variables": [{"name": "TEST1", "value": "VAL1"}]
}
response = http_compute.put(
"/projects/{project_id}".format(project_id="51010203-0405-0607-0809-0a0b0c0d0e0f"),
query,
example=True
)
assert response.status == 200
assert response.json["variables"] == [{"name": "TEST1", "value": "VAL1"}]
def test_delete_project_invalid_uuid(http_compute): def test_delete_project_invalid_uuid(http_compute):
response = http_compute.delete("/projects/{project_id}".format(project_id=uuid.uuid4())) response = http_compute.delete("/projects/{project_id}".format(project_id=uuid.uuid4()))
assert response.status == 404 assert response.status == 404

View File

@ -67,6 +67,31 @@ def test_create_project_with_uuid(http_controller):
assert response.json["name"] == "test" assert response.json["name"] == "test"
def test_create_project_with_variables(http_controller):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
query = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables}
response = http_controller.post("/projects", query)
assert response.status == 201
assert response.json["variables"] == [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
def test_create_project_with_supplier(http_controller):
supplier = {
'logo': 'logo.png',
'url': 'http://example.com'
}
query = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "supplier": supplier}
response = http_controller.post("/projects", query)
assert response.status == 201
assert response.json["supplier"] == supplier
def test_update_project(http_controller): def test_update_project(http_controller):
query = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f"} query = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f"}
response = http_controller.post("/projects", query) response = http_controller.post("/projects", query)
@ -79,6 +104,20 @@ def test_update_project(http_controller):
assert response.json["name"] == "test2" assert response.json["name"] == "test2"
def test_update_project_with_variables(http_controller):
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
query = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables}
response = http_controller.post("/projects", query)
assert response.status == 201
query = {"name": "test2"}
response = http_controller.put("/projects/10010203-0405-0607-0809-0a0b0c0d0e0f", query, example=True)
assert response.status == 200
assert response.json["variables"] == variables
def test_list_projects(http_controller, tmpdir): def test_list_projects(http_controller, tmpdir):
http_controller.post("/projects", {"name": "test", "path": str(tmpdir), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"}) http_controller.post("/projects", {"name": "test", "path": str(tmpdir), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"})
response = http_controller.get("/projects", example=True) response = http_controller.get("/projects", example=True)