diff --git a/docs/general.rst b/docs/general.rst index f3e66c85..3e8a2ca9 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -29,7 +29,7 @@ You can check the server version with a simple curl command: .. code-block:: shell-session - # curl "http://localhost:8000/v1/version" + # curl "http://localhost:3080/v1/version" { "version": "1.3.dev1" } @@ -39,7 +39,7 @@ The next step is to create a project. .. code-block:: shell-session - # curl -X POST "http://localhost:8000/v1/projects" -d '{"name": "test"}' + # curl -X POST "http://localhost:3080/v1/projects" -d '{"name": "test"}' { "project_id": "42f9feee-3217-4104-981e-85d5f0a806ec", "temporary": false, @@ -50,7 +50,7 @@ With this project id we can now create two VPCS VM. .. code-block:: shell-session - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms" -d '{"name": "VPCS 1"}' + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms" -d '{"name": "VPCS 1"}' { "console": 2000, "name": "VPCS 1", @@ -58,7 +58,7 @@ With this project id we can now create two VPCS VM. "vm_id": "24d2e16b-fbef-4259-ae34-7bc21a41ee28" }% - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms" -d '{"name": "VPCS 2"}' + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms" -d '{"name": "VPCS 2"}' { "console": 2001, "name": "VPCS 2", @@ -70,12 +70,12 @@ two UDP ports. .. code-block:: shell-session - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/ports/udp" -d '{}' + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/ports/udp" -d '{}' { "udp_port": 10000 } - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/ports/udp" -d '{}' + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/ports/udp" -d '{}' { "udp_port": 10001 } @@ -86,7 +86,7 @@ communication is made by creating two UDP tunnels. .. code-block:: shell-session - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/24d2e16b-fbef-4259-ae34-7bc21a41ee28/adapters/0/ports/0/nio" -d '{"lport": 10000, "rhost": "127.0.0.1", "rport": 10001, "type": "nio_udp"}' + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/24d2e16b-fbef-4259-ae34-7bc21a41ee28/adapters/0/ports/0/nio" -d '{"lport": 10000, "rhost": "127.0.0.1", "rport": 10001, "type": "nio_udp"}' { "lport": 10000, "rhost": "127.0.0.1", @@ -94,7 +94,7 @@ communication is made by creating two UDP tunnels. "type": "nio_udp" } - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/daefc24a-103c-4717-8e01-6517d931c1ae/adapters/0/ports/0/nio" -d '{"lport": 10001, "rhost": "127.0.0.1", "rport": 10000, "type": "nio_udp"}' + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/daefc24a-103c-4717-8e01-6517d931c1ae/adapters/0/ports/0/nio" -d '{"lport": 10001, "rhost": "127.0.0.1", "rport": 10000, "type": "nio_udp"}' { "lport": 10001, "rhost": "127.0.0.1", @@ -106,8 +106,8 @@ Now we can start the two VM .. code-block:: shell-session - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/24d2e16b-fbef-4259-ae34-7bc21a41ee28/start" -d "{}" - # curl -X POST "http://localhost:8000/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/daefc24a-103c-4717-8e01-6517d931c1ae/start" -d '{}' + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/24d2e16b-fbef-4259-ae34-7bc21a41ee28/start" -d "{}" + # curl -X POST "http://localhost:3080/v1/projects/42f9feee-3217-4104-981e-85d5f0a806ec/vpcs/vms/daefc24a-103c-4717-8e01-6517d931c1ae/start" -d '{}' Everything should be started now. You can connect via telnet to the different VM. The port is the field console in the create VM request. @@ -190,7 +190,7 @@ complexity for the client due to the fact only some command on some VM can be concurrent. -Authentification +Authentication ----------------- In this version of the API you have no authentification system. If you diff --git a/gns3server/handlers/api/hypervisor/docker_handler.py b/gns3server/handlers/api/hypervisor/docker_handler.py index 3d449394..6964fb26 100644 --- a/gns3server/handlers/api/hypervisor/docker_handler.py +++ b/gns3server/handlers/api/hypervisor/docker_handler.py @@ -73,6 +73,7 @@ class DockerHandler: adapters=request.json.get("adapters"), console=request.json.get("console"), console_type=request.json.get("console_type"), + console_resolution=request.json.get("console_resolution", "1024x768"), aux=request.json.get("aux") ) for name, value in request.json.items(): @@ -277,8 +278,11 @@ class DockerHandler: vm = docker_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.name = request.json.get("name", vm.name) vm.console = request.json.get("console", vm.console) + vm.aux = request.json.get("aux", vm.aux) + vm.console_resolution = request.json.get("console_resolution", vm.console_resolution) vm.start_command = request.json.get("start_command", vm.start_command) vm.environment = request.json.get("environment", vm.environment) + vm.adapters = request.json.get("adapters", vm.adapters) yield from vm.update() response.json(vm) diff --git a/gns3server/handlers/api/hypervisor/project_handler.py b/gns3server/handlers/api/hypervisor/project_handler.py index 29c6bc82..a348b34d 100644 --- a/gns3server/handlers/api/hypervisor/project_handler.py +++ b/gns3server/handlers/api/hypervisor/project_handler.py @@ -20,6 +20,7 @@ import asyncio import json import os import psutil +import tempfile from ....web.route import Route from ....schemas.project import PROJECT_OBJECT_SCHEMA, PROJECT_CREATE_SCHEMA, PROJECT_UPDATE_SCHEMA, PROJECT_FILE_LIST_SCHEMA, PROJECT_LIST_SCHEMA @@ -56,6 +57,7 @@ class ProjectHandler: description="Create a new project on the server", status_codes={ 201: "Project created", + 403: "You are not allowed to modify this property", 409: "Project already created" }, output=PROJECT_OBJECT_SCHEMA, @@ -301,4 +303,111 @@ class ProjectHandler: except FileNotFoundError: raise aiohttp.web.HTTPNotFound() except PermissionError: + raise aiohttp.web.HTTPForbidden() + + @classmethod + @Route.post( + r"/projects/{project_id}/files/{path:.+}", + description="Get a file of a project", + parameters={ + "project_id": "The UUID of the project", + }, + raw=True, + status_codes={ + 200: "Return the file", + 403: "Permission denied", + 404: "The path doesn't exist" + }) + def write_file(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["project_id"]) + path = request.match_info["path"] + path = os.path.normpath(path) + + # Raise error if user try to escape + if path[0] == ".": raise aiohttp.web.HTTPForbidden + path = os.path.join(project.path, path) + + response.set_status(200) + + try: + with open(path, 'wb+') as f: + while True: + packet = yield from request.content.read(512) + if not packet: + break + f.write(packet) + + except FileNotFoundError: + raise aiohttp.web.HTTPNotFound() + except PermissionError: + raise aiohttp.web.HTTPForbidden() + + @classmethod + @Route.get( + r"/projects/{project_id}/export", + description="Export a project as a portable archive", + parameters={ + "project_id": "The UUID of the project", + }, + raw=True, + status_codes={ + 200: "Return the file", + 404: "The project doesn't exist" + }) + def export_project(request, response): + + pm = ProjectManager.instance() + project = pm.get_project(request.match_info["project_id"]) + response.content_type = 'application/gns3z' + response.headers['CONTENT-DISPOSITION'] = 'attachment; filename="{}.gns3z"'.format(project.name) + response.enable_chunked_encoding() + # Very important: do not send a content length otherwise QT close the connection but curl can consume the Feed + response.content_length = None + response.start(request) + + for data in project.export(): + response.write(data) + yield from response.drain() + + yield from response.write_eof() + + @classmethod + @Route.post( + r"/projects/{project_id}/import", + description="Import a project from a portable archive", + parameters={ + "project_id": "The UUID of the project", + }, + raw=True, + output=PROJECT_OBJECT_SCHEMA, + status_codes={ + 200: "Project imported", + 403: "You are not allowed to modify this property" + }) + def import_project(request, response): + + pm = ProjectManager.instance() + project_id = request.match_info["project_id"] + project = pm.create_project(project_id=project_id) + + # We write the content to a temporary location + # and after extract all. It could be more optimal to stream + # this but it's not implemented in Python. + #  + # Spooled mean the file is temporary keep in ram until max_size + try: + with tempfile.SpooledTemporaryFile(max_size=10000) as temp: + while True: + packet = yield from request.content.read(512) + if not packet: + break + temp.write(packet) + project.import_zip(temp, gns3vm=bool(request.GET.get("gns3vm", "1"))) + except OSError as e: + raise aiohttp.web.HTTPInternalServerError(text="Could not import the project: {}".format(e)) + + response.json(project) + response.set_status(201) diff --git a/gns3server/hypervisor/base_vm.py b/gns3server/hypervisor/base_vm.py index 154ad05f..f355ab14 100644 --- a/gns3server/hypervisor/base_vm.py +++ b/gns3server/hypervisor/base_vm.py @@ -340,13 +340,17 @@ class BaseVM: return if self._console_type == "vnc" and console is not None and console < 5900: - raise VMError("VNC console require a port superior or equal to 5900") + raise VMError("VNC console require a port superior or equal to 5900 currently it's {}".format(console)) if self._console: self._manager.port_manager.release_tcp_port(self._console, self._project) self._console = None if console is not None: - self._console = self._manager.port_manager.reserve_tcp_port(console, self._project) + if self.console_type == "vnc": + self._console = self._manager.port_manager.reserve_tcp_port(console, self._project, port_range_start=5900, port_range_end=6000) + else: + self._console = self._manager.port_manager.reserve_tcp_port(console, self._project) + log.info("{module}: '{name}' [{id}]: console port set to {port}".format(module=self.manager.module_name, name=self.name, id=self.id, @@ -401,7 +405,7 @@ class BaseVM: if path == "ubridge": path = shutil.which("ubridge") - if path is None: + if path is None or len(path) == 0: raise VMError("uBridge is not installed") return path diff --git a/gns3server/hypervisor/docker/docker_vm.py b/gns3server/hypervisor/docker/docker_vm.py index 367b780d..63c2f00b 100644 --- a/gns3server/hypervisor/docker/docker_vm.py +++ b/gns3server/hypervisor/docker/docker_vm.py @@ -53,11 +53,13 @@ class DockerVM(BaseVM): :param console: TCP console port :param console_type: Console type :param aux: TCP aux console port + :param console_resolution: Resolution of the VNC display """ def __init__(self, name, vm_id, project, manager, image, console=None, aux=None, start_command=None, - adapters=None, environment=None, console_type="telnet"): + adapters=None, environment=None, console_type="telnet", + console_resolution="1024x768"): super().__init__(name, vm_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type) self._image = image @@ -68,6 +70,8 @@ class DockerVM(BaseVM): self._ubridge_hypervisor = None self._temporary_directory = None self._telnet_servers = [] + self._x11vnc_process = None + self._console_resolution = console_resolution if adapters is None: self.adapters = 1 @@ -90,6 +94,7 @@ class DockerVM(BaseVM): "adapters": self.adapters, "console": self.console, "console_type": self.console_type, + "console_resolution": self.console_resolution, "aux": self.aux, "start_command": self.start_command, "environment": self.environment, @@ -120,6 +125,14 @@ class DockerVM(BaseVM): else: self._start_command = command + @property + def console_resolution(self): + return self._console_resolution + + @console_resolution.setter + def console_resolution(self, resolution): + self._console_resolution = resolution + @property def environment(self): return self._environment @@ -159,6 +172,10 @@ class DockerVM(BaseVM): binds.append("{}:/gns3:ro".format(get_resource("hypervisor/docker/resources"))) + # We mount our own etc/network + network_config = self._create_network_config() + binds.append("{}:/etc/network:rw".format(network_config)) + volumes = image_infos.get("ContainerConfig", {}).get("Volumes") if volumes is None: return binds @@ -169,6 +186,39 @@ class DockerVM(BaseVM): return binds + def _create_network_config(self): + """ + If network config is empty we create a sample config + """ + path = os.path.join(self.working_dir, "etc", "network") + os.makedirs(path, exist_ok=True) + os.makedirs(os.path.join(path, "if-up.d"), exist_ok=True) + os.makedirs(os.path.join(path, "if-down.d"), exist_ok=True) + os.makedirs(os.path.join(path, "if-pre-up.d"), exist_ok=True) + os.makedirs(os.path.join(path, "if-post-down.d"), exist_ok=True) + + if not os.path.exists(os.path.join(path, "interfaces")): + with open(os.path.join(path, "interfaces"), "w+") as f: + f.write("""# +# This is a sample network config uncomment lines to configure the network +# + +""") + for adapter in range(0, self.adapters): + f.write(""" +# Static config for eth{adapter} +#auto eth{adapter} +#iface eth{adapter} inet static +#\taddress 192.168.{adapter}.2 +#\tnetmask 255.255.255.0 +#\tgateway 192.168.{adapter}.1 +#\tup echo nameserver 192.168.{adapter}.1 > /etc/resolv.conf + +# DHCP config for eth{adapter} +# auto eth{adapter} +# iface eth{adapter} inet dhcp""".format(adapter=adapter)) + return path + @asyncio.coroutine def create(self): """Creates the Docker container.""" @@ -199,7 +249,6 @@ class DockerVM(BaseVM): "Entrypoint": image_infos.get("Config", {"Entrypoint": []})["Entrypoint"] } - if params["Entrypoint"] is None: params["Entrypoint"] = [] if self._start_command: @@ -233,11 +282,13 @@ class DockerVM(BaseVM): """ # We need to save the console and state and restore it console = self.console + aux = self.aux state = yield from self._get_container_state() yield from self.close() yield from self.create() self.console = console + self.aux = aux if state == "running": yield from self.start() @@ -287,7 +338,7 @@ class DockerVM(BaseVM): # We can not use the API because docker doesn't expose a websocket api for exec # https://github.com/GNS3/gns3-gui/issues/1039 process = yield from asyncio.subprocess.create_subprocess_exec( - "docker", "exec", "-i", self._cid, "/bin/sh", "-i", + "docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "sh", "-i", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, stdin=asyncio.subprocess.PIPE) @@ -304,8 +355,8 @@ class DockerVM(BaseVM): self._display = self._get_free_display_port() if shutil.which("Xvfb") is None or shutil.which("x11vnc") is None: raise DockerError("Please install Xvfb and x11vnc before using the VNC support") - self._xvfb_process = yield from asyncio.create_subprocess_exec("Xvfb", "-nolisten", "tcp", ":{}".format(self._display), "-screen", "0", "1024x768x16") - self._x11vnc_process = yield from asyncio.create_subprocess_exec("x11vnc", "-forever", "-nopw", "-display", "WAIT:{}".format(self._display), "-rfbport", str(self.console), "-noncache", "-listen", self._manager.port_manager.console_host) + self._xvfb_process = yield from asyncio.create_subprocess_exec("Xvfb", "-nolisten", "tcp", ":{}".format(self._display), "-screen", "0", self._console_resolution + "x16") + self._x11vnc_process = yield from asyncio.create_subprocess_exec("x11vnc", "-forever", "-nopw", "-shared", "-geometry", self._console_resolution, "-display", "WAIT:{}".format(self._display), "-rfbport", str(self.console), "-noncache", "-listen", self._manager.port_manager.console_host) x11_socket = os.path.join("/tmp/.X11-unix/", "X{}".format(self._display)) yield from wait_for_file_creation(x11_socket) @@ -433,10 +484,11 @@ class DockerVM(BaseVM): try: if self.console_type == "vnc": - self._x11vnc_process.terminate() - self._xvfb_process.terminate() - yield from self._x11vnc_process.wait() - yield from self._xvfb_process.wait() + if self._x11vnc_process: + self._x11vnc_process.terminate() + self._xvfb_process.terminate() + yield from self._x11vnc_process.wait() + yield from self._xvfb_process.wait() state = yield from self._get_container_state() if state == "paused" or state == "running": @@ -521,11 +573,17 @@ class DockerVM(BaseVM): if not self._ubridge_hypervisor or not self._ubridge_hypervisor.is_running(): return - yield from self._ubridge_hypervisor.send("bridge delete bridge{name}".format( - name=adapter_number)) - adapter = self._ethernet_adapters[adapter_number] - yield from self._ubridge_hypervisor.send('docker delete_veth {hostif}'.format(hostif=adapter.host_ifc)) + + try: + yield from self._ubridge_hypervisor.send("bridge delete bridge{name}".format( + name=adapter_number)) + except UbridgeError as e: + log.debug(str(e)) + try: + yield from self._ubridge_hypervisor.send('docker delete_veth {hostif}'.format(hostif=adapter.host_ifc)) + except UbridgeError as e: + log.debug(str(e)) @asyncio.coroutine def _get_namespace(self): diff --git a/gns3server/hypervisor/docker/resources/init.sh b/gns3server/hypervisor/docker/resources/init.sh index abd3aa4f..c3df5ca1 100755 --- a/gns3server/hypervisor/docker/resources/init.sh +++ b/gns3server/hypervisor/docker/resources/init.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/gns3/bin/busybox sh # # Copyright (C) 2016 GNS3 Technologies Inc. # @@ -19,6 +19,14 @@ # This script is injected into the container and launch before # the start command of the container # +OLD_PATH="$PATH" +PATH=/gns3/bin:/tmp/gns3/bin + +# bootstrap busybox commands +if [ ! -d /tmp/gns3/bin ]; then + busybox mkdir -p /tmp/gns3/bin + /gns3/bin/busybox --install -s /tmp/gns3/bin +fi # Wait 2 seconds to settle the network interfaces sleep 2 @@ -37,10 +45,14 @@ __EOF__ # configure loopback interface ip link set dev lo up -# configure eth interfaces +# activate eth interfaces sed -n 's/^ *\(eth[0-9]*\):.*/\1/p' < /proc/net/dev | while read dev; do ip link set dev $dev up done +# configure network interfaces +ifup -a -f + # continue normal docker startup +PATH="$OLD_PATH" exec "$@" diff --git a/gns3server/hypervisor/dynamips/__init__.py b/gns3server/hypervisor/dynamips/__init__.py index ac5aef26..a1deb920 100644 --- a/gns3server/hypervisor/dynamips/__init__.py +++ b/gns3server/hypervisor/dynamips/__init__.py @@ -32,7 +32,7 @@ import glob log = logging.getLogger(__name__) -from gns3server.utils.interfaces import get_windows_interfaces, is_interface_up +from gns3server.utils.interfaces import interfaces, is_interface_up from gns3server.utils.asyncio import wait_run_in_executor from pkg_resources import parse_version from uuid import UUID, uuid4 @@ -439,9 +439,9 @@ class Dynamips(BaseManager): ethernet_device = nio_settings["ethernet_device"] if sys.platform.startswith("win"): # replace the interface name by the GUID on Windows - interfaces = get_windows_interfaces() + windows_interfaces = interfaces() npf_interface = None - for interface in interfaces: + for interface in windows_interfaces: if interface["name"] == ethernet_device: npf_interface = interface["id"] if not npf_interface: diff --git a/gns3server/hypervisor/dynamips/nodes/router.py b/gns3server/hypervisor/dynamips/nodes/router.py index 50a45734..84439021 100644 --- a/gns3server/hypervisor/dynamips/nodes/router.py +++ b/gns3server/hypervisor/dynamips/nodes/router.py @@ -895,7 +895,7 @@ class Router(BaseVM): """ self.console = console - yield from self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=console)) + yield from self._hypervisor.send('vm set_con_tcp_port "{name}" {console}'.format(name=self._name, console=self.console)) @asyncio.coroutine def set_aux(self, aux): diff --git a/gns3server/hypervisor/port_manager.py b/gns3server/hypervisor/port_manager.py index c0b168b1..02fddfe6 100644 --- a/gns3server/hypervisor/port_manager.py +++ b/gns3server/hypervisor/port_manager.py @@ -42,8 +42,8 @@ class PortManager: server_config = Config.instance().get_section_config("Server") remote_console_connections = server_config.getboolean("allow_remote_console") - console_start_port_range = server_config.getint("console_start_port_range", 2001) - console_end_port_range = server_config.getint("console_end_port_range", 7000) + console_start_port_range = server_config.getint("console_start_port_range", 5000) + console_end_port_range = server_config.getint("console_end_port_range", 10000) self._console_port_range = (console_start_port_range, console_end_port_range) log.debug("Console port range is {}-{}".format(console_start_port_range, console_end_port_range)) @@ -225,15 +225,15 @@ class PortManager: old_port = port port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) msg = "TCP port {} already in use on host {}. Port has been replaced by {}".format(old_port, self._console_host, port) - log.warning(msg) - project.emit("log.warning", {"message": msg}) + log.debug(msg) + #project.emit("log.warning", {"message": msg}) return port - if port < self._console_port_range[0] or port > self._console_port_range[1]: + if port < port_range_start or port > port_range_end: old_port = port port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) msg = "TCP port {} is outside the range {}-{} on host {}. Port has been replaced by {}".format(old_port, port_range_start, port_range_end, self._console_host, port) - log.warning(msg) - project.emit("log.warning", {"message": msg}) + log.debug(msg) + #project.emit("log.warning", {"message": msg}) return port try: PortManager._check_port(self._console_host, port, "TCP") @@ -241,8 +241,8 @@ class PortManager: old_port = port port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) msg = "TCP port {} already in use on host {}. Port has been replaced by {}".format(old_port, self._console_host, port) - log.warning(msg) - project.emit("log.warning", {"message": msg}) + log.debug(msg) + #project.emit("log.warning", {"message": msg}) return port self._used_tcp_ports.add(port) diff --git a/gns3server/hypervisor/project.py b/gns3server/hypervisor/project.py index e548b94e..2dfce723 100644 --- a/gns3server/hypervisor/project.py +++ b/gns3server/hypervisor/project.py @@ -15,11 +15,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import aiohttp import os +import aiohttp import shutil import asyncio import hashlib +import zipstream +import zipfile +import json from uuid import UUID, uuid4 from .port_manager import PortManager @@ -143,6 +146,8 @@ class Project: @name.setter def name(self, name): + if "/" in name or "\\" in name: + raise aiohttp.web.HTTPForbidden(text="Name can not contain path separator") self._name = name @property @@ -460,3 +465,108 @@ class Project: break m.update(buf) return m.hexdigest() + + def export(self): + """ + Export the project as zip. It's a ZipStream object. + The file will be read chunk by chunk when you iterate on + the zip. + + It will ignore some files like snapshots and + + :returns: ZipStream object + """ + + z = zipstream.ZipFile() + # topdown allo to modify the list of directory in order to ignore + # directory + for root, dirs, files in os.walk(self._path, topdown=True): + # Remove snapshots + if os.path.split(root)[-1:][0] == "project-files": + dirs[:] = [d for d in dirs if d != "snapshots"] + + # Ignore log files and OS noise + files = [f for f in files if not f.endswith('_log.txt') and not f.endswith('.log') and f != '.DS_Store'] + + for file in files: + path = os.path.join(root, file) + # We rename the .gns3 project.gns3 to avoid the task to the client to guess the file name + if file.endswith(".gns3"): + z.write(path, "project.gns3") + else: + # We merge the data from all server in the same project-files directory + vm_directory = os.path.join(self._path, "servers", "vm") + if os.path.commonprefix([root, vm_directory]) == vm_directory: + z.write(path, os.path.relpath(path, vm_directory)) + else: + z.write(path, os.path.relpath(path, self._path)) + return z + + def import_zip(self, stream, gns3vm=True): + """ + Import a project contain in a zip file + + :param stream: A io.BytesIO of the zipfile + :param gns3vm: True move docker, iou and qemu to the GNS3 VM + """ + + with zipfile.ZipFile(stream) as myzip: + myzip.extractall(self.path) + + project_file = os.path.join(self.path, "project.gns3") + if os.path.exists(project_file): + with open(project_file) as f: + topology = json.load(f) + topology["project_id"] = self.id + topology["name"] = self.name + topology.setdefault("topology", {}) + topology["topology"].setdefault("nodes", []) + topology["topology"]["servers"] = [ + { + "id": 1, + "local": True, + "vm": False + } + ] + + # By default all node run on local server + for node in topology["topology"]["nodes"]: + node["server_id"] = 1 + + if gns3vm: + # Move to servers/vm directory the data that should be import on remote server + modules_to_vm = { + "qemu": "QemuVM", + "iou": "IOUDevice", + "docker": "DockerVM" + } + + vm_directory = os.path.join(self.path, "servers", "vm", "project-files") + vm_server_use = False + + for module, device_type in modules_to_vm.items(): + module_directory = os.path.join(self.path, "project-files", module) + if os.path.exists(module_directory): + os.makedirs(vm_directory, exist_ok=True) + shutil.move(module_directory, os.path.join(vm_directory, module)) + + # Patch node to use the GNS3 VM + for node in topology["topology"]["nodes"]: + if node["type"] == device_type: + node["server_id"] = 2 + vm_server_use = True + + # We use the GNS3 VM. We need to add the server to the list + if vm_server_use: + topology["topology"]["servers"].append({ + "id": 2, + "vm": True, + "local": False + }) + + # Write the modified topology + with open(project_file, "w") as f: + json.dump(topology, f, indent=4) + + # Rename to a human distinctive name + shutil.move(project_file, os.path.join(self.path, self.name + ".gns3")) diff --git a/gns3server/hypervisor/qemu/qemu_vm.py b/gns3server/hypervisor/qemu/qemu_vm.py index 72f0cc7b..849c5e1b 100644 --- a/gns3server/hypervisor/qemu/qemu_vm.py +++ b/gns3server/hypervisor/qemu/qemu_vm.py @@ -40,6 +40,7 @@ from ..base_vm import BaseVM from ...schemas.qemu import QEMU_OBJECT_SCHEMA, QEMU_PLATFORMS from ...utils.asyncio import monitor_process from ...utils.images import md5sum +from .qcow2 import Qcow2, Qcow2Error import logging log = logging.getLogger(__name__) @@ -1233,90 +1234,45 @@ class QemuVM(BaseVM): options = [] qemu_img_path = self._get_qemu_img() - if self._hda_disk_image: - if not os.path.isfile(self._hda_disk_image) or not os.path.exists(self._hda_disk_image): - if os.path.islink(self._hda_disk_image): - raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image))) + drives = ["a", "b", "c", "d"] + + for disk_index, drive in enumerate(drives): + disk_image = getattr(self, "_hd{}_disk_image".format(drive)) + interface = getattr(self, "hd{}_disk_interface".format(drive)) + + if not disk_image: + continue + + disk_name = "hd" + drive + + if not os.path.isfile(disk_image) or not os.path.exists(disk_image): + if os.path.islink(disk_image): + raise QemuError("{} disk image '{}' linked to '{}' is not accessible".format(disk_name, disk_image, os.path.realpath(disk_image))) else: - raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image)) + raise QemuError("{} disk image '{}' is not accessible".format(disk_name, disk_image)) if self._linked_clone: - hda_disk = os.path.join(self.working_dir, "hda_disk.qcow2") - if not os.path.exists(hda_disk): + disk = os.path.join(self.working_dir, "{}_disk.qcow2".format(disk_name)) + if not os.path.exists(disk): # create the disk try: process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", - "backing_file={}".format(self._hda_disk_image), - "-f", "qcow2", hda_disk) + "backing_file={}".format(disk_image), + "-f", "qcow2", disk) retcode = yield from process.wait() log.info("{} returned with {}".format(qemu_img_path, retcode)) except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create hda disk image {}".format(e)) - else: - hda_disk = self._hda_disk_image - options.extend(["-drive", 'file={},if={},index=0,media=disk'.format(hda_disk, self.hda_disk_interface)]) - - if self._hdb_disk_image: - if not os.path.isfile(self._hdb_disk_image) or not os.path.exists(self._hdb_disk_image): - if os.path.islink(self._hdb_disk_image): - raise QemuError("hdb disk image '{}' linked to '{}' is not accessible".format(self._hdb_disk_image, os.path.realpath(self._hdb_disk_image))) + raise QemuError("Could not create {} disk image {}".format(disk_name, e)) else: - raise QemuError("hdb disk image '{}' is not accessible".format(self._hdb_disk_image)) - if self._linked_clone: - hdb_disk = os.path.join(self.working_dir, "hdb_disk.qcow2") - if not os.path.exists(hdb_disk): + # The disk exists we check if the clone work try: - process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", - "backing_file={}".format(self._hdb_disk_image), - "-f", "qcow2", hdb_disk) - retcode = yield from process.wait() - log.info("{} returned with {}".format(qemu_img_path, retcode)) - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create hdb disk image {}".format(e)) - else: - hdb_disk = self._hdb_disk_image - options.extend(["-drive", 'file={},if={},index=1,media=disk'.format(hdb_disk, self.hdb_disk_interface)]) + qcow2 = Qcow2(disk) + yield from qcow2.rebase(qemu_img_path, disk_image) + except (Qcow2Error, OSError) as e: + raise QemuError("Could not use qcow2 disk image {} for {} {}".format(disk_image, disk_name, e)) - if self._hdc_disk_image: - if not os.path.isfile(self._hdc_disk_image) or not os.path.exists(self._hdc_disk_image): - if os.path.islink(self._hdc_disk_image): - raise QemuError("hdc disk image '{}' linked to '{}' is not accessible".format(self._hdc_disk_image, os.path.realpath(self._hdc_disk_image))) - else: - raise QemuError("hdc disk image '{}' is not accessible".format(self._hdc_disk_image)) - if self._linked_clone: - hdc_disk = os.path.join(self.working_dir, "hdc_disk.qcow2") - if not os.path.exists(hdc_disk): - try: - process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", - "backing_file={}".format(self._hdc_disk_image), - "-f", "qcow2", hdc_disk) - retcode = yield from process.wait() - log.info("{} returned with {}".format(qemu_img_path, retcode)) - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create hdc disk image {}".format(e)) else: - hdc_disk = self._hdc_disk_image - options.extend(["-drive", 'file={},if={},index=2,media=disk'.format(hdc_disk, self.hdc_disk_interface)]) - - if self._hdd_disk_image: - if not os.path.isfile(self._hdd_disk_image) or not os.path.exists(self._hdd_disk_image): - if os.path.islink(self._hdd_disk_image): - raise QemuError("hdd disk image '{}' linked to '{}' is not accessible".format(self._hdd_disk_image, os.path.realpath(self._hdd_disk_image))) - else: - raise QemuError("hdd disk image '{}' is not accessible".format(self._hdd_disk_image)) - if self._linked_clone: - hdd_disk = os.path.join(self.working_dir, "hdd_disk.qcow2") - if not os.path.exists(hdd_disk): - try: - process = yield from asyncio.create_subprocess_exec(qemu_img_path, "create", "-o", - "backing_file={}".format(self._hdd_disk_image), - "-f", "qcow2", hdd_disk) - retcode = yield from process.wait() - log.info("{} returned with {}".format(qemu_img_path, retcode)) - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create hdd disk image {}".format(e)) - else: - hdd_disk = self._hdd_disk_image - options.extend(["-drive", 'file={},if={},index=3,media=disk'.format(hdd_disk, self.hdd_disk_interface)]) + disk = disk_image + options.extend(["-drive", 'file={},if={},index={},media=disk'.format(disk, interface, disk_index)]) return options diff --git a/gns3server/hypervisor/vmware/__init__.py b/gns3server/hypervisor/vmware/__init__.py index 88fa8942..3dac122e 100644 --- a/gns3server/hypervisor/vmware/__init__.py +++ b/gns3server/hypervisor/vmware/__init__.py @@ -594,8 +594,10 @@ class VMware(BaseManager): """ if sys.platform.startswith("win"): - from win32com.shell import shell, shellcon - documents_folder = shell.SHGetSpecialFolderPath(None, shellcon.CSIDL_PERSONAL) + import ctypes + path = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) + ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, path) + documents_folder = path.value windows_type = sys.getwindowsversion().product_type if windows_type == 2 or windows_type == 3: return '{}\My Virtual Machines'.format(documents_folder) diff --git a/gns3server/hypervisor/vmware/vmware_vm.py b/gns3server/hypervisor/vmware/vmware_vm.py index 879ca934..ecd27f26 100644 --- a/gns3server/hypervisor/vmware/vmware_vm.py +++ b/gns3server/hypervisor/vmware/vmware_vm.py @@ -26,7 +26,7 @@ import asyncio import tempfile from gns3server.utils.telnet_server import TelnetServer -from gns3server.utils.interfaces import interfaces, get_windows_interfaces +from gns3server.utils.interfaces import interfaces from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation from collections import OrderedDict from .vmware_error import VMwareError @@ -144,6 +144,8 @@ class VMwareVM(BaseVM): yield from self.manager.check_vmrun_version() if self._linked_clone and not os.path.exists(os.path.join(self.working_dir, os.path.basename(self._vmx_path))): + if self.manager.host_type == "player": + raise VMwareError("Linked clones are not supported by VMware Player") # create the base snapshot for linked clones base_snapshot_name = "GNS3 Linked Base for clones" vmsd_path = os.path.splitext(self._vmx_path)[0] + ".vmsd" @@ -320,7 +322,7 @@ class VMwareVM(BaseVM): yield from self._ubridge_hypervisor.send('bridge add_nio_linux_raw {name} "{interface}"'.format(name=vnet, interface=vmnet_interface)) elif sys.platform.startswith("win"): - windows_interfaces = get_windows_interfaces() + windows_interfaces = interfaces() npf = None source_mac = None for interface in windows_interfaces: diff --git a/gns3server/modules/docker/resources/bin/busybox b/gns3server/modules/docker/resources/bin/busybox new file mode 100755 index 00000000..08235d5e Binary files /dev/null and b/gns3server/modules/docker/resources/bin/busybox differ diff --git a/gns3server/modules/docker/resources/etc/udhcpc/default.script b/gns3server/modules/docker/resources/etc/udhcpc/default.script new file mode 100755 index 00000000..404fb190 --- /dev/null +++ b/gns3server/modules/docker/resources/etc/udhcpc/default.script @@ -0,0 +1,138 @@ +#!/tmp/gns3/bin/sh + +# script for udhcpc +# Copyright (c) 2008 Natanael Copa + +UDHCPC="/gns3/etc/udhcpc" +UDHCPC_CONF="$UDHCPC/udhcpc.conf" + +RESOLV_CONF="/etc/resolv.conf" +[ -f $UDHCPC_CONF ] && . $UDHCPC_CONF + +export broadcast +export dns +export domain +export interface +export ip +export mask +export metric +export router +export subnet + +#export PATH=/usr/bin:/bin:/usr/sbin:/sbin + +run_scripts() { + local dir=$1 + if [ -d $dir ]; then + for i in $dir/*; do + [ -f $i ] && $i + done + fi +} + +deconfig() { + ip addr flush dev $interface +} + +is_wifi() { + test -e /sys/class/net/$interface/phy80211 +} + +if_index() { + if [ -e /sys/class/net/$interface/ifindex ]; then + cat /sys/class/net/$interface/ifindex + else + ip link show dev $interface | head -n1 | cut -d: -f1 + fi +} + +calc_metric() { + local base= + if is_wifi; then + base=300 + else + base=200 + fi + echo $(( $base + $(if_index) )) +} + +routes() { + [ -z "$router" ] && return + local gw= num= + while ip route del default via dev $interface 2>/dev/null; do + : + done + num=0 + for gw in $router; do + ip route add 0.0.0.0/0 via $gw dev $interface \ + metric $(( $num + ${IF_METRIC:-$(calc_metric)} )) + num=$(( $num + 1 )) + done +} + +resolvconf() { + local i + [ -n "$IF_PEER_DNS" ] && [ "$IF_PEER_DNS" != "yes" ] && return + if [ "$RESOLV_CONF" = "no" ] || [ "$RESOLV_CONF" = "NO" ] \ + || [ -z "$RESOLV_CONF" ]; then + return + fi + echo -n > "$RESOLV_CONF" + [ -n "$domain" ] && echo "search $domain" >> "$RESOLV_CONF" + for i in $dns; do + echo "nameserver $i" >> "$RESOLV_CONF" + done +} + +bound() { + ip addr add $ip/$mask ${broadcast:+broadcast $broadcast} dev $interface + ip link set dev $interface up + routes + resolvconf +} + +renew() { + if ! ip addr show dev $interface | grep $ip/$mask; then + ip addr flush dev $interface + ip addr add $ip/$mask ${broadcast:+broadcast $broadcast} dev $interface + fi + + local i + for i in $router; do + if ! ip route show | grep ^default | grep $i; then + routes + break + fi + done + + if ! grep "^search $domain"; then + resolvconf + return + fi + for i in $dns; do + if ! grep "^nameserver $i"; then + resolvconf + return + fi + done +} + +case "$1" in + deconfig|renew|bound) + run_scripts $UDHCPC/pre-$1 + $1 + run_scripts $UDHCPC/post-$1 + ;; + leasefail) + echo "udhcpc failed to get a DHCP lease" >&2 + ;; + nak) + echo "udhcpc received DHCP NAK" >&2 + ;; + *) + echo "Error: this script should be called from udhcpc" >&2 + exit 1 + ;; +esac +exit 0 + diff --git a/gns3server/run.py b/gns3server/run.py index c71a17b9..9338e73b 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -112,7 +112,7 @@ def parse_arguments(argv): config = Config.instance().get_section_config("Server") defaults = { "host": config.get("host", "0.0.0.0"), - "port": config.get("port", 8000), + "port": config.get("port", 3080), "ssl": config.getboolean("ssl", False), "certfile": config.get("certfile", ""), "certkey": config.get("certkey", ""), diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py index 47baf97a..dc69fb36 100644 --- a/gns3server/schemas/docker.py +++ b/gns3server/schemas/docker.py @@ -43,6 +43,11 @@ DOCKER_CREATE_SCHEMA = { "description": "console type", "enum": ["telnet", "vnc"] }, + "console_resolution": { + "description": "console resolution for VNC", + "type": ["string", "null"], + "pattern": "^[0-9]+x[0-9]+$" + }, "aux": { "description": "auxilary TCP port", "minimum": 1, @@ -92,6 +97,11 @@ DOCKER_UPDATE_SCHEMA = { "maximum": 65535, "type": ["integer", "null"] }, + "console_resolution": { + "description": "console resolution for VNC", + "type": ["string", "null"], + "pattern": "^[0-9]+x[0-9]+$" + }, "console_type": { "description": "console type", "enum": ["telnet", "vnc"] @@ -143,13 +153,18 @@ DOCKER_OBJECT_SCHEMA = { "description": "auxilary TCP port", "minimum": 1, "maximum": 65535, - "type": ["integer", "null"] + "type": "integer" }, "console": { "description": "console TCP port", "minimum": 1, "maximum": 65535, - "type": ["integer", "null"] + "type": "integer" + }, + "console_resolution": { + "description": "console resolution for VNC", + "type": "string", + "pattern": "^[0-9]+x[0-9]+$" }, "console_type": { "description": "console type", @@ -196,7 +211,7 @@ DOCKER_OBJECT_SCHEMA = { } }, "additionalProperties": False, - "required": ["vm_id", "project_id", "image", "container_id", "adapters", "aux", "console", "console_type", "start_command", "environment", "vm_directory"] + "required": ["vm_id", "project_id", "image", "container_id", "adapters", "aux", "console", "console_type", "console_resolution", "start_command", "environment", "vm_directory"] } diff --git a/gns3server/utils/asyncio/telnet_server.py b/gns3server/utils/asyncio/telnet_server.py index 9bb1ee6c..783f0970 100644 --- a/gns3server/utils/asyncio/telnet_server.py +++ b/gns3server/utils/asyncio/telnet_server.py @@ -148,11 +148,11 @@ class AsyncioTelnetServer: return_when=asyncio.FIRST_COMPLETED) for coro in done: data = coro.result() - # Console is closed - if len(data) == 0: - raise ConnectionResetError() if coro == network_read: + if network_reader.at_eof(): + raise ConnectionResetError() + network_read = asyncio.async(network_reader.read(READ_SIZE)) if IAC in data: @@ -167,6 +167,9 @@ class AsyncioTelnetServer: self._writer.write(data) yield from self._writer.drain() elif coro == reader_read: + if self._reader.at_eof(): + raise ConnectionResetError() + reader_read = yield from self._get_reader(network_reader) # Replicate the output on all clients diff --git a/gns3server/utils/interfaces.py b/gns3server/utils/interfaces.py index 2ff5cc0a..1f53e2c0 100644 --- a/gns3server/utils/interfaces.py +++ b/gns3server/utils/interfaces.py @@ -163,6 +163,19 @@ def interfaces(): "mac_address": mac_address}) else: try: + import pywintypes + import win32service + import win32serviceutil + + try: + if win32serviceutil.QueryServiceStatus("npf", None)[1] != win32service.SERVICE_RUNNING: + raise aiohttp.web.HTTPInternalServerError(text="The NPF service is not running") + except pywintypes.error as e: + if e[0] == 1060: + raise aiohttp.web.HTTPInternalServerError(text="The NPF service is not installed") + else: + raise aiohttp.web.HTTPInternalServerError(text="Could not check if the NPF service is running: {}".format(e[2])) + results = get_windows_interfaces() except ImportError: message = "pywin32 module is not installed, please install it on the server to get the available interface names" diff --git a/gns3server/web/response.py b/gns3server/web/response.py index fc87e809..cba7308d 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -19,6 +19,7 @@ import json import jsonschema import asyncio import aiohttp.web +import asyncio import logging import sys import jinja2 diff --git a/requirements.txt b/requirements.txt index 81c0f1c5..59e6145b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ jsonschema>=2.4.0 -aiohttp==0.21.2 +aiohttp>=0.21.5 Jinja2>=2.7.3 raven>=5.2.0 psutil>=3.0.0 +zipstream>=1.1.3 diff --git a/scripts/remote-install.sh b/scripts/remote-install.sh index 61f7a683..f89b45a2 100644 --- a/scripts/remote-install.sh +++ b/scripts/remote-install.sh @@ -24,6 +24,8 @@ function help { echo "Usage:" >&2 echo "--with-openvpn: Install Open VPN" >&2 + echo "--with-iou: Install IOU" >&2 + echo "--with-i386-repository: Add i386 repositories require by IOU if they are not available on the system. Warning this will replace your source.list in order to use official ubuntu mirror" >&2 echo "--help: This help" >&2 } @@ -42,8 +44,10 @@ fi # Read the options USE_VPN=0 +USE_IOU=0 +I386_REPO=0 -TEMP=`getopt -o h --long with-openvpn,help -n 'gns3-remote-install.sh' -- "$@"` +TEMP=`getopt -o h --long with-openvpn,with-iou,with-i386-repository,help -n 'gns3-remote-install.sh' -- "$@"` if [ $? != 0 ] then help @@ -58,6 +62,14 @@ while true ; do USE_VPN=1 shift ;; + --with-iou) + USE_IOU=1 + shift + ;; + --with-i386-repository) + I386_REPO=1 + shift + ;; -h|--help) help exit 1 @@ -73,17 +85,31 @@ set -e export DEBIAN_FRONTEND="noninteractive" log "Add GNS3 repository" -cat > /etc/apt/sources.list.d/gns3.list << EOF +cat < /etc/apt/sources.list.d/gns3.list deb http://ppa.launchpad.net/gns3/ppa/ubuntu trusty main deb-src http://ppa.launchpad.net/gns3/ppa/ubuntu trusty main deb http://ppa.launchpad.net/gns3/qemu/ubuntu trusty main deb-src http://ppa.launchpad.net/gns3/qemu/ubuntu trusty main -EOF +EOFLIST + +if [ $I386_REPO == 1 ] +then + cat <> /etc/apt/sources.list +###### Ubuntu Main Repos +deb http://archive.ubuntu.com/ubuntu/ trusty main universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ trusty main universe multiverse + +###### Ubuntu Update Repos +deb http://archive.ubuntu.com/ubuntu/ trusty-security main universe multiverse +deb http://archive.ubuntu.com/ubuntu/ trusty-updates main universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ trusty-security main universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ trusty-updates main universe multiverse +EOFLIST2 +fi apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A2E3EF7B log "Update system packages" -dpkg --add-architecture i386 apt-get update log "Upgrade packages" @@ -107,53 +133,46 @@ fi log "Add GNS3 to the docker group" usermod -aG docker gns3 -log "IOU setup" -#apt-get install -y gns3-iou +if [ $USE_IOU == 1 ] +then + log "IOU setup" + dpkg --add-architecture i386 + apt-get update -# Force the host name to gns3vm -hostnamectl set-hostname gns3vm + apt-get install -y gns3-iou -# Force hostid for IOU -dd if=/dev/zero bs=4 count=1 of=/etc/hostid + # Force the host name to gns3vm + hostnamectl set-hostname gns3vm -# Block iou call. The server is down -echo "127.0.0.254 xml.cisco.com" | tee --append /etc/hosts + # Force hostid for IOU + dd if=/dev/zero bs=4 count=1 of=/etc/hostid + + # Block iou call. The server is down + echo "127.0.0.254 xml.cisco.com" | tee --append /etc/hosts +fi log "Add gns3 to the kvm group" usermod -aG kvm gns3 -log "Setup VDE network" - -apt-get install -y vde2 uml-utilities - -usermod -a -G vde2-net gns3 - -cat < /etc/network/interfaces.d/qemu0.conf -# A vde network -auto qemu0 - iface qemu0 inet static - address 172.16.0.1 - netmask 255.255.255.0 - vde2-switch -t qemu0 -EOF - log "Setup GNS3 server" - -#TODO: 1.4.5 allow /etc/gns3/gns3_server.conf it's cleaner -cat < /opt/gns3/gns3_server.conf +mkdir -p /etc/gns3 +cat < /etc/gns3/gns3_server.conf [Server] host = 0.0.0.0 -port = 8000 +port = 3080 images_path = /opt/gns3/images projects_path = /opt/gns3/projects report_errors = True [Qemu] enable_kvm = True -EOF +EOFC -cat < /etc/init/gns3.conf +chown -R gns3:gns3 /etc/gns3 +chmod -R 700 /etc/gns3 + +cat < /etc/init/gns3.conf description "GNS3 server" author "GNS3 Team" @@ -175,7 +194,7 @@ end script pre-stop script echo "[`date`] GNS3 Stopping" end script -EOF +EOFI chown root:root /etc/init/gns3.conf chmod 644 /etc/init/gns3.conf @@ -193,17 +212,17 @@ if [ $USE_VPN == 1 ] then log "Setup VPN" -cat < /opt/gns3/gns3_server.conf +cat < /etc/gns3/gns3_server.conf [Server] host = 172.16.253.1 -port = 8000 +port = 3080 images_path = /opt/gns3/images projects_path = /opt/gns3/projects report_errors = True [Qemu] enable_kvm = True -EOF +EOFSERVER log "Install packages for Open VPN" @@ -221,7 +240,7 @@ UUID=$(uuid) log "Update motd" -cat < /etc/update-motd.d/70-openvpn +cat < /etc/update-motd.d/70-openvpn #!/bin/sh echo "" echo "_______________________________________________________________________________________________" @@ -232,7 +251,7 @@ echo "And add it to your openvpn client." echo "" echo "apt-get remove nginx-light to disable the HTTP server." echo "And remove this file with rm /etc/update-motd.d/70-openvpn" -EOF +EOFMOTD chmod 755 /etc/update-motd.d/70-openvpn @@ -250,7 +269,7 @@ chmod 600 /etc/openvpn/key.pem [ -f /etc/openvpn/cert.pem ] || openssl x509 -req -in /etc/openvpn/csr.pem -out /etc/openvpn/cert.pem -signkey /etc/openvpn/key.pem -days 24855 log "Create client configuration" -cat < /root/client.ovpn +cat < /root/client.ovpn client nobind comp-lzo @@ -302,7 +321,7 @@ server { listen 8003; root /usr/share/nginx/openvpn; } -EOF +EOFCLIENT [ -f /etc/nginx/sites-enabled/openvpn ] || ln -s /etc/nginx/sites-available/openvpn /etc/nginx/sites-enabled/ service nginx stop service nginx start diff --git a/tests/handlers/api/base.py b/tests/handlers/api/base.py index caa96bff..28499c8a 100644 --- a/tests/handlers/api/base.py +++ b/tests/handlers/api/base.py @@ -133,7 +133,7 @@ class Query: if path is None: return with open(self._example_file_path(method, route), 'w+') as f: - f.write("curl -i -X {} 'http://localhost:8000/v{}{}{}'".format(method, self._api_version, self._prefix, path)) + f.write("curl -i -X {} 'http://localhost:3080/v{}{}{}'".format(method, self._api_version, self._prefix, path)) if body: f.write(" -d '{}'".format(re.sub(r"\n", "", json.dumps(json.loads(body), sort_keys=True)))) f.write("\n\n") diff --git a/tests/handlers/api/hypervisor/test_docker.py b/tests/handlers/api/hypervisor/test_docker.py index 32c55222..6aeee271 100644 --- a/tests/handlers/api/hypervisor/test_docker.py +++ b/tests/handlers/api/hypervisor/test_docker.py @@ -30,7 +30,7 @@ from gns3server.hypervisor.docker import Docker @pytest.fixture def base_params(): """Return standard parameters""" - return {"name": "PC TEST 1", "image": "nginx", "start_command": "nginx-daemon", "adapters": 2, "environment": "YES=1\nNO=0", "console_type": "telnet"} + return {"name": "PC TEST 1", "image": "nginx", "start_command": "nginx-daemon", "adapters": 2, "environment": "YES=1\nNO=0", "console_type": "telnet", "console_resolution": "1280x1024"} @pytest.yield_fixture(autouse=True) @@ -65,6 +65,7 @@ def test_docker_create(http_hypervisor, project, base_params): assert response.json["image"] == "nginx" assert response.json["adapters"] == 2 assert response.json["environment"] == "YES=1\nNO=0" + assert response.json["console_resolution"] == "1280x1024" def test_docker_start(http_hypervisor, vm): diff --git a/tests/handlers/api/hypervisor/test_project.py b/tests/handlers/api/hypervisor/test_project.py index 4ced22d1..c8a11832 100644 --- a/tests/handlers/api/hypervisor/test_project.py +++ b/tests/handlers/api/hypervisor/test_project.py @@ -23,6 +23,7 @@ import uuid import os import asyncio import aiohttp +import zipfile from unittest.mock import patch from tests.utils import asyncio_patch @@ -216,3 +217,40 @@ def test_get_file(http_hypervisor, tmpdir): response = http_hypervisor.get("/projects/{project_id}/files/../hello".format(project_id=project.id), raw=True) assert response.status == 403 + + +def test_export(http_hypervisor, tmpdir, loop, project): + + os.makedirs(project.path, exist_ok=True) + with open(os.path.join(project.path, 'a'), 'w+') as f: + f.write('hello') + + response = http_hypervisor.get("/projects/{project_id}/export".format(project_id=project.id), raw=True) + assert response.status == 200 + assert response.headers['CONTENT-TYPE'] == 'application/gns3z' + assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{}.gns3z"'.format(project.name) + + with open(str(tmpdir / 'project.zip'), 'wb+') as f: + f.write(response.body) + + with zipfile.ZipFile(str(tmpdir / 'project.zip')) as myzip: + with myzip.open("a") as myfile: + content = myfile.read() + assert content == b"hello" + + +def test_import(http_hypervisor, tmpdir, loop, project): + + with zipfile.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip: + myzip.writestr("demo", b"hello") + + project_id = project.id + + with open(str(tmpdir / "test.zip"), "rb") as f: + response = http_hypervisor.post("/projects/{project_id}/import".format(project_id=project_id), body=f.read(), raw=True) + assert response.status == 201 + + project = ProjectManager.instance().get_project(project_id=project_id) + with open(os.path.join(project.path, "demo")) as f: + content = f.read() + assert content == "hello" diff --git a/tests/handlers/test_upload.py b/tests/handlers/test_upload.py index 14f967f5..c799ad2e 100644 --- a/tests/handlers/test_upload.py +++ b/tests/handlers/test_upload.py @@ -219,7 +219,6 @@ def test_backup_projects(http_root, tmpdir, loop): assert response.headers['CONTENT-TYPE'] == 'application/x-gtar' with open(str(tmpdir / 'projects.tar'), 'wb+') as f: - print(len(response.body)) f.write(response.body) tar = tarfile.open(str(tmpdir / 'projects.tar'), 'r') diff --git a/tests/hypervisor/docker/test_docker_vm.py b/tests/hypervisor/docker/test_docker_vm.py index fe82634c..79bc1b0c 100644 --- a/tests/hypervisor/docker/test_docker_vm.py +++ b/tests/hypervisor/docker/test_docker_vm.py @@ -57,6 +57,7 @@ def test_json(vm, project): 'adapters': 1, 'console': vm.console, 'console_type': 'telnet', + 'console_resolution': '1024x768', 'aux': vm.aux, 'start_command': vm.start_command, 'environment': vm.environment, @@ -89,7 +90,10 @@ def test_create(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("hypervisor/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -113,7 +117,7 @@ def test_create_vnc(loop, project, manager): with asyncio_patch("gns3server.hypervisor.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]) as mock_list_images: with asyncio_patch("gns3server.hypervisor.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", console_type="vnc") + vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu", console_type="vnc", console=5900) vm._start_vnc = MagicMock() vm._display = 42 loop.run_until_complete(asyncio.async(vm.create())) @@ -126,6 +130,7 @@ def test_create_vnc(loop, project, manager): "CapAdd": ["ALL"], "Binds": [ "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")), '/tmp/.X11-unix/:/tmp/.X11-unix/' ], "Privileged": True @@ -141,6 +146,7 @@ def test_create_vnc(loop, project, manager): }) assert vm._start_vnc.called assert vm._cid == "e90e34656806" + assert vm._console_type == "vnc" def test_create_start_cmd(loop, project, manager): @@ -161,7 +167,10 @@ def test_create_start_cmd(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("hypervisor/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -194,7 +203,10 @@ def test_create_environment(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("hypervisor/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Env": ["YES=1", "NO=0"], @@ -241,7 +253,10 @@ def test_create_image_not_available(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("hypervisor/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -438,6 +453,7 @@ def test_update(loop, vm): } original_console = vm.console + original_aux = vm.aux with asyncio_patch("gns3server.hypervisor.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]) as mock_list_images: with asyncio_patch("gns3server.hypervisor.docker.DockerVM._get_container_state", return_value="stopped"): @@ -452,7 +468,10 @@ def test_update(loop, vm): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("hypervisor/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -465,6 +484,30 @@ def test_update(loop, vm): "Cmd": ["/bin/sh"] }) assert vm.console == original_console + assert vm.aux == original_aux + + +def test_update_vnc(loop, vm): + + response = { + "Id": "e90e34656806", + "Warnings": [] + } + + vm.console_type = "vnc" + vm.console = 5900 + vm._display = "display" + original_console = vm.console + original_aux = vm.aux + + with asyncio_patch("gns3server.hypervisor.docker.DockerVM._start_vnc"): + with asyncio_patch("gns3server.hypervisor.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]) as mock_list_images: + with asyncio_patch("gns3server.hypervisor.docker.DockerVM._get_container_state", return_value="stopped"): + with asyncio_patch("gns3server.hypervisor.docker.Docker.query", return_value=response) as mock_query: + loop.run_until_complete(asyncio.async(vm.update())) + + assert vm.console == original_console + assert vm.aux == original_aux def test_update_running(loop, vm): @@ -490,7 +533,10 @@ def test_update_running(loop, vm): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("hypervisor/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -763,6 +809,7 @@ def test_mount_binds(vm, tmpdir): dst = os.path.join(vm.working_dir, "test/experimental") assert vm._mount_binds(image_infos) == [ "{}:/gns3:ro".format(get_resource("hypervisor/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")), "{}:{}".format(dst, "/test/experimental") ] @@ -770,13 +817,14 @@ def test_mount_binds(vm, tmpdir): def test_start_vnc(vm, loop): + vm.console_resolution = "1280x1024" with patch("shutil.which", return_value="/bin/x"): with asyncio_patch("gns3server.hypervisor.docker.docker_vm.wait_for_file_creation") as mock_wait: with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec: loop.run_until_complete(asyncio.async(vm._start_vnc())) assert vm._display is not None - mock_exec.assert_any_call("Xvfb", "-nolisten", "tcp", ":{}".format(vm._display), "-screen", "0", "1024x768x16") - mock_exec.assert_any_call("x11vnc", "-forever", "-nopw", "-display", "WAIT:{}".format(vm._display), "-rfbport", str(vm.console), "-noncache", "-listen", "127.0.0.1") + mock_exec.assert_any_call("Xvfb", "-nolisten", "tcp", ":{}".format(vm._display), "-screen", "0", "1280x1024x16") + mock_exec.assert_any_call("x11vnc", "-forever", "-nopw", "-shared", "-geometry", "1280x1024", "-display", "WAIT:{}".format(vm._display), "-rfbport", str(vm.console), "-noncache", "-listen", "127.0.0.1") mock_wait.assert_called_with("/tmp/.X11-unix/X{}".format(vm._display)) @@ -789,3 +837,17 @@ def test_start_aux(vm, loop): with asyncio_patch("asyncio.subprocess.create_subprocess_exec", return_value=MagicMock()) as mock_exec: loop.run_until_complete(asyncio.async(vm._start_aux())) + + +def test_create_network_interfaces(vm): + + vm.adapters = 5 + network_config = vm._create_network_config() + assert os.path.exists(os.path.join(network_config, "interfaces")) + assert os.path.exists(os.path.join(network_config, "if-up.d")) + + with open(os.path.join(network_config, "interfaces")) as f: + content = f.read() + assert "eth0" in content + assert "eth4" in content + assert "eth5" not in content diff --git a/tests/hypervisor/qemu/test_qemu_vm.py b/tests/hypervisor/qemu/test_qemu_vm.py index 936dac0c..8d0152da 100644 --- a/tests/hypervisor/qemu/test_qemu_vm.py +++ b/tests/hypervisor/qemu/test_qemu_vm.py @@ -323,11 +323,35 @@ def test_disk_options(vm, tmpdir, loop, fake_qemu_img_binary): open(vm._hda_disk_image, "w+").close() with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: - loop.run_until_complete(asyncio.async(vm._disk_options())) + options = loop.run_until_complete(asyncio.async(vm._disk_options())) assert process.called args, kwargs = process.call_args assert args == (fake_qemu_img_binary, "create", "-o", "backing_file={}".format(vm._hda_disk_image), "-f", "qcow2", os.path.join(vm.working_dir, "hda_disk.qcow2")) + assert options == ['-drive', 'file=' + os.path.join(vm.working_dir, "hda_disk.qcow2") + ',if=ide,index=0,media=disk'] + + +def test_disk_options_multiple_disk(vm, tmpdir, loop, fake_qemu_img_binary): + + vm._hda_disk_image = str(tmpdir / "test0.qcow2") + vm._hdb_disk_image = str(tmpdir / "test1.qcow2") + vm._hdc_disk_image = str(tmpdir / "test2.qcow2") + vm._hdd_disk_image = str(tmpdir / "test3.qcow2") + open(vm._hda_disk_image, "w+").close() + open(vm._hdb_disk_image, "w+").close() + open(vm._hdc_disk_image, "w+").close() + open(vm._hdd_disk_image, "w+").close() + + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + options = loop.run_until_complete(asyncio.async(vm._disk_options())) + + assert options == [ + '-drive', 'file=' + os.path.join(vm.working_dir, "hda_disk.qcow2") + ',if=ide,index=0,media=disk', + '-drive', 'file=' + os.path.join(vm.working_dir, "hdb_disk.qcow2") + ',if=ide,index=1,media=disk', + '-drive', 'file=' + os.path.join(vm.working_dir, "hdc_disk.qcow2") + ',if=ide,index=2,media=disk', + '-drive', 'file=' + os.path.join(vm.working_dir, "hdd_disk.qcow2") + ',if=ide,index=3,media=disk' + ] + @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") def test_set_process_priority(vm, loop, fake_qemu_img_binary): diff --git a/tests/hypervisor/test_base_vm.py b/tests/hypervisor/test_base_vm.py index 85edd9ce..aea8fa42 100644 --- a/tests/hypervisor/test_base_vm.py +++ b/tests/hypervisor/test_base_vm.py @@ -49,8 +49,8 @@ def test_temporary_directory(project, manager): def test_console(project, manager): vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) - vm.console = 2111 - assert vm.console == 2111 + vm.console = 5011 + assert vm.console == 5011 vm.console = None assert vm.console is None diff --git a/tests/hypervisor/test_port_manager.py b/tests/hypervisor/test_port_manager.py index c2b26751..9e4750e0 100644 --- a/tests/hypervisor/test_port_manager.py +++ b/tests/hypervisor/test_port_manager.py @@ -32,7 +32,6 @@ def test_reserve_tcp_port(): with patch("gns3server.hypervisor.project.Project.emit") as mock_emit: port = pm.reserve_tcp_port(2001, project) assert port != 2001 - assert mock_emit.call_args[0][0] == "log.warning" def test_reserve_tcp_port_outside_range(): @@ -41,7 +40,6 @@ def test_reserve_tcp_port_outside_range(): with patch("gns3server.hypervisor.project.Project.emit") as mock_emit: port = pm.reserve_tcp_port(80, project) assert port != 80 - assert mock_emit.call_args[0][0] == "log.warning" def test_reserve_tcp_port_already_used_by_another_program(): @@ -65,7 +63,6 @@ def test_reserve_tcp_port_already_used_by_another_program(): with patch("gns3server.hypervisor.project.Project.emit") as mock_emit: port = pm.reserve_tcp_port(2001, project) assert port != 2001 - assert mock_emit.call_args[0][0] == "log.warning" def test_reserve_tcp_port_already_used(): @@ -89,7 +86,6 @@ def test_reserve_tcp_port_already_used(): with patch("gns3server.hypervisor.project.Project.emit") as mock_emit: port = pm.reserve_tcp_port(2001, project) assert port != 2001 - assert mock_emit.call_args[0][0] == "log.warning" def test_reserve_udp_port(): diff --git a/tests/hypervisor/test_project.py b/tests/hypervisor/test_project.py index c2364cb8..35475cc7 100644 --- a/tests/hypervisor/test_project.py +++ b/tests/hypervisor/test_project.py @@ -17,9 +17,12 @@ # along with this program. If not, see . import os +import uuid +import json import asyncio import pytest import aiohttp +import zipfile from uuid import uuid4 from unittest.mock import patch @@ -269,3 +272,140 @@ def test_emit(async_run): (action, event, context) = async_run(queue.get(0.5)) assert action == "test" assert context["project_id"] == project.id + + +def test_export(tmpdir): + project = Project() + path = project.path + os.makedirs(os.path.join(path, "vm-1", "dynamips")) + + # The .gns3 should be renamed project.gns3 in order to simplify import + with open(os.path.join(path, "test.gns3"), 'w+') as f: + f.write("{}") + + with open(os.path.join(path, "vm-1", "dynamips", "test"), 'w+') as f: + f.write("HELLO") + with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f: + f.write("LOG") + os.makedirs(os.path.join(path, "project-files", "snapshots")) + with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f: + f.write("WORLD") + + z = project.export() + + with open(str(tmpdir / 'zipfile.zip'), 'wb') as f: + for data in z: + f.write(data) + + with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with myzip.open("vm-1/dynamips/test") as myfile: + content = myfile.read() + assert content == b"HELLO" + + assert 'test.gns3' not in myzip.namelist() + assert 'project.gns3' in myzip.namelist() + assert 'project-files/snapshots/test' not in myzip.namelist() + assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist() + + +def test_export(tmpdir): + project = Project(project_id=str(uuid.uuid4())) + path = project.path + os.makedirs(os.path.join(path, "vm-1", "dynamips")) + + # The .gns3 should be renamed project.gns3 in order to simplify import + with open(os.path.join(path, "test.gns3"), 'w+') as f: + f.write("{}") + + with open(os.path.join(path, "vm-1", "dynamips", "test"), 'w+') as f: + f.write("HELLO") + with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f: + f.write("LOG") + os.makedirs(os.path.join(path, "project-files", "snapshots")) + with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f: + f.write("WORLD") + + os.makedirs(os.path.join(path, "servers", "vm", "project-files", "docker")) + with open(os.path.join(path, "servers", "vm", "project-files", "docker", "busybox"), 'w+') as f: + f.write("DOCKER") + + z = project.export() + + with open(str(tmpdir / 'zipfile.zip'), 'wb') as f: + for data in z: + f.write(data) + + with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with myzip.open("vm-1/dynamips/test") as myfile: + content = myfile.read() + assert content == b"HELLO" + + assert 'test.gns3' not in myzip.namelist() + assert 'project.gns3' in myzip.namelist() + assert 'project-files/snapshots/test' not in myzip.namelist() + assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist() + assert 'servers/vm/project-files/docker/busybox' not in myzip.namelist() + assert 'project-files/docker/busybox' in myzip.namelist() + + +def test_import(tmpdir): + + project_id = str(uuid.uuid4()) + project = Project(name="test", project_id=project_id) + + topology = { + "project_id": str(uuid.uuid4()), + "name": "testtest", + "topology": { + "nodes": [ + { + "server_id": 3, + "type": "VPCSDevice" + }, + { + "server_id": 3, + "type": "QemuVM" + } + ] + } + } + + with open(str(tmpdir / "project.gns3"), 'w+') as f: + json.dump(topology, f) + with open(str(tmpdir / "b.png"), 'w+') as f: + f.write("B") + + zip_path = str(tmpdir / "project.zip") + with zipfile.ZipFile(zip_path, 'w') as myzip: + myzip.write(str(tmpdir / "project.gns3"), "project.gns3") + myzip.write(str(tmpdir / "b.png"), "b.png") + myzip.write(str(tmpdir / "b.png"), "project-files/dynamips/test") + myzip.write(str(tmpdir / "b.png"), "project-files/qemu/test") + + with open(zip_path, "rb") as f: + project.import_zip(f) + + assert os.path.exists(os.path.join(project.path, "b.png")) + assert os.path.exists(os.path.join(project.path, "test.gns3")) + assert os.path.exists(os.path.join(project.path, "project-files/dynamips/test")) + assert os.path.exists(os.path.join(project.path, "servers/vm/project-files/qemu/test")) + + with open(os.path.join(project.path, "test.gns3")) as f: + content = json.load(f) + + assert content["name"] == "test" + assert content["project_id"] == project_id + assert content["topology"]["servers"] == [ + { + "id": 1, + "local": True, + "vm": False + }, + { + "id": 2, + "local": False, + "vm": True + }, + ] + assert content["topology"]["nodes"][0]["server_id"] == 1 + assert content["topology"]["nodes"][1]["server_id"] == 2 diff --git a/tests/resources/empty8G.qcow2 b/tests/resources/empty8G.qcow2 new file mode 100644 index 00000000..beec669a Binary files /dev/null and b/tests/resources/empty8G.qcow2 differ diff --git a/tests/resources/linked.qcow2 b/tests/resources/linked.qcow2 new file mode 100644 index 00000000..8fad5487 Binary files /dev/null and b/tests/resources/linked.qcow2 differ diff --git a/tests/test_run.py b/tests/test_run.py index 9c0c6a38..1f706847 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -77,7 +77,7 @@ def test_parse_arguments(capsys, tmpdir): assert run.parse_arguments([]).host == "192.168.1.2" assert run.parse_arguments(["--port", "8002"]).port == 8002 - assert run.parse_arguments([]).port == 8000 + assert run.parse_arguments([]).port == 3080 server_config["port"] = "8003" assert run.parse_arguments([]).port == 8003