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

Merge branch '2.0' into 2.1

This commit is contained in:
Julien Duponchelle 2017-02-28 14:25:38 +01:00
commit 1ab9ca2333
No known key found for this signature in database
GPG Key ID: CE8B29639E07F5E8
29 changed files with 548 additions and 55 deletions

View File

@ -1,6 +1,8 @@
language: python language: python
python: python:
- '3.4'
- '3.5' - '3.5'
- '3.6'
sudo: false sudo: false
cache: pip cache: pip
install: install:

View File

@ -1,6 +1,6 @@
-rrequirements.txt -rrequirements.txt
sphinx==1.5.2 sphinx==1.5.3
pytest==3.0.6 pytest==3.0.6
pep8==1.7.0 pep8==1.7.0
pytest-catchlog==1.2.2 pytest-catchlog==1.2.2

View File

@ -211,7 +211,8 @@ class EthernetSwitch(Device):
nio = self._nios[port_number] nio = self._nios[port_number]
if isinstance(nio, NIOUDP): if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project) self.manager.port_manager.release_udp_port(nio.lport, self._project)
yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio)) if self._hypervisor:
yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio))
log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name, log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
id=self._id, id=self._id,

View File

@ -24,6 +24,7 @@ import asyncio
import time import time
import sys import sys
import os import os
import re
import glob import glob
import shlex import shlex
import base64 import base64
@ -1474,7 +1475,7 @@ class Router(BaseNode):
try: try:
with open(startup_config_path, "r+", encoding="utf-8", errors="replace") as f: with open(startup_config_path, "r+", encoding="utf-8", errors="replace") as f:
old_config = f.read() old_config = f.read()
new_config = old_config.replace(self.name, new_name) new_config = re.sub(r"^hostname .+$", "hostname " + new_name, old_config, flags=re.MULTILINE)
f.seek(0) f.seek(0)
f.write(new_config) f.write(new_config)
except OSError as e: except OSError as e:

View File

@ -301,7 +301,7 @@ class IOUVM(BaseNode):
if self.startup_config_file: if self.startup_config_file:
content = self.startup_config_content content = self.startup_config_content
content = content.replace(self._name, new_name) content = re.sub(r"^hostname .+$", "hostname " + new_name, content, flags=re.MULTILINE)
self.startup_config_content = content self.startup_config_content = content
super(IOUVM, IOUVM).name.__set__(self, new_name) super(IOUVM, IOUVM).name.__set__(self, new_name)
@ -1161,7 +1161,7 @@ class IOUVM(BaseNode):
bay=adapter_number, bay=adapter_number,
unit=port_number, unit=port_number,
output_file=output_file, output_file=output_file,
data_link_type=data_link_type)) data_link_type=re.sub("^DLT_", "", data_link_type)))
@asyncio.coroutine @asyncio.coroutine
def stop_capture(self, adapter_number, port_number): def stop_capture(self, adapter_number, port_number):

View File

@ -303,6 +303,16 @@ class VirtualBoxVM(BaseNode):
if self.acpi_shutdown: if self.acpi_shutdown:
# use ACPI to shutdown the VM # use ACPI to shutdown the VM
result = yield from self._control_vm("acpipowerbutton") result = yield from self._control_vm("acpipowerbutton")
trial = 0
while True:
vm_state = yield from self._get_vm_state()
if vm_state == "poweroff":
break
yield from asyncio.sleep(1)
trial += 1
if trial >= 120:
yield from self._control_vm("poweroff")
break
self.status = "stopped" self.status = "stopped"
log.debug("ACPI shutdown result: {}".format(result)) log.debug("ACPI shutdown result: {}".format(result))
else: else:

View File

@ -104,7 +104,7 @@ class VPCSVM(BaseNode):
Check if VPCS is available with the correct version. Check if VPCS is available with the correct version.
""" """
path = self.vpcs_path path = self._vpcs_path()
if not path: if not path:
raise VPCSError("No path to a VPCS executable has been set") raise VPCSError("No path to a VPCS executable has been set")
@ -144,8 +144,7 @@ class VPCSVM(BaseNode):
else: else:
return None return None
@property def _vpcs_path(self):
def vpcs_path(self):
""" """
Returns the VPCS executable path. Returns the VPCS executable path.
@ -170,6 +169,7 @@ class VPCSVM(BaseNode):
if self.script_file: if self.script_file:
content = self.startup_script content = self.startup_script
content = content.replace(self._name, new_name) content = content.replace(self._name, new_name)
content = re.sub(r"^set pcname .+$", "set pcname " + new_name, content, flags=re.MULTILINE)
self.startup_script = content self.startup_script = content
super(VPCSVM, VPCSVM).name.__set__(self, new_name) super(VPCSVM, VPCSVM).name.__set__(self, new_name)
@ -215,7 +215,7 @@ class VPCSVM(BaseNode):
Checks if the VPCS executable version is >= 0.8b or == 0.6.1. Checks if the VPCS executable version is >= 0.8b or == 0.6.1.
""" """
try: try:
output = yield from subprocess_check_output(self.vpcs_path, "-v", cwd=self.working_dir) output = yield from subprocess_check_output(self._vpcs_path(), "-v", cwd=self.working_dir)
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output) match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output)
if match: if match:
version = match.group(1) version = match.group(1)
@ -223,7 +223,7 @@ class VPCSVM(BaseNode):
if self._vpcs_version < parse_version("0.6.1"): if self._vpcs_version < parse_version("0.6.1"):
raise VPCSError("VPCS executable version must be >= 0.6.1 but not a 0.8") raise VPCSError("VPCS executable version must be >= 0.6.1 but not a 0.8")
else: else:
raise VPCSError("Could not determine the VPCS version for {}".format(self.vpcs_path)) raise VPCSError("Could not determine the VPCS version for {}".format(self._vpcs_path()))
except (OSError, subprocess.SubprocessError) as e: except (OSError, subprocess.SubprocessError) as e:
raise VPCSError("Error while looking for the VPCS version: {}".format(e)) raise VPCSError("Error while looking for the VPCS version: {}".format(e))
@ -268,8 +268,8 @@ class VPCSVM(BaseNode):
self.status = "started" self.status = "started"
except (OSError, subprocess.SubprocessError) as e: except (OSError, subprocess.SubprocessError) as e:
vpcs_stdout = self.read_vpcs_stdout() vpcs_stdout = self.read_vpcs_stdout()
log.error("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout)) log.error("Could not start VPCS {}: {}\n{}".format(self._vpcs_path(), e, vpcs_stdout))
raise VPCSError("Could not start VPCS {}: {}\n{}".format(self.vpcs_path, e, vpcs_stdout)) raise VPCSError("Could not start VPCS {}: {}\n{}".format(self._vpcs_path(), e, vpcs_stdout))
def _termination_callback(self, returncode): def _termination_callback(self, returncode):
""" """
@ -512,7 +512,7 @@ class VPCSVM(BaseNode):
""" """
command = [self.vpcs_path] command = [self._vpcs_path()]
command.extend(["-p", str(self._internal_console_port)]) # listen to console port command.extend(["-p", str(self._internal_console_port)]) # listen to console port
command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset
command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-i", "1"]) # option to start only one VPC instance

View File

@ -55,16 +55,21 @@ class Controller:
def start(self): def start(self):
log.info("Start controller") log.info("Start controller")
self.load_base_files() self.load_base_files()
yield from self.load()
server_config = Config.instance().get_section_config("Server") server_config = Config.instance().get_section_config("Server")
host = server_config.get("host", "localhost") host = server_config.get("host", "localhost")
# If console_host is 0.0.0.0 client will use the ip they use # If console_host is 0.0.0.0 client will use the ip they use
# to connect to the controller # to connect to the controller
console_host = host console_host = host
if host == "0.0.0.0": if host == "0.0.0.0":
host = "127.0.0.1" host = "127.0.0.1"
name = socket.gethostname()
if name == "gns3vm":
name = "Main server"
yield from self.add_compute(compute_id="local", yield from self.add_compute(compute_id="local",
name=socket.gethostname(), name=name,
protocol=server_config.get("protocol", "http"), protocol=server_config.get("protocol", "http"),
host=host, host=host,
console_host=console_host, console_host=console_host,
@ -72,6 +77,7 @@ class Controller:
user=server_config.get("user", ""), user=server_config.get("user", ""),
password=server_config.get("password", ""), password=server_config.get("password", ""),
force=True) force=True)
yield from self._load_controller_settings()
yield from self.load_projects() yield from self.load_projects()
yield from self.gns3vm.auto_start_vm() yield from self.gns3vm.auto_start_vm()
yield from self._project_auto_open() yield from self._project_auto_open()
@ -118,7 +124,7 @@ class Controller:
json.dump(data, f, indent=4) json.dump(data, f, indent=4)
@asyncio.coroutine @asyncio.coroutine
def load(self): def _load_controller_settings(self):
""" """
Reload the controller configuration from disk Reload the controller configuration from disk
""" """
@ -279,7 +285,7 @@ class Controller:
return None return None
for compute in self._computes.values(): for compute in self._computes.values():
if name and compute.name == name: if name and compute.name == name and not force:
raise aiohttp.web.HTTPConflict(text='Compute name "{}" already exists'.format(name)) raise aiohttp.web.HTTPConflict(text='Compute name "{}" already exists'.format(name))
compute = Compute(compute_id=compute_id, controller=self, name=name, **kwargs) compute = Compute(compute_id=compute_id, controller=self, name=name, **kwargs)

View File

@ -426,7 +426,7 @@ class Compute:
except aiohttp.errors.WSServerHandshakeError: except aiohttp.errors.WSServerHandshakeError:
self._ws = None self._ws = None
break break
if response.tp == aiohttp.MsgType.closed or response.tp == aiohttp.MsgType.error: if response.tp == aiohttp.MsgType.closed or response.tp == aiohttp.MsgType.error or response.data is None:
self._connected = False self._connected = False
break break
msg = json.loads(response.data) msg = json.loads(response.data)

View File

@ -43,6 +43,7 @@ class Drawing:
self._id = str(uuid.uuid4()) self._id = str(uuid.uuid4())
else: else:
self._id = drawing_id self._id = drawing_id
self._svg = "<svg></svg>"
self.svg = svg self.svg = svg
self._x = x self._x = x
self._y = y self._y = y

View File

@ -47,6 +47,9 @@ def export_project(project, temporary_dir, include_images=False, keep_compute_id
if project.is_running(): if project.is_running():
raise aiohttp.web.HTTPConflict(text="Running topology could not be exported") raise aiohttp.web.HTTPConflict(text="Running topology could not be exported")
# Make sure we save the project
project.dump()
z = zipstream.ZipFile(allowZip64=True) z = zipstream.ZipFile(allowZip64=True)
if not os.path.exists(project._path): if not os.path.exists(project._path):

View File

@ -222,8 +222,14 @@ class GNS3VM:
""" """
engine = self._get_engine(engine) engine = self._get_engine(engine)
vms = [] vms = []
for vm in (yield from engine.list()): try:
vms.append({"vmname": vm["vmname"]}) for vm in (yield from engine.list()):
vms.append({"vmname": vm["vmname"]})
except GNS3VMError as e:
# We raise error only if user activated the GNS3 VM
# otherwise you have noise when VMware is not installed
if self.enable:
raise e
return vms return vms
@asyncio.coroutine @asyncio.coroutine
@ -267,6 +273,7 @@ class GNS3VM:
engine.vmname = self._settings["vmname"] engine.vmname = self._settings["vmname"]
engine.ram = self._settings["ram"] engine.ram = self._settings["ram"]
engine.vpcus = self._settings["vcpus"] engine.vpcus = self._settings["vcpus"]
engine.headless = self._settings["headless"]
compute = yield from self._controller.add_compute(compute_id="vm", compute = yield from self._controller.add_compute(compute_id="vm",
name="GNS3 VM is starting ({})".format(engine.vmname), name="GNS3 VM is starting ({})".format(engine.vmname),
host=None, host=None,
@ -277,6 +284,7 @@ class GNS3VM:
except Exception as e: except Exception as e:
yield from self._controller.delete_compute("vm") yield from self._controller.delete_compute("vm")
log.error("Can't start the GNS3 VM: {}", str(e)) log.error("Can't start the GNS3 VM: {}", str(e))
yield from compute.update(name="GNS3 VM ({})".format(engine.vmname))
raise e raise e
yield from compute.update(name="GNS3 VM ({})".format(engine.vmname), yield from compute.update(name="GNS3 VM ({})".format(engine.vmname),
protocol=self.protocol, protocol=self.protocol,

View File

@ -221,7 +221,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
second to a GNS3 endpoint in order to get the list of the interfaces and second to a GNS3 endpoint in order to get the list of the interfaces and
their IP and after that match it with VirtualBox host only. their IP and after that match it with VirtualBox host only.
""" """
remaining_try = 240 remaining_try = 300
while remaining_try > 0: while remaining_try > 0:
json_data = None json_data = None
session = aiohttp.ClientSession() session = aiohttp.ClientSession()

View File

@ -15,6 +15,8 @@
# 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/>.
import aiohttp
from .atm_port import ATMPort from .atm_port import ATMPort
from .frame_relay_port import FrameRelayPort from .frame_relay_port import FrameRelayPort
from .gigabitethernet_port import GigabitEthernetPort from .gigabitethernet_port import GigabitEthernetPort
@ -64,11 +66,14 @@ class StandardPortFactory:
port_name = first_port_name port_name = first_port_name
port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet") port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet")
else: else:
port_name = port_name_format.format( try:
interface_number, port_name = port_name_format.format(
segment_number, interface_number,
adapter=adapter_number, segment_number,
**cls._generate_replacement(interface_number, segment_number)) adapter=adapter_number,
**cls._generate_replacement(interface_number, segment_number))
except (ValueError, KeyError) as e:
raise aiohttp.web.HTTPConflict(text="Invalid port name format {}: {}".format(port_name_format, str(e)))
port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet") port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet")
interface_number += 1 interface_number += 1
if port_segment_size: if port_segment_size:

View File

@ -293,6 +293,8 @@ class Project:
name = base_name.format(number, id=number, name="Node") name = base_name.format(number, id=number, name="Node")
except KeyError as e: except KeyError as e:
raise aiohttp.web.HTTPConflict(text="{" + e.args[0] + "} is not a valid replacement string in the node name") raise aiohttp.web.HTTPConflict(text="{" + e.args[0] + "} is not a valid replacement string in the node name")
except ValueError as e:
raise aiohttp.web.HTTPConflict(text="{} is not a valid replacement string in the node name".format(base_name))
if name not in self._allocated_node_names: if name not in self._allocated_node_names:
self._allocated_node_names.add(name) self._allocated_node_names.add(name)
return name return name

View File

@ -39,16 +39,17 @@ class Symbols:
def list(self): def list(self):
self._symbols_path = {} self._symbols_path = {}
symbols = [] symbols = []
for file in os.listdir(get_resource("symbols")): if get_resource("symbols"):
if file.startswith('.'): for file in os.listdir(get_resource("symbols")):
continue if file.startswith('.'):
symbol_id = ':/symbols/' + file continue
symbols.append({ symbol_id = ':/symbols/' + file
'symbol_id': symbol_id, symbols.append({
'filename': file, 'symbol_id': symbol_id,
'builtin': True, 'filename': file,
}) 'builtin': True,
self._symbols_path[symbol_id] = os.path.join(get_resource("symbols"), file) })
self._symbols_path[symbol_id] = os.path.join(get_resource("symbols"), file)
directory = self.symbols_path() directory = self.symbols_path()
if directory: if directory:
for file in os.listdir(directory): for file in os.listdir(directory):

View File

@ -363,6 +363,14 @@ def _convert_1_3_later(topo, topo_path):
node["symbol"] = ":/symbols/vbox_guest.svg" node["symbol"] = ":/symbols/vbox_guest.svg"
elif old_node["type"] == "IOUDevice": elif old_node["type"] == "IOUDevice":
node["node_type"] = "iou" node["node_type"] = "iou"
node["port_name_format"] = old_node.get("port_name_format", "Ethernet{segment0}/{port0}")
node["port_segment_size"] = int(old_node.get("port_segment_size", "4"))
if node["symbol"] is None:
if "l2" in node["properties"].get("path", ""):
node["symbol"] = ":/symbols/multilayer_switch.svg"
else:
node["symbol"] = ":/symbols/router.svg"
elif old_node["type"] == "Cloud": elif old_node["type"] == "Cloud":
old_node["ports"] = _create_cloud(node, old_node, ":/symbols/cloud.svg") old_node["ports"] = _create_cloud(node, old_node, ":/symbols/cloud.svg")
elif old_node["type"] == "Host": elif old_node["type"] == "Host":

View File

@ -18,6 +18,7 @@
import os import os
import sys import sys
import struct import struct
import aiohttp
import platform import platform
@ -94,6 +95,7 @@ class CrashReport:
"os:win_32": " ".join(platform.win32_ver()), "os:win_32": " ".join(platform.win32_ver()),
"os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]), "os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]),
"os:linux": " ".join(platform.linux_distribution()), "os:linux": " ".join(platform.linux_distribution()),
"aiohttp:version": aiohttp.__version__,
"python:version": "{}.{}.{}".format(sys.version_info[0], "python:version": "{}.{}.{}".format(sys.version_info[0],
sys.version_info[1], sys.version_info[1],
sys.version_info[2]), sys.version_info[2]),

View File

@ -120,7 +120,8 @@ def _svg_convert_size(size):
"pc": 15, "pc": 15,
"mm": 3.543307, "mm": 3.543307,
"cm": 35.43307, "cm": 35.43307,
"in": 90 "in": 90,
"px": 1
} }
if len(size) > 3: if len(size) > 3:
if size[-2:] in conversion_table: if size[-2:] in conversion_table:

View File

@ -32,7 +32,7 @@ if "dev" in __version__:
import os import os
import subprocess import subprocess
if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")): if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")):
r = subprocess.run(["git", "rev-parse", "--short", "HEAD"], stdout=subprocess.PIPE).stdout.decode().strip("\n") r = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip("\n")
__version__ += "-" + r __version__ += "-" + r
except Exception as e: except Exception as e:
print(e) print(e)

View File

@ -39,6 +39,7 @@ class Response(aiohttp.web.Response):
self._route = route self._route = route
self._output_schema = output_schema self._output_schema = output_schema
self._request = request self._request = request
headers['Connection'] = "close" # Disable keep alive because create trouble with old Qt (5.2, 5.3 and 5.4)
headers['X-Route'] = self._route headers['X-Route'] = self._route
headers['Server'] = "Python/{0[0]}.{0[1]} GNS3/{1}".format(sys.version_info, __version__) headers['Server'] = "Python/{0[0]}.{0[1]} GNS3/{1}".format(sys.version_info, __version__)
super().__init__(headers=headers, **kwargs) super().__init__(headers=headers, **kwargs)

View File

@ -1,7 +1,7 @@
jsonschema>=2.4.0 jsonschema>=2.4.0
aiohttp==1.2.0 aiohttp>=1.3.0,<=1.4.0
aiohttp_cors>=0.4.0 aiohttp_cors>=0.4.0
yarl>=0.9.6 yarl>=0.9.8
typing>=3.5.3.0 # Otherwise yarl fail with python 3.4 typing>=3.5.3.0 # Otherwise yarl fail with python 3.4
Jinja2>=2.7.3 Jinja2>=2.7.3
raven>=5.23.0 raven>=5.23.0

View File

@ -298,6 +298,14 @@ def test_change_name(vm, tmpdir):
assert vm.name == "hello" assert vm.name == "hello"
with open(path) as f: with open(path) as f:
assert f.read() == "hostname hello" assert f.read() == "hostname hello"
# support hostname not sync
vm.name = "alpha"
with open(path, 'w+') as f:
f.write("no service password-encryption\nhostname beta\nno ip icmp rate-limit unreachable")
vm.name = "charlie"
assert vm.name == "charlie"
with open(path) as f:
assert f.read() == "no service password-encryption\nhostname charlie\nno ip icmp rate-limit unreachable"
def test_library_check(loop, vm): def test_library_check(loop, vm):

View File

@ -75,7 +75,7 @@ def test_vm_invalid_vpcs_version(loop, manager, vm):
def test_vm_invalid_vpcs_path(vm, manager, loop): def test_vm_invalid_vpcs_path(vm, manager, loop):
with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.vpcs_path", return_value="/tmp/fake/path/vpcs"): with patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM._vpcs_path", return_value="/tmp/fake/path/vpcs"):
with pytest.raises(VPCSError): with pytest.raises(VPCSError):
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"})
vm.port_add_nio_binding(0, nio) vm.port_add_nio_binding(0, nio)
@ -97,7 +97,7 @@ def test_start(loop, vm, async_run):
nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"})
async_run(vm.port_add_nio_binding(0, nio)) async_run(vm.port_add_nio_binding(0, nio))
loop.run_until_complete(asyncio.async(vm.start())) loop.run_until_complete(asyncio.async(vm.start()))
assert mock_exec.call_args[0] == (vm.vpcs_path, assert mock_exec.call_args[0] == (vm._vpcs_path(),
'-p', '-p',
str(vm._internal_console_port), str(vm._internal_console_port),
'-m', '1', '-m', '1',
@ -133,7 +133,7 @@ def test_start_0_6_1(loop, vm, async_run):
nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"})
async_run(vm.port_add_nio_binding(0, nio)) async_run(vm.port_add_nio_binding(0, nio))
async_run(vm.start()) async_run(vm.start())
assert mock_exec.call_args[0] == (vm.vpcs_path, assert mock_exec.call_args[0] == (vm._vpcs_path(),
'-p', '-p',
str(vm._internal_console_port), str(vm._internal_console_port),
'-m', '1', '-m', '1',
@ -243,12 +243,12 @@ def test_update_startup_script(vm):
def test_update_startup_script_h(vm): def test_update_startup_script_h(vm):
content = "setname %h\n" content = "set pcname %h\n"
vm.name = "pc1" vm.name = "pc1"
vm.startup_script = content vm.startup_script = content
assert os.path.exists(vm.script_file) assert os.path.exists(vm.script_file)
with open(vm.script_file) as f: with open(vm.script_file) as f:
assert f.read() == "setname pc1\n" assert f.read() == "set pcname pc1\n"
def test_get_startup_script(vm): def test_get_startup_script(vm):
@ -275,11 +275,18 @@ def test_change_name(vm, tmpdir):
path = os.path.join(vm.working_dir, 'startup.vpc') path = os.path.join(vm.working_dir, 'startup.vpc')
vm.name = "world" vm.name = "world"
with open(path, 'w+') as f: with open(path, 'w+') as f:
f.write("name world") f.write("set pcname world")
vm.name = "hello" vm.name = "hello"
assert vm.name == "hello" assert vm.name == "hello"
with open(path) as f: with open(path) as f:
assert f.read() == "name hello" assert f.read() == "set pcname hello"
# Support when the name is not sync with config
with open(path, 'w+') as f:
f.write("set pcname alpha")
vm.name = "beta"
assert vm.name == "beta"
with open(path) as f:
assert f.read() == "set pcname beta"
def test_close(vm, port_manager, loop): def test_close(vm, port_manager, loop):

View File

@ -42,7 +42,7 @@ def test_save(controller, controller_config_path):
assert data["gns3vm"] == controller.gns3vm.__json__() assert data["gns3vm"] == controller.gns3vm.__json__()
def test_load(controller, controller_config_path, async_run): def test_load_controller_settings(controller, controller_config_path, async_run):
controller.save() controller.save()
with open(controller_config_path) as f: with open(controller_config_path) as f:
data = json.load(f) data = json.load(f)
@ -60,7 +60,7 @@ def test_load(controller, controller_config_path, async_run):
data["gns3vm"] = {"vmname": "Test VM"} data["gns3vm"] = {"vmname": "Test VM"}
with open(controller_config_path, "w+") as f: with open(controller_config_path, "w+") as f:
json.dump(data, f) json.dump(data, f)
async_run(controller.load()) async_run(controller._load_controller_settings())
assert controller.settings["IOU"] assert controller.settings["IOU"]
assert controller.computes["test1"].__json__() == { assert controller.computes["test1"].__json__() == {
"compute_id": "test1", "compute_id": "test1",
@ -104,7 +104,7 @@ def test_import_computes_1_x(controller, controller_config_path, async_run):
with open(os.path.join(config_dir, "gns3_gui.conf"), "w+") as f: with open(os.path.join(config_dir, "gns3_gui.conf"), "w+") as f:
json.dump(gns3_gui_conf, f) json.dump(gns3_gui_conf, f)
async_run(controller.load()) async_run(controller._load_controller_settings())
for compute in controller.computes.values(): for compute in controller.computes.values():
if compute.id != "local": if compute.id != "local":
assert len(compute.id) == 36 assert len(compute.id) == 36
@ -146,7 +146,7 @@ def test_import_gns3vm_1_x(controller, controller_config_path, async_run):
json.dump(gns3_gui_conf, f) json.dump(gns3_gui_conf, f)
controller.gns3vm.settings["engine"] = None controller.gns3vm.settings["engine"] = None
async_run(controller.load()) async_run(controller._load_controller_settings())
assert controller.gns3vm.settings["engine"] == "vmware" assert controller.gns3vm.settings["engine"] == "vmware"
assert controller.gns3vm.settings["enable"] assert controller.gns3vm.settings["enable"]
assert controller.gns3vm.settings["headless"] assert controller.gns3vm.settings["headless"]
@ -202,7 +202,7 @@ def test_import_remote_gns3vm_1_x(controller, controller_config_path, async_run)
json.dump(gns3_gui_conf, f) json.dump(gns3_gui_conf, f)
with asyncio_patch("gns3server.controller.compute.Compute.connect"): with asyncio_patch("gns3server.controller.compute.Compute.connect"):
async_run(controller.load()) async_run(controller._load_controller_settings())
assert controller.gns3vm.settings["engine"] == "remote" assert controller.gns3vm.settings["engine"] == "remote"
assert controller.gns3vm.settings["vmname"] == "http://127.0.0.1:3081" assert controller.gns3vm.settings["vmname"] == "http://127.0.0.1:3081"

View File

@ -33,7 +33,9 @@ from gns3server.controller.export_project import export_project, _filter_files
@pytest.fixture @pytest.fixture
def project(controller): def project(controller):
return Project(controller=controller, name="Test") p = Project(controller=controller, name="Test")
p.dump = MagicMock()
return p
@pytest.fixture @pytest.fixture

420
tests/resources/firefox.svg Normal file
View File

@ -0,0 +1,420 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="66px" height="70px" viewBox="0 0 66 70" enable-background="new 0 0 66 70" xml:space="preserve"> <image id="image0" width="66" height="70" x="0" y="0"
xlink:href="data:image/png;base64,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" />
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -30,8 +30,8 @@
"name": "IOU1", "name": "IOU1",
"node_id": "aaeb2288-a7d8-42a9-b9d8-c42ab464a390", "node_id": "aaeb2288-a7d8-42a9-b9d8-c42ab464a390",
"node_type": "iou", "node_type": "iou",
"port_name_format": "Ethernet{0}", "port_name_format": "Ethernet{segment0}/{port0}",
"port_segment_size": 0, "port_segment_size": 4,
"first_port_name": null, "first_port_name": null,
"properties": { "properties": {
"ethernet_adapters": 2, "ethernet_adapters": 2,

View File

@ -39,3 +39,7 @@ def test_get_size():
with open("gns3server/symbols/cloud.svg", "rb") as f: with open("gns3server/symbols/cloud.svg", "rb") as f:
res = get_size(f.read()) res = get_size(f.read())
assert res == (159, 71, "svg") assert res == (159, 71, "svg")
# Size with px
with open("tests/resources/firefox.svg", "rb") as f:
res = get_size(f.read())
assert res == (66, 70, "svg")