Rename hypervisor to compute

Fix #487
This commit is contained in:
Julien Duponchelle committed 2016-04-15 17:59:59 +02:00
1 parent 3185baff0d
commit 58d4a529d4
186 files changed
+1173 -1206

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
from .vpcs import VPCS
from .virtualbox import VirtualBox
from .dynamips import Dynamips
from .qemu import Qemu
from .vmware import VMware
MODULES = [VPCS, VirtualBox, Dynamips, Qemu, VMware]
if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1":
from .docker import Docker
MODULES.append(Docker)
# IOU runs only on Linux but testsuite work on UNIX platform
if not sys.platform.startswith("win"):
from .iou import IOU
MODULES.append(IOU)
Whitespace-only changes.
+105
View File
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Adapter(object):
"""
Base class for adapters.
:param interfaces: number of interfaces supported by this adapter.
"""
def __init__(self, interfaces=1):
self._interfaces = interfaces
self._ports = {}
for port_number in range(0, interfaces):
self._ports[port_number] = None
def removable(self):
"""
Returns True if the adapter can be removed from a slot
and False if not.
:returns: boolean
"""
return True
def port_exists(self, port_number):
"""
Checks if a port exists on this adapter.
:returns: True is the port exists,
False otherwise.
"""
if port_number in self._ports:
return True
return False
def add_nio(self, port_number, nio):
"""
Adds a NIO to a port on this adapter.
:param port_number: port number (integer)
:param nio: NIO instance
"""
self._ports[port_number] = nio
def remove_nio(self, port_number):
"""
Removes a NIO from a port on this adapter.
:param port_number: port number (integer)
"""
self._ports[port_number] = None
def get_nio(self, port_number):
"""
Returns the NIO assigned to a port.
:params port_number: port number (integer)
:returns: NIO instance
"""
return self._ports[port_number]
@property
def ports(self):
"""
Returns port to NIO mapping
:returns: dictionary port -> NIO
"""
return self._ports
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this adapter.
:returns: number of interfaces
"""
return self._interfaces
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class EthernetAdapter(Adapter):
"""
Ethernet adapter.
"""
def __init__(self, interfaces=1):
super().__init__(interfaces)
self.host_ifc = None
def __str__(self):
return "Ethernet adapter"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class SerialAdapter(Adapter):
"""
Serial adapter.
"""
def __init__(self, interfaces=1):
super().__init__(interfaces)
def __str__(self):
return "Serial adapter"
+499
View File
@@ -0,0 +1,499 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import struct
import stat
import asyncio
import aiohttp
import socket
import shutil
import re
import logging
log = logging.getLogger(__name__)
from uuid import UUID, uuid4
from gns3server.utils.interfaces import is_interface_up
from ..config import Config
from ..utils.asyncio import wait_run_in_executor
from ..utils import force_unix_path
from .project_manager import ProjectManager
from .nios.nio_udp import NIOUDP
from .nios.nio_tap import NIOTAP
from .nios.nio_nat import NIONAT
from .nios.nio_generic_ethernet import NIOGenericEthernet
from ..utils.images import md5sum, remove_checksum
from .vm_error import VMError
class BaseManager:
"""
Base class for all Manager classes.
Responsible of management of a VM pool of the same type.
"""
_convert_lock = None
def __init__(self):
BaseManager._convert_lock = asyncio.Lock()
self._vms = {}
self._port_manager = None
self._config = Config.instance()
@classmethod
def instance(cls):
"""
Singleton to return only one instance of BaseManager.
:returns: instance of BaseManager
"""
if not hasattr(cls, "_instance") or cls._instance is None:
cls._instance = cls()
return cls._instance
@property
def module_name(self):
"""
Returns the module name.
:returns: module name
"""
return self.__class__.__name__
@property
def port_manager(self):
"""
Returns the port manager.
:returns: Port manager
"""
return self._port_manager
@port_manager.setter
def port_manager(self, new_port_manager):
self._port_manager = new_port_manager
@property
def config(self):
"""
Returns the server config.
:returns: Config
"""
return self._config
@asyncio.coroutine
def unload(self):
tasks = []
for vm_id in self._vms.keys():
tasks.append(asyncio.async(self.close_vm(vm_id)))
if tasks:
done, _ = yield from asyncio.wait(tasks)
for future in done:
try:
future.result()
except (Exception, GeneratorExit) as e:
log.error("Could not close VM {}".format(e), exc_info=1)
continue
if hasattr(BaseManager, "_instance"):
BaseManager._instance = None
log.debug("Module {} unloaded".format(self.module_name))
def get_vm(self, vm_id, project_id=None):
"""
Returns a VM instance.
:param vm_id: VM identifier
:param project_id: Project identifier
:returns: VM instance
"""
if project_id:
# check the project_id exists
project = ProjectManager.instance().get_project(project_id)
try:
UUID(vm_id, version=4)
except ValueError:
raise aiohttp.web.HTTPBadRequest(text="VM ID {} is not a valid UUID".format(vm_id))
if vm_id not in self._vms:
raise aiohttp.web.HTTPNotFound(text="VM ID {} doesn't exist".format(vm_id))
vm = self._vms[vm_id]
if project_id:
if vm.project.id != project.id:
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't belong to VM {}".format(project_id, vm.name))
return vm
@asyncio.coroutine
def convert_old_project(self, project, legacy_id, name):
"""
Convert projects made before version 1.3
:param project: Project instance
:param legacy_id: old identifier
:param name: node name
:returns: new identifier
"""
new_id = str(uuid4())
legacy_project_files_path = os.path.join(project.path, "{}-files".format(project.name))
new_project_files_path = os.path.join(project.path, "project-files")
if os.path.exists(legacy_project_files_path) and not os.path.exists(new_project_files_path):
# move the project files
log.info("Converting old project...")
try:
log.info('Moving "{}" to "{}"'.format(legacy_project_files_path, new_project_files_path))
yield from wait_run_in_executor(shutil.move, legacy_project_files_path, new_project_files_path)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not move project files directory: {} to {} {}".format(legacy_project_files_path,
new_project_files_path, e))
if project.is_local() is False:
legacy_remote_project_path = os.path.join(project.location, project.name, self.module_name.lower())
new_remote_project_path = os.path.join(project.path, "project-files", self.module_name.lower())
if os.path.exists(legacy_remote_project_path) and not os.path.exists(new_remote_project_path):
# move the legacy remote project (remote servers only)
log.info("Converting old remote project...")
try:
log.info('Moving "{}" to "{}"'.format(legacy_remote_project_path, new_remote_project_path))
yield from wait_run_in_executor(shutil.move, legacy_remote_project_path, new_remote_project_path)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not move directory: {} to {} {}".format(legacy_remote_project_path,
new_remote_project_path, e))
if hasattr(self, "get_legacy_vm_workdir"):
# rename old project VM working dir
log.info("Converting old VM working directory...")
legacy_vm_dir = self.get_legacy_vm_workdir(legacy_id, name)
legacy_vm_working_path = os.path.join(new_project_files_path, legacy_vm_dir)
new_vm_working_path = os.path.join(new_project_files_path, self.module_name.lower(), new_id)
if os.path.exists(legacy_vm_working_path) and not os.path.exists(new_vm_working_path):
try:
log.info('Moving "{}" to "{}"'.format(legacy_vm_working_path, new_vm_working_path))
yield from wait_run_in_executor(shutil.move, legacy_vm_working_path, new_vm_working_path)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not move VM working directory: {} to {} {}".format(legacy_vm_working_path,
new_vm_working_path, e))
return new_id
@asyncio.coroutine
def create_vm(self, name, project_id, vm_id, *args, **kwargs):
"""
Create a new VM
:param name: VM name
:param project_id: Project identifier
:param vm_id: restore a VM identifier
"""
if vm_id in self._vms:
return self._vms[vm_id]
project = ProjectManager.instance().get_project(project_id)
if vm_id and isinstance(vm_id, int):
with (yield from BaseManager._convert_lock):
vm_id = yield from self.convert_old_project(project, vm_id, name)
if not vm_id:
vm_id = str(uuid4())
vm = self._VM_CLASS(name, vm_id, project, self, *args, **kwargs)
if asyncio.iscoroutinefunction(vm.create):
yield from vm.create()
else:
vm.create()
self._vms[vm.id] = vm
project.add_vm(vm)
return vm
@asyncio.coroutine
def close_vm(self, vm_id):
"""
Close a VM
:param vm_id: VM identifier
:returns: VM instance
"""
vm = self.get_vm(vm_id)
if asyncio.iscoroutinefunction(vm.close):
yield from vm.close()
else:
vm.close()
return vm
@asyncio.coroutine
def project_closing(self, project):
"""
Called when a project is about to be closed.
:param project: Project instance
"""
pass
@asyncio.coroutine
def project_closed(self, project):
"""
Called when a project is closed.
:param project: Project instance
"""
for vm in project.vms:
if vm.id in self._vms:
del self._vms[vm.id]
@asyncio.coroutine
def project_moved(self, project):
"""
Called when a project is moved
:param project: project instance
"""
pass
@asyncio.coroutine
def project_committed(self, project):
"""
Called when a project is committed.
:param project: Project instance
"""
pass
@asyncio.coroutine
def delete_vm(self, vm_id):
"""
Delete a VM. VM working directory will be destroy when
we receive a commit.
:param vm_id: VM identifier
:returns: VM instance
"""
vm = yield from self.close_vm(vm_id)
vm.project.mark_vm_for_destruction(vm)
if vm.id in self._vms:
del self._vms[vm.id]
return vm
@staticmethod
def has_privileged_access(executable):
"""
Check if an executable can access Ethernet and TAP devices in
RAW mode.
:param executable: executable path
:returns: True or False
"""
if sys.platform.startswith("win"):
# do not check anything on Windows
return True
if os.geteuid() == 0:
# we are root, so we should have privileged access.
return True
if os.stat(executable).st_uid == 0 and (os.stat(executable).st_mode & stat.S_ISUID or os.stat(executable).st_mode & stat.S_ISGID):
# the executable has set UID bit.
return True
# test if the executable has the CAP_NET_RAW capability (Linux only)
try:
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable):
caps = os.getxattr(executable, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return True
except OSError as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(executable, e))
return False
def create_nio(self, executable, nio_settings):
"""
Creates a new NIO.
:param nio_settings: information to create the NIO
:returns: a NIO object
"""
nio = None
if nio_settings["type"] == "nio_udp":
lport = nio_settings["lport"]
rhost = nio_settings["rhost"]
rport = nio_settings["rport"]
try:
info = socket.getaddrinfo(rhost, rport, socket.AF_UNSPEC, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE)
if not info:
raise aiohttp.web.HTTPInternalServerError(text="getaddrinfo returns an empty list on {}:{}".format(rhost, rport))
for res in info:
af, socktype, proto, _, sa = res
with socket.socket(af, socktype, proto) as sock:
sock.connect(sa)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIOUDP(lport, rhost, rport)
elif nio_settings["type"] == "nio_tap":
tap_device = nio_settings["tap_device"]
# if not is_interface_up(tap_device):
# raise aiohttp.web.HTTPConflict(text="TAP interface {} does not exist or is down".format(tap_device))
# FIXME: check for permissions on tap device
# if not self.has_privileged_access(executable):
# raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device))
nio = NIOTAP(tap_device)
elif nio_settings["type"] == "nio_generic_ethernet":
ethernet_device = nio_settings["ethernet_device"]
if not is_interface_up(ethernet_device):
raise aiohttp.web.HTTPConflict(text="Ethernet interface {} does not exist or is down".format(ethernet_device))
nio = NIOGenericEthernet(ethernet_device)
elif nio_settings["type"] == "nio_nat":
nio = NIONAT()
assert nio is not None
return nio
def get_abs_image_path(self, path):
"""
Get the absolute path of an image
:param path: file path
:return: file path
"""
if not path:
return ""
img_directory = self.get_images_directory()
# Windows path should not be send to a unix server
if not sys.platform.startswith("win"):
if re.match(r"^[A-Z]:", path) is not None:
raise VMError("{} is not allowed on this remote server. Please use only a filename in {}.".format(path, img_directory))
if not os.path.isabs(path):
s = os.path.split(path)
path = os.path.normpath(os.path.join(img_directory, *s))
# Compatibility with old topologies we look in parent directory
# We look at first in new location
if not os.path.exists(path):
old_path = os.path.normpath(os.path.join(img_directory, '..', *s))
if os.path.exists(old_path):
return force_unix_path(old_path)
return force_unix_path(path)
else:
# For non local server we disallow using absolute path outside image directory
if Config.instance().get_section_config("Server").get("local", False) is False:
img_directory = self.config.get_section_config("Server").get("images_path", os.path.expanduser("~/GNS3/images"))
img_directory = force_unix_path(img_directory)
path = force_unix_path(path)
if len(os.path.commonprefix([img_directory, path])) < len(img_directory):
raise VMError("{} is not allowed on this remote server. Please use only a filename in {}.".format(path, img_directory))
return force_unix_path(path)
def get_relative_image_path(self, path):
"""
Get a path relative to images directory path
or an abspath if the path is not located inside
image directory
:param path: file path
:return: file path
"""
if not path:
return ""
img_directory = force_unix_path(self.get_images_directory())
path = force_unix_path(self.get_abs_image_path(path))
if os.path.commonprefix([img_directory, path]) == img_directory:
return os.path.relpath(path, img_directory)
return path
@asyncio.coroutine
def list_images(self):
"""
Return the list of available images for this VM type
:returns: Array of hash
"""
images = []
img_dir = self.get_images_directory()
for root, dirs, files in os.walk(img_dir):
for filename in files:
if filename[0] != "." and not filename.endswith(".md5sum"):
path = os.path.relpath(os.path.join(root, filename), img_dir)
images.append({
"filename": filename,
"path": path})
return images
def get_images_directory(self):
"""
Get the image directory on disk
"""
raise NotImplementedError
@asyncio.coroutine
def write_image(self, filename, stream):
directory = self.get_images_directory()
path = os.path.abspath(os.path.join(directory, *os.path.split(filename)))
if os.path.commonprefix([directory, path]) != directory:
raise aiohttp.web.HTTPForbidden(text="Could not write image: {}, {} is forbiden".format(filename, path))
log.info("Writting image file %s", path)
try:
remove_checksum(path)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb+') as f:
while True:
packet = yield from stream.read(512)
if not packet:
break
f.write(packet)
os.chmod(path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
md5sum(path)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Could not write image: {} because {}".format(filename, e))
+455
View File
@@ -0,0 +1,455 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
import aiohttp
import shutil
import asyncio
import tempfile
import psutil
import platform
from pkg_resources import parse_version
from ..utils.asyncio import wait_run_in_executor
from ..ubridge.hypervisor import Hypervisor
from .vm_error import VMError
log = logging.getLogger(__name__)
class BaseVM:
"""
Base vm implementation.
:param name: name of this IOU vm
:param vm_id: IOU instance identifier
:param project: Project instance
:param manager: parent VM Manager
:param console: TCP console port
:param aux: TCP aux console port
:param allocate_aux: Boolean if true will allocate an aux console port
"""
def __init__(self, name, vm_id, project, manager, console=None, console_type="telnet", aux=None, allocate_aux=False):
self._name = name
self._usage = ""
self._id = vm_id
self._project = project
self._manager = manager
self._console = console
self._aux = aux
self._console_type = console_type
self._temporary_directory = None
self._hw_virtualization = False
self._ubridge_hypervisor = None
self._closed = False
self._vm_status = "stopped"
self._command_line = ""
self._allocate_aux = allocate_aux
if self._console is not None:
if console_type == "vnc":
self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project, port_range_start=5900, port_range_end=6000)
else:
self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project)
# We need to allocate aux before giving a random console port
if self._aux is not None:
self._aux = self._manager.port_manager.reserve_tcp_port(self._aux, self._project)
if self._console is None:
if console_type == "vnc":
# VNC is a special case and the range must be 5900-6000
self._console = self._manager.port_manager.get_free_tcp_port(self._project, port_range_start=5900, port_range_end=6000)
else:
self._console = self._manager.port_manager.get_free_tcp_port(self._project)
if self._aux is None and allocate_aux:
self._aux = self._manager.port_manager.get_free_tcp_port(self._project)
log.debug("{module}: {name} [{id}] initialized. Console port {console}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
console=self._console))
def __del__(self):
if self._temporary_directory is not None:
if os.path.exists(self._temporary_directory):
shutil.rmtree(self._temporary_directory, ignore_errors=True)
@property
def status(self):
"""Return current VM status"""
return self._vm_status
@status.setter
def status(self, status):
self._vm_status = status
self._project.emit("vm.{}".format(status), self)
@property
def command_line(self):
"""Return command used to start the VM"""
return self._command_line
@command_line.setter
def command_line(self, command_line):
self._command_line = command_line
@property
def project(self):
"""
Returns the VM current project.
:returns: Project instance.
"""
return self._project
@property
def name(self):
"""
Returns the name for this VM.
:returns: name
"""
return self._name
@name.setter
def name(self, new_name):
"""
Sets the name of this VM.
:param new_name: name
"""
log.info("{module}: {name} [{id}] renamed to {new_name}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
new_name=new_name))
self._name = new_name
@property
def usage(self):
"""
Returns the usage for this VM.
:returns: usage
"""
return self._usage
@usage.setter
def usage(self, new_usage):
"""
Sets the usage of this VM.
:param new_usage: usage
"""
self._usage = new_usage
@property
def id(self):
"""
Returns the ID for this VM.
:returns: VM identifier (string)
"""
return self._id
@property
def manager(self):
"""
Returns the manager for this VM.
:returns: instance of manager
"""
return self._manager
@property
def working_dir(self):
"""
Return VM working directory
"""
return self._project.vm_working_directory(self)
@property
def temporary_directory(self):
if self._temporary_directory is None:
try:
self._temporary_directory = tempfile.mkdtemp()
except OSError as e:
raise VMError("Can't create temporary directory: {}".format(e))
return self._temporary_directory
def create(self):
"""
Creates the VM.
"""
log.info("{module}: {name} [{id}] created".format(module=self.manager.module_name,
name=self.name,
id=self.id))
self._project.emit("vm.created", self)
@asyncio.coroutine
def delete(self):
"""
Delete the VM (including all its files).
"""
self._project.emit("vm.deleted", self)
directory = self.project.vm_working_directory(self)
if os.path.exists(directory):
try:
yield from wait_run_in_executor(shutil.rmtree, directory)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not delete the VM working directory: {}".format(e))
def start(self):
"""
Starts the VM process.
"""
raise NotImplementedError
def stop(self):
"""
Starts the VM process.
"""
raise NotImplementedError
@asyncio.coroutine
def close(self):
"""
Close the VM process.
"""
if self._closed:
return False
log.info("{module}: '{name}' [{id}]: is closing".format(
module=self.manager.module_name,
name=self.name,
id=self.id))
if self._console:
self._manager.port_manager.release_tcp_port(self._console, self._project)
self._console = None
if self._aux:
self._manager.port_manager.release_tcp_port(self._aux, self._project)
self._aux = None
self._closed = True
return True
@property
def allocate_aux(self):
"""
:returns: Boolean allocate or not an aux console
"""
return self._allocate_aux
@allocate_aux.setter
def allocate_aux(self, allocate_aux):
"""
:returns: Boolean allocate or not an aux console
"""
self._allocate_aux = allocate_aux
@property
def aux(self):
"""
Returns the aux console port of this VM.
:returns: aux console port
"""
return self._aux
@aux.setter
def aux(self, aux):
"""
Changes the aux port
:params aux: Console port (integer) or None to free the port
"""
if aux == self._aux:
return
if self._aux:
self._manager.port_manager.release_tcp_port(self._aux, self._project)
self._aux = None
if aux is not None:
self._aux = self._manager.port_manager.reserve_tcp_port(aux, self._project)
log.info("{module}: '{name}' [{id}]: aux port set to {port}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
port=aux))
@property
def console(self):
"""
Returns the console port of this VM.
:returns: console port
"""
return self._console
@console.setter
def console(self, console):
"""
Changes the console port
:params console: Console port (integer) or None to free the port
"""
if console == self._console:
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 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:
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,
port=console))
@property
def console_type(self):
"""
Returns the console type for this VM.
:returns: console type (string)
"""
return self._console_type
@console_type.setter
def console_type(self, console_type):
"""
Sets the console type for this VM.
:param console_type: console type (string)
"""
log.info('QEMU VM "{name}" [{id}] has set the console type to {console_type}'.format(name=self._name,
id=self._id,
console_type=console_type))
if console_type != self._console_type:
# get a new port if the console type change
self._manager.port_manager.release_tcp_port(self._console, self._project)
if console_type == "vnc":
# VNC is a special case and the range must be 5900-6000
self._console = self._manager.port_manager.get_free_tcp_port(self._project, 5900, 6000)
else:
self._console = self._manager.port_manager.get_free_tcp_port(self._project)
self._console_type = console_type
log.info("{module}: '{name}' [{id}]: console type set to {console_type}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
console_type=console_type))
@property
def ubridge_path(self):
"""
Returns the uBridge executable path.
:returns: path to uBridge
"""
path = self._manager.config.get_section_config("Server").get("ubridge_path", "ubridge")
if path == "ubridge":
path = shutil.which("ubridge")
if path is None or len(path) == 0:
raise VMError("uBridge is not installed")
return path
@asyncio.coroutine
def _start_ubridge(self):
"""
Starts uBridge (handles connections to and from this VMware VM).
"""
if not self._manager.has_privileged_access(self.ubridge_path):
raise VMError("uBridge requires root access or capability to interact with network adapters")
server_config = self._manager.config.get_section_config("Server")
server_host = server_config.get("host")
self._ubridge_hypervisor = Hypervisor(self._project, self.ubridge_path, self.working_dir, server_host)
log.info("Starting new uBridge hypervisor {}:{}".format(self._ubridge_hypervisor.host, self._ubridge_hypervisor.port))
yield from self._ubridge_hypervisor.start()
log.info("Hypervisor {}:{} has successfully started".format(self._ubridge_hypervisor.host, self._ubridge_hypervisor.port))
yield from self._ubridge_hypervisor.connect()
@property
def hw_virtualization(self):
"""
Returns either the VM is using hardware virtualization or not.
:return: boolean
"""
return self._hw_virtualization
def check_available_ram(self, requested_ram):
"""
Sends a warning notification if there is not enough RAM on the system to allocate requested RAM.
:param requested_ram: requested amount of RAM in MB
"""
available_ram = int(psutil.virtual_memory().available / (1024 * 1024))
percentage_left = psutil.virtual_memory().percent
if requested_ram > available_ram:
message = '"{}" requires {}MB of RAM to run but there is only {}MB - {}% of RAM left on "{}"'.format(self.name,
requested_ram,
available_ram,
percentage_left,
platform.node())
self.project.emit("log.warning", {"message": message})
+155
View File
@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Docker server module.
"""
import asyncio
import logging
import aiohttp
import urllib
import json
from pkg_resources import parse_version
log = logging.getLogger(__name__)
from ..base_manager import BaseManager
from ..project_manager import ProjectManager
from .docker_vm import DockerVM
from .docker_error import *
DOCKER_MINIMUM_API_VERSION = "1.21"
class Docker(BaseManager):
_VM_CLASS = DockerVM
def __init__(self):
super().__init__()
self._server_url = '/var/run/docker.sock'
self._connected = False
# Allow locking during ubridge operations
self.ubridge_lock = asyncio.Lock()
@asyncio.coroutine
def connector(self):
if not self._connected:
try:
self._connector = aiohttp.connector.UnixConnector(self._server_url)
self._connected = True
version = yield from self.query("GET", "version")
except (aiohttp.errors.ClientOSError, FileNotFoundError):
self._connected = False
raise DockerError("Can't connect to docker daemon")
if parse_version(version["ApiVersion"]) < parse_version(DOCKER_MINIMUM_API_VERSION):
raise DockerError("Docker API version is {}. But GNS3 require a minimum API version {}".format(version["ApiVersion"], DOCKER_MINIMUM_API_VERSION))
return self._connector
def __del__(self):
if self._connected:
self._connector.close()
@asyncio.coroutine
def query(self, method, path, data={}, params={}):
"""
Make a query to the docker daemon and decode the request
:param method: HTTP method
:param path: Endpoint in API
:param data: Dictionnary with the body. Will be transformed to a JSON
:param params: Parameters added as a query arg
"""
response = yield from self.http_query(method, path, data=data, params=params)
body = yield from response.read()
if len(body):
if response.headers['CONTENT-TYPE'] == 'application/json':
body = json.loads(body.decode("utf-8"))
else:
body = body.decode("utf-8")
log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body)
return body
@asyncio.coroutine
def http_query(self, method, path, data={}, params={}):
"""
Make a query to the docker daemon
:param method: HTTP method
:param path: Endpoint in API
:param data: Dictionnary with the body. Will be transformed to a JSON
:param params: Parameters added as a query arg
:returns: HTTP response
"""
data = json.dumps(data)
url = "http://docker/" + path
response = yield from aiohttp.request(
method,
url,
connector=(yield from self.connector()),
params=params,
data=data,
headers={"content-type": "application/json", },
)
if response.status >= 300:
body = yield from response.read()
try:
body = json.loads(body.decode("utf-8"))["message"]
except ValueError:
pass
log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body)
if response.status == 304:
raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body))
elif response.status == 404:
raise DockerHttp404Error("Docker has returned an error: {} {}".format(response.status, body))
else:
raise DockerError("Docker has returned an error: {} {}".format(response.status, body))
return response
@asyncio.coroutine
def websocket_query(self, path, params={}):
"""
Open a websocket connection
:param path: Endpoint in API
:param params: Parameters added as a query arg
:returns: Websocket
"""
url = "http://docker/" + path
connection = yield from aiohttp.ws_connect(url,
connector=(yield from self.connector()),
origin="http://docker",
autoping=True)
return connection
@asyncio.coroutine
def list_images(self):
"""Gets Docker image list.
:returns: list of dicts
:rtype: list
"""
images = []
for image in (yield from self.query("GET", "images/json", params={"all": 0})):
for tag in image['RepoTags']:
if tag != "<none>:<none>":
images.append({'image': tag})
return sorted(images, key=lambda i: i['image'])
+34
View File
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Custom exceptions for the Docker module.
"""
from ..vm_error import VMError
class DockerError(VMError):
pass
class DockerHttp304Error(DockerError):
pass
class DockerHttp404Error(DockerError):
pass
+794
View File
@@ -0,0 +1,794 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Docker container instance.
"""
import asyncio
import shutil
import psutil
import shlex
import aiohttp
import json
import os
from ...ubridge.hypervisor import Hypervisor
from .docker_error import *
from ..base_vm import BaseVM
from ..adapters.ethernet_adapter import EthernetAdapter
from ..nios.nio_udp import NIOUDP
from ...utils.asyncio.telnet_server import AsyncioTelnetServer
from ...utils.asyncio import wait_for_file_creation
from ...utils.get_resource import get_resource
from ...ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError
import logging
log = logging.getLogger(__name__)
class DockerVM(BaseVM):
"""Docker container implementation.
:param name: Docker container name
:param vm_id: Docker VM identifier
:param project: Project instance
:param manager: Manager instance
:param image: Docker image
: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",
console_resolution="1024x768"):
super().__init__(name, vm_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type)
self._image = image
self._start_command = start_command
self._environment = environment
self._cid = None
self._ethernet_adapters = []
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
else:
self.adapters = adapters
log.debug(
"{module}: {name} [{image}] initialized.".format(
module=self.manager.module_name,
name=self.name,
image=self._image))
def __json__(self):
return {
"name": self._name,
"vm_id": self._id,
"container_id": self._cid,
"project_id": self._project.id,
"image": self._image,
"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,
"vm_directory": self.working_dir
}
def _get_free_display_port(self):
"""
Search a free display port
"""
display = 100
if not os.path.exists("/tmp/.X11-unix/"):
return display
while True:
if not os.path.exists("/tmp/.X11-unix/X{}".format(display)):
return display
display += 1
@property
def start_command(self):
return self._start_command
@start_command.setter
def start_command(self, command):
command = command.strip()
if len(command) == 0:
self._start_command = None
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
@environment.setter
def environment(self, command):
self._environment = command
@asyncio.coroutine
def _get_container_state(self):
"""Returns the container state (e.g. running, paused etc.)
:returns: state
:rtype: str
"""
result = yield from self.manager.query("GET", "containers/{}/json".format(self._cid))
if result["State"]["Paused"]:
return "paused"
if result["State"]["Running"]:
return "running"
return "exited"
@asyncio.coroutine
def _get_image_informations(self):
"""
:returns: Dictionnary informations about the container image
"""
result = yield from self.manager.query("GET", "images/{}/json".format(self._image))
return result
def _mount_binds(self, image_infos):
"""
:returns: Return the path that we need to map to local folders
"""
binds = []
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
for volume in volumes.keys():
source = os.path.join(self.working_dir, os.path.relpath(volume, "/"))
os.makedirs(source, exist_ok=True)
binds.append("{}:{}".format(source, volume))
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."""
try:
image_infos = yield from self._get_image_informations()
except DockerHttp404Error:
log.info("Image %s is missing pulling it from docker hub", self._image)
yield from self.pull_image(self._image)
image_infos = yield from self._get_image_informations()
params = {
"Hostname": self._name,
"Name": self._name,
"Image": self._image,
"NetworkDisabled": True,
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
"HostConfig": {
"CapAdd": ["ALL"],
"Privileged": True,
"Binds": self._mount_binds(image_infos)
},
"Volumes": {},
"Env": [],
"Cmd": [],
"Entrypoint": image_infos.get("Config", {"Entrypoint": []})["Entrypoint"]
}
if params["Entrypoint"] is None:
params["Entrypoint"] = []
if self._start_command:
params["Cmd"] = shlex.split(self._start_command)
if len(params["Cmd"]) == 0:
params["Cmd"] = image_infos.get("Config", {"Cmd": []})["Cmd"]
if params["Cmd"] is None:
params["Cmd"] = []
if len(params["Cmd"]) == 0 and len(params["Entrypoint"]) == 0:
params["Cmd"] = ["/bin/sh"]
params["Entrypoint"].insert(0, "/gns3/init.sh")
if self._environment:
params["Env"] += [e.strip() for e in self._environment.split("\n")]
if self._console_type == "vnc":
yield from self._start_vnc()
params["Env"].append("DISPLAY=:{}".format(self._display))
params["HostConfig"]["Binds"].append("/tmp/.X11-unix/:/tmp/.X11-unix/")
result = yield from self.manager.query("POST", "containers/create", data=params)
self._cid = result['Id']
log.info("Docker container '{name}' [{id}] created".format(
name=self._name, id=self._id))
return True
@asyncio.coroutine
def update(self):
"""
Destroy an recreate the container with the new settings
"""
# 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()
@asyncio.coroutine
def start(self):
"""Starts this Docker container."""
state = yield from self._get_container_state()
if state == "paused":
yield from self.unpause()
else:
result = yield from self.manager.query("POST", "containers/{}/start".format(self._cid))
namespace = yield from self._get_namespace()
yield from self._start_ubridge()
for adapter_number in range(0, self.adapters):
nio = self._ethernet_adapters[adapter_number].get_nio(0)
with (yield from self.manager.ubridge_lock):
try:
yield from self._add_ubridge_connection(nio, adapter_number, namespace)
except UbridgeNamespaceError:
yield from self.stop()
# The container can crash soon after the start this mean we can not move the interface to the container namespace
logdata = yield from self._get_log()
for line in logdata.split('\n'):
log.error(line)
raise DockerError(logdata)
if self.console_type == "telnet":
yield from self._start_console()
if self.allocate_aux:
yield from self._start_aux()
self.status = "started"
log.info("Docker container '{name}' [{image}] started listen for {console_type} on {console}".format(name=self._name, image=self._image, console=self.console, console_type=self.console_type))
@asyncio.coroutine
def _start_aux(self):
"""
Start an auxilary console
"""
# 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, "/gns3/bin/busybox", "sh", "-i",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
stdin=asyncio.subprocess.PIPE)
server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin, binary=False, echo=False)
self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux)))
log.debug("Docker container '%s' started listen for auxilary telnet on %d", self.name, self.aux)
@asyncio.coroutine
def _start_vnc(self):
"""
Start a VNC server for this container
"""
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", 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)
@asyncio.coroutine
def _start_console(self):
"""
Start streaming the console via telnet
"""
class InputStream:
def __init__(self):
self._data = b""
def write(self, data):
self._data += data
@asyncio.coroutine
def drain(self):
if not self.ws.closed:
self.ws.send_bytes(self._data)
self._data = b""
output_stream = asyncio.StreamReader()
input_stream = InputStream()
telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True)
self._telnet_servers.append((yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console)))
ws = yield from self.manager.websocket_query("containers/{}/attach/ws?stream=1&stdin=1&stdout=1&stderr=1".format(self._cid))
input_stream.ws = ws
output_stream.feed_data(self.name.encode() + b" console is now available... Press RETURN to get started.\r\n")
asyncio.async(self._read_console_output(ws, output_stream))
@asyncio.coroutine
def _read_console_output(self, ws, out):
"""
Read websocket and forward it to the telnet
:params ws: Websocket connection
:param out: Output stream
"""
while True:
msg = yield from ws.receive()
if msg.tp == aiohttp.MsgType.text:
out.feed_data(msg.data.encode())
else:
out.feed_eof()
ws.close()
break
def is_running(self):
"""Checks if the container is running.
:returns: True or False
:rtype: bool
"""
state = yield from self._get_container_state()
if state == "running":
return True
return False
@asyncio.coroutine
def restart(self):
"""Restart this Docker container."""
yield from self.manager.query("POST", "containers/{}/restart".format(self._cid))
log.info("Docker container '{name}' [{image}] restarted".format(
name=self._name, image=self._image))
@asyncio.coroutine
def stop(self):
"""Stops this Docker container."""
try:
if len(self._telnet_servers) > 0:
for telnet_server in self._telnet_servers:
telnet_server.close()
yield from telnet_server.wait_closed()
self._telnet_servers = []
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
yield from self._ubridge_hypervisor.stop()
state = yield from self._get_container_state()
if state == "paused":
yield from self.unpause()
# t=5 number of seconds to wait before killing the container
try:
yield from self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5})
log.info("Docker container '{name}' [{image}] stopped".format(
name=self._name, image=self._image))
except DockerHttp304Error:
# Container is already stopped
pass
# Ignore runtime error because when closing the server
except RuntimeError as e:
log.debug("Docker runtime error when closing: {}".format(str(e)))
return
@asyncio.coroutine
def pause(self):
"""Pauses this Docker container."""
yield from self.manager.query("POST", "containers/{}/pause".format(self._cid))
log.info("Docker container '{name}' [{image}] paused".format(
name=self._name, image=self._image))
self.status = "paused"
@asyncio.coroutine
def unpause(self):
"""Unpauses this Docker container."""
yield from self.manager.query("POST", "containers/{}/unpause".format(self._cid))
log.info("Docker container '{name}' [{image}] unpaused".format(
name=self._name, image=self._image))
self.status = "started"
@asyncio.coroutine
def close(self):
"""Closes this Docker container."""
if not (yield from super().close()):
return False
try:
if self.console_type == "vnc":
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":
yield from self.stop()
yield from self.manager.query("DELETE", "containers/{}".format(self._cid), params={"force": 1})
log.info("Docker container '{name}' [{image}] removed".format(
name=self._name, image=self._image))
for adapter in self._ethernet_adapters:
if adapter is not None:
for nio in adapter.ports.values():
if nio and isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
# Ignore runtime error because when closing the server
except (DockerHttp404Error, RuntimeError) as e:
log.debug("Docker error when closing: {}".format(str(e)))
return
@asyncio.coroutine
def _add_ubridge_connection(self, nio, adapter_number, namespace):
"""
Creates a connection in uBridge.
:param nio: NIO instance or None if it's a dummu interface (if an interface is missing in ubridge you can't see it via ifconfig in the container)
:param adapter_number: adapter number
:param namespace: Container namespace (pid)
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except IndexError:
raise DockerError(
"Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(name=self.name, adapter_number=adapter_number))
for index in range(128):
if "veth-gns3-ext{}".format(index) not in psutil.net_if_addrs():
adapter.ifc = "eth{}".format(str(index))
adapter.host_ifc = "veth-gns3-ext{}".format(str(index))
adapter.guest_ifc = "veth-gns3-int{}".format(str(index))
break
if not hasattr(adapter, "ifc"):
raise DockerError(
"Adapter {adapter_number} couldn't allocate interface on Docker container '{name}'. Too many Docker interfaces already exists".format(
name=self.name, adapter_number=adapter_number))
yield from self._ubridge_hypervisor.send(
'docker create_veth {hostif} {guestif}'.format(
guestif=adapter.guest_ifc, hostif=adapter.host_ifc))
log.debug("Move container %s adapter %s to namespace %s", self.name, adapter.guest_ifc, namespace)
try:
yield from self._ubridge_hypervisor.send(
'docker move_to_ns {ifc} {ns} eth{adapter}'.format(
ifc=adapter.guest_ifc, ns=namespace, adapter=adapter_number))
except UbridgeError as e:
raise UbridgeNamespaceError(e)
if isinstance(nio, NIOUDP):
yield from self._ubridge_hypervisor.send(
'bridge create bridge{}'.format(adapter_number))
yield from self._ubridge_hypervisor.send(
'bridge add_nio_linux_raw bridge{adapter} {ifc}'.format(
ifc=adapter.host_ifc, adapter=adapter_number))
yield from self._ubridge_hypervisor.send(
'bridge add_nio_udp bridge{adapter} {lport} {rhost} {rport}'.format(
adapter=adapter_number, lport=nio.lport, rhost=nio.rhost,
rport=nio.rport))
if nio.capturing:
yield from self._ubridge_hypervisor.send(
'bridge start_capture bridge{adapter} "{pcap_file}"'.format(
adapter=adapter_number, pcap_file=nio.pcap_output_file))
yield from self._ubridge_hypervisor.send(
'bridge start bridge{adapter}'.format(adapter=adapter_number))
def _delete_ubridge_connection(self, adapter_number):
"""Deletes a connection in uBridge.
:param adapter_number: adapter number
"""
if not self._ubridge_hypervisor or not self._ubridge_hypervisor.is_running():
return
adapter = self._ethernet_adapters[adapter_number]
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):
result = yield from self.manager.query("GET", "containers/{}/json".format(self._cid))
return int(result['State']['Pid'])
@asyncio.coroutine
def adapter_add_nio_binding(self, adapter_number, nio):
"""Adds an adapter NIO binding.
:param adapter_number: adapter number
:param nio: NIO instance to add to the slot/port
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except IndexError:
raise DockerError(
"Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(
name=self.name, adapter_number=adapter_number))
adapter.add_nio(0, nio)
log.info(
"Docker container '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(
name=self.name,
id=self._id,
nio=nio,
adapter_number=adapter_number))
@asyncio.coroutine
def adapter_remove_nio_binding(self, adapter_number):
"""
Removes an adapter NIO binding.
:param adapter_number: adapter number
:returns: NIO instance
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except IndexError:
raise DockerError(
"Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(
name=self.name, adapter_number=adapter_number))
adapter.remove_nio(0)
yield from self._delete_ubridge_connection(adapter_number)
log.info(
"Docker VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(
name=self.name, id=self.id, nio=adapter.host_ifc,
adapter_number=adapter_number))
@property
def adapters(self):
"""Returns the number of Ethernet adapters for this Docker VM.
:returns: number of adapters
:rtype: int
"""
return len(self._ethernet_adapters)
@adapters.setter
def adapters(self, adapters):
"""Sets the number of Ethernet adapters for this Docker container.
:param adapters: number of adapters
"""
self._ethernet_adapters.clear()
for adapter_number in range(0, adapters):
self._ethernet_adapters.append(EthernetAdapter())
log.info(
'Docker container "{name}" [{id}]: number of Ethernet adapters changed to {adapters}'.format(
name=self._name,
id=self._id,
adapters=adapters))
@asyncio.coroutine
def pull_image(self, image):
"""
Pull image from docker repository
"""
log.info("Pull %s from docker hub", image)
response = yield from self.manager.http_query("POST", "images/create", params={"fromImage": image})
# The pull api will stream status via an HTTP JSON stream
content = ""
while True:
chunk = yield from response.content.read(1024)
if not chunk:
break
content += chunk.decode("utf-8")
try:
while True:
content = content.lstrip(" \r\n\t")
answer, index = json.JSONDecoder().raw_decode(content)
if "progress" in answer:
self.project.emit("log.info", {"message": "Pulling image {}:{}: {}".format(self._image, answer["id"], answer["progress"])})
content = content[index:]
except ValueError: # Partial JSON
pass
self.project.emit("log.info", {"message": "Success pulling image {}".format(self._image)})
@asyncio.coroutine
def _start_ubridge_capture(self, adapter_number, output_file):
"""
Start a packet capture in uBridge.
:param adapter_number: adapter number
:param output_file: PCAP destination file for the capture
"""
adapter = "bridge{}".format(adapter_number)
if not self._ubridge_hypervisor or not self._ubridge_hypervisor.is_running():
raise VMwareError("Cannot start the packet capture: uBridge is not running")
yield from self._ubridge_hypervisor.send('bridge start_capture {name} "{output_file}"'.format(name=adapter, output_file=output_file))
@asyncio.coroutine
def _stop_ubridge_capture(self, adapter_number):
"""
Stop a packet capture in uBridge.
:param adapter_number: adapter number
"""
adapter = "bridge{}".format(adapter_number)
if not self._ubridge_hypervisor or not self._ubridge_hypervisor.is_running():
raise VMwareError("Cannot stop the packet capture: uBridge is not running")
yield from self._ubridge_hypervisor.send("bridge stop_capture {name}".format(name=adapter))
@asyncio.coroutine
def start_capture(self, adapter_number, output_file):
"""
Starts a packet capture.
:param adapter_number: adapter number
:param output_file: PCAP destination file for the capture
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except KeyError:
raise DockerError("Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
nio = adapter.get_nio(0)
if not nio:
raise DockerError("Adapter {} is not connected".format(adapter_number))
if nio.capturing:
raise DockerError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number))
nio.startPacketCapture(output_file)
if self.status == "started":
yield from self._start_ubridge_capture(adapter_number, output_file)
log.info("Docker VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name,
id=self.id,
adapter_number=adapter_number))
def stop_capture(self, adapter_number):
"""
Stops a packet capture.
:param adapter_number: adapter number
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except KeyError:
raise DockerError("Adapter {adapter_number} doesn't exist on Docker VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
nio = adapter.get_nio(0)
if not nio:
raise DockerError("Adapter {} is not connected".format(adapter_number))
nio.stopPacketCapture()
if self.status == "started":
yield from self._stop_ubridge_capture(adapter_number)
log.info("Docker VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name,
id=self.id,
adapter_number=adapter_number))
@asyncio.coroutine
def _get_log(self):
"""
Return the log from the container
:returns: string
"""
result = yield from self.manager.query("GET", "containers/{}/logs".format(self._cid), params={"stderr": 1, "stdout": 1})
return result
@asyncio.coroutine
def delete(self):
"""
Delete the VM (including all its files).
"""
yield from self.close()
yield from super().delete()
@@ -0,0 +1,138 @@
#!/tmp/gns3/bin/sh
# script for udhcpc
# Copyright (c) 2008 Natanael Copa <natanael.copa@gmail.com>
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
+58
View File
@@ -0,0 +1,58 @@
#!/gns3/bin/busybox sh
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# 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
# /etc/hosts
[ -s /etc/hosts ] || cat > /etc/hosts << __EOF__
127.0.1.1 $HOSTNAME
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
__EOF__
# configure loopback interface
ip link set dev lo up
# 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 "$@"
+734
View File
@@ -0,0 +1,734 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Dynamips server module.
"""
import aiohttp
import sys
import os
import shutil
import socket
import time
import asyncio
import tempfile
import logging
import glob
log = logging.getLogger(__name__)
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
from ..base_manager import BaseManager
from ..project_manager import ProjectManager
from ..port_manager import PortManager
from .dynamips_error import DynamipsError
from .hypervisor import Hypervisor
from .nodes.router import Router
from .dynamips_vm import DynamipsVM
from .dynamips_device import DynamipsDevice
# NIOs
from .nios.nio_udp import NIOUDP
from .nios.nio_unix import NIOUNIX
from .nios.nio_vde import NIOVDE
from .nios.nio_tap import NIOTAP
from .nios.nio_generic_ethernet import NIOGenericEthernet
from .nios.nio_linux_ethernet import NIOLinuxEthernet
from .nios.nio_null import NIONull
# Adapters
from .adapters.c7200_io_2fe import C7200_IO_2FE
from .adapters.c7200_io_fe import C7200_IO_FE
from .adapters.c7200_io_ge_e import C7200_IO_GE_E
from .adapters.nm_16esw import NM_16ESW
from .adapters.nm_1e import NM_1E
from .adapters.nm_1fe_tx import NM_1FE_TX
from .adapters.nm_4e import NM_4E
from .adapters.nm_4t import NM_4T
from .adapters.pa_2fe_tx import PA_2FE_TX
from .adapters.pa_4e import PA_4E
from .adapters.pa_4t import PA_4T
from .adapters.pa_8e import PA_8E
from .adapters.pa_8t import PA_8T
from .adapters.pa_a1 import PA_A1
from .adapters.pa_fe_tx import PA_FE_TX
from .adapters.pa_ge import PA_GE
from .adapters.pa_pos_oc3 import PA_POS_OC3
from .adapters.wic_1enet import WIC_1ENET
from .adapters.wic_1t import WIC_1T
from .adapters.wic_2t import WIC_2T
ADAPTER_MATRIX = {"C7200-IO-2FE": C7200_IO_2FE,
"C7200-IO-FE": C7200_IO_FE,
"C7200-IO-GE-E": C7200_IO_GE_E,
"NM-16ESW": NM_16ESW,
"NM-1E": NM_1E,
"NM-1FE-TX": NM_1FE_TX,
"NM-4E": NM_4E,
"NM-4T": NM_4T,
"PA-2FE-TX": PA_2FE_TX,
"PA-4E": PA_4E,
"PA-4T+": PA_4T,
"PA-8E": PA_8E,
"PA-8T": PA_8T,
"PA-A1": PA_A1,
"PA-FE-TX": PA_FE_TX,
"PA-GE": PA_GE,
"PA-POS-OC3": PA_POS_OC3}
WIC_MATRIX = {"WIC-1ENET": WIC_1ENET,
"WIC-1T": WIC_1T,
"WIC-2T": WIC_2T}
class Dynamips(BaseManager):
_VM_CLASS = DynamipsVM
_DEVICE_CLASS = DynamipsDevice
_ghost_ios_lock = None
def __init__(self):
super().__init__()
Dynamips._ghost_ios_lock = asyncio.Lock()
self._devices = {}
self._ghost_files = set()
self._dynamips_path = None
self._dynamips_ids = {}
def get_dynamips_id(self, project_id):
"""
:param project_id: UUID of the project
:returns: a free dynamips id
"""
self._dynamips_ids.setdefault(project_id, set())
dynamips_id = 0
for dynamips_id in range(1, 4097):
if dynamips_id not in self._dynamips_ids[project_id]:
self._dynamips_ids[project_id].add(dynamips_id)
return dynamips_id
raise DynamipsError("Maximum number of Dynamips instances reached")
def take_dynamips_id(self, project_id, dynamips_id):
"""
Reserve a dynamips id or raise an error
:param project_id: UUID of the project
:param dynamips_id: Asked id
"""
self._dynamips_ids.setdefault(project_id, set())
if dynamips_id in self._dynamips_ids[project_id]:
raise DynamipsError("Dynamips identifier {} is already used by another router".format(dynamips_id))
self._dynamips_ids[project_id].add(dynamips_id)
def release_dynamips_id(self, project_id, dynamips_id):
"""
A dynamips id can be reused by another vm
:param project_id: UUID of the project
:param dynamips_id: Asked id
"""
self._dynamips_ids.setdefault(project_id, set())
if dynamips_id in self._dynamips_ids[project_id]:
self._dynamips_ids[project_id].remove(dynamips_id)
@asyncio.coroutine
def unload(self):
yield from BaseManager.unload(self)
tasks = []
for device in self._devices.values():
tasks.append(asyncio.async(device.hypervisor.stop()))
if tasks:
done, _ = yield from asyncio.wait(tasks)
for future in done:
try:
future.result()
except (Exception, GeneratorExit) as e:
log.error("Could not stop device hypervisor {}".format(e), exc_info=1)
continue
@asyncio.coroutine
def project_closing(self, project):
"""
Called when a project is about to be closed.
:param project: Project instance
"""
yield from super().project_closing(project)
# delete the Dynamips devices corresponding to the project
tasks = []
for device in self._devices.values():
if device.project.id == project.id:
tasks.append(asyncio.async(device.delete()))
if tasks:
done, _ = yield from asyncio.wait(tasks)
for future in done:
try:
future.result()
except (Exception, GeneratorExit) as e:
log.error("Could not delete device {}".format(e), exc_info=1)
@asyncio.coroutine
def project_closed(self, project):
"""
Called when a project is closed.
:param project: Project instance
"""
yield from super().project_closed(project)
# delete useless Dynamips files
project_dir = project.module_working_path(self.module_name.lower())
files = glob.glob(os.path.join(glob.escape(project_dir), "*.ghost"))
files += glob.glob(os.path.join(glob.escape(project_dir), "*_lock"))
files += glob.glob(os.path.join(glob.escape(project_dir), "ilt_*"))
files += glob.glob(os.path.join(glob.escape(project_dir), "c[0-9][0-9][0-9][0-9]_i[0-9]*_rommon_vars"))
files += glob.glob(os.path.join(glob.escape(project_dir), "c[0-9][0-9][0-9][0-9]_i[0-9]*_log.txt"))
for file in files:
try:
log.debug("Deleting file {}".format(file))
if file in self._ghost_files:
self._ghost_files.remove(file)
yield from wait_run_in_executor(os.remove, file)
except OSError as e:
log.warn("Could not delete file {}: {}".format(file, e))
continue
# Release the dynamips ids if we want to reload the same project
# later
if project.id in self._dynamips_ids:
del self._dynamips_ids[project.id]
@asyncio.coroutine
def project_moved(self, project):
"""
Called when a project is moved.
:param project: Project instance
"""
for vm in self._vms.values():
if vm.project.id == project.id:
yield from vm.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower()))
for device in self._devices.values():
if device.project.id == project.id:
yield from device.hypervisor.set_working_dir(project.module_working_directory(self.module_name.lower()))
@asyncio.coroutine
def project_committed(self, project):
"""
Called when a project has been committed.
:param project: Project instance
"""
# save the configs when the project is committed
for vm in self._vms.copy().values():
if vm.project.id == project.id:
try:
yield from vm.save_configs()
except DynamipsError as e:
log.warning(e)
continue
@property
def dynamips_path(self):
"""
Returns the path to Dynamips.
:returns: path
"""
return self._dynamips_path
@asyncio.coroutine
def create_device(self, name, project_id, device_id, device_type, *args, **kwargs):
"""
Create a new Dynamips device.
:param name: Device name
:param project_id: Project identifier
:param vm_id: restore a VM identifier
"""
project = ProjectManager.instance().get_project(project_id)
if device_id and isinstance(device_id, int):
with (yield from BaseManager._convert_lock):
device_id = yield from self.convert_old_project(project, device_id, name)
if not device_id:
device_id = str(uuid4())
device = self._DEVICE_CLASS(name, device_id, project, self, device_type, *args, **kwargs)
yield from device.create()
self._devices[device.id] = device
return device
def get_device(self, device_id, project_id=None):
"""
Returns a device instance.
:param device_id: Device identifier
:param project_id: Project identifier
:returns: Device instance
"""
if project_id:
# check the project_id exists
project = ProjectManager.instance().get_project(project_id)
try:
UUID(device_id, version=4)
except ValueError:
raise aiohttp.web.HTTPBadRequest(text="Device ID} is not a valid UUID".format(device_id))
if device_id not in self._devices:
raise aiohttp.web.HTTPNotFound(text="Device ID {} doesn't exist".format(device_id))
device = self._devices[device_id]
if project_id:
if device.project.id != project.id:
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't belong to device {}".format(project_id, device.name))
return device
@asyncio.coroutine
def delete_device(self, device_id):
"""
Delete a device
:param device_id: Device identifier
:returns: Device instance
"""
device = self.get_device(device_id)
yield from device.delete()
del self._devices[device.id]
return device
def find_dynamips(self):
# look for Dynamips
dynamips_path = self.config.get_section_config("Dynamips").get("dynamips_path")
if not dynamips_path:
dynamips_path = shutil.which("dynamips")
if not dynamips_path:
raise DynamipsError("Could not find Dynamips")
if not os.path.isfile(dynamips_path):
raise DynamipsError("Dynamips {} is not accessible".format(dynamips_path))
if not os.access(dynamips_path, os.X_OK):
raise DynamipsError("Dynamips is not executable")
self._dynamips_path = dynamips_path
return dynamips_path
@asyncio.coroutine
def start_new_hypervisor(self, working_dir=None):
"""
Creates a new Dynamips process and start it.
:param working_dir: working directory
:returns: the new hypervisor instance
"""
if not self._dynamips_path:
self.find_dynamips()
if not working_dir:
working_dir = tempfile.gettempdir()
# FIXME: hypervisor should always listen to 127.0.0.1
# See https://github.com/GNS3/dynamips/issues/62
server_config = self.config.get_section_config("Server")
server_host = server_config.get("host")
try:
info = socket.getaddrinfo(server_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
if not info:
raise DynamipsError("getaddrinfo returns an empty list on {}".format(server_host))
for res in info:
af, socktype, proto, _, sa = res
# let the OS find an unused port for the Dynamips hypervisor
with socket.socket(af, socktype, proto) as sock:
sock.bind(sa)
port = sock.getsockname()[1]
break
except OSError as e:
raise DynamipsError("Could not find free port for the Dynamips hypervisor: {}".format(e))
port_manager = PortManager.instance()
hypervisor = Hypervisor(self._dynamips_path, working_dir, server_host, port, port_manager.console_host)
log.info("Creating new hypervisor {}:{} with working directory {}".format(hypervisor.host, hypervisor.port, working_dir))
yield from hypervisor.start()
log.info("Hypervisor {}:{} has successfully started".format(hypervisor.host, hypervisor.port))
yield from hypervisor.connect()
if parse_version(hypervisor.version) < parse_version('0.2.11'):
raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(hypervisor.version))
return hypervisor
@asyncio.coroutine
def ghost_ios_support(self, vm):
ghost_ios_support = self.config.get_section_config("Dynamips").getboolean("ghost_ios_support", True)
if ghost_ios_support:
with (yield from Dynamips._ghost_ios_lock):
try:
yield from self._set_ghost_ios(vm)
except GeneratorExit:
log.warning("Could not create ghost IOS image {} (GeneratorExit)".format(vm.name))
@asyncio.coroutine
def create_nio(self, node, nio_settings):
"""
Creates a new NIO.
:param node: Dynamips node instance
:param nio_settings: information to create the NIO
:returns: a NIO object
"""
nio = None
if nio_settings["type"] == "nio_udp":
lport = nio_settings["lport"]
rhost = nio_settings["rhost"]
rport = nio_settings["rport"]
try:
info = socket.getaddrinfo(rhost, rport, socket.AF_UNSPEC, socket.SOCK_DGRAM, 0, socket.AI_PASSIVE)
if not info:
raise DynamipsError("getaddrinfo returns an empty list on {}:{}".format(rhost, rport))
for res in info:
af, socktype, proto, _, sa = res
with socket.socket(af, socktype, proto) as sock:
sock.connect(sa)
except OSError as e:
raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIOUDP(node.hypervisor, lport, rhost, rport)
elif nio_settings["type"] == "nio_generic_ethernet":
ethernet_device = nio_settings["ethernet_device"]
if sys.platform.startswith("win"):
# replace the interface name by the GUID on Windows
windows_interfaces = interfaces()
npf_interface = None
for interface in windows_interfaces:
if interface["name"] == ethernet_device:
npf_interface = interface["id"]
if not npf_interface:
raise DynamipsError("Could not find interface {} on this host".format(ethernet_device))
else:
ethernet_device = npf_interface
if not is_interface_up(ethernet_device):
raise aiohttp.web.HTTPConflict(text="Ethernet interface {} is down".format(ethernet_device))
nio = NIOGenericEthernet(node.hypervisor, ethernet_device)
elif nio_settings["type"] == "nio_linux_ethernet":
if sys.platform.startswith("win"):
raise DynamipsError("This NIO type is not supported on Windows")
ethernet_device = nio_settings["ethernet_device"]
nio = NIOLinuxEthernet(node.hypervisor, ethernet_device)
elif nio_settings["type"] == "nio_tap":
tap_device = nio_settings["tap_device"]
nio = NIOTAP(node.hypervisor, tap_device)
if not is_interface_up(tap_device):
# test after the TAP interface has been created (if it doesn't exist yet)
raise aiohttp.web.HTTPConflict(text="TAP interface {} is down".format(tap_device))
elif nio_settings["type"] == "nio_unix":
local_file = nio_settings["local_file"]
remote_file = nio_settings["remote_file"]
nio = NIOUNIX(node.hypervisor, local_file, remote_file)
elif nio_settings["type"] == "nio_vde":
control_file = nio_settings["control_file"]
local_file = nio_settings["local_file"]
nio = NIOVDE(node.hypervisor, control_file, local_file)
elif nio_settings["type"] == "nio_null":
nio = NIONull(node.hypervisor)
else:
raise aiohttp.web.HTTPConflict(text="NIO of type {} is not supported".format(nio_settings["type"]))
yield from nio.create()
return nio
@asyncio.coroutine
def _set_ghost_ios(self, vm):
"""
Manages Ghost IOS support.
:param vm: VM instance
"""
if not vm.mmap:
raise DynamipsError("mmap support is required to enable ghost IOS support")
if vm.platform == "c7200" and vm.npe == "npe-g2":
log.warning("Ghost IOS is not supported for c7200 with NPE-G2")
return
ghost_file = vm.formatted_ghost_file()
ghost_file_path = os.path.join(vm.hypervisor.working_dir, ghost_file)
if ghost_file_path not in self._ghost_files:
# create a new ghost IOS instance
ghost_id = str(uuid4())
ghost = Router("ghost-" + ghost_file, ghost_id, vm.project, vm.manager, platform=vm.platform, hypervisor=vm.hypervisor, ghost_flag=True)
try:
yield from ghost.create()
yield from ghost.set_image(vm.image)
yield from ghost.set_ghost_status(1)
yield from ghost.set_ghost_file(ghost_file)
yield from ghost.set_ram(vm.ram)
try:
yield from ghost.start()
yield from ghost.stop()
self._ghost_files.add(ghost_file_path)
except DynamipsError:
raise
finally:
yield from ghost.clean_delete()
except DynamipsError as e:
log.warn("Could not create ghost instance: {}".format(e))
if vm.ghost_file != ghost_file and os.path.isfile(ghost_file_path):
# set the ghost file to the router
yield from vm.set_ghost_status(2)
yield from vm.set_ghost_file(ghost_file)
@asyncio.coroutine
def update_vm_settings(self, vm, settings):
"""
Updates the VM settings.
:param vm: VM instance
:param settings: settings to update (dict)
"""
for name, value in settings.items():
if hasattr(vm, name) and getattr(vm, name) != value:
if hasattr(vm, "set_{}".format(name)):
setter = getattr(vm, "set_{}".format(name))
yield from setter(value)
elif name.startswith("slot") and value in ADAPTER_MATRIX:
slot_id = int(name[-1])
adapter_name = value
adapter = ADAPTER_MATRIX[adapter_name]()
try:
if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)):
yield from vm.slot_remove_binding(slot_id)
if not isinstance(vm.slots[slot_id], type(adapter)):
yield from vm.slot_add_binding(slot_id, adapter)
except IndexError:
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
elif name.startswith("slot") and value is None:
slot_id = int(name[-1])
try:
if vm.slots[slot_id]:
yield from vm.slot_remove_binding(slot_id)
except IndexError:
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
elif name.startswith("wic") and value in WIC_MATRIX:
wic_slot_id = int(name[-1])
wic_name = value
wic = WIC_MATRIX[wic_name]()
try:
if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
yield from vm.uninstall_wic(wic_slot_id)
if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
yield from vm.install_wic(wic_slot_id, wic)
except IndexError:
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
elif name.startswith("wic") and value is None:
wic_slot_id = int(name[-1])
try:
if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]:
yield from vm.uninstall_wic(wic_slot_id)
except IndexError:
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
mmap_support = self.config.get_section_config("Dynamips").getboolean("mmap_support", True)
if mmap_support is False:
yield from vm.set_mmap(False)
sparse_memory_support = self.config.get_section_config("Dynamips").getboolean("sparse_memory_support", True)
if sparse_memory_support is False:
yield from vm.set_sparsemem(False)
# update the configs if needed
yield from self.set_vm_configs(vm, settings)
@asyncio.coroutine
def set_vm_configs(self, vm, settings):
"""
Set VM configs from pushed content or existing config files.
:param vm: VM instance
:param settings: VM settings
"""
module_workdir = vm.project.module_working_directory(self.module_name.lower())
default_startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id))
default_private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(vm.dynamips_id))
startup_config_path = settings.get("startup_config")
startup_config_content = settings.get("startup_config_content")
if startup_config_path:
yield from vm.set_configs(startup_config_path)
elif startup_config_content:
startup_config_path = self._create_config(vm, default_startup_config_path, startup_config_content)
yield from vm.set_configs(startup_config_path)
# An empty startup config crash dynamips
else:
startup_config_path = self._create_config(vm, default_startup_config_path, "!\n")
yield from vm.set_configs(startup_config_path)
private_config_path = settings.get("private_config")
private_config_content = settings.get("private_config_content")
if private_config_path:
yield from vm.set_configs(vm.startup_config, private_config_path)
elif private_config_content:
private_config_path = self._create_config(vm, default_private_config_path, private_config_content)
yield from vm.set_configs(vm.startup_config, private_config_path)
def _create_config(self, vm, path, content=None):
"""
Creates a config file.
:param vm: VM instance
:param path: path to the destination config file
:param content: config content
:returns: relative path to the created config file
"""
log.info("Creating config file {}".format(path))
config_dir = os.path.dirname(path)
try:
os.makedirs(config_dir, exist_ok=True)
except OSError as e:
raise DynamipsError("Could not create Dynamips configs directory: {}".format(e))
try:
with open(path, "wb") as f:
if content:
content = "!\n" + content.replace("\r", "")
content = content.replace('%h', vm.name)
f.write(content.encode("utf-8"))
except OSError as e:
raise DynamipsError("Could not create config file {}: {}".format(path, e))
return os.path.join("configs", os.path.basename(path))
@asyncio.coroutine
def auto_idlepc(self, vm):
"""
Try to find the best possible idle-pc value.
:param vm: VM instance
"""
yield from vm.set_idlepc("0x0")
was_auto_started = False
try:
status = yield from vm.get_status()
if status != "running":
yield from vm.start()
was_auto_started = True
yield from asyncio.sleep(20) # leave time to the router to boot
validated_idlepc = None
idlepcs = yield from vm.get_idle_pc_prop()
if not idlepcs:
raise DynamipsError("No Idle-PC values found")
for idlepc in idlepcs:
yield from vm.set_idlepc(idlepc.split()[0])
log.debug("Auto Idle-PC: trying idle-PC value {}".format(vm.idlepc))
start_time = time.time()
initial_cpu_usage = yield from vm.get_cpu_usage()
log.debug("Auto Idle-PC: initial CPU usage is {}%".format(initial_cpu_usage))
yield from asyncio.sleep(3) # wait 3 seconds to probe the cpu again
elapsed_time = time.time() - start_time
cpu_usage = yield from vm.get_cpu_usage()
cpu_elapsed_usage = cpu_usage - initial_cpu_usage
cpu_usage = abs(cpu_elapsed_usage * 100.0 / elapsed_time)
if cpu_usage > 100:
cpu_usage = 100
log.debug("Auto Idle-PC: CPU usage is {}% after {:.2} seconds".format(cpu_usage, elapsed_time))
if cpu_usage < 70:
validated_idlepc = vm.idlepc
log.debug("Auto Idle-PC: idle-PC value {} has been validated".format(validated_idlepc))
break
if validated_idlepc is None:
raise DynamipsError("Sorry, no idle-pc value was suitable")
except DynamipsError:
raise
finally:
if was_auto_started:
yield from vm.stop()
return validated_idlepc
def get_images_directory(self):
"""
Return the full path of the images directory on disk
"""
return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "IOS")
@asyncio.coroutine
def list_images(self):
"""
Return the list of available IOS images.
:returns: Array of hash
"""
image_dir = self.get_images_directory()
if not os.path.exists(image_dir):
return []
try:
files = os.listdir(image_dir)
except OSError as e:
raise DynamipsError("Can not list {}: {}".format(image_dir, str(e)))
files.sort()
images = []
for filename in files:
if filename[0] != "." and not filename.endswith(".md5sum"):
try:
path = os.path.join(image_dir, filename)
with open(path, "rb") as f:
# read the first 7 bytes of the file.
elf_header_start = f.read(7)
except OSError as e:
continue
# valid IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1
if elf_header_start == b'\x7fELF\x01\x02\x01':
path = os.path.relpath(path, image_dir)
images.append({"filename": filename, "path": path})
return images
Whitespace-only changes.
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Adapter(object):
"""
Base class for adapters.
:param interfaces: number of interfaces supported by this adapter.
:param wics: number of wics supported by this adapter.
"""
def __init__(self, interfaces=0, wics=0):
self._interfaces = interfaces
self._ports = {}
for port_number in range(0, interfaces):
self._ports[port_number] = None
self._wics = wics * [None]
def removable(self):
"""
Returns True if the adapter can be removed from a slot
and False if not.
:returns: boolean
"""
return True
def port_exists(self, port_number):
"""
Checks if a port exists on this adapter.
:returns: True is the port exists,
False otherwise.
"""
if port_number in self._ports:
return True
return False
def wic_slot_available(self, wic_slot_id):
"""
Checks if a WIC slot is available
:returns: True is the WIC slot is available,
False otherwise.
"""
if self._wics[wic_slot_id] is None:
return True
return False
def install_wic(self, wic_slot_id, wic):
"""
Installs a WIC on this adapter.
:param wic_slot_id: WIC slot ID (integer)
:param wic: WIC instance
"""
self._wics[wic_slot_id] = wic
# Dynamips WICs ports start on a multiple of 16 + port number
# WIC1 port 1 = 16, WIC1 port 2 = 17
# WIC2 port 1 = 32, WIC2 port 2 = 33
# WIC3 port 1 = 48, WIC3 port 2 = 49
base = 16 * (wic_slot_id + 1)
for wic_port in range(0, wic.interfaces):
port_number = base + wic_port
self._ports[port_number] = None
def uninstall_wic(self, wic_slot_id):
"""
Removes a WIC from this adapter.
:param wic_slot_id: WIC slot ID (integer)
"""
wic = self._wics[wic_slot_id]
# Dynamips WICs ports start on a multiple of 16 + port number
# WIC1 port 1 = 16, WIC1 port 2 = 17
# WIC2 port 1 = 32, WIC2 port 2 = 33
# WIC3 port 1 = 48, WIC3 port 2 = 49
base = 16 * (wic_slot_id + 1)
for wic_port in range(0, wic.interfaces):
port_number = base + wic_port
del self._ports[port_number]
self._wics[wic_slot_id] = None
def add_nio(self, port_number, nio):
"""
Adds a NIO to a port on this adapter.
:param port_number: port number (integer)
:param nio: NIO instance
"""
self._ports[port_number] = nio
def remove_nio(self, port_number):
"""
Removes a NIO from a port on this adapter.
:param port_number: port number (integer)
"""
self._ports[port_number] = None
def get_nio(self, port_number):
"""
Returns the NIO assigned to a port.
:params port_number: port number (integer)
:returns: NIO instance
"""
return self._ports[port_number]
@property
def ports(self):
"""
Returns port to NIO mapping
:returns: dictionary port -> NIO
"""
return self._ports
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this adapter.
:returns: number of interfaces
"""
return self._interfaces
@property
def wics(self):
"""
Returns the wics adapters inserted in this adapter.
:returns: list WIC instances
"""
return self._wics
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C1700_MB_1FE(Adapter):
"""
Integrated 1 port FastEthernet adapter for c1700 platform.
"""
def __init__(self):
super().__init__(interfaces=1, wics=2)
def __str__(self):
return "C1700-MB-1FE"
def removable(self):
return False
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C1700_MB_WIC1(Adapter):
"""
Fake module to provide a placeholder for slot 1 interfaces when WICs
are inserted into WIC slot 1.
"""
def __init__(self):
super().__init__(interfaces=0, wics=2)
def __str__(self):
return "C1700-MB-WIC1"
def removable(self):
return False
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C2600_MB_1E(Adapter):
"""
Integrated 1 port Ethernet adapter for the c2600 platform.
"""
def __init__(self):
super().__init__(interfaces=1, wics=3)
def __str__(self):
return "C2600-MB-1E"
def removable(self):
return False
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C2600_MB_1FE(Adapter):
"""
Integrated 1 port FastEthernet adapter for the c2600 platform.
"""
def __init__(self):
super().__init__(interfaces=1, wics=3)
self._interfaces = 1
def __str__(self):
return "C2600-MB-1FE"
def removable(self):
return False
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C2600_MB_2E(Adapter):
"""
Integrated 2 port Ethernet adapter for the c2600 platform.
"""
def __init__(self):
super().__init__(interfaces=2, wics=3)
def __str__(self):
return "C2600-MB-2E"
def removable(self):
return False
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C2600_MB_2FE(Adapter):
"""
Integrated 2 port FastEthernet adapter for the c2600 platform.
"""
def __init__(self):
super().__init__(interfaces=2, wics=3)
def __str__(self):
return "C2600-MB-2FE"
def removable(self):
return False
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C7200_IO_2FE(Adapter):
"""
C7200-IO-2FE FastEthernet Input/Ouput controller.
"""
def __init__(self):
super().__init__(interfaces=2)
def __str__(self):
return "C7200-IO-2FE"
def removable(self):
return False
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C7200_IO_FE(Adapter):
"""
C7200-IO-FE FastEthernet Input/Ouput controller.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "C7200-IO-FE"
def removable(self):
return False
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class C7200_IO_GE_E(Adapter):
"""
C7200-IO-GE-E GigabitEthernet Input/Ouput controller.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "C7200-IO-GE-E"
def removable(self):
return False
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class GT96100_FE(Adapter):
def __init__(self):
super().__init__(interfaces=2, wics=3)
def __str__(self):
return "GT96100-FE"
def removable(self):
return False
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class Leopard_2FE(Adapter):
"""
Integrated 2 port FastEthernet adapter for c3660 router.
"""
def __init__(self):
super().__init__(interfaces=2)
self._interfaces = 2
def __str__(self):
return "Leopard-2FE"
def removable(self):
return False
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class NM_16ESW(Adapter):
"""
NM-16ESW FastEthernet network module.
"""
def __init__(self):
super().__init__(interfaces=16)
def __str__(self):
return "NM-16ESW"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class NM_1E(Adapter):
"""
NM-1E Ethernet network module.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "NM-1E"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class NM_1FE_TX(Adapter):
"""
NM-1FE-TX FastEthernet network module.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "NM-1FE-TX"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class NM_4E(Adapter):
"""
NM-4E Ethernet network module.
"""
def __init__(self):
super().__init__(interfaces=4)
def __str__(self):
return "NM-4E"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class NM_4T(Adapter):
"""
NM-4T Serial network module.
"""
def __init__(self):
super().__init__(interfaces=4)
def __str__(self):
return "NM-4T"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_2FE_TX(Adapter):
"""
PA-2FE-TX FastEthernet port adapter.
"""
def __init__(self):
super().__init__(interfaces=2)
def __str__(self):
return "PA-2FE-TX"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_4E(Adapter):
"""
PA-4E Ethernet port adapter.
"""
def __init__(self):
super().__init__(interfaces=4)
def __str__(self):
return "PA-4E"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_4T(Adapter):
"""
PA-4T+ Serial port adapter.
"""
def __init__(self):
super().__init__(interfaces=4)
def __str__(self):
return "PA-4T+"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_8E(Adapter):
"""
PA-8E Ethernet port adapter.
"""
def __init__(self):
super().__init__(interfaces=8)
def __str__(self):
return "PA-8E"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_8T(Adapter):
"""
PA-8T Serial port adapter.
"""
def __init__(self):
super().__init__(interfaces=8)
def __str__(self):
return "PA-8T"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_A1(Adapter):
"""
PA-A1 ATM port adapter.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "PA-A1"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_FE_TX(Adapter):
"""
PA-FE-TX FastEthernet port adapter.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "PA-FE-TX"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_GE(Adapter):
"""
PA-GE GigabitEthernet port adapter.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "PA-GE"
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class PA_POS_OC3(Adapter):
"""
PA-POS-OC3 port adapter.
"""
def __init__(self):
super().__init__(interfaces=1)
def __str__(self):
return "PA-POS-OC3"
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class WIC_1ENET(object):
"""
WIC-1ENET Ethernet
"""
def __init__(self):
self._interfaces = 2
def __str__(self):
return "WIC-1ENET"
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this WIC.
:returns: number of interfaces
"""
return self._interfaces
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class WIC_1T(object):
"""
WIC-1T Serial
"""
def __init__(self):
self._interfaces = 1
def __str__(self):
return "WIC-1T"
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this WIC.
:returns: number of interfaces
"""
return self._interfaces
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class WIC_2T(object):
"""
WIC-2T Serial
"""
def __init__(self):
self._interfaces = 2
def __str__(self):
return "WIC-2T"
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this WIC.
:returns: number of interfaces
"""
return self._interfaces
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .dynamips_error import DynamipsError
from .nodes.atm_switch import ATMSwitch
from .nodes.ethernet_switch import EthernetSwitch
from .nodes.ethernet_hub import EthernetHub
from .nodes.frame_relay_switch import FrameRelaySwitch
import logging
log = logging.getLogger(__name__)
DEVICES = {'atm_switch': ATMSwitch,
'frame_relay_switch': FrameRelaySwitch,
'ethernet_switch': EthernetSwitch,
'ethernet_hub': EthernetHub}
class DynamipsDevice:
"""
Factory to create an Device object based on the type
"""
def __new__(cls, name, device_id, project, manager, device_type, **kwargs):
if device_type not in DEVICES:
raise DynamipsError("Unknown device type: {}".format(device_type))
return DEVICES[device_type](name, device_id, project, manager, **kwargs)
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Custom exceptions for the Dynamips module.
"""
from ..vm_error import VMError
class DynamipsError(VMError):
pass
@@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips hypervisor management module ("hypervisor")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L46
"""
import re
import time
import logging
import asyncio
from .dynamips_error import DynamipsError
log = logging.getLogger(__name__)
class DynamipsHypervisor:
"""
Creates a new connection to a Dynamips server (also called hypervisor)
:param working_dir: working directory
:param host: the hostname or ip address string of the Dynamips server
:param port: the tcp port integer (defaults to 7200)
:param timeout: timeout integer for how long to wait for a response to commands sent to the
hypervisor (defaults to 30 seconds)
"""
# Used to parse Dynamips response codes
error_re = re.compile(r"""^2[0-9]{2}-""")
success_re = re.compile(r"""^1[0-9]{2}\s{1}""")
def __init__(self, working_dir, host, port=7200, timeout=30.0):
self._host = host
self._port = port
self._devices = []
self._working_dir = working_dir
self._version = "N/A"
self._timeout = timeout
self._reader = None
self._writer = None
self._io_lock = asyncio.Lock()
@asyncio.coroutine
def connect(self, timeout=10):
"""
Connects to the hypervisor.
"""
# connect to a local address by default
# if listening to all addresses (IPv4 or IPv6)
if self._host == "0.0.0.0":
host = "127.0.0.1"
elif self._host == "::":
host = "::1"
else:
host = self._host
begin = time.time()
connection_success = False
last_exception = None
while time.time() - begin < timeout:
yield from asyncio.sleep(0.01)
try:
self._reader, self._writer = yield from asyncio.wait_for(asyncio.open_connection(host, self._port), timeout=1)
except (asyncio.TimeoutError, OSError) as e:
last_exception = e
continue
connection_success = True
break
if not connection_success:
raise DynamipsError("Couldn't connect to hypervisor on {}:{} :{}".format(host, self._port, last_exception))
else:
log.info("Connected to Dynamips hypervisor after {:.4f} seconds".format(time.time() - begin))
try:
version = yield from self.send("hypervisor version")
self._version = version[0].split("-", 1)[0]
except IndexError:
self._version = "Unknown"
# this forces to send the working dir to Dynamips
yield from self.set_working_dir(self._working_dir)
@property
def version(self):
"""
Returns Dynamips version.
:returns: version string
"""
return self._version
@asyncio.coroutine
def close(self):
"""
Closes the connection to this hypervisor (but leave it running).
"""
yield from self.send("hypervisor close")
self._writer.close()
self._reader, self._writer = None
@asyncio.coroutine
def stop(self):
"""
Stops this hypervisor (will no longer run).
"""
try:
# try to properly stop the hypervisor
yield from self.send("hypervisor stop")
except DynamipsError:
pass
try:
if self._writer is not None:
yield from self._writer.drain()
self._writer.close()
except OSError as e:
log.debug("Stopping hypervisor {}:{} {}".format(self._host, self._port, e))
self._reader = self._writer = None
@asyncio.coroutine
def reset(self):
"""
Resets this hypervisor (used to get an empty configuration).
"""
yield from self.send("hypervisor reset")
@asyncio.coroutine
def set_working_dir(self, working_dir):
"""
Sets the working directory for this hypervisor.
:param working_dir: path to the working directory
"""
# encase working_dir in quotes to protect spaces in the path
yield from self.send('hypervisor working_dir "{}"'.format(working_dir))
self._working_dir = working_dir
log.debug("Working directory set to {}".format(self._working_dir))
@property
def working_dir(self):
"""
Returns current working directory
:returns: path to the working directory
"""
return self._working_dir
@property
def devices(self):
"""
Returns the list of devices managed by this hypervisor instance.
:returns: a list of device instances
"""
return self._devices
@property
def port(self):
"""
Returns the port used to start the hypervisor.
:returns: port number (integer)
"""
return self._port
@port.setter
def port(self, port):
"""
Sets the port used to start the hypervisor.
:param port: port number (integer)
"""
self._port = port
@property
def host(self):
"""
Returns the host (binding) used to start the hypervisor.
:returns: host/address (string)
"""
return self._host
@host.setter
def host(self, host):
"""
Sets the host (binding) used to start the hypervisor.
:param host: host/address (string)
"""
self._host = host
@asyncio.coroutine
def send(self, command):
"""
Sends commands to this hypervisor.
:param command: a Dynamips hypervisor command
:returns: results as a list
"""
# Dynamips responses are of the form:
# 1xx yyyyyy\r\n
# 1xx yyyyyy\r\n
# ...
# 100-yyyy\r\n
# or
# 2xx-yyyy\r\n
#
# Where 1xx is a code from 100-199 for a success or 200-299 for an error
# The result might be multiple lines and might be less than the buffer size
# but still have more data. The only thing we know for sure is the last line
# will begin with '100-' or a '2xx-' and end with '\r\n'
with (yield from self._io_lock):
if self._writer is None or self._reader is None:
raise DynamipsError("Not connected")
try:
command = command.strip() + '\n'
log.debug("sending {}".format(command))
self._writer.write(command.encode())
yield from self._writer.drain()
except OSError as e:
raise DynamipsError("Could not send Dynamips command '{command}' to {host}:{port}: {error}, process running: {run}"
.format(command=command.strip(), host=self._host, port=self._port, error=e, run=self.is_running()))
# Now retrieve the result
data = []
buf = ''
while True:
try:
try:
# line = yield from self._reader.readline() # this can lead to ValueError: Line is too long
chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size
except asyncio.CancelledError:
# task has been canceled but continue to read
# any remaining data sent by the hypervisor
continue
except ConnectionResetError as e:
# Sometimes WinError 64 (ERROR_NETNAME_DELETED) is returned here on Windows.
# These happen if connection reset is received before IOCP could complete
# a previous operation. Ignore and try again....
log.warning("Connection reset received while reading Dynamips response: {}".format(e))
continue
if not chunk:
raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, run=self.is_running()))
buf += chunk.decode("utf-8", errors="ignore")
except OSError as e:
raise DynamipsError("Could not read response for '{command}' from {host}:{port}: {error}, process running: {run}"
.format(command=command.strip(), host=self._host, port=self._port, error=e, run=self.is_running()))
# If the buffer doesn't end in '\n' then we can't be done
try:
if buf[-1] != '\n':
continue
except IndexError:
raise DynamipsError("Could not communicate with {host}:{port}, Dynamips process running: {run}"
.format(host=self._host, port=self._port, run=self.is_running()))
data += buf.split('\r\n')
if data[-1] == '':
data.pop()
buf = ''
# Does it contain an error code?
if self.error_re.search(data[-1]):
raise DynamipsError(data[-1][4:])
# Or does the last line begin with '100-'? Then we are done!
if data[-1][:4] == '100-':
data[-1] = data[-1][4:]
if data[-1] == 'OK':
data.pop()
break
# Remove success responses codes
for index in range(len(data)):
if self.success_re.search(data[index]):
data[index] = data[index][4:]
log.debug("returned result {}".format(data))
return data
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .dynamips_error import DynamipsError
from .nodes.c1700 import C1700
from .nodes.c2600 import C2600
from .nodes.c2691 import C2691
from .nodes.c3600 import C3600
from .nodes.c3725 import C3725
from .nodes.c3745 import C3745
from .nodes.c7200 import C7200
import logging
log = logging.getLogger(__name__)
PLATFORMS = {'c1700': C1700,
'c2600': C2600,
'c2691': C2691,
'c3725': C3725,
'c3745': C3745,
'c3600': C3600,
'c7200': C7200}
class DynamipsVM:
"""
Factory to create an Router object based on the correct platform.
"""
def __new__(cls, name, vm_id, project, manager, dynamips_id, platform, **kwargs):
if platform not in PLATFORMS:
raise DynamipsError("Unknown router platform: {}".format(platform))
return PLATFORMS[platform](name, vm_id, project, manager, dynamips_id, **kwargs)
+208
View File
@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Represents a Dynamips hypervisor and starts/stops the associated Dynamips process.
"""
import os
import subprocess
import asyncio
from gns3server.utils.asyncio import wait_for_process_termination
from .dynamips_hypervisor import DynamipsHypervisor
from .dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class Hypervisor(DynamipsHypervisor):
"""
Hypervisor.
:param path: path to Dynamips executable
:param working_dir: working directory
:param host: host/address for this hypervisor
:param port: port for this hypervisor
:param console_host: host/address for console connections
"""
_instance_count = 1
def __init__(self, path, working_dir, host, port, console_host):
super().__init__(working_dir, host, port)
# create an unique ID
self._id = Hypervisor._instance_count
Hypervisor._instance_count += 1
self._console_host = console_host
self._path = path
self._command = []
self._process = None
self._stdout_file = ""
self._started = False
@property
def id(self):
"""
Returns the unique ID for this hypervisor.
:returns: id (integer)
"""
return self._id
@property
def process(self):
"""
Returns the subprocess of the Hypervisor
:returns: subprocess
"""
return self._process
@property
def started(self):
"""
Returns either this hypervisor has been started or not.
:returns: boolean
"""
return self._started
@property
def path(self):
"""
Returns the path to the Dynamips executable.
:returns: path to Dynamips
"""
return self._path
@path.setter
def path(self, path):
"""
Sets the path to the Dynamips executable.
:param path: path to Dynamips
"""
self._path = path
@asyncio.coroutine
def start(self):
"""
Starts the Dynamips hypervisor process.
"""
self._command = self._build_command()
try:
log.info("Starting Dynamips: {}".format(self._command))
self._stdout_file = os.path.join(self.working_dir, "dynamips_i{}_stdout.txt".format(self._id))
log.info("Dynamips process logging to {}".format(self._stdout_file))
with open(self._stdout_file, "w", encoding="utf-8") as fd:
self._process = yield from asyncio.create_subprocess_exec(*self._command,
stdout=fd,
stderr=subprocess.STDOUT,
cwd=self._working_dir)
log.info("Dynamips process started PID={}".format(self._process.pid))
self._started = True
except (OSError, subprocess.SubprocessError) as e:
log.error("Could not start Dynamips: {}".format(e))
raise DynamipsError("Could not start Dynamips: {}".format(e))
@asyncio.coroutine
def stop(self):
"""
Stops the Dynamips hypervisor process.
"""
if self.is_running():
log.info("Stopping Dynamips process PID={}".format(self._process.pid))
yield from DynamipsHypervisor.stop(self)
# give some time for the hypervisor to properly stop.
# time to delete UNIX NIOs for instance.
yield from asyncio.sleep(0.01)
try:
yield from wait_for_process_termination(self._process, timeout=3)
except asyncio.TimeoutError:
if self._process.returncode is None:
log.warn("Dynamips process {} is still running... killing it".format(self._process.pid))
try:
self._process.kill()
except OSError as e:
log.error("Cannot stop the Dynamips process: {}".format(e))
if self._process.returncode is None:
log.warn('Dynamips hypervisor with PID={} is still running'.format(self._process.pid))
if self._stdout_file and os.access(self._stdout_file, os.W_OK):
try:
os.remove(self._stdout_file)
except OSError as e:
log.warning("could not delete temporary Dynamips log file: {}".format(e))
self._started = False
def read_stdout(self):
"""
Reads the standard output of the Dynamips process.
Only use when the process has been stopped or has crashed.
"""
output = ""
if self._stdout_file and os.access(self._stdout_file, os.R_OK):
try:
with open(self._stdout_file, "rb") as file:
output = file.read().decode("utf-8", errors="replace")
except OSError as e:
log.warn("could not read {}: {}".format(self._stdout_file, e))
return output
def is_running(self):
"""
Checks if the process is running
:returns: True or False
"""
if self._process and self._process.returncode is None:
return True
return False
def _build_command(self):
"""
Command to start the Dynamips hypervisor process.
(to be passed to subprocess.Popen())
"""
command = [self._path]
command.extend(["-N1"]) # use instance IDs for filenames
command.extend(["-l", "dynamips_i{}_log.txt".format(self._id)]) # log file
# Dynamips cannot listen for hypervisor commands and for console connections on
# 2 different IP addresses.
# See https://github.com/GNS3/dynamips/issues/62
if self._console_host != "0.0.0.0" and self._console_host != "::":
command.extend(["-H", "{}:{}".format(self._host, self._port)])
else:
command.extend(["-H", str(self._port)])
return command
Whitespace-only changes.
+256
View File
@@ -0,0 +1,256 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base interface for Dynamips Network Input/Output (NIO) module ("nio").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L451
"""
import asyncio
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class NIO:
"""
Base NIO class
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, name, hypervisor):
self._hypervisor = hypervisor
self._name = name
self._bandwidth = None # no bandwidth constraint by default
self._input_filter = None # no input filter applied by default
self._output_filter = None # no output filter applied by default
self._input_filter_options = None # no input filter options by default
self._output_filter_options = None # no output filter options by default
self._dynamips_direction = {"in": 0, "out": 1, "both": 2}
@asyncio.coroutine
def list(self):
"""
Returns all NIOs.
:returns: NIO list
"""
nio_list = yield from self._hypervisor.send("nio list")
return nio_list
@asyncio.coroutine
def delete(self):
"""
Deletes this NIO.
"""
if self._input_filter or self._output_filter:
yield from self.unbind_filter("both")
yield from self._hypervisor.send("nio delete {}".format(self._name))
log.info("NIO {name} has been deleted".format(name=self._name))
@asyncio.coroutine
def rename(self, new_name):
"""
Renames this NIO
:param new_name: new NIO name
"""
yield from self._hypervisor.send("nio rename {name} {new_name}".format(name=self._name, new_name=new_name))
log.info("NIO {name} renamed to {new_name}".format(name=self._name, new_name=new_name))
self._name = new_name
@asyncio.coroutine
def debug(self, debug):
"""
Enables/Disables debugging for this NIO.
:param debug: debug value (0 = disable, enable = 1)
"""
yield from self._hypervisor.send("nio set_debug {name} {debug}".format(name=self._name, debug=debug))
@asyncio.coroutine
def bind_filter(self, direction, filter_name):
"""
Adds a packet filter to this NIO.
Filter "freq_drop" drops packets.
Filter "capture" captures packets.
:param direction: "in", "out" or "both"
:param filter_name: name of the filter to apply
"""
if direction not in self._dynamips_direction:
raise DynamipsError("Unknown direction {} to bind filter {}:".format(direction, filter_name))
dynamips_direction = self._dynamips_direction[direction]
yield from self._hypervisor.send("nio bind_filter {name} {direction} {filter}".format(name=self._name,
direction=dynamips_direction,
filter=filter_name))
if direction == "in":
self._input_filter = filter_name
elif direction == "out":
self._output_filter = filter_name
elif direction == "both":
self._input_filter = filter_name
self._output_filter = filter_name
@asyncio.coroutine
def unbind_filter(self, direction):
"""
Removes packet filter for this NIO.
:param direction: "in", "out" or "both"
"""
if direction not in self._dynamips_direction:
raise DynamipsError("Unknown direction {} to unbind filter:".format(direction))
dynamips_direction = self._dynamips_direction[direction]
yield from self._hypervisor.send("nio unbind_filter {name} {direction}".format(name=self._name,
direction=dynamips_direction))
if direction == "in":
self._input_filter = None
elif direction == "out":
self._output_filter = None
elif direction == "both":
self._input_filter = None
self._output_filter = None
@asyncio.coroutine
def setup_filter(self, direction, options):
"""
Setups a packet filter bound with this NIO.
Filter "freq_drop" has 1 argument "<frequency>". It will drop
everything with a -1 frequency, drop every Nth packet with a
positive frequency, or drop nothing.
Filter "capture" has 2 arguments "<link_type_name> <output_file>".
It will capture packets to the target output file. The link type
name is a case-insensitive DLT_ name from the PCAP library
constants with the DLT_ part removed.See http://www.tcpdump.org/linktypes.html
for a list of all available DLT values.
:param direction: "in", "out" or "both"
:param options: options for the packet filter (string)
"""
if direction not in self._dynamips_direction:
raise DynamipsError("Unknown direction {} to setup filter:".format(direction))
dynamips_direction = self._dynamips_direction[direction]
yield from self._hypervisor.send("nio setup_filter {name} {direction} {options}".format(name=self._name,
direction=dynamips_direction,
options=options))
if direction == "in":
self._input_filter_options = options
elif direction == "out":
self._output_filter_options = options
elif direction == "both":
self._input_filter_options = options
self._output_filter_options = options
@property
def input_filter(self):
"""
Returns the input packet filter for this NIO.
:returns: tuple (filter name, filter options)
"""
return self._input_filter, self._input_filter_options
@property
def output_filter(self):
"""
Returns the output packet filter for this NIO.
:returns: tuple (filter name, filter options)
"""
return self._output_filter, self._output_filter_options
@asyncio.coroutine
def get_stats(self):
"""
Gets statistics for this NIO.
:returns: NIO statistics (string with packets in, packets out, bytes in, bytes out)
"""
stats = yield from self._hypervisor.send("nio get_stats {}".format(self._name))
return stats[0]
@asyncio.coroutine
def reset_stats(self):
"""
Resets statistics for this NIO.
"""
yield from self._hypervisor.send("nio reset_stats {}".format(self._name))
@property
def bandwidth(self):
"""
Returns the bandwidth constraint for this NIO.
:returns: bandwidth integer value (in Kb/s)
"""
return self._bandwidth
@asyncio.coroutine
def set_bandwidth(self, bandwidth):
"""
Sets bandwidth constraint.
:param bandwidth: bandwidth integer value (in Kb/s)
"""
yield from self._hypervisor.send("nio set_bandwidth {name} {bandwidth}".format(name=self._name, bandwidth=bandwidth))
self._bandwidth = bandwidth
def __str__(self):
"""
NIO string representation.
:returns: NIO name
"""
return self._name
@property
def name(self):
"""
Returns the NIO name.
:returns: NIO name
"""
return self._name
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for generic Ethernet NIOs (PCAP library).
"""
import asyncio
import uuid
from .nio import NIO
import logging
log = logging.getLogger(__name__)
class NIOGenericEthernet(NIO):
"""
Dynamips generic Ethernet NIO.
:param hypervisor: Dynamips hypervisor instance
:param ethernet_device: Ethernet device name (e.g. eth0)
"""
def __init__(self, hypervisor, ethernet_device):
# create an unique name
name = 'generic_ethernet-{}'.format(uuid.uuid4())
self._ethernet_device = ethernet_device
super().__init__(name, hypervisor)
@asyncio.coroutine
def create(self):
yield from self._hypervisor.send("nio create_gen_eth {name} {eth_device}".format(name=self._name,
eth_device=self._ethernet_device))
log.info("NIO Generic Ethernet {name} created with device {device}".format(name=self._name,
device=self._ethernet_device))
@property
def ethernet_device(self):
"""
Returns the Ethernet device used by this NIO.
:returns: the Ethernet device name
"""
return self._ethernet_device
def __json__(self):
return {"type": "nio_generic_ethernet",
"ethernet_device": self._ethernet_device}
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Linux Ethernet NIOs (Linux only).
"""
import asyncio
import uuid
from .nio import NIO
import logging
log = logging.getLogger(__name__)
class NIOLinuxEthernet(NIO):
"""
Dynamips Linux Ethernet NIO.
:param hypervisor: Dynamips hypervisor instance
:param ethernet_device: Ethernet device name (e.g. eth0)
"""
def __init__(self, hypervisor, ethernet_device):
# create an unique name
name = 'linux_ethernet-{}'.format(uuid.uuid4())
self._ethernet_device = ethernet_device
super().__init__(name, hypervisor)
@asyncio.coroutine
def create(self):
yield from self._hypervisor.send("nio create_linux_eth {name} {eth_device}".format(name=self._name,
eth_device=self._ethernet_device))
log.info("NIO Linux Ethernet {name} created with device {device}".format(name=self._name,
device=self._ethernet_device))
@property
def ethernet_device(self):
"""
Returns the Ethernet device used by this NIO.
:returns: the Ethernet device name
"""
return self._ethernet_device
def __json__(self):
return {"type": "nio_linux_ethernet",
"ethernet_device": self._ethernet_device}
@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for dummy NIOs (mostly for tests).
"""
import asyncio
import uuid
from .nio import NIO
import logging
log = logging.getLogger(__name__)
class NIONull(NIO):
"""
Dynamips NULL NIO.
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, hypervisor):
# create an unique name
name = 'null-{}'.format(uuid.uuid4())
super().__init__(name, hypervisor)
@asyncio.coroutine
def create(self):
yield from self._hypervisor.send("nio create_null {}".format(self._name))
log.info("NIO NULL {name} created.".format(name=self._name))
def __json__(self):
return {"type": "nio_null"}
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for TAP NIOs (UNIX based OSes only).
"""
import asyncio
import uuid
from .nio import NIO
import logging
log = logging.getLogger(__name__)
class NIOTAP(NIO):
"""
Dynamips TAP NIO.
:param hypervisor: Dynamips hypervisor instance
:param tap_device: TAP device name (e.g. tap0)
"""
def __init__(self, hypervisor, tap_device):
# create an unique name
name = 'tap-{}'.format(uuid.uuid4())
self._tap_device = tap_device
super().__init__(name, hypervisor)
@asyncio.coroutine
def create(self):
yield from self._hypervisor.send("nio create_tap {name} {tap}".format(name=self._name, tap=self._tap_device))
log.info("NIO TAP {name} created with device {device}".format(name=self._name, device=self._tap_device))
@property
def tap_device(self):
"""
Returns the TAP device used by this NIO.
:returns: the TAP device name
"""
return self._tap_device
def __json__(self):
return {"type": "nio_tap",
"tap_device": self._tap_device}
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for UDP NIOs.
"""
import asyncio
import uuid
from .nio import NIO
import logging
log = logging.getLogger(__name__)
class NIOUDP(NIO):
"""
Dynamips UDP NIO.
:param hypervisor: Dynamips hypervisor instance
:param lport: local port number
:param rhost: remote address/host
:param rport: remote port number
"""
def __init__(self, hypervisor, lport, rhost, rport):
# create an unique name
name = 'udp-{}'.format(uuid.uuid4())
self._lport = lport
self._rhost = rhost
self._rport = rport
super().__init__(name, hypervisor)
@asyncio.coroutine
def create(self):
yield from self._hypervisor.send("nio create_udp {name} {lport} {rhost} {rport}".format(name=self._name,
lport=self._lport,
rhost=self._rhost,
rport=self._rport))
log.info("NIO UDP {name} created with lport={lport}, rhost={rhost}, rport={rport}".format(name=self._name,
lport=self._lport,
rhost=self._rhost,
rport=self._rport))
@property
def lport(self):
"""
Returns the local port
:returns: local port number
"""
return self._lport
@property
def rhost(self):
"""
Returns the remote host
:returns: remote address/host
"""
return self._rhost
@property
def rport(self):
"""
Returns the remote port
:returns: remote port number
"""
return self._rport
def __json__(self):
return {"type": "nio_udp",
"lport": self._lport,
"rport": self._rport,
"rhost": self._rhost}
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for UNIX NIOs (Unix based OSes only).
"""
import asyncio
import uuid
from .nio import NIO
import logging
log = logging.getLogger(__name__)
class NIOUNIX(NIO):
"""
Dynamips UNIX NIO.
:param hypervisor: Dynamips hypervisor instance
:param local_file: local UNIX socket filename
:param remote_file: remote UNIX socket filename
"""
def __init__(self, hypervisor, local_file, remote_file):
# create an unique name
name = 'unix-{}'.format(uuid.uuid4())
self._local_file = local_file
self._remote_file = remote_file
super().__init__(name, hypervisor)
@asyncio.coroutine
def create(self):
yield from self._hypervisor.send("nio create_unix {name} {local} {remote}".format(name=self._name,
local=self._local_file,
remote=self._remote_file))
log.info("NIO UNIX {name} created with local file {local} and remote file {remote}".format(name=self._name,
local=self._local_file,
remote=self._remote_file))
@property
def local_file(self):
"""
Returns the local UNIX socket.
:returns: local UNIX socket filename
"""
return self._local_file
@property
def remote_file(self):
"""
Returns the remote UNIX socket.
:returns: remote UNIX socket filename
"""
return self._remote_file
def __json__(self):
return {"type": "nio_unix",
"local_file": self._local_file,
"remote_file": self._remote_file}
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for VDE (Virtual Distributed Ethernet) NIOs (Unix based OSes only).
"""
import asyncio
import uuid
from .nio import NIO
import logging
log = logging.getLogger(__name__)
class NIOVDE(NIO):
"""
Dynamips VDE NIO.
:param hypervisor: Dynamips hypervisor instance
:param control_file: VDE control filename
:param local_file: VDE local filename
"""
def __init__(self, hypervisor, control_file, local_file):
# create an unique name
name = 'vde-{}'.format(uuid.uuid4())
self._control_file = control_file
self._local_file = local_file
super().__init__(name, hypervisor)
@asyncio.coroutine
def create(self):
yield from self._hypervisor.send("nio create_vde {name} {control} {local}".format(name=self._name,
control=self._control_file,
local=self._local_file))
log.info("NIO VDE {name} created with control={control}, local={local}".format(name=self._name,
control=self._control_file,
local=self._local_file))
@property
def control_file(self):
"""
Returns the VDE control file.
:returns: VDE control filename
"""
return self._control_file
@property
def local_file(self):
"""
Returns the VDE local file.
:returns: VDE local filename
"""
return self._local_file
def __json__(self):
return {"type": "nio_vde",
"local_file": self._local_file,
"control_file": self._control_file}
Whitespace-only changes.
@@ -0,0 +1,423 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual ATM switch module ("atmsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593
"""
import asyncio
import re
from .device import Device
from ..nios.nio_udp import NIOUDP
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class ATMSwitch(Device):
"""
Dynamips ATM switch.
:param name: name for this switch
:param device_id: Device instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, name, device_id, project, manager, hypervisor=None):
super().__init__(name, device_id, project, manager, hypervisor)
self._nios = {}
self._mappings = {}
def __json__(self):
mappings = {}
for source, destination in self._mappings.items():
mappings[str(source)] = str(destination)
return {"name": self.name,
"device_id": self.id,
"project_id": self.project.id,
"mappings": mappings}
@asyncio.coroutine
def create(self):
if self._hypervisor is None:
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir)
yield from self._hypervisor.send('atmsw create "{}"'.format(self._name))
log.info('ATM switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
self._hypervisor.devices.append(self)
@asyncio.coroutine
def set_name(self, new_name):
"""
Renames this ATM switch.
:param new_name: New name for this switch
"""
yield from self._hypervisor.send('atm rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name))
log.info('ATM switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name,
id=self._id,
new_name=new_name))
self._name = new_name
@property
def nios(self):
"""
Returns all the NIOs member of this ATM switch.
:returns: nio list
"""
return self._nios
@property
def mappings(self):
"""
Returns port mappings
:returns: mappings list
"""
return self._mappings
@asyncio.coroutine
def delete(self):
"""
Deletes this ATM switch.
"""
for nio in self._nios.values():
if nio and isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
try:
yield from self._hypervisor.send('atmsw delete "{}"'.format(self._name))
log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
except DynamipsError:
log.debug("Could not properly delete ATM switch {}".format(self._name))
if self._hypervisor and self in self._hypervisor.devices:
self._hypervisor.devices.remove(self)
if self._hypervisor and not self._hypervisor.devices:
yield from self.hypervisor.stop()
def has_port(self, port):
"""
Checks if a port exists on this ATM switch.
:returns: boolean
"""
if port in self._nios:
return True
return False
def add_nio(self, nio, port_number):
"""
Adds a NIO as new port on ATM switch.
:param nio: NIO instance to add
:param port_number: port to allocate for the NIO
"""
if port_number in self._nios:
raise DynamipsError("Port {} isn't free".format(port_number))
log.info('ATM switch "{name}" [id={id}]: NIO {nio} bound to port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
self._nios[port_number] = nio
@asyncio.coroutine
def remove_nio(self, port_number):
"""
Removes the specified NIO as member of this ATM switch.
:param port_number: allocated port number
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
# remove VCs mapped with the port
for source, destination in self._mappings.copy().items():
if len(source) == 3 and len(destination) == 3:
# remove the virtual channels mapped with this port/nio
source_port, source_vpi, source_vci = source
destination_port, destination_vpi, destination_vci = destination
if port_number == source_port:
yield from self.unmap_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci)
yield from self.unmap_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci)
else:
# remove the virtual paths mapped with this port/nio
source_port, source_vpi = source
destination_port, destination_vpi = destination
if port_number == source_port:
yield from self.unmap_vp(source_port, source_vpi, destination_port, destination_vpi)
yield from self.unmap_vp(destination_port, destination_vpi, source_port, source_vpi)
nio = self._nios[port_number]
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
log.info('ATM switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
del self._nios[port_number]
return nio
@asyncio.coroutine
def set_mappings(self, mappings):
"""
Applies VC mappings
:param mappings: mappings (dict)
"""
pvc_entry = re.compile(r"""^([0-9]*):([0-9]*):([0-9]*)$""")
for source, destination in mappings.items():
if not isinstance(source, str) or not isinstance(destination, str):
raise DynamipsError("Invalid ATM mappings")
match_source_pvc = pvc_entry.search(source)
match_destination_pvc = pvc_entry.search(destination)
if match_source_pvc and match_destination_pvc:
# add the virtual channels
source_port, source_vpi, source_vci = map(int, match_source_pvc.group(1, 2, 3))
destination_port, destination_vpi, destination_vci = map(int, match_destination_pvc.group(1, 2, 3))
if self.has_port(destination_port):
if (source_port, source_vpi, source_vci) not in self.mappings and \
(destination_port, destination_vpi, destination_vci) not in self.mappings:
yield from self.map_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci)
yield from self.map_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci)
else:
# add the virtual paths
source_port, source_vpi = map(int, source.split(':'))
destination_port, destination_vpi = map(int, destination.split(':'))
if self.has_port(destination_port):
if (source_port, source_vpi) not in self.mappings and (destination_port, destination_vpi) not in self.mappings:
yield from self.map_vp(source_port, source_vpi, destination_port, destination_vpi)
yield from self.map_vp(destination_port, destination_vpi, source_port, source_vpi)
@asyncio.coroutine
def map_vp(self, port1, vpi1, port2, vpi2):
"""
Creates a new Virtual Path connection.
:param port1: input port
:param vpi1: input vpi
:param port2: output port
:param vpi2: output vpi
"""
if port1 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port1))
if port2 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port2))
nio1 = self._nios[port1]
nio2 = self._nios[port2]
yield from self._hypervisor.send('atmsw create_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name,
input_nio=nio1,
input_vpi=vpi1,
output_nio=nio2,
output_vpi=vpi2))
log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} created'.format(name=self._name,
id=self._id,
port1=port1,
vpi1=vpi1,
port2=port2,
vpi2=vpi2))
self._mappings[(port1, vpi1)] = (port2, vpi2)
@asyncio.coroutine
def unmap_vp(self, port1, vpi1, port2, vpi2):
"""
Deletes a new Virtual Path connection.
:param port1: input port
:param vpi1: input vpi
:param port2: output port
:param vpi2: output vpi
"""
if port1 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port1))
if port2 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port2))
nio1 = self._nios[port1]
nio2 = self._nios[port2]
yield from self._hypervisor.send('atmsw delete_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name,
input_nio=nio1,
input_vpi=vpi1,
output_nio=nio2,
output_vpi=vpi2))
log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} deleted'.format(name=self._name,
id=self._id,
port1=port1,
vpi1=vpi1,
port2=port2,
vpi2=vpi2))
del self._mappings[(port1, vpi1)]
@asyncio.coroutine
def map_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2):
"""
Creates a new Virtual Channel connection (unidirectional).
:param port1: input port
:param vpi1: input vpi
:param vci1: input vci
:param port2: output port
:param vpi2: output vpi
:param vci2: output vci
"""
if port1 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port1))
if port2 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port2))
nio1 = self._nios[port1]
nio2 = self._nios[port2]
yield from self._hypervisor.send('atmsw create_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name,
input_nio=nio1,
input_vpi=vpi1,
input_vci=vci1,
output_nio=nio2,
output_vpi=vpi2,
output_vci=vci2))
log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} created'.format(name=self._name,
id=self._id,
port1=port1,
vpi1=vpi1,
vci1=vci1,
port2=port2,
vpi2=vpi2,
vci2=vci2))
self._mappings[(port1, vpi1, vci1)] = (port2, vpi2, vci2)
@asyncio.coroutine
def unmap_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2):
"""
Deletes a new Virtual Channel connection (unidirectional).
:param port1: input port
:param vpi1: input vpi
:param vci1: input vci
:param port2: output port
:param vpi2: output vpi
:param vci2: output vci
"""
if port1 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port1))
if port2 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port2))
nio1 = self._nios[port1]
nio2 = self._nios[port2]
yield from self._hypervisor.send('atmsw delete_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name,
input_nio=nio1,
input_vpi=vpi1,
input_vci=vci1,
output_nio=nio2,
output_vpi=vpi2,
output_vci=vci2))
log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} deleted'.format(name=self._name,
id=self._id,
port1=port1,
vpi1=vpi1,
vci1=vci1,
port2=port2,
vpi2=vpi2,
vci2=vci2))
del self._mappings[(port1, vpi1, vci1)]
@asyncio.coroutine
def start_capture(self, port_number, output_file, data_link_type="DLT_ATM_RFC1483"):
"""
Starts a packet capture.
:param port_number: allocated port number
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port_number))
yield from nio.bind_filter("both", "capture")
yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file))
log.info('ATM switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
@asyncio.coroutine
def stop_capture(self, port_number):
"""
Stops a packet capture.
:param port_number: allocated port number
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
yield from nio.unbind_filter("both")
log.info('ATM switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
+108
View File
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips NIO bridge module ("nio_bridge").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L538
"""
import asyncio
from .device import Device
class Bridge(Device):
"""
Dynamips bridge.
:param name: name for this bridge
:param node_id: Node instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, name, node_id, project, manager, hypervisor=None):
super().__init__(name, node_id, project, manager, hypervisor)
self._nios = []
@asyncio.coroutine
def create(self):
if self._hypervisor is None:
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir)
yield from self._hypervisor.send('nio_bridge create "{}"'.format(self._name))
self._hypervisor.devices.append(self)
@asyncio.coroutine
def set_name(self, new_name):
"""
Renames this bridge.
:param new_name: New name for this bridge
"""
yield from self._hypervisor.send('nio_bridge rename "{name}" "{new_name}"'.format(name=self._name,
new_name=new_name))
self._name = new_name
@property
def nios(self):
"""
Returns all the NIOs member of this bridge.
:returns: nio list
"""
return self._nios
@asyncio.coroutine
def delete(self):
"""
Deletes this bridge.
"""
if self._hypervisor and self in self._hypervisor.devices:
self._hypervisor.devices.remove(self)
if self._hypervisor and not self._hypervisor.devices:
yield from self._hypervisor.send('nio_bridge delete "{}"'.format(self._name))
@asyncio.coroutine
def add_nio(self, nio):
"""
Adds a NIO as new port on this bridge.
:param nio: NIO instance to add
"""
yield from self._hypervisor.send('nio_bridge add_nio "{name}" {nio}'.format(name=self._name, nio=nio))
self._nios.append(nio)
@asyncio.coroutine
def remove_nio(self, nio):
"""
Removes the specified NIO as member of this bridge.
:param nio: NIO instance to remove
"""
yield from self._hypervisor.send('nio_bridge remove_nio "{name}" {nio}'.format(name=self._name, nio=nio))
self._nios.remove(nio)
+148
View File
@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Cisco 1700 instances module ("c1700")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L428
"""
import asyncio
from .router import Router
from ..adapters.c1700_mb_1fe import C1700_MB_1FE
from ..adapters.c1700_mb_wic1 import C1700_MB_WIC1
import logging
log = logging.getLogger(__name__)
class C1700(Router):
"""
Dynamips c1700 router.
:param name: The name of this router
:param vm_id: Router instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param aux: auxiliary console port
:param chassis: chassis for this router:
1720, 1721, 1750, 1751 or 1760 (default = 1720).
1710 is not supported.
"""
def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis="1720"):
super().__init__(name, vm_id, project, manager, dynamips_id, console, aux, platform="c1700")
# Set default values for this platform (must be the same as Dynamips)
self._ram = 64
self._nvram = 32
self._disk0 = 0
self._disk1 = 0
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
self._sparsemem = False # never activate sparsemem for c1700 (unstable)
def __json__(self):
c1700_router_info = {"iomem": self._iomem,
"chassis": self._chassis,
"sparsemem": self._sparsemem}
router_info = Router.__json__(self)
router_info.update(c1700_router_info)
return router_info
@asyncio.coroutine
def create(self):
yield from Router.create(self)
if self._chassis != "1720":
yield from self.set_chassis(self._chassis)
self._setup_chassis()
def _setup_chassis(self):
"""
Sets up the router with the corresponding chassis
(create slots and insert default adapters).
"""
# With 1751 and 1760, WICs in WIC slot 1 show up as in slot 1, not 0
# e.g. s1/0 not s0/2
if self._chassis in ['1751', '1760']:
self._create_slots(2)
self._slots[1] = C1700_MB_WIC1()
else:
self._create_slots(1)
self._slots[0] = C1700_MB_1FE()
@property
def chassis(self):
"""
Returns the chassis.
:returns: chassis string
"""
return self._chassis
@asyncio.coroutine
def set_chassis(self, chassis):
"""
Sets the chassis.
:param: chassis string:
1720, 1721, 1750, 1751 or 1760
"""
yield from self._hypervisor.send('c1700 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis))
log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name,
id=self._id,
chassis=chassis))
self._chassis = chassis
self._setup_chassis()
@property
def iomem(self):
"""
Returns I/O memory size for this router.
:returns: I/O memory size (integer)
"""
return self._iomem
@asyncio.coroutine
def set_iomem(self, iomem):
"""
Sets I/O memory size for this router.
:param iomem: I/O memory size
"""
yield from self._hypervisor.send('c1700 set_iomem "{name}" {size}'.format(name=self._name, size=iomem))
log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name,
id=self._id,
old_iomem=self._iomem,
new_iomem=iomem))
self._iomem = iomem
+157
View File
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Cisco 2600 instances module ("c2600")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L404
"""
import asyncio
from .router import Router
from ..adapters.c2600_mb_1e import C2600_MB_1E
from ..adapters.c2600_mb_2e import C2600_MB_2E
from ..adapters.c2600_mb_1fe import C2600_MB_1FE
from ..adapters.c2600_mb_2fe import C2600_MB_2FE
import logging
log = logging.getLogger(__name__)
class C2600(Router):
"""
Dynamips c2600 router.
:param name: The name of this router
:param vm_id: Router instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param aux: auxiliary console port
:param chassis: chassis for this router:
2610, 2611, 2620, 2621, 2610XM, 2611XM
2620XM, 2621XM, 2650XM or 2651XM (default = 2610).
"""
# adapters to insert by default corresponding the
# chosen chassis.
integrated_adapters = {"2610": C2600_MB_1E,
"2611": C2600_MB_2E,
"2620": C2600_MB_1FE,
"2621": C2600_MB_2FE,
"2610XM": C2600_MB_1FE,
"2611XM": C2600_MB_2FE,
"2620XM": C2600_MB_1FE,
"2621XM": C2600_MB_2FE,
"2650XM": C2600_MB_1FE,
"2651XM": C2600_MB_2FE}
def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis="2610"):
super().__init__(name, vm_id, project, manager, dynamips_id, console, aux, platform="c2600")
# Set default values for this platform (must be the same as Dynamips)
self._ram = 64
self._nvram = 128
self._disk0 = 0
self._disk1 = 0
self._chassis = chassis
self._iomem = 15 # percentage
self._clock_divisor = 8
self._sparsemem = False # never activate sparsemem for c2600 (unstable)
def __json__(self):
c2600_router_info = {"iomem": self._iomem,
"chassis": self._chassis,
"sparsemem": self._sparsemem}
router_info = Router.__json__(self)
router_info.update(c2600_router_info)
return router_info
@asyncio.coroutine
def create(self):
yield from Router.create(self)
if self._chassis != "2610":
yield from self.set_chassis(self._chassis)
self._setup_chassis()
def _setup_chassis(self):
"""
Sets up the router with the corresponding chassis
(create slots and insert default adapters).
"""
self._create_slots(2)
self._slots[0] = self.integrated_adapters[self._chassis]()
@property
def chassis(self):
"""
Returns the chassis.
:returns: chassis string
"""
return self._chassis
@asyncio.coroutine
def set_chassis(self, chassis):
"""
Sets the chassis.
:param: chassis string:
2610, 2611, 2620, 2621, 2610XM, 2611XM
2620XM, 2621XM, 2650XM or 2651XM
"""
yield from self._hypervisor.send('c2600 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis))
log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name,
id=self._id,
chassis=chassis))
self._chassis = chassis
self._setup_chassis()
@property
def iomem(self):
"""
Returns I/O memory size for this router.
:returns: I/O memory size (integer)
"""
return self._iomem
@asyncio.coroutine
def set_iomem(self, iomem):
"""
Sets I/O memory size for this router.
:param iomem: I/O memory size
"""
yield from self._hypervisor.send('c2600 set_iomem "{name}" {size}'.format(name=self._name, size=iomem))
log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name,
id=self._id,
old_iomem=self._iomem,
new_iomem=iomem))
self._iomem = iomem
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Cisco 2691 instances module ("c2691")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L387
"""
import asyncio
from .router import Router
from ..adapters.gt96100_fe import GT96100_FE
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class C2691(Router):
"""
Dynamips c2691 router.
:param name: The name of this router
:param vm_id: Router instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param aux: auxiliary console port
"""
def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis=None):
super().__init__(name, vm_id, project, manager, dynamips_id, console, aux, platform="c2691")
# Set default values for this platform (must be the same as Dynamips)
self._ram = 128
self._nvram = 112
self._disk0 = 16
self._disk1 = 0
self._iomem = 5 # percentage
self._clock_divisor = 8
self._create_slots(2)
self._slots[0] = GT96100_FE()
if chassis is not None:
raise DynamipsError("c2691 routers do not have chassis")
def __json__(self):
c2691_router_info = {"iomem": self._iomem}
router_info = Router.__json__(self)
router_info.update(c2691_router_info)
return router_info
@property
def iomem(self):
"""
Returns I/O memory size for this router.
:returns: I/O memory size (integer)
"""
return self._iomem
@asyncio.coroutine
def set_iomem(self, iomem):
"""
Sets I/O memory size for this router.
:param iomem: I/O memory size
"""
yield from self._hypervisor.send('c2691 set_iomem "{name}" {size}'.format(name=self._name, size=iomem))
log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name,
id=self._id,
old_iomem=self._iomem,
new_iomem=iomem))
self._iomem = iomem
+142
View File
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Cisco 3600 instances module ("c3600")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L366
"""
import asyncio
from .router import Router
from ..adapters.leopard_2fe import Leopard_2FE
import logging
log = logging.getLogger(__name__)
class C3600(Router):
"""
Dynamips c3600 router.
:param name: The name of this router
:param vm_id: Router instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param aux: auxiliary console port
:param chassis: chassis for this router:
3620, 3640 or 3660 (default = 3640).
"""
def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis="3640"):
super().__init__(name, vm_id, project, manager, dynamips_id, console, aux, platform="c3600")
# Set default values for this platform (must be the same as Dynamips)
self._ram = 128
self._nvram = 128
self._disk0 = 0
self._disk1 = 0
self._iomem = 5 # percentage
self._chassis = chassis
self._clock_divisor = 4
def __json__(self):
c3600_router_info = {"iomem": self._iomem,
"chassis": self._chassis}
router_info = Router.__json__(self)
router_info.update(c3600_router_info)
return router_info
@asyncio.coroutine
def create(self):
yield from Router.create(self)
if self._chassis != "3640":
yield from self.set_chassis(self._chassis)
self._setup_chassis()
def _setup_chassis(self):
"""
Sets up the router with the corresponding chassis
(create slots and insert default adapters).
"""
if self._chassis == "3620":
self._create_slots(2)
elif self._chassis == "3640":
self._create_slots(4)
elif self._chassis == "3660":
self._create_slots(7)
self._slots[0] = Leopard_2FE()
@property
def chassis(self):
"""
Returns the chassis.
:returns: chassis string
"""
return self._chassis
@asyncio.coroutine
def set_chassis(self, chassis):
"""
Sets the chassis.
:param: chassis string: 3620, 3640 or 3660
"""
yield from self._hypervisor.send('c3600 set_chassis "{name}" {chassis}'.format(name=self._name, chassis=chassis))
log.info('Router "{name}" [{id}]: chassis set to {chassis}'.format(name=self._name,
id=self._id,
chassis=chassis))
self._chassis = chassis
self._setup_chassis()
@property
def iomem(self):
"""
Returns I/O memory size for this router.
:returns: I/O memory size (integer)
"""
return self._iomem
@asyncio.coroutine
def set_iomem(self, iomem):
"""
Set I/O memory size for this router.
:param iomem: I/O memory size
"""
yield from self._hypervisor.send('c3600 set_iomem "{name}" {size}'.format(name=self._name, size=iomem))
log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name,
id=self._id,
old_iomem=self._iomem,
new_iomem=iomem))
self._iomem = iomem
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Cisco 3725 instances module ("c3725")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L346
"""
import asyncio
from .router import Router
from ..adapters.gt96100_fe import GT96100_FE
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class C3725(Router):
"""
Dynamips c3725 router.
:param name: The name of this router
:param vm_id: Router instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param aux: auxiliary console port
"""
def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis=None):
super().__init__(name, vm_id, project, manager, dynamips_id, console, aux, platform="c3725")
# Set default values for this platform (must be the same as Dynamips)
self._ram = 128
self._nvram = 112
self._disk0 = 16
self._disk1 = 0
self._iomem = 5 # percentage
self._clock_divisor = 8
self._create_slots(3)
self._slots[0] = GT96100_FE()
if chassis is not None:
raise DynamipsError("c3725 routers do not have chassis")
def __json__(self):
c3725_router_info = {"iomem": self._iomem}
router_info = Router.__json__(self)
router_info.update(c3725_router_info)
return router_info
@property
def iomem(self):
"""
Returns I/O memory size for this router.
:returns: I/O memory size (integer)
"""
return self._iomem
@asyncio.coroutine
def set_iomem(self, iomem):
"""
Sets I/O memory size for this router.
:param iomem: I/O memory size
"""
yield from self._hypervisor.send('c3725 set_iomem "{name}" {size}'.format(name=self._name, size=iomem))
log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name,
id=self._id,
old_iomem=self._iomem,
new_iomem=iomem))
self._iomem = iomem
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Cisco 3745 instances module ("c3745")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L326
"""
import asyncio
from .router import Router
from ..adapters.gt96100_fe import GT96100_FE
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class C3745(Router):
"""
Dynamips c3745 router.
:param name: The name of this router
:param vm_id: Router instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param aux: auxiliary console port
"""
def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, chassis=None):
super().__init__(name, vm_id, project, manager, dynamips_id, console, aux, platform="c3745")
# Set default values for this platform (must be the same as Dynamips)
self._ram = 128
self._nvram = 304
self._disk0 = 16
self._disk1 = 0
self._iomem = 5 # percentage
self._clock_divisor = 8
self._create_slots(5)
self._slots[0] = GT96100_FE()
if chassis is not None:
raise DynamipsError("c3745 routers do not have chassis")
def __json__(self):
c3745_router_info = {"iomem": self._iomem}
router_info = Router.__json__(self)
router_info.update(c3745_router_info)
return router_info
@property
def iomem(self):
"""
Returns I/O memory size for this router.
:returns: I/O memory size (integer)
"""
return self._iomem
@asyncio.coroutine
def set_iomem(self, iomem):
"""
Sets I/O memory size for this router.
:param iomem: I/O memory size
"""
yield from self._hypervisor.send('c3745 set_iomem "{name}" {size}'.format(name=self._name, size=iomem))
log.info('Router "{name}" [{id}]: I/O memory updated from {old_iomem}% to {new_iomem}%'.format(name=self._name,
id=self._id,
old_iomem=self._iomem,
new_iomem=iomem))
self._iomem = iomem
+244
View File
@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Cisco 7200 instances module ("c7200")
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L294
"""
import asyncio
from .router import Router
from ..adapters.c7200_io_fe import C7200_IO_FE
from ..adapters.c7200_io_ge_e import C7200_IO_GE_E
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class C7200(Router):
"""
Dynamips c7200 router (model is 7206).
:param name: The name of this router
:param vm_id: Router instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param aux: auxiliary console port
:param npe: Default NPE
"""
def __init__(self, name, vm_id, project, manager, dynamips_id, console=None, aux=None, npe="npe-400", chassis=None):
super().__init__(name, vm_id, project, manager, dynamips_id, console, aux, platform="c7200")
# Set default values for this platform (must be the same as Dynamips)
self._ram = 256
self._nvram = 128
self._disk0 = 64
self._disk1 = 0
self._npe = npe
self._midplane = "vxr"
self._clock_divisor = 4
self._npe = npe
# 4 sensors with a default temperature of 22C:
# sensor 1 = I/0 controller inlet
# sensor 2 = I/0 controller outlet
# sensor 3 = NPE inlet
# sensor 4 = NPE outlet
self._sensors = [22, 22, 22, 22]
# 2 power supplies powered on
self._power_supplies = [1, 1]
self._create_slots(7)
if chassis is not None:
raise DynamipsError("c7200 routers do not have chassis")
def __json__(self):
c7200_router_info = {"npe": self._npe,
"midplane": self._midplane,
"sensors": self._sensors,
"power_supplies": self._power_supplies}
router_info = Router.__json__(self)
router_info.update(c7200_router_info)
return router_info
@asyncio.coroutine
def create(self):
yield from Router.create(self)
if self._npe != "npe-400":
yield from self.set_npe(self._npe)
# first slot is a mandatory Input/Output controller (based on NPE type)
if self.npe == "npe-g2":
yield from self.slot_add_binding(0, C7200_IO_GE_E())
else:
yield from self.slot_add_binding(0, C7200_IO_FE())
@property
def npe(self):
"""
Returns the NPE model.
:returns: NPE model string (e.g. "npe-200")
"""
return self._npe
@asyncio.coroutine
def set_npe(self, npe):
"""
Sets the NPE model.
:params npe: NPE model string (e.g. "npe-200")
NPE models are npe-100, npe-150, npe-175, npe-200,
npe-225, npe-300, npe-400 and npe-g2 (PowerPC c7200 only)
"""
if (yield from self.is_running()):
raise DynamipsError("Cannot change NPE on running router")
yield from self._hypervisor.send('c7200 set_npe "{name}" {npe}'.format(name=self._name, npe=npe))
log.info('Router "{name}" [{id}]: NPE updated from {old_npe} to {new_npe}'.format(name=self._name,
id=self._id,
old_npe=self._npe,
new_npe=npe))
self._npe = npe
@property
def midplane(self):
"""
Returns the midplane model.
:returns: midplane model string (e.g. "vxr" or "std")
"""
return self._midplane
@asyncio.coroutine
def set_midplane(self, midplane):
"""
Sets the midplane model.
:returns: midplane model string (e.g. "vxr" or "std")
"""
yield from self._hypervisor.send('c7200 set_midplane "{name}" {midplane}'.format(name=self._name, midplane=midplane))
log.info('Router "{name}" [{id}]: midplane updated from {old_midplane} to {new_midplane}'.format(name=self._name,
id=self._id,
old_midplane=self._midplane,
new_midplane=midplane))
self._midplane = midplane
@property
def sensors(self):
"""
Returns the 4 sensors with temperature in degree Celcius.
:returns: list of 4 sensor temperatures
"""
return self._sensors
@asyncio.coroutine
def set_sensors(self, sensors):
"""
Sets the 4 sensors with temperature in degree Celcius.
:param sensors: list of 4 sensor temperatures corresponding to
sensor 1 = I/0 controller inlet
sensor 2 = I/0 controller outlet
sensor 3 = NPE inlet
sensor 4 = NPE outlet
Example: [22, 22, 22, 22]
"""
sensor_id = 0
for sensor in sensors:
yield from self._hypervisor.send('c7200 set_temp_sensor "{name}" {sensor_id} {temp}'.format(name=self._name,
sensor_id=sensor_id,
temp=sensor))
log.info('Router "{name}" [{id}]: sensor {sensor_id} temperature updated from {old_temp}C to {new_temp}C'.format(name=self._name,
id=self._id,
sensor_id=sensor_id,
old_temp=self._sensors[sensor_id],
new_temp=sensors[sensor_id]))
sensor_id += 1
self._sensors = sensors
@property
def power_supplies(self):
"""
Returns the 2 power supplies with 0 = off, 1 = on.
:returns: list of 2 power supplies.
"""
return self._power_supplies
@asyncio.coroutine
def set_power_supplies(self, power_supplies):
"""
Sets the 2 power supplies with 0 = off, 1 = on.
:param power_supplies: list of 2 power supplies.
Example: [1, 0] = first power supply is on, second is off.
"""
power_supply_id = 0
for power_supply in power_supplies:
yield from self._hypervisor.send('c7200 set_power_supply "{name}" {power_supply_id} {powered_on}'.format(name=self._name,
power_supply_id=power_supply_id,
powered_on=power_supply))
log.info('Router "{name}" [{id}]: power supply {power_supply_id} state updated to {powered_on}'.format(name=self._name,
id=self._id,
power_supply_id=power_supply_id,
powered_on=power_supply))
power_supply_id += 1
self._power_supplies = power_supplies
@asyncio.coroutine
def start(self):
"""
Starts this router.
At least the IOS image must be set before starting it.
"""
# trick: we must send sensors and power supplies info after starting the router
# otherwise they are not taken into account (Dynamips bug?)
yield from Router.start(self)
if self._sensors != [22, 22, 22, 22]:
yield from self.set_sensors(self._sensors)
if self._power_supplies != [1, 1]:
yield from self.set_power_supplies(self._power_supplies)
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Device:
"""
Base device for switches and hubs
:param name: name for this device
:param device_id: Device instance identifier
:param project: Project instance
:param manager: Parent manager
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, name, device_id, project, manager, hypervisor=None):
self._name = name
self._id = device_id
self._project = project
self._manager = manager
self._hypervisor = hypervisor
@property
def hypervisor(self):
"""
Returns the current hypervisor.
:returns: hypervisor instance
"""
return self._hypervisor
@property
def project(self):
"""
Returns the device current project.
:returns: Project instance.
"""
return self._project
@property
def name(self):
"""
Returns the name for this device.
:returns: name
"""
return self._name
@name.setter
def name(self, new_name):
"""
Sets the name of this VM.
:param new_name: name
"""
self._name = new_name
@property
def id(self):
"""
Returns the ID for this device.
:returns: device identifier (string)
"""
return self._id
@property
def manager(self):
"""
Returns the manager for this device.
:returns: instance of manager
"""
return self._manager
def create(self):
"""
Creates the device.
"""
raise NotImplementedError
@@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Hub object that uses the Bridge interface to create a hub with ports.
"""
import asyncio
from .bridge import Bridge
from ..nios.nio_udp import NIOUDP
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class EthernetHub(Bridge):
"""
Dynamips Ethernet hub (based on Bridge)
:param name: name for this hub
:param device_id: Device instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, name, device_id, project, manager, hypervisor=None):
super().__init__(name, device_id, project, manager, hypervisor)
self._mappings = {}
def __json__(self):
return {"name": self.name,
"device_id": self.id,
"project_id": self.project.id}
@asyncio.coroutine
def create(self):
yield from Bridge.create(self)
log.info('Ethernet hub "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
@property
def mappings(self):
"""
Returns port mappings
:returns: mappings list
"""
return self._mappings
@asyncio.coroutine
def delete(self):
"""
Deletes this hub.
"""
for nio in self._nios:
if nio and isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
try:
yield from Bridge.delete(self)
log.info('Ethernet hub "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
except DynamipsError:
log.debug("Could not properly delete Ethernet hub {}".format(self._name))
if self._hypervisor and not self._hypervisor.devices:
yield from self.hypervisor.stop()
@asyncio.coroutine
def add_nio(self, nio, port_number):
"""
Adds a NIO as new port on this hub.
:param nio: NIO instance to add
:param port_number: port to allocate for the NIO
"""
if port_number in self._mappings:
raise DynamipsError("Port {} isn't free".format(port_number))
yield from Bridge.add_nio(self, nio)
log.info('Ethernet hub "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
self._mappings[port_number] = nio
@asyncio.coroutine
def remove_nio(self, port_number):
"""
Removes the specified NIO as member of this hub.
:param port_number: allocated port number
:returns: the NIO that was bound to the allocated port
"""
if port_number not in self._mappings:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._mappings[port_number]
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
yield from Bridge.remove_nio(self, nio)
log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
del self._mappings[port_number]
return nio
@asyncio.coroutine
def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port_number: allocated port number
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port_number not in self._mappings:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._mappings[port_number]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port_number))
yield from nio.bind_filter("both", "capture")
yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file))
log.info('Ethernet hub "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
@asyncio.coroutine
def stop_capture(self, port_number):
"""
Stops a packet capture.
:param port_number: allocated port number
"""
if port_number not in self._mappings:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._mappings[port_number]
yield from nio.unbind_filter("both")
log.info('Ethernet hub "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
@@ -0,0 +1,343 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Ethernet switch module ("ethsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558
"""
import asyncio
from pkg_resources import parse_version
from .device import Device
from ..nios.nio_udp import NIOUDP
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class EthernetSwitch(Device):
"""
Dynamips Ethernet switch.
:param name: name for this switch
:param device_id: Device instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, name, device_id, project, manager, hypervisor=None):
super().__init__(name, device_id, project, manager, hypervisor)
self._nios = {}
self._mappings = {}
def __json__(self):
ethernet_switch_info = {"name": self.name,
"device_id": self.id,
"project_id": self.project.id}
ports = []
for port_number, settings in self._mappings.items():
ports.append({"port": port_number,
"type": settings[0],
"vlan": settings[1],
"ethertype": settings[2] if len(settings) > 2 else ""})
ethernet_switch_info["ports"] = ports
return ethernet_switch_info
@asyncio.coroutine
def create(self):
if self._hypervisor is None:
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir)
yield from self._hypervisor.send('ethsw create "{}"'.format(self._name))
log.info('Ethernet switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
self._hypervisor.devices.append(self)
@asyncio.coroutine
def set_name(self, new_name):
"""
Renames this Ethernet switch.
:param new_name: New name for this switch
"""
yield from self._hypervisor.send('ethsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name))
log.info('Ethernet switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name,
id=self._id,
new_name=new_name))
self._name = new_name
@property
def nios(self):
"""
Returns all the NIOs member of this Ethernet switch.
:returns: nio list
"""
return self._nios
@property
def mappings(self):
"""
Returns port mappings
:returns: mappings list
"""
return self._mappings
@asyncio.coroutine
def delete(self):
"""
Deletes this Ethernet switch.
"""
for nio in self._nios.values():
if nio and isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
try:
yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name))
log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
except DynamipsError:
log.debug("Could not properly delete Ethernet switch {}".format(self._name))
if self._hypervisor and self in self._hypervisor.devices:
self._hypervisor.devices.remove(self)
if self._hypervisor and not self._hypervisor.devices:
yield from self.hypervisor.stop()
@asyncio.coroutine
def add_nio(self, nio, port_number):
"""
Adds a NIO as new port on Ethernet switch.
:param nio: NIO instance to add
:param port_number: port to allocate for the NIO
"""
if port_number in self._nios:
raise DynamipsError("Port {} isn't free".format(port_number))
yield from self._hypervisor.send('ethsw add_nio "{name}" {nio}'.format(name=self._name, nio=nio))
log.info('Ethernet switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
self._nios[port_number] = nio
@asyncio.coroutine
def remove_nio(self, port_number):
"""
Removes the specified NIO as member of this Ethernet switch.
:param port_number: allocated port number
:returns: the NIO that was bound to the port
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
if isinstance(nio, NIOUDP):
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))
log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
del self._nios[port_number]
if port_number in self._mappings:
del self._mappings[port_number]
return nio
@asyncio.coroutine
def set_port_settings(self, port_number, settings):
"""
Applies port settings to a specific port.
:param port_number: port number to set the settings
:param settings: port settings
"""
if settings["type"] == "access":
yield from self.set_access_port(port_number, settings["vlan"])
elif settings["type"] == "dot1q":
yield from self.set_dot1q_port(port_number, settings["vlan"])
elif settings["type"] == "qinq":
yield from self.set_qinq_port(port_number, settings["vlan"], settings["ethertype"])
@asyncio.coroutine
def set_access_port(self, port_number, vlan_id):
"""
Sets the specified port as an ACCESS port.
:param port_number: allocated port number
:param vlan_id: VLAN number membership
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
yield from self._hypervisor.send('ethsw set_access_port "{name}" {nio} {vlan_id}'.format(name=self._name,
nio=nio,
vlan_id=vlan_id))
log.info('Ethernet switch "{name}" [{id}]: port {port} set as an access port in VLAN {vlan_id}'.format(name=self._name,
id=self._id,
port=port_number,
vlan_id=vlan_id))
self._mappings[port_number] = ("access", vlan_id)
@asyncio.coroutine
def set_dot1q_port(self, port_number, native_vlan):
"""
Sets the specified port as a 802.1Q trunk port.
:param port_number: allocated port number
:param native_vlan: native VLAN for this trunk port
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
yield from self._hypervisor.send('ethsw set_dot1q_port "{name}" {nio} {native_vlan}'.format(name=self._name,
nio=nio,
native_vlan=native_vlan))
log.info('Ethernet switch "{name}" [{id}]: port {port} set as a 802.1Q port with native VLAN {vlan_id}'.format(name=self._name,
id=self._id,
port=port_number,
vlan_id=native_vlan))
self._mappings[port_number] = ("dot1q", native_vlan)
@asyncio.coroutine
def set_qinq_port(self, port_number, outer_vlan, ethertype):
"""
Sets the specified port as a trunk (QinQ) port.
:param port_number: allocated port number
:param outer_vlan: outer VLAN (transport VLAN) for this QinQ port
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
if ethertype != "0x8100" and parse_version(self.hypervisor.version) < parse_version('0.2.16'):
raise DynamipsError("Dynamips version required is >= 0.2.16 to change the default QinQ Ethernet type, detected version is {}".format(self.hypervisor.version))
yield from self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan} {ethertype}'.format(name=self._name,
nio=nio,
outer_vlan=outer_vlan,
ethertype=ethertype if ethertype != "0x8100" else ""))
log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ ({ethertype}) port with outer VLAN {vlan_id}'.format(name=self._name,
id=self._id,
port=port_number,
vlan_id=outer_vlan,
ethertype=ethertype))
self._mappings[port_number] = ("qinq", outer_vlan, ethertype)
@asyncio.coroutine
def get_mac_addr_table(self):
"""
Returns the MAC address table for this Ethernet switch.
:returns: list of entries (Ethernet address, VLAN, NIO)
"""
mac_addr_table = yield from self._hypervisor.send('ethsw show_mac_addr_table "{}"'.format(self._name))
return mac_addr_table
@asyncio.coroutine
def clear_mac_addr_table(self):
"""
Clears the MAC address table for this Ethernet switch.
"""
yield from self._hypervisor.send('ethsw clear_mac_addr_table "{}"'.format(self._name))
@asyncio.coroutine
def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port_number: allocated port number
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
if not nio:
raise DynamipsError("Port {} is not connected".format(port_number))
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port_number))
yield from nio.bind_filter("both", "capture")
yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file))
log.info('Ethernet switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
@asyncio.coroutine
def stop_capture(self, port_number):
"""
Stops a packet capture.
:param port_number: allocated port number
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
if not nio:
raise DynamipsError("Port {} is not connected".format(port_number))
yield from nio.unbind_filter("both")
log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
@@ -0,0 +1,320 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for Dynamips virtual Frame-Relay switch module.
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642
"""
import asyncio
from .device import Device
from ..nios.nio_udp import NIOUDP
from ..dynamips_error import DynamipsError
import logging
log = logging.getLogger(__name__)
class FrameRelaySwitch(Device):
"""
Dynamips Frame Relay switch.
:param name: name for this switch
:param device_id: Device instance identifier
:param project: Project instance
:param manager: Parent VM Manager
:param hypervisor: Dynamips hypervisor instance
"""
def __init__(self, name, device_id, project, manager, hypervisor=None):
super().__init__(name, device_id, project, manager, hypervisor)
self._nios = {}
self._mappings = {}
def __json__(self):
mappings = {}
for source, destination in self._mappings.items():
mappings[str(source)] = str(destination)
return {"name": self.name,
"device_id": self.id,
"project_id": self.project.id,
"mappings": mappings}
@asyncio.coroutine
def create(self):
if self._hypervisor is None:
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir)
yield from self._hypervisor.send('frsw create "{}"'.format(self._name))
log.info('Frame Relay switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
self._hypervisor.devices.append(self)
@asyncio.coroutine
def set_name(self, new_name):
"""
Renames this Frame Relay switch.
:param new_name: New name for this switch
"""
yield from self._hypervisor.send('frsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name))
log.info('Frame Relay switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name,
id=self._id,
new_name=new_name))
self._name = new_name
@property
def nios(self):
"""
Returns all the NIOs member of this Frame Relay switch.
:returns: nio list
"""
return self._nios
@property
def mappings(self):
"""
Returns port mappings
:returns: mappings list
"""
return self._mappings
@asyncio.coroutine
def delete(self):
"""
Deletes this Frame Relay switch.
"""
for nio in self._nios.values():
if nio and isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
try:
yield from self._hypervisor.send('frsw delete "{}"'.format(self._name))
log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
except DynamipsError:
log.debug("Could not properly delete Frame relay switch {}".format(self._name))
if self._hypervisor and self in self._hypervisor.devices:
self._hypervisor.devices.remove(self)
if self._hypervisor and not self._hypervisor.devices:
yield from self.hypervisor.stop()
def has_port(self, port):
"""
Checks if a port exists on this Frame Relay switch.
:returns: boolean
"""
if port in self._nios:
return True
return False
def add_nio(self, nio, port_number):
"""
Adds a NIO as new port on Frame Relay switch.
:param nio: NIO instance to add
:param port_number: port to allocate for the NIO
"""
if port_number in self._nios:
raise DynamipsError("Port {} isn't free".format(port_number))
log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
self._nios[port_number] = nio
@asyncio.coroutine
def remove_nio(self, port_number):
"""
Removes the specified NIO as member of this Frame Relay switch.
:param port_number: allocated port number
:returns: the NIO that was bound to the allocated port
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
# remove VCs mapped with the port
for source, destination in self._mappings.copy().items():
source_port, source_dlci = source
destination_port, destination_dlci = destination
if port_number == source_port:
yield from self.unmap_vc(source_port, source_dlci, destination_port, destination_dlci)
yield from self.unmap_vc(destination_port, destination_dlci, source_port, source_dlci)
nio = self._nios[port_number]
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
id=self._id,
nio=nio,
port=port_number))
del self._nios[port_number]
return nio
@asyncio.coroutine
def set_mappings(self, mappings):
"""
Applies VC mappings
:param mappings: mappings (dict)
"""
for source, destination in mappings.items():
if not isinstance(source, str) or not isinstance(destination, str):
raise DynamipsError("Invalid Frame-Relay mappings")
source_port, source_dlci = map(int, source.split(':'))
destination_port, destination_dlci = map(int, destination.split(':'))
if self.has_port(destination_port):
if (source_port, source_dlci) not in self.mappings and (destination_port, destination_dlci) not in self.mappings:
yield from self.map_vc(source_port, source_dlci, destination_port, destination_dlci)
yield from self.map_vc(destination_port, destination_dlci, source_port, source_dlci)
@asyncio.coroutine
def map_vc(self, port1, dlci1, port2, dlci2):
"""
Creates a new Virtual Circuit connection (unidirectional).
:param port1: input port
:param dlci1: input DLCI
:param port2: output port
:param dlci2: output DLCI
"""
if port1 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port1))
if port2 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port2))
nio1 = self._nios[port1]
nio2 = self._nios[port2]
yield from self._hypervisor.send('frsw create_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name,
input_nio=nio1,
input_dlci=dlci1,
output_nio=nio2,
output_dlci=dlci2))
log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} created'.format(name=self._name,
id=self._id,
port1=port1,
dlci1=dlci1,
port2=port2,
dlci2=dlci2))
self._mappings[(port1, dlci1)] = (port2, dlci2)
@asyncio.coroutine
def unmap_vc(self, port1, dlci1, port2, dlci2):
"""
Deletes a Virtual Circuit connection (unidirectional).
:param port1: input port
:param dlci1: input DLCI
:param port2: output port
:param dlci2: output DLCI
"""
if port1 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port1))
if port2 not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port2))
nio1 = self._nios[port1]
nio2 = self._nios[port2]
yield from self._hypervisor.send('frsw delete_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name,
input_nio=nio1,
input_dlci=dlci1,
output_nio=nio2,
output_dlci=dlci2))
log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} deleted'.format(name=self._name,
id=self._id,
port1=port1,
dlci1=dlci1,
port2=port2,
dlci2=dlci2))
del self._mappings[(port1, dlci1)]
@asyncio.coroutine
def start_capture(self, port_number, output_file, data_link_type="DLT_FRELAY"):
"""
Starts a packet capture.
:param port_number: allocated port number
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port_number))
yield from nio.bind_filter("both", "capture")
yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file))
log.info('Frame relay switch "{name}" [{id}]: starting packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
@asyncio.coroutine
def stop_capture(self, port_number):
"""
Stops a packet capture.
:param port_number: allocated port number
"""
if port_number not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
yield from nio.unbind_filter("both")
log.info('Frame relay switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name,
id=self._id,
port=port_number))
File diff suppressed because it is too large. Load diff
+119
View File
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
IOU server module.
"""
import os
import asyncio
from ..base_manager import BaseManager
from .iou_error import IOUError
from .iou_vm import IOUVM
import logging
log = logging.getLogger(__name__)
class IOU(BaseManager):
_VM_CLASS = IOUVM
def __init__(self):
super().__init__()
self._free_application_ids = list(range(1, 512))
self._used_application_ids = {}
@asyncio.coroutine
def create_vm(self, *args, **kwargs):
"""
Creates a new IOU VM.
:returns: IOUVM instance
"""
vm = yield from super().create_vm(*args, **kwargs)
try:
self._used_application_ids[vm.id] = self._free_application_ids.pop(0)
except IndexError:
raise IOUError("Cannot create a new IOU VM (limit of 512 VMs reached on this host)")
return vm
@asyncio.coroutine
def close_vm(self, vm_id, *args, **kwargs):
"""
Closes an IOU VM.
:returns: IOUVM instance
"""
vm = self.get_vm(vm_id)
if vm_id in self._used_application_ids:
i = self._used_application_ids[vm_id]
self._free_application_ids.insert(0, i)
del self._used_application_ids[vm_id]
yield from super().close_vm(vm_id, *args, **kwargs)
return vm
@asyncio.coroutine
def project_committed(self, project):
"""
Called when a project has been committed.
:param project: Project instance
"""
# save the configs when the project is committed
for vm in self._vms.copy().values():
if vm.project.id == project.id:
try:
vm.save_configs()
except IOUError as e:
log.warning(e)
continue
def get_application_id(self, vm_id):
"""
Get an unique application identifier for IOU.
:param vm_id: IOU VM identifier
:returns: IOU application identifier
"""
return self._used_application_ids.get(vm_id, 1)
@staticmethod
def get_legacy_vm_workdir(legacy_vm_id, name):
"""
Returns the name of the legacy working directory (pre 1.3) name for a VM.
:param legacy_vm_id: legacy VM identifier (integer)
:param name: VM name (not used)
:returns: working directory name
"""
return os.path.join("iou", "device-{}".format(legacy_vm_id))
def get_images_directory(self):
"""
Return the full path of the images directory on disk
"""
return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "IOU")
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Custom exceptions for the IOU module.
"""
from ..vm_error import VMError
class IOUError(VMError):
pass
File diff suppressed because it is too large. Load diff
+674
View File
@@ -0,0 +1,674 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013, 2014 James E. Carpenter
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import socket
import sys
import os
import select
try:
import fcntl
import termios
import tty
except ImportError:
# On windows it's not available but this module can be included by the test suite
pass
import struct
import time
import argparse
import traceback
import logging
log = logging.getLogger(__name__)
# Escape characters
ESC_CHAR = '^^' # can be overriden from command line
ESC_QUIT = 'q'
# IOU seems to only send *1* byte at a time. If
# they ever fix that we'll be ready for it.
BUFFER_SIZE = 1024
# How long to wait before retrying a connection (seconds)
RETRY_DELAY = 3
# How often to test an idle connection (seconds)
POLL_TIMEOUT = 3
EXIT_SUCCESS = 0
EXIT_FAILURE = 1
EXIT_ABORT = 2
# Mostly from:
# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
# --[ Telnet Commands ]---------------------------------------------------------
SE = 240 # End of sub-negotiation parameters
NOP = 241 # No operation
DATMK = 242 # Data stream portion of a sync.
BREAK = 243 # NVT Character BRK
IP = 244 # Interrupt Process
AO = 245 # Abort Output
AYT = 246 # Are you there
EC = 247 # Erase Character
EL = 248 # Erase Line
GA = 249 # The Go Ahead Signal
SB = 250 # Sub-option to follow
WILL = 251 # Will; request or confirm option begin
WONT = 252 # Wont; deny option request
DO = 253 # Do = Request or confirm remote option
DONT = 254 # Don't = Demand or confirm option halt
IAC = 255 # Interpret as Command
SEND = 1 # Sub-process negotiation SEND command
IS = 0 # Sub-process negotiation IS command
# --[ Telnet Options ]----------------------------------------------------------
BINARY = 0 # Transmit Binary
ECHO = 1 # Echo characters back to sender
RECON = 2 # Reconnection
SGA = 3 # Suppress Go-Ahead
TMARK = 6 # Timing Mark
TTYPE = 24 # Terminal Type
NAWS = 31 # Negotiate About Window Size
LINEMO = 34 # Line Mode
class FileLock:
# struct flock { /* from fcntl(2) */
# ...
# short l_type; /* Type of lock: F_RDLCK,
# F_WRLCK, F_UNLCK */
# short l_whence; /* How to interpret l_start:
# SEEK_SET, SEEK_CUR, SEEK_END */
# off_t l_start; /* Starting offset for lock */
# off_t l_len; /* Number of bytes to lock */
# pid_t l_pid; /* PID of process blocking our lock
# (F_GETLK only) */
# ...
# };
_flock = struct.Struct('hhqql')
def __init__(self, fname=None):
self.fd = None
self.fname = fname
def get_lock(self):
flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET,
0, 0, os.getpid())
flk = self._flock.unpack(
fcntl.fcntl(self.fd, fcntl.F_GETLK, flk))
# If it's not locked (or is locked by us) then return None,
# otherwise return the PID of the owner.
if flk[0] == fcntl.F_UNLCK:
return None
return flk[4]
def lock(self):
try:
self.fd = open('{}.lck'.format(self.fname), 'a')
except Exception as e:
raise LockError("Couldn't get lock on {}: {}"
.format(self.fname, e))
flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0)
try:
fcntl.fcntl(self.fd, fcntl.F_SETLK, flk)
except BlockingIOError:
raise LockError("Already connected. PID {} has lock on {}"
.format(self.get_lock(), self.fname))
# If we got here then we must have the lock. Store the PID.
self.fd.truncate(0)
self.fd.write('{}\n'.format(os.getpid()))
self.fd.flush()
def unlock(self):
if self.fd:
# Deleting first prevents a race condition
try:
os.unlink(self.fd.name)
except FileNotFoundError as e:
log.debug("{}".format(e))
self.fd.close()
def __enter__(self):
self.lock()
def __exit__(self, exc_type, exc_val, exc_tb):
self.unlock()
return False
class Console:
def fileno(self):
raise NotImplementedError("Only routers have fileno()")
class TTY(Console):
def read(self, fileno, bufsize):
return self.fd.read(bufsize)
def write(self, buf):
return self.fd.write(buf)
def register(self, epoll):
self.epoll = epoll
epoll.register(self.fd, select.EPOLLIN | select.EPOLLET)
def unregister(self, epoll):
epoll.unregister(self.fd)
def __enter__(self):
try:
self.fd = open('/dev/tty', 'r+b', buffering=0)
except OSError as e:
raise TTYError("Couldn't open controlling TTY: {}".format(e))
# Save original flags
self.termios = termios.tcgetattr(self.fd)
self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
# Update flags
tty.setraw(self.fd, termios.TCSANOW)
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Restore flags to original settings
termios.tcsetattr(self.fd, termios.TCSANOW, self.termios)
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl)
self.fd.close()
return False
class TelnetServer(Console):
def __init__(self, addr, port, stop_event):
self.addr = addr
self.port = port
self.fd_dict = {}
self.stop_event = stop_event
def read(self, fileno, bufsize):
# Someone wants to connect?
if fileno == self.sock_fd.fileno():
self._accept()
return None
self._cur_fileno = fileno
# Read a maximum of _bufsize_ bytes without blocking. When it
# would want to block it means there's no more data. An empty
# buffer normally means that we've been disconnected.
try:
buf = self._read_cur(bufsize, socket.MSG_DONTWAIT)
except BlockingIOError:
return None
except ConnectionResetError:
buf = b''
if not buf:
self._disconnect(fileno)
# Process and remove any telnet commands from the buffer
if IAC in buf:
buf = self._IAC_parser(buf)
return buf
def write(self, buf):
for fd in self.fd_dict.values():
fd.send(buf)
def register(self, epoll):
self.epoll = epoll
epoll.register(self.sock_fd, select.EPOLLIN)
def unregister(self, epoll):
epoll.unregister(self.sock_fd)
def _read_block(self, bufsize):
buf = self._read_cur(bufsize, socket.MSG_WAITALL)
# If we don't get everything we were looking for then the
# client probably disconnected.
if len(buf) < bufsize:
self._disconnect(self._cur_fileno)
return buf
def _read_cur(self, bufsize, flags):
return self.fd_dict[self._cur_fileno].recv(bufsize, flags)
def _write_cur(self, buf):
return self.fd_dict[self._cur_fileno].send(buf)
def _IAC_parser(self, buf):
skip_to = 0
while not self.stop_event.is_set():
# Locate an IAC to process
iac_loc = buf.find(IAC, skip_to)
if iac_loc < 0:
break
# Get the TELNET command
iac_cmd = bytearray([IAC])
try:
iac_cmd.append(buf[iac_loc + 1])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 1])
# Is this just a 2-byte TELNET command?
if iac_cmd[1] not in [WILL, WONT, DO, DONT]:
if iac_cmd[1] == AYT:
log.debug("Telnet server received Are-You-There (AYT)")
self._write_cur(
b'\r\nYour Are-You-There received. I am here.\r\n'
)
elif iac_cmd[1] == IAC:
# It's data, not an IAC
iac_cmd.pop()
# This prevents the 0xff from being
# interputed as yet another IAC
skip_to = iac_loc + 1
log.debug("Received IAC IAC")
elif iac_cmd[1] == NOP:
pass
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x}".format(*iac_cmd))
# This must be a 3-byte TELNET command
else:
try:
iac_cmd.append(buf[iac_loc + 2])
except IndexError:
buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 2])
# We do ECHO, SGA, and BINARY. Period.
if iac_cmd[1] == DO:
if iac_cmd[2] not in [ECHO, SGA, BINARY]:
self._write_cur(bytes([IAC, WONT, iac_cmd[2]]))
log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
elif iac_cmd[1] == WILL and iac_cmd[2] == BINARY:
pass # It's standard negociation we can ignore it
else:
log.debug("Unhandled telnet command: "
"{0:#x} {1:#x} {2:#x}".format(*iac_cmd))
# Remove the entire TELNET command from the buffer
buf = buf.replace(iac_cmd, b'', 1)
# Return the new copy of the buffer, minus telnet commands
return buf
def _accept(self):
fd, addr = self.sock_fd.accept()
self.fd_dict[fd.fileno()] = fd
self.epoll.register(fd, select.EPOLLIN | select.EPOLLET)
log.info("Telnet connection from {}:{}".format(addr[0], addr[1]))
# This is a one-way negotiation. This is very basic so there
# shouldn't be any problems with any decent client.
fd.send(bytes([IAC, WILL, ECHO,
IAC, WILL, SGA,
IAC, WILL, BINARY,
IAC, DO, BINARY]))
if args.telnet_limit and len(self.fd_dict) > args.telnet_limit:
fd.send(b'\r\nToo many connections\r\n')
self._disconnect(fd.fileno())
log.warn("Client disconnected because of too many connections. "
"(limit currently {})".format(args.telnet_limit))
def _disconnect(self, fileno):
fd = self.fd_dict.pop(fileno)
log.info("Telnet client disconnected")
try:
fd.shutdown(socket.SHUT_RDWR)
except OSError as e:
log.warn("shutdown: {}".format(e))
fd.close()
def __enter__(self):
# Open a socket and start listening
info = socket.getaddrinfo(self.addr, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
if not info:
raise TelnetServerError("getaddrinfo returns an empty list on {}:{}".format(self.addr, self.port))
for res in info:
af, socktype, proto, _, sa = res
sock_fd = socket.socket(af, socktype, proto)
sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock_fd.bind(sa)
except OSError:
raise TelnetServerError("Cannot bind to {}:{}"
.format(self.addr, self.port))
sock_fd.listen(socket.SOMAXCONN)
self.sock_fd = sock_fd
log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port))
return self
def __exit__(self, exc_type, exc_val, exc_tb):
for fileno in list(self.fd_dict.keys()):
self._disconnect(fileno)
self.sock_fd.close()
return False
class IOU:
def __init__(self, ttyC, ttyS, stop_event):
self.ttyC = ttyC
self.ttyS = ttyS
self.stop_event = stop_event
def read(self, bufsize):
try:
buf = self.fd.recv(bufsize)
except BlockingIOError:
return None
return buf
def write(self, buf):
try:
self.fd.send(buf)
except BlockingIOError:
return
def _open(self):
self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.fd.setblocking(False)
def _bind(self):
try:
os.unlink(self.ttyC)
except FileNotFoundError:
pass
except Exception as e:
raise NetioError("Couldn't unlink socket {}: {}".format(self.ttyC, e))
try:
self.fd.bind(self.ttyC)
except Exception as e:
raise NetioError("Couldn't create socket {}: {}".format(self.ttyC, e))
def _connect(self):
# Keep trying until we connect or die trying
while not self.stop_event.is_set():
try:
self.fd.connect(self.ttyS)
except FileNotFoundError:
log.debug("Waiting to connect to {}".format(self.ttyS))
time.sleep(RETRY_DELAY)
except Exception as e:
raise NetioError("Couldn't connect to socket {}: {}".format(self.ttyS, e))
else:
break
def register(self, epoll):
self.epoll = epoll
epoll.register(self.fd, select.EPOLLIN | select.EPOLLET)
def unregister(self, epoll):
epoll.unregister(self.fd)
def fileno(self):
return self.fd.fileno()
def __enter__(self):
self._open()
self._bind()
self._connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
os.unlink(self.ttyC)
self.fd.close()
return False
class IOUConError(Exception):
pass
class LockError(IOUConError):
pass
class NetioError(IOUConError):
pass
class TTYError(IOUConError):
pass
class TelnetServerError(IOUConError):
pass
class ConfigError(IOUConError):
pass
def mkdir_netio(netio_dir):
try:
os.mkdir(netio_dir)
except FileExistsError:
pass
except Exception as e:
raise NetioError("Couldn't create directory {}: {}".format(netio_dir, e))
def send_recv_loop(epoll, console, router, esc_char, stop_event):
router.register(epoll)
console.register(epoll)
try:
router_fileno = router.fileno()
esc_quit = bytes(ESC_QUIT.upper(), 'ascii')
esc_state = False
while not stop_event.is_set():
event_list = epoll.poll(timeout=POLL_TIMEOUT)
# When/if the poll times out we send an empty datagram. If IOU
# has gone away then this will toss a ConnectionRefusedError
# exception.
if not event_list:
router.write(b'')
continue
for fileno, event in event_list:
buf = bytearray()
# IOU --> tty(s)
if fileno == router_fileno:
while not stop_event.is_set():
data = router.read(BUFFER_SIZE)
if not data:
break
buf.extend(data)
console.write(buf)
# tty --> IOU
else:
while not stop_event.is_set():
data = console.read(fileno, BUFFER_SIZE)
if not data:
break
buf.extend(data)
# If we just received the escape character then
# enter the escape state.
#
# If we are in the escape state then check for a
# quit command. Or if it's the escape character then
# send the escape character. Else, send the escape
# character we ate earlier and whatever character we
# just got. Exit escape state.
#
# If we're not in the escape state and this isn't an
# escape character then just send it to IOU.
if esc_state:
if buf.upper() == esc_quit:
sys.exit(EXIT_SUCCESS)
elif buf == esc_char:
router.write(esc_char)
else:
router.write(esc_char)
router.write(buf)
esc_state = False
elif buf == esc_char:
esc_state = True
else:
router.write(buf)
finally:
router.unregister(epoll)
console.unregister(epoll)
def get_args():
parser = argparse.ArgumentParser(
description='Connect to an IOU console port.')
parser.add_argument('-d', '--debug', action='store_true',
help='display some debugging information')
parser.add_argument('-e', '--escape',
help='set escape character (default: %(default)s)',
default=ESC_CHAR, metavar='CHAR')
parser.add_argument('-t', '--telnet-server',
help='start telnet server listening on ADDR:PORT',
metavar='ADDR:PORT', default=False)
parser.add_argument('-l', '--telnet-limit',
help='maximum number of simultaneous '
'telnet connections (default: %(default)s)',
metavar='LIMIT', type=int, default=1)
parser.add_argument('appl_id', help='IOU instance identifier')
return parser.parse_args()
def get_escape_character(escape):
# Figure out the escape character to use.
# Can be any ASCII character or a spelled out control
# character, like "^e". The string "none" disables it.
if escape.lower() == 'none':
esc_char = b''
elif len(escape) == 2 and escape[0] == '^':
c = ord(escape[1].upper()) - 0x40
if not 0 <= c <= 0x1f: # control code range
raise ConfigError("Invalid control code")
esc_char = bytes([c])
elif len(escape) == 1:
try:
esc_char = bytes(escape, 'ascii')
except ValueError as e:
raise ConfigError("Invalid escape character") from e
else:
raise ConfigError("Invalid length for escape character")
return esc_char
def start_ioucon(cmdline_args, stop_event):
global args
args = cmdline_args
if args.debug:
logging.basicConfig(level=logging.DEBUG)
else:
# default logging level
logging.basicConfig(level=logging.INFO)
# Create paths for the Unix domain sockets
netio = '/tmp/netio{}'.format(os.getuid())
ttyC = '{}/ttyC{}'.format(netio, args.appl_id)
ttyS = '{}/ttyS{}'.format(netio, args.appl_id)
try:
mkdir_netio(netio)
with FileLock(ttyC):
esc_char = get_escape_character(args.escape)
if args.telnet_server:
addr, _, port = args.telnet_server.partition(':')
nport = 0
try:
nport = int(port)
except ValueError:
pass
if addr == '' or nport == 0:
raise ConfigError('format for --telnet-server must be '
'ADDR:PORT (like 127.0.0.1:20000)')
while not stop_event.is_set():
epoll = select.epoll()
try:
if args.telnet_server:
with TelnetServer(addr, nport, stop_event) as console:
# We loop inside the Telnet server otherwise the client is disconnected when user use the reload command inside a terminal
while not stop_event.is_set():
try:
with IOU(ttyC, ttyS, stop_event) as router:
send_recv_loop(epoll, console, router, b'', stop_event)
except ConnectionRefusedError:
pass
else:
with IOU(ttyC, ttyS, stop_event) as router, TTY() as console:
send_recv_loop(epoll, console, router, esc_char, stop_event)
except ConnectionRefusedError:
pass
except KeyboardInterrupt:
sys.exit(EXIT_ABORT)
finally:
# Put us at the beginning of a line
if not args.telnet_server:
print()
except IOUConError as e:
if args.debug:
traceback.print_exc(file=sys.stderr)
else:
log.error("ioucon: {}".format(e))
sys.exit(EXIT_FAILURE)
log.info("exiting...")
def main():
import threading
stop_event = threading.Event()
args = get_args()
start_ioucon(args, stop_event)
if __name__ == '__main__':
main()
Whitespace-only changes.
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# To use python v2.7 change the first line to:
#!/usr/bin/env python
# Copyright (C) 2015 Bernhard Ehlers
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# This utility is a stripped down version of dynamips' nvram_export,
# ported from C to Python, see https://github.com/GNS3/dynamips
# nvram_export is (c) 2013 Flávio J. Saraiva
"""
iou_export exports startup/private configuration from IOU NVRAM file.
usage: iou_export [-h] NVRAM startup-config [private-config]
positional arguments:
NVRAM NVRAM file
startup-config startup configuration
private-config private configuration
optional arguments:
-h, --help show this help message and exit
"""
import argparse
import sys
# Uncompress data in .Z file format.
# Ported from dynamips' fs_nvram.c to python
# Adapted from 7zip's ZDecoder.cpp, which is licensed under LGPL 2.1.
def uncompress_LZC(data):
LZC_NUM_BITS_MIN = 9
LZC_NUM_BITS_MAX = 16
in_data = bytearray(data)
in_len = len(in_data)
out_data = bytearray()
if in_len == 0:
return out_data
if in_len < 3:
raise ValueError('invalid length')
if in_data[0] != 0x1F or in_data[1] != 0x9D:
raise ValueError('invalid header')
maxbits = in_data[2] & 0x1F
numItems = 1 << maxbits
blockMode = (in_data[2] & 0x80) != 0
if maxbits < LZC_NUM_BITS_MIN or maxbits > LZC_NUM_BITS_MAX:
raise ValueError('not supported')
parents = [0] * numItems
suffixes = [0] * numItems
in_pos = 3
numBits = LZC_NUM_BITS_MIN
head = 256
if blockMode:
head += 1
needPrev = 0
bitPos = 0
numBufBits = 0
parents[256] = 0
suffixes[256] = 0
buf_extend = bytearray([0] * 3)
while True:
# fill buffer, when empty
if numBufBits == bitPos:
buf_len = min(in_len - in_pos, numBits)
buf = in_data[in_pos:in_pos + buf_len] + buf_extend
numBufBits = buf_len << 3
bitPos = 0
in_pos += buf_len
# extract next symbol
bytePos = bitPos >> 3
symbol = buf[bytePos] | buf[bytePos + 1] << 8 | buf[bytePos + 2] << 16
symbol >>= bitPos & 7
symbol &= (1 << numBits) - 1
bitPos += numBits
# check for special conditions: end, bad data, re-initialize dictionary
if bitPos > numBufBits:
break
if symbol >= head:
raise ValueError('invalid data')
if blockMode and symbol == 256:
numBufBits = bitPos = 0
numBits = LZC_NUM_BITS_MIN
head = 257
needPrev = 0
continue
# convert symbol to string
stack = []
cur = symbol
while cur >= 256:
stack.append(suffixes[cur])
cur = parents[cur]
stack.append(cur)
if needPrev:
suffixes[head - 1] = cur
if symbol == head - 1:
stack[0] = cur
stack.reverse()
out_data.extend(stack)
# update parents, check for numBits change
if head < numItems:
needPrev = 1
parents[head] = symbol
head += 1
if head > (1 << numBits):
if numBits < maxbits:
numBufBits = bitPos = 0
numBits += 1
else:
needPrev = 0
return out_data
# extract 16 bit unsigned int from data
def get_uint16(data, off):
return data[off] << 8 | data[off + 1]
# extract 32 bit unsigned int from data
def get_uint32(data, off):
return data[off] << 24 | data[off + 1] << 16 | data[off + 2] << 8 | data[off + 3]
# export IOU NVRAM
def nvram_export(nvram):
nvram = bytearray(nvram)
# extract startup config
offset = 0
if len(nvram) < offset + 36:
raise ValueError('invalid length')
if get_uint16(nvram, offset + 0) != 0xABCD:
raise ValueError('no startup config')
format = get_uint16(nvram, offset + 2)
length = get_uint32(nvram, offset + 16)
offset += 36
if len(nvram) < offset + length:
raise ValueError('invalid length')
startup = nvram[offset:offset + length]
# compressed startup config
if format == 2:
try:
startup = uncompress_LZC(startup)
except ValueError as err:
raise ValueError('uncompress startup: ' + str(err))
offset += length
# alignment to multiple of 4
offset = (offset + 3) & ~3
# check for additonal offset of 4
if len(nvram) >= offset + 8 and \
get_uint16(nvram, offset + 4) == 0xFEDC and \
get_uint16(nvram, offset + 6) == 1:
offset += 4
# extract private config
private = None
if len(nvram) >= offset + 16 and get_uint16(nvram, offset + 0) == 0xFEDC:
length = get_uint32(nvram, offset + 12)
offset += 16
if len(nvram) >= offset + length:
private = nvram[offset:offset + length]
return (startup, private)
if __name__ == '__main__':
# Main program
parser = argparse.ArgumentParser(description='%(prog)s exports startup/private configuration from IOU NVRAM file.')
parser.add_argument('nvram', metavar='NVRAM',
help='NVRAM file')
parser.add_argument('startup', metavar='startup-config',
help='startup configuration')
parser.add_argument('private', metavar='private-config', nargs='?',
help='private configuration')
args = parser.parse_args()
try:
fd = open(args.nvram, 'rb')
nvram = fd.read()
fd.close()
except (IOError, OSError) as err:
sys.stderr.write("Error reading file: {}\n".format(err))
sys.exit(1)
try:
startup, private = nvram_export(nvram)
except ValueError as err:
sys.stderr.write("nvram_export: {}\n".format(err))
sys.exit(3)
try:
fd = open(args.startup, 'wb')
fd.write(startup)
fd.close()
if args.private is not None:
if private is None:
sys.stderr.write("Warning: No private config\n")
else:
fd = open(args.private, 'wb')
fd.write(private)
fd.close()
except (IOError, OSError) as err:
sys.stderr.write("Error writing file: {}\n".format(err))
sys.exit(1)
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# To use python v2.7 change the first line to:
#!/usr/bin/env python
# Copyright (C) 2015 Bernhard Ehlers
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
iou_import imports startup/private configuration into IOU NVRAM file.
usage: iou_import [-h] [-c size] NVRAM startup-config [private-config]
positional arguments:
NVRAM NVRAM file
startup-config startup configuration
private-config private configuration
optional arguments:
-h, --help show this help message and exit
-c size, --create size
create NVRAM file, size in kByte
"""
import argparse
import sys
# extract 16 bit unsigned int from data
def get_uint16(data, off):
return data[off] << 8 | data[off + 1]
# extract 32 bit unsigned int from data
def get_uint32(data, off):
return data[off] << 24 | data[off + 1] << 16 | data[off + 2] << 8 | data[off + 3]
# insert 16 bit unsigned int into data
def put_uint16(data, off, value):
data[off] = (value >> 8) & 0xff
data[off + 1] = value & 0xff
# insert 32 bit unsigned int into data
def put_uint32(data, off, value):
data[off] = (value >> 24) & 0xff
data[off + 1] = (value >> 16) & 0xff
data[off + 2] = (value >> 8) & 0xff
data[off + 3] = value & 0xff
# calculate padding
def padding(off, ios):
pad = (4 - off % 4) % 4 # padding to alignment of 4
if ios <= 0x0F00 and pad != 0: # add 4 if IOS <= 15.0
pad += 4
return pad
# update checksum
def checksum(data, start, end):
put_uint16(data, start + 4, 0) # set checksum to 0
chk = 0
idx = start
while idx < end - 1:
chk += get_uint16(data, idx)
idx += 2
if idx < end:
chk += data[idx] << 8
while chk >> 16:
chk = (chk & 0xffff) + (chk >> 16)
chk = chk ^ 0xffff
put_uint16(data, start + 4, chk) # set checksum
# import IOU NVRAM
def nvram_import(nvram, startup, private, size):
BASE_ADDRESS = 0x10000000
DEFAULT_IOS = 0x0F04 # IOS 15.4
# check size parameter
if size is not None and (size < 8 or size > 1024):
raise ValueError('invalid size')
# create new nvram if nvram is empty or has wrong size
if nvram is None or (size is not None and len(nvram) != size * 1024):
nvram = bytearray([0] * (size * 1024))
else:
nvram = bytearray(nvram)
# check nvram size
nvram_len = len(nvram)
if nvram_len < 8 * 1024 or nvram_len > 1024 * 1024 or nvram_len % 1024 != 0:
raise ValueError('invalid NVRAM length')
nvram_len = nvram_len // 2
# get size of current config
config_len = 0
ios = None
try:
if get_uint16(nvram, 0) == 0xABCD:
ios = get_uint16(nvram, 6)
config_len = 36 + get_uint32(nvram, 16)
config_len += padding(config_len, ios)
if get_uint16(nvram, config_len) == 0xFEDC:
config_len += 16 + get_uint32(nvram, config_len + 12)
except IndexError:
raise ValueError('unknown nvram format')
if config_len > nvram_len:
raise ValueError('unknown nvram format')
# calculate max. config size
max_config = nvram_len - 2 * 1024 # reserve 2k for files
idx = max_config
empty_sector = bytearray([0] * 1024)
while True:
idx -= 1024
if idx < config_len:
break
# if valid file header:
if get_uint16(nvram, idx + 0) == 0xDCBA and \
get_uint16(nvram, idx + 4) < 8 and \
get_uint16(nvram, idx + 6) <= 992:
max_config = idx
elif nvram[idx:idx + 1024] != empty_sector:
break
# import startup config
startup = bytearray(startup)
if ios is None:
# Target IOS version is unknown. As some IOU don't work nicely with
# the padding of a different version, the startup config is padded
# with '\n' to the alignment of 4.
ios = DEFAULT_IOS
startup.extend([ord('\n')] * ((4 - len(startup) % 4) % 4))
new_nvram = bytearray([0] * 36) # startup hdr
put_uint16(new_nvram, 0, 0xABCD) # magic
put_uint16(new_nvram, 2, 1) # raw data
put_uint16(new_nvram, 6, ios) # IOS version
put_uint32(new_nvram, 8, BASE_ADDRESS + 36) # start address
put_uint32(new_nvram, 12, BASE_ADDRESS + 36 + len(startup)) # end address
put_uint32(new_nvram, 16, len(startup)) # length
new_nvram.extend(startup)
new_nvram.extend([0] * padding(len(new_nvram), ios))
# import private config
if private is None:
private = bytearray()
else:
private = bytearray(private)
offset = len(new_nvram)
new_nvram.extend([0] * 16) # private hdr
put_uint16(new_nvram, 0 + offset, 0xFEDC) # magic
put_uint16(new_nvram, 2 + offset, 1) # raw data
put_uint32(new_nvram, 4 + offset,
BASE_ADDRESS + offset + 16) # start address
put_uint32(new_nvram, 8 + offset,
BASE_ADDRESS + offset + 16 + len(private)) # end address
put_uint32(new_nvram, 12 + offset, len(private)) # length
new_nvram.extend(private)
# add rest
if len(new_nvram) > max_config:
raise ValueError('NVRAM size too small')
new_nvram.extend([0] * (max_config - len(new_nvram)))
new_nvram.extend(nvram[max_config:])
checksum(new_nvram, 0, nvram_len)
return new_nvram
if __name__ == '__main__':
# Main program
def check_size(string):
try:
value = int(string)
except ValueError:
raise argparse.ArgumentTypeError('invalid int value: ' + string)
if value < 8 or value > 1024:
raise argparse.ArgumentTypeError('size must be 8..1024')
return value
parser = argparse.ArgumentParser(description='%(prog)s imports startup/private configuration into IOU NVRAM file.')
parser.add_argument('-c', '--create', metavar='size', type=check_size,
help='create NVRAM file, size in kByte')
parser.add_argument('nvram', metavar='NVRAM',
help='NVRAM file')
parser.add_argument('startup', metavar='startup-config',
help='startup configuration')
parser.add_argument('private', metavar='private-config', nargs='?',
help='private configuration')
args = parser.parse_args()
try:
if args.create is None:
fd = open(args.nvram, 'rb')
nvram = fd.read()
fd.close()
else:
nvram = None
fd = open(args.startup, 'rb')
startup = fd.read()
fd.close()
if args.private is None:
private = None
else:
fd = open(args.private, 'rb')
private = fd.read()
fd.close()
except (IOError, OSError) as err:
sys.stderr.write("Error reading file: {}\n".format(err))
sys.exit(1)
try:
nvram = nvram_import(nvram, startup, private, args.create)
except ValueError as err:
sys.stderr.write("nvram_import: {}\n".format(err))
sys.exit(3)
try:
fd = open(args.nvram, 'wb')
fd.write(nvram)
fd.close()
except (IOError, OSError) as err:
sys.stderr.write("Error writing file: {}\n".format(err))
sys.exit(1)
View File
Whitespace-only changes.
+76
View File
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base interface for NIOs.
"""
class NIO(object):
"""
IOU NIO.
"""
def __init__(self):
self._capturing = False
self._pcap_output_file = ""
self._pcap_data_link_type = ""
def startPacketCapture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"):
"""
:param pcap_output_file: PCAP destination file for the capture
:param pcap_data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
self._capturing = True
self._pcap_output_file = pcap_output_file
self._pcap_data_link_type = pcap_data_link_type
def stopPacketCapture(self):
self._capturing = False
self._pcap_output_file = ""
self._pcap_data_link_type = ""
@property
def capturing(self):
"""
Returns either a capture is configured on this NIO.
:returns: boolean
"""
return self._capturing
@property
def pcap_output_file(self):
"""
Returns the path to the PCAP output file.
:returns: path to the PCAP output file
"""
return self._pcap_output_file
@property
def pcap_data_link_type(self):
"""
Returns the PCAP data link type
:returns: PCAP data link type (DLT_* value)
"""
return self._pcap_data_link_type
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for generic Ethernet NIOs (PCAP library).
"""
from .nio import NIO
class NIOGenericEthernet(NIO):
"""
Generic Ethernet NIO.
:param ethernet_device: Ethernet device name (e.g. eth0)
"""
def __init__(self, ethernet_device):
super().__init__()
self._ethernet_device = ethernet_device
@property
def ethernet_device(self):
"""
Returns the Ethernet device used by this NIO.
:returns: the Ethernet device name
"""
return self._ethernet_device
def __str__(self):
return "NIO Ethernet"
def __json__(self):
return {"type": "nio_generic_ethernet",
"ethernet_device": self._ethernet_device}
+41
View File
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for NAT NIOs.
"""
from .nio import NIO
class NIONAT(NIO):
"""
NAT NIO.
"""
def __init__(self):
super().__init__()
def __str__(self):
return "NIO TAP"
def __json__(self):
return {"type": "nio_nat"}
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for TAP NIOs (UNIX based OSes only).
"""
from .nio import NIO
class NIOTAP(NIO):
"""
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""
def __init__(self, tap_device):
super().__init__()
self._tap_device = tap_device
@property
def tap_device(self):
"""
Returns the TAP device used by this NIO.
:returns: the TAP device name
"""
return self._tap_device
def __str__(self):
return "NIO TAP"
def __json__(self):
return {"type": "nio_tap",
"tap_device": self._tap_device}
+81
View File
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for UDP NIOs.
"""
from .nio import NIO
class NIOUDP(NIO):
"""
UDP NIO.
:param lport: local port number
:param rhost: remote address/host
:param rport: remote port number
"""
def __init__(self, lport, rhost, rport):
super().__init__()
self._lport = lport
self._rhost = rhost
self._rport = rport
@property
def lport(self):
"""
Returns the local port
:returns: local port number
"""
return self._lport
@property
def rhost(self):
"""
Returns the remote host
:returns: remote address/host
"""
return self._rhost
@property
def rport(self):
"""
Returns the remote port
:returns: remote port number
"""
return self._rport
def __str__(self):
return "NIO UDP"
def __json__(self):
return {"type": "nio_udp",
"lport": self._lport,
"rport": self._rport,
"rhost": self._rhost}
@@ -0,0 +1,69 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from contextlib import contextmanager
from ..notification_queue import NotificationQueue
class NotificationManager:
"""
Manage the notification queue where the controller
will connect to get notifications from computes
"""
def __init__(self):
self._listeners = set()
@contextmanager
def queue(self):
"""
Get a queue of notifications
Use it with Python with
"""
queue = NotificationQueue()
self._listeners.add(queue)
yield queue
self._listeners.remove(queue)
def emit(self, action, event, **kwargs):
"""
Send an event to all the client listening for notifications
:param action: Action name
:param event: Event to send
:param kwargs: Add this meta to the notif (project_id for example)
"""
for listener in self._listeners:
listener.put_nowait((action, event, kwargs))
@staticmethod
def reset():
NotificationManager._instance = None
@staticmethod
def instance():
"""
Singleton to return only on instance of NotificationManager.
:returns: instance of NotificationManager
"""
if not hasattr(NotificationManager, '_instance') or NotificationManager._instance is None:
NotificationManager._instance = NotificationManager()
return NotificationManager._instance
+310
View File
@@ -0,0 +1,310 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import socket
import ipaddress
from aiohttp.web import HTTPConflict
from gns3server.config import Config
import logging
log = logging.getLogger(__name__)
class PortManager:
"""
:param host: IP address to bind for console connections
"""
def __init__(self, host="127.0.0.1"):
self._console_host = host
# UDP host must be 0.0.0.0, reason: https://github.com/GNS3/gns3-server/issues/265
self._udp_host = "0.0.0.0"
self._used_tcp_ports = set()
self._used_udp_ports = set()
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", 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))
udp_start_port_range = server_config.getint("udp_start_port_range", 10000)
udp_end_port_range = server_config.getint("udp_end_port_range", 20000)
self._udp_port_range = (udp_start_port_range, udp_end_port_range)
log.debug("UDP port range is {}-{}".format(udp_start_port_range, udp_end_port_range))
if remote_console_connections:
log.warning("Remote console connections are allowed")
if ipaddress.ip_address(host).version == 6:
self._console_host = "::"
else:
self._console_host = "0.0.0.0"
else:
self._console_host = host
PortManager._instance = self
@classmethod
def instance(cls):
"""
Singleton to return only one instance of PortManager.
:returns: instance of PortManager
"""
if not hasattr(cls, "_instance") or cls._instance is None:
cls._instance = cls()
return cls._instance
@property
def console_host(self):
return self._console_host
@console_host.setter
def console_host(self, new_host):
self._console_host = new_host
@property
def console_port_range(self):
return self._console_port_range
@console_port_range.setter
def console_port_range(self, new_range):
assert isinstance(new_range, tuple)
self._console_port_range = new_range
@property
def udp_host(self):
return self._udp_host
@udp_host.setter
def host(self, new_host):
self._udp_host = new_host
@property
def udp_port_range(self):
return self._udp_port_range
@udp_port_range.setter
def udp_port_range(self, new_range):
assert isinstance(new_range, tuple)
self._udp_port_range = new_range
@property
def tcp_ports(self):
return self._used_tcp_ports
@property
def udp_ports(self):
return self._used_udp_ports
@staticmethod
def find_unused_port(start_port, end_port, host="127.0.0.1", socket_type="TCP", ignore_ports=[]):
"""
Finds an unused port in a range.
:param start_port: first port in the range
:param end_port: last port in the range
:param host: host/address for bind()
:param socket_type: TCP (default) or UDP
:param ignore_ports: list of port to ignore within the range
"""
if end_port < start_port:
raise HTTPConflict(text="Invalid port range {}-{}".format(start_port, end_port))
last_exception = None
for port in range(start_port, end_port + 1):
if port in ignore_ports:
continue
try:
PortManager._check_port(host, port, socket_type)
return port
except OSError as e:
last_exception = e
if port + 1 == end_port:
break
else:
continue
raise HTTPConflict(text="Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port,
end_port,
host,
last_exception))
@staticmethod
def _check_port(host, port, socket_type):
"""
Check if an a port is available and raise an OSError if port is not available
:returns: boolean
"""
if socket_type == "UDP":
socket_type = socket.SOCK_DGRAM
else:
socket_type = socket.SOCK_STREAM
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket_type, 0, socket.AI_PASSIVE):
af, socktype, proto, _, sa = res
with socket.socket(af, socktype, proto) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(sa) # the port is available if bind is a success
return True
def get_free_tcp_port(self, project, port_range_start=None, port_range_end=None):
"""
Get an available TCP port and reserve it
:param project: Project instance
"""
# use the default range is not specific one is given
if port_range_start is None and port_range_end is None:
port_range_start = self._console_port_range[0]
port_range_end = self._console_port_range[1]
port = self.find_unused_port(port_range_start,
port_range_end,
host=self._console_host,
socket_type="TCP",
ignore_ports=self._used_tcp_ports)
self._used_tcp_ports.add(port)
project.record_tcp_port(port)
log.debug("TCP port {} has been allocated".format(port))
return port
def reserve_tcp_port(self, port, project, port_range_start=None, port_range_end=None):
"""
Reserve a specific TCP port number. If not available replace it
by another.
:param port: TCP port number
:param project: Project instance
:param port_range_start: Port range to use
:param port_range_end: Port range to use
:returns: The TCP port
"""
# use the default range is not specific one is given
if port_range_start is None and port_range_end is None:
port_range_start = self._console_port_range[0]
port_range_end = self._console_port_range[1]
if port in self._used_tcp_ports:
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.debug(msg)
#project.emit("log.warning", {"message": msg})
return port
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.debug(msg)
#project.emit("log.warning", {"message": msg})
return port
try:
PortManager._check_port(self._console_host, port, "TCP")
except OSError:
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.debug(msg)
#project.emit("log.warning", {"message": msg})
return port
self._used_tcp_ports.add(port)
project.record_tcp_port(port)
log.debug("TCP port {} has been reserved".format(port))
return port
def release_tcp_port(self, port, project):
"""
Release a specific TCP port number
:param port: TCP port number
:param project: Project instance
"""
if port in self._used_tcp_ports:
self._used_tcp_ports.remove(port)
project.remove_tcp_port(port)
log.debug("TCP port {} has been released".format(port))
def get_free_udp_port(self, project):
"""
Get an available UDP port and reserve it
:param project: Project instance
"""
port = self.find_unused_port(self._udp_port_range[0],
self._udp_port_range[1],
host=self._udp_host,
socket_type="UDP",
ignore_ports=self._used_udp_ports)
self._used_udp_ports.add(port)
project.record_udp_port(port)
log.debug("UDP port {} has been allocated".format(port))
return port
def reserve_udp_port(self, port, project):
"""
Reserve a specific UDP port number
:param port: UDP port number
:param project: Project instance
"""
if port in self._used_udp_ports:
raise HTTPConflict(text="UDP port {} already in use on host {}".format(port, self._console_host))
if port < self._udp_port_range[0] or port > self._udp_port_range[1]:
raise HTTPConflict(text="UDP port {} is outside the range {}-{}".format(port, self._udp_port_range[0], self._udp_port_range[1]))
self._used_udp_ports.add(port)
project.record_udp_port(port)
log.debug("UDP port {} has been reserved".format(port))
def release_udp_port(self, port, project):
"""
Release a specific UDP port number
:param port: UDP port number
:param project: Project instance
"""
if port in self._used_udp_ports:
self._used_udp_ports.remove(port)
project.remove_udp_port(port)
log.debug("UDP port {} has been released".format(port))
+641
View File
@@ -0,0 +1,641 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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
from .notification_manager import NotificationManager
from ..config import Config
from ..utils.asyncio import wait_run_in_executor
import logging
log = logging.getLogger(__name__)
class Project:
"""
A project contains a list of VM.
In theory VM are isolated project/project.
:param project_id: force project identifier (None by default auto generate an UUID)
:param path: path of the project. (None use the standard directory)
:param temporary: boolean to tell if the project is a temporary project (destroy when closed)
"""
def __init__(self, name=None, project_id=None, path=None, temporary=False):
self._name = name
try:
UUID(project_id, version=4)
except ValueError:
raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id))
self._id = project_id
self._vms = set()
self._vms_to_destroy = set()
self.temporary = temporary
self._used_tcp_ports = set()
self._used_udp_ports = set()
if path is None:
location = self._config().get("project_directory", self._get_default_project_directory())
path = os.path.join(location, self._id)
try:
os.makedirs(path, exist_ok=True)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e))
self.path = path
log.info("Project {id} with path '{path}' created".format(path=self._path, id=self._id))
def __json__(self):
return {
"name": self._name,
"project_id": self._id,
"temporary": self._temporary
}
def _config(self):
return Config.instance().get_section_config("Server")
def is_local(self):
return self._config().getboolean("local", False)
@classmethod
def _get_default_project_directory(cls):
"""
Return the default location for the project directory
depending of the operating system
"""
server_config = Config.instance().get_section_config("Server")
path = os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects"))
path = os.path.normpath(path)
try:
os.makedirs(path, exist_ok=True)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create project directory: {}".format(e))
return path
@property
def id(self):
return self._id
@property
def path(self):
return self._path
@path.setter
def path(self, path):
if hasattr(self, "_path"):
if path != self._path and self.is_local() is False:
raise aiohttp.web.HTTPForbidden(text="You are not allowed to modify the project directory path")
if '"' in path:
raise aiohttp.web.HTTPForbidden(text="You are not allowed to use \" in the project directory path. It's not supported by Dynamips.")
self._path = path
self._update_temporary_file()
@asyncio.coroutine
def clean_old_path(self, old_path):
"""
Called after a project location change. All the compute should
have been notified before
"""
if self._temporary:
try:
yield from wait_run_in_executor(shutil.rmtree, old_path)
except OSError as e:
log.warn("Can't remove temporary directory {}: {}".format(old_path, e))
@property
def name(self):
return self._name
@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
def vms(self):
return self._vms
@property
def temporary(self):
return self._temporary
@temporary.setter
def temporary(self, temporary):
if hasattr(self, 'temporary') and temporary == self._temporary:
return
self._temporary = temporary
self._update_temporary_file()
def record_tcp_port(self, port):
"""
Associate a reserved TCP port number with this project.
:param port: TCP port number
"""
if port not in self._used_tcp_ports:
self._used_tcp_ports.add(port)
def record_udp_port(self, port):
"""
Associate a reserved UDP port number with this project.
:param port: UDP port number
"""
if port not in self._used_udp_ports:
self._used_udp_ports.add(port)
def remove_tcp_port(self, port):
"""
Removes an associated TCP port number from this project.
:param port: TCP port number
"""
if port in self._used_tcp_ports:
self._used_tcp_ports.remove(port)
def remove_udp_port(self, port):
"""
Removes an associated UDP port number from this project.
:param port: UDP port number
"""
if port in self._used_udp_ports:
self._used_udp_ports.remove(port)
def _update_temporary_file(self):
"""
Update the .gns3_temporary file in order to reflect current
project status.
"""
if not hasattr(self, "_path"):
return
if self._temporary:
try:
with open(os.path.join(self._path, ".gns3_temporary"), 'w+') as f:
f.write("1")
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create temporary project: {}".format(e))
else:
if os.path.exists(os.path.join(self._path, ".gns3_temporary")):
try:
os.remove(os.path.join(self._path, ".gns3_temporary"))
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not mark project as no longer temporary: {}".format(e))
def module_working_directory(self, module_name):
"""
Returns a working directory for the module
If the directory doesn't exist, the directory is created.
:param module_name: name for the module
:returns: working directory
"""
workdir = self.module_working_path(module_name)
try:
os.makedirs(workdir, exist_ok=True)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create module working directory: {}".format(e))
return workdir
def module_working_path(self, module_name):
"""
Returns the working directory for the module. If you want
to be sure to have the directory on disk take a look on:
module_working_directory
"""
return os.path.join(self._path, "project-files", module_name)
def vm_working_directory(self, vm):
"""
Returns a working directory for a specific VM.
If the directory doesn't exist, the directory is created.
:param vm: VM instance
:returns: VM working directory
"""
workdir = os.path.join(self._path, "project-files", vm.manager.module_name.lower(), vm.id)
try:
os.makedirs(workdir, exist_ok=True)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create the VM working directory: {}".format(e))
return workdir
def capture_working_directory(self):
"""
Returns a working directory where to store packet capture files.
:returns: path to the directory
"""
workdir = os.path.join(self._path, "project-files", "captures")
try:
os.makedirs(workdir, exist_ok=True)
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not create the capture working directory: {}".format(e))
return workdir
def mark_vm_for_destruction(self, vm):
"""
:param vm: An instance of VM
"""
self.remove_vm(vm)
self._vms_to_destroy.add(vm)
def add_vm(self, vm):
"""
Adds a VM to the project.
In theory this should be called by the VM manager.
:param vm: VM instance
"""
self._vms.add(vm)
def remove_vm(self, vm):
"""
Removes a VM from the project.
In theory this should be called by the VM manager.
:param vm: VM instance
"""
if vm in self._vms:
self._vms.remove(vm)
@asyncio.coroutine
def close(self):
"""
Closes the project, but keep information on disk
"""
for module in self.compute():
yield from module.instance().project_closing(self)
yield from self._close_and_clean(self._temporary)
for module in self.compute():
yield from module.instance().project_closed(self)
@asyncio.coroutine
def _close_and_clean(self, cleanup):
"""
Closes the project, and cleanup the disk if cleanup is True
:param cleanup: If True drop the project directory
"""
tasks = []
for vm in self._vms:
tasks.append(asyncio.async(vm.manager.close_vm(vm.id)))
if tasks:
done, _ = yield from asyncio.wait(tasks)
for future in done:
try:
future.result()
except (Exception, GeneratorExit) as e:
log.error("Could not close VM or device {}".format(e), exc_info=1)
if cleanup and os.path.exists(self.path):
try:
yield from wait_run_in_executor(shutil.rmtree, self.path)
log.info("Project {id} with path '{path}' deleted".format(path=self._path, id=self._id))
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not delete the project directory: {}".format(e))
else:
log.info("Project {id} with path '{path}' closed".format(path=self._path, id=self._id))
if self._used_tcp_ports:
log.warning("Project {} has TCP ports still in use: {}".format(self.id, self._used_tcp_ports))
if self._used_udp_ports:
log.warning("Project {} has UDP ports still in use: {}".format(self.id, self._used_udp_ports))
# clean the remaining ports that have not been cleaned by their respective VM or device.
port_manager = PortManager.instance()
for port in self._used_tcp_ports.copy():
port_manager.release_tcp_port(port, self)
for port in self._used_udp_ports.copy():
port_manager.release_udp_port(port, self)
@asyncio.coroutine
def commit(self):
"""
Writes project changes on disk
"""
while self._vms_to_destroy:
vm = self._vms_to_destroy.pop()
yield from vm.delete()
self.remove_vm(vm)
for module in self.compute():
yield from module.instance().project_committed(self)
@asyncio.coroutine
def delete(self):
"""
Removes project from disk
"""
for module in self.compute():
yield from module.instance().project_closing(self)
yield from self._close_and_clean(True)
for module in self.compute():
yield from module.instance().project_closed(self)
@classmethod
def clean_project_directory(cls):
"""
At startup drop old temporary project. After a crash for example
"""
config = Config.instance().get_section_config("Server")
directory = config.get("project_directory", cls._get_default_project_directory())
if os.path.exists(directory):
for project in os.listdir(directory):
path = os.path.join(directory, project)
if os.path.exists(os.path.join(path, ".gns3_temporary")):
log.warning("Purge old temporary project {}".format(project))
shutil.rmtree(path)
def compute(self):
"""
Returns all loaded VM compute.
"""
# We import it at the last time to avoid circular dependencies
from ..compute import MODULES
return MODULES
def emit(self, action, event):
"""
Send an event to all the client listening for notifications
:param action: Action name
:param event: Event to send
"""
NotificationManager.instance().emit(action, event, project_id=self.id)
@asyncio.coroutine
def list_files(self):
"""
:returns: Array of files in project without temporary files. The files are dictionnary {"path": "test.bin", "md5sum": "aaaaa"}
"""
files = []
for (dirpath, dirnames, filenames) in os.walk(self.path):
for filename in filenames:
if not filename.endswith(".ghost"):
path = os.path.relpath(dirpath, self.path)
path = os.path.join(path, filename)
path = os.path.normpath(path)
file_info = {"path": path}
try:
file_info["md5sum"] = yield from wait_run_in_executor(self._hash_file, os.path.join(dirpath, filename))
except OSError:
continue
files.append(file_info)
return files
def _hash_file(self, path):
"""
Compute and md5 hash for file
:returns: hexadecimal md5
"""
m = hashlib.md5()
with open(path, "rb") as f:
while True:
buf = f.read(128)
if not buf:
break
m.update(buf)
return m.hexdigest()
def export(self, include_images=False):
"""
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"):
self._export_project_file(path, z, include_images)
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 _export_images(self, image, type, z):
"""
Take a project file (.gns3) and export images to the zip
:param image: Image path
:param type: Type of image
:param z: Zipfile instance for the export
"""
from . import MODULES
for module in MODULES:
try:
img_directory = module.instance().get_images_directory()
except NotImplementedError:
# Some modules don't have images
continue
directory = os.path.split(img_directory)[-1:][0]
if os.path.exists(image):
path = image
else:
path = os.path.join(img_directory, image)
if os.path.exists(path):
arcname = os.path.join("images", directory, os.path.basename(image))
z.write(path, arcname)
break
def _export_project_file(self, path, z, include_images):
"""
Take a project file (.gns3) and patch it for the export
:param path: Path of the .gns3
"""
with open(path) as f:
topology = json.load(f)
if "topology" in topology and "nodes" in topology["topology"]:
for node in topology["topology"]["nodes"]:
if "properties" in node and node["type"] != "DockerVM":
for prop, value in node["properties"].items():
if prop.endswith("image"):
node["properties"][prop] = os.path.basename(value)
if include_images:
self._export_images(value, node["type"], z)
z.writestr("project.gns3", json.dumps(topology).encode())
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"))
if os.path.exists(os.path.join(self.path, "images")):
self._import_images()
def _import_images(self):
"""
Copy images to the images directory or delete them if they
already exists.
"""
image_dir = self._config().get("images_path")
root = os.path.join(self.path, "images")
for (dirpath, dirnames, filenames) in os.walk(root):
for filename in filenames:
path = os.path.join(dirpath, filename)
dst = os.path.join(image_dir, os.path.relpath(path, root))
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.move(path, dst)
# Cleanup the project
shutil.rmtree(root)
+110
View File
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import aiohttp
from .project import Project
from uuid import UUID
class ProjectManager:
"""
This singleton keeps track of available projects.
"""
def __init__(self):
self._projects = {}
@classmethod
def instance(cls):
"""
Singleton to return only one instance of ProjectManager.
:returns: instance of ProjectManager
"""
if not hasattr(cls, "_instance"):
cls._instance = cls()
return cls._instance
@property
def projects(self):
"""
Returns all projects.
:returns: Project instances
"""
return self._projects.values()
def get_project(self, project_id):
"""
Returns a Project instance.
:param project_id: Project identifier
:returns: Project instance
"""
try:
UUID(project_id, version=4)
except ValueError:
raise aiohttp.web.HTTPBadRequest(text="Project ID {} is not a valid UUID".format(project_id))
if project_id not in self._projects:
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id))
return self._projects[project_id]
def create_project(self, name=None, project_id=None, path=None, temporary=False):
"""
Create a project and keep a references to it in project manager.
See documentation of Project for arguments
"""
if project_id is not None and project_id in self._projects:
return self._projects[project_id]
project = Project(name=name, project_id=project_id, path=path, temporary=temporary)
self._projects[project.id] = project
return project
def remove_project(self, project_id):
"""
Removes a Project instance from the list of projects in use.
:param project_id: Project identifier
"""
if project_id not in self._projects:
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id))
del self._projects[project_id]
def check_hardware_virtualization(self, source_vm):
"""
Checks if hardware virtualization can be used.
:returns: boolean
"""
for project in self._projects.values():
for vm in project.vms:
if vm == source_vm:
continue
if vm.hw_virtualization and vm.__class__.__name__ != source_vm.__class__.__name__:
return False
return True
+267
View File
@@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Qemu server module.
"""
import asyncio
import os
import platform
import sys
import re
import subprocess
from ...utils.asyncio import subprocess_check_output
from ..base_manager import BaseManager
from .qemu_error import QemuError
from .qemu_vm import QemuVM
import logging
log = logging.getLogger(__name__)
class Qemu(BaseManager):
_VM_CLASS = QemuVM
@staticmethod
@asyncio.coroutine
def get_kvm_archs():
"""
Gets a list of architectures for which KVM is available on this server.
:returns: List of architectures for which KVM is available on this server.
"""
kvm = []
if not os.path.exists("/dev/kvm"):
return kvm
arch = platform.machine()
if arch == "x86_64":
kvm.append("x86_64")
kvm.append("i386")
elif arch == "i386":
kvm.append("i386")
else:
kvm.append(platform.machine())
return kvm
@staticmethod
def paths_list():
"""
Gets a folder list of possibly available QEMU binaries on the host.
:returns: List of folders where Qemu binaries MAY reside.
"""
paths = set()
try:
paths.add(os.getcwd())
except FileNotFoundError:
log.warning("The current working directory doesn't exist")
if "PATH" in os.environ:
paths.update(os.environ["PATH"].split(os.pathsep))
else:
log.warning("The PATH environment variable doesn't exist")
# look for Qemu binaries in the current working directory and $PATH
if sys.platform.startswith("win"):
# add specific Windows paths
if hasattr(sys, "frozen"):
# add any qemu dir in the same location as gns3server.exe to the list of paths
exec_dir = os.path.dirname(os.path.abspath(sys.executable))
for f in os.listdir(exec_dir):
if f.lower().startswith("qemu"):
paths.add(os.path.join(exec_dir, f))
if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
paths.add(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]):
paths.add(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
elif sys.platform.startswith("darwin"):
if hasattr(sys, "frozen"):
# add specific locations on Mac OS X regardless of what's in $PATH
paths.update(["/usr/bin", "/usr/local/bin", "/opt/local/bin"])
try:
exec_dir = os.path.dirname(os.path.abspath(sys.executable))
paths.add(os.path.abspath(os.path.join(exec_dir, "../Resources/qemu/bin/")))
# If the user run the server by hand from outside
except FileNotFoundError:
paths.add("/Applications/GNS3.app/Contents/Resources/qemu/bin")
return paths
@staticmethod
def binary_list(archs=None):
"""
Gets QEMU binaries list available on the host.
:returns: Array of dictionary {"path": Qemu binary path, "version": version of Qemu}
"""
qemus = []
for path in Qemu.paths_list():
try:
for f in os.listdir(path):
if f.endswith("-spice"):
continue
if (f.startswith("qemu-system") or f.startswith("qemu-kvm") or f == "qemu" or f == "qemu.exe") and \
os.access(os.path.join(path, f), os.X_OK) and \
os.path.isfile(os.path.join(path, f)):
if archs is not None:
for arch in archs:
if f.endswith(arch) or f.endswith("{}.exe".format(arch)) or f.endswith("{}w.exe".format(arch)):
qemu_path = os.path.join(path, f)
version = yield from Qemu.get_qemu_version(qemu_path)
qemus.append({"path": qemu_path, "version": version})
else:
qemu_path = os.path.join(path, f)
version = yield from Qemu.get_qemu_version(qemu_path)
qemus.append({"path": qemu_path, "version": version})
except OSError:
continue
return qemus
@staticmethod
def img_binary_list():
"""
Gets QEMU-img binaries list available on the host.
:returns: Array of dictionary {"path": Qemu-img binary path, "version": version of Qemu-img}
"""
qemu_imgs = []
for path in Qemu.paths_list():
try:
for f in os.listdir(path):
if (f == "qemu-img" or f == "qemu-img.exe") and \
os.access(os.path.join(path, f), os.X_OK) and \
os.path.isfile(os.path.join(path, f)):
qemu_path = os.path.join(path, f)
version = yield from Qemu._get_qemu_img_version(qemu_path)
qemu_imgs.append({"path": qemu_path, "version": version})
except OSError:
continue
return qemu_imgs
@staticmethod
@asyncio.coroutine
def get_qemu_version(qemu_path):
"""
Gets the Qemu version.
:param qemu_path: path to Qemu executable.
"""
if sys.platform.startswith("win"):
# Qemu on Windows doesn't return anything with parameter -version
# look for a version number in version.txt file in the same directory instead
version_file = os.path.join(os.path.dirname(qemu_path), "version.txt")
if os.path.isfile(version_file):
try:
with open(version_file, "rb") as file:
version = file.read().decode("utf-8").strip()
match = re.search("[0-9\.]+", version)
if match:
return version
except (UnicodeDecodeError, OSError) as e:
log.warn("could not read {}: {}".format(version_file, e))
return ""
else:
try:
output = yield from subprocess_check_output(qemu_path, "-version")
match = re.search("version\s+([0-9a-z\-\.]+)", output)
if match:
version = match.group(1)
return version
else:
raise QemuError("Could not determine the Qemu version for {}".format(qemu_path))
except subprocess.SubprocessError as e:
raise QemuError("Error while looking for the Qemu version: {}".format(e))
@staticmethod
@asyncio.coroutine
def _get_qemu_img_version(qemu_img_path):
"""
Gets the Qemu-img version.
:param qemu_img_path: path to Qemu-img executable.
"""
try:
output = yield from subprocess_check_output(qemu_img_path, "--version")
match = re.search("version\s+([0-9a-z\-\.]+)", output)
if match:
version = match.group(1)
return version
else:
raise QemuError("Could not determine the Qemu-img version for {}".format(qemu_img_path))
except subprocess.SubprocessError as e:
raise QemuError("Error while looking for the Qemu-img version: {}".format(e))
@staticmethod
def get_legacy_vm_workdir(legacy_vm_id, name):
"""
Returns the name of the legacy working directory name for a VM.
:param legacy_vm_id: legacy VM identifier (integer)
:param: VM name (not used)
:returns: working directory name
"""
return os.path.join("qemu", "vm-{}".format(legacy_vm_id))
def get_images_directory(self):
"""
Return the full path of the images directory on disk
"""
return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "QEMU")
@asyncio.coroutine
def create_disk(self, qemu_img, path, options):
"""
Create a qemu disk with qemu-img
:param qemu_img: qemu-img binary path
:param path: Image path
:param options: Disk image creation options
"""
try:
img_format = options.pop("format")
img_size = options.pop("size")
if not os.path.isabs(path):
directory = self.get_images_directory()
os.makedirs(directory, exist_ok=True)
path = os.path.join(directory, os.path.basename(path))
if os.path.exists(path):
raise QemuError("Could not create disk image {} already exist".format(path))
command = [qemu_img, "create", "-f", img_format]
for option in sorted(options.keys()):
command.extend(["-o", "{}={}".format(option, options[option])])
command.append(path)
command.append("{}M".format(img_size))
process = yield from asyncio.create_subprocess_exec(*command)
yield from process.wait()
except (OSError, subprocess.SubprocessError) as e:
raise QemuError("Could not create disk image {}:{}".format(path, e))
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import asyncio
import struct
class Qcow2Error(Exception):
pass
class Qcow2:
"""
Allow to parse a Qcow2 file
"""
def __init__(self, path):
self.path = path
self._reload()
def _reload(self):
# Each QCOW2 file begins with a header, in big endian format, as follows:
#
# typedef struct QCowHeader {
# uint32_t magic;
# uint32_t version;
#
# uint64_t backing_file_offset;
# uint32_t backing_file_size;
#
# uint32_t cluster_bits;
# uint64_t size; /* in bytes */
# uint32_t crypt_method;
#
# uint32_t l1_size;
# uint64_t l1_table_offset;
#
# uint64_t refcount_table_offset;
# uint32_t refcount_table_clusters;
#
# uint32_t nb_snapshots;
# uint64_t snapshots_offset;
# } QCowHeader;
struct_format = ">IIQi"
with open(self.path, 'rb') as f:
content = f.read(struct.calcsize(struct_format))
self.magic, self.version, self.backing_file_offset, self.backing_file_size = struct.unpack_from(struct_format, content)
if self.magic != 1363560955: # The first 4 bytes contain the characters 'Q', 'F', 'I' followed by 0xfb.
raise Qcow2Error("Invalid magic for {}".format(self.path))
@property
def backing_file(self):
"""
When using linked clone this will return the path to the base image
:returns: None if it's not a linked clone, the path otherwise
"""
with open(self.path, 'rb') as f:
f.seek(self.backing_file_offset)
content = f.read(self.backing_file_size)
path = content.decode()
if len(path) == 0:
return None
return path
@asyncio.coroutine
def rebase(self, qemu_img, base_image):
"""
Rebase a linked clone in order to use the correct disk
:param qemu_img: Path to the qemu-img binary
:param base_image: Path to the base image
"""
if not os.path.exists(base_image):
raise FileNotFoundError(base_image)
command = [qemu_img, "rebase", "-u", "-b", base_image, self.path]
process = yield from asyncio.create_subprocess_exec(*command)
retcode = yield from process.wait()
if retcode != 0:
raise Qcow2Error("Could not rebase the image")
self._reload()
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Custom exceptions for the Qemu module.
"""
from ..vm_error import VMError
class QemuError(VMError):
pass
File diff suppressed because it is too large. Load diff
+199
View File
@@ -0,0 +1,199 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
VirtualBox server module.
"""
import os
import sys
import shutil
import asyncio
import subprocess
import logging
log = logging.getLogger(__name__)
from ..base_manager import BaseManager
from .virtualbox_vm import VirtualBoxVM
from .virtualbox_error import VirtualBoxError
class VirtualBox(BaseManager):
_VM_CLASS = VirtualBoxVM
def __init__(self):
super().__init__()
self._vboxmanage_path = None
self._execute_lock = asyncio.Lock()
@property
def vboxmanage_path(self):
"""
Returns the path to VBoxManage.
:returns: path
"""
return self._vboxmanage_path
def find_vboxmanage(self):
# look for VBoxManage
vboxmanage_path = self.config.get_section_config("VirtualBox").get("vboxmanage_path")
if not vboxmanage_path:
if sys.platform.startswith("win"):
if "VBOX_INSTALL_PATH" in os.environ:
vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
elif "VBOX_MSI_INSTALL_PATH" in os.environ:
vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe")
elif sys.platform.startswith("darwin"):
vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
else:
vboxmanage_path = shutil.which("vboxmanage")
if not vboxmanage_path:
raise VirtualBoxError("Could not find VBoxManage")
if not os.path.isfile(vboxmanage_path):
raise VirtualBoxError("VBoxManage {} is not accessible".format(vboxmanage_path))
if not os.access(vboxmanage_path, os.X_OK):
raise VirtualBoxError("VBoxManage is not executable")
if os.path.basename(vboxmanage_path) not in ["VBoxManage", "VBoxManage.exe", "vboxmanage"]:
raise VirtualBoxError("Invalid VBoxManage executable name {}".format(os.path.basename(vboxmanage_path)))
self._vboxmanage_path = vboxmanage_path
return vboxmanage_path
@asyncio.coroutine
def execute(self, subcommand, args, timeout=60):
# We use a lock prevent parallel execution due to strange errors
# reported by a user and reproduced by us.
# https://github.com/GNS3/gns3-gui/issues/261
with (yield from self._execute_lock):
vboxmanage_path = self.vboxmanage_path
if not vboxmanage_path:
vboxmanage_path = self.find_vboxmanage()
command = [vboxmanage_path, "--nologo", subcommand]
command.extend(args)
command_string = " ".join(command)
log.info("Executing VBoxManage with command: {}".format(command_string))
try:
process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
except (OSError, subprocess.SubprocessError) as e:
raise VirtualBoxError("Could not execute VBoxManage: {}".format(e))
try:
stdout_data, stderr_data = yield from asyncio.wait_for(process.communicate(), timeout=timeout)
except asyncio.TimeoutError:
raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout))
if process.returncode:
vboxmanage_error = stderr_data.decode("utf-8", errors="ignore")
raise VirtualBoxError("VirtualBox has returned an error: {}".format(vboxmanage_error))
return stdout_data.decode("utf-8", errors="ignore").splitlines()
@asyncio.coroutine
def _find_inaccessible_hdd_files(self):
"""
Finds inaccessible disk files (to clean up the VirtualBox media manager)
"""
hdds = []
try:
properties = yield from self.execute("list", ["hdds"])
# If VirtualBox is not available we have no inaccessible hdd
except VirtualBoxError:
return hdds
flag_inaccessible = False
for prop in properties:
try:
name, value = prop.split(':', 1)
except ValueError:
continue
if name.strip() == "State" and value.strip() == "inaccessible":
flag_inaccessible = True
if flag_inaccessible and name.strip() == "Location":
hdds.append(value.strip())
flag_inaccessible = False
return reversed(hdds)
@asyncio.coroutine
def project_closed(self, project):
"""
Called when a project is closed.
:param project: Project instance
"""
yield from super().project_closed(project)
hdd_files_to_close = yield from self._find_inaccessible_hdd_files()
for hdd_file in hdd_files_to_close:
log.info("Closing VirtualBox VM disk file {}".format(os.path.basename(hdd_file)))
try:
yield from self.execute("closemedium", ["disk", hdd_file])
except VirtualBoxError as e:
log.warning("Could not close VirtualBox VM disk file {}: {}".format(os.path.basename(hdd_file), e))
continue
@asyncio.coroutine
def list_images(self):
"""
Gets VirtualBox VM list.
"""
vms = []
result = yield from self.execute("list", ["vms"])
for line in result:
if len(line) == 0 or line[0] != '"' or line[-1:] != "}":
continue # Broken output (perhaps a carriage return in VM name)
vmname, _ = line.rsplit(' ', 1)
vmname = vmname.strip('"')
if vmname == "<inaccessible>":
continue # ignore inaccessible VMs
extra_data = yield from self.execute("getextradata", [vmname, "GNS3/Clone"])
if not extra_data[0].strip() == "Value: yes":
# get the amount of RAM
info_results = yield from self.execute("showvminfo", [vmname, "--machinereadable"])
ram = 0
for info in info_results:
try:
name, value = info.split('=', 1)
if name.strip() == "memory":
ram = int(value.strip())
break
except ValueError:
continue
vms.append({"vmname": vmname, "ram": ram})
return vms
@staticmethod
def get_legacy_vm_workdir(legacy_vm_id, name):
"""
Returns the name of the legacy working directory name for a VM.
:param legacy_vm_id: legacy VM identifier (not used)
:param name: VM name
:returns: working directory name
"""
return os.path.join("vbox", "{}".format(name))
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Custom exceptions for the VirtualBox module.
"""
from ..vm_error import VMError
class VirtualBoxError(VMError):
pass
@@ -0,0 +1,977 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
VirtualBox VM instance.
"""
import sys
import shlex
import re
import os
import tempfile
import json
import socket
import asyncio
from pkg_resources import parse_version
from gns3server.utils.telnet_server import TelnetServer
from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation
from .virtualbox_error import VirtualBoxError
from ..nios.nio_udp import NIOUDP
from ..nios.nio_nat import NIONAT
from ..adapters.ethernet_adapter import EthernetAdapter
from ..base_vm import BaseVM
if sys.platform.startswith('win'):
import msvcrt
import win32file
import logging
log = logging.getLogger(__name__)
class VirtualBoxVM(BaseVM):
"""
VirtualBox VM implementation.
"""
def __init__(self, name, vm_id, project, manager, vmname, linked_clone, console=None, adapters=0):
super().__init__(name, vm_id, project, manager, console=console)
self._maximum_adapters = 8
self._linked_clone = linked_clone
self._system_properties = {}
self._telnet_server_thread = None
self._serial_pipe = None
# VirtualBox settings
self._adapters = adapters
self._ethernet_adapters = {}
self._headless = False
self._acpi_shutdown = False
self._enable_remote_console = False
self._vmname = vmname
self._use_any_adapter = False
self._ram = 0
self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)"
def __json__(self):
json = {"name": self.name,
"vm_id": self.id,
"console": self.console,
"project_id": self.project.id,
"vmname": self.vmname,
"headless": self.headless,
"acpi_shutdown": self.acpi_shutdown,
"enable_remote_console": self.enable_remote_console,
"adapters": self._adapters,
"adapter_type": self.adapter_type,
"ram": self.ram,
"use_any_adapter": self.use_any_adapter}
if self._linked_clone:
json["vm_directory"] = self.working_dir
else:
json["vm_directory"] = None
return json
@asyncio.coroutine
def _get_system_properties(self):
properties = yield from self.manager.execute("list", ["systemproperties"])
for prop in properties:
try:
name, value = prop.split(':', 1)
except ValueError:
continue
self._system_properties[name.strip()] = value.strip()
@asyncio.coroutine
def _get_vm_state(self):
"""
Returns the VM state (e.g. running, paused etc.)
:returns: state (string)
"""
results = yield from self.manager.execute("showvminfo", [self._vmname, "--machinereadable"])
for info in results:
if '=' in info:
name, value = info.split('=', 1)
if name == "VMState":
return value.strip('"')
raise VirtualBoxError("Could not get VM state for {}".format(self._vmname))
@asyncio.coroutine
def _control_vm(self, params):
"""
Change setting in this VM when running.
:param params: params to use with sub-command controlvm
:returns: result of the command.
"""
args = shlex.split(params)
result = yield from self.manager.execute("controlvm", [self._vmname] + args)
return result
@asyncio.coroutine
def _modify_vm(self, params):
"""
Change setting in this VM when not running.
:param params: params to use with sub-command modifyvm
"""
args = shlex.split(params)
yield from self.manager.execute("modifyvm", [self._vmname] + args)
@asyncio.coroutine
def create(self):
yield from self._get_system_properties()
if "API version" not in self._system_properties:
raise VirtualBoxError("Can't access to VirtualBox API version:\n{}".format(self._system_properties))
if parse_version(self._system_properties["API version"]) < parse_version("4_3"):
raise VirtualBoxError("The VirtualBox API version is lower than 4.3")
log.info("VirtualBox VM '{name}' [{id}] created".format(name=self.name, id=self.id))
if self._linked_clone:
if self.id and os.path.isdir(os.path.join(self.working_dir, self._vmname)):
vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox")
yield from self.manager.execute("registervm", [vbox_file])
yield from self._reattach_hdds()
else:
yield from self._create_linked_clone()
if self._adapters:
yield from self.set_adapters(self._adapters)
vm_info = yield from self._get_vm_info()
if "memory" in vm_info:
self._ram = int(vm_info["memory"])
@asyncio.coroutine
def check_hw_virtualization(self):
"""
Returns either hardware virtualization is activated or not.
:returns: boolean
"""
vm_info = yield from self._get_vm_info()
if "hwvirtex" in vm_info and vm_info["hwvirtex"] == "on":
return True
return False
@asyncio.coroutine
def start(self):
"""
Starts this VirtualBox VM.
"""
# resume the VM if it is paused
vm_state = yield from self._get_vm_state()
if vm_state == "paused":
yield from self.resume()
return
# VM must be powered off to start it
if vm_state != "poweroff":
raise VirtualBoxError("VirtualBox VM not powered off")
yield from self._set_network_options()
yield from self._set_serial_console()
# check if there is enough RAM to run
self.check_available_ram(self.ram)
args = [self._vmname]
if self._headless:
args.extend(["--type", "headless"])
result = yield from self.manager.execute("startvm", args)
log.info("VirtualBox VM '{name}' [{id}] started".format(name=self.name, id=self.id))
log.debug("Start result: {}".format(result))
# add a guest property to let the VM know about the GNS3 name
yield from self.manager.execute("guestproperty", ["set", self._vmname, "NameInGNS3", self.name])
# add a guest property to let the VM know about the GNS3 project directory
yield from self.manager.execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self.working_dir])
if self._enable_remote_console and self._console is not None:
try:
# wait for VirtualBox to create the pipe file.
if sys.platform.startswith("win"):
yield from wait_for_named_pipe_creation(self._get_pipe_name())
else:
yield from wait_for_file_creation(self._get_pipe_name())
except asyncio.TimeoutError:
raise VirtualBoxError('Pipe file "{}" for remote console has not been created by VirtualBox'.format(self._get_pipe_name()))
self._start_remote_console()
if (yield from self.check_hw_virtualization()):
self._hw_virtualization = True
@asyncio.coroutine
def stop(self):
"""
Stops this VirtualBox VM.
"""
self._hw_virtualization = False
self._stop_remote_console()
vm_state = yield from self._get_vm_state()
if vm_state == "running" or vm_state == "paused" or vm_state == "stuck":
if self.acpi_shutdown:
# use ACPI to shutdown the VM
result = yield from self._control_vm("acpipowerbutton")
log.debug("ACPI shutdown result: {}".format(result))
else:
# power off the VM
result = yield from self._control_vm("poweroff")
log.debug("Stop result: {}".format(result))
log.info("VirtualBox VM '{name}' [{id}] stopped".format(name=self.name, id=self.id))
# yield from asyncio.sleep(0.5) # give some time for VirtualBox to unlock the VM
try:
# deactivate the first serial port
yield from self._modify_vm("--uart1 off")
except VirtualBoxError as e:
log.warn("Could not deactivate the first serial port: {}".format(e))
for adapter_number in range(0, self._adapters):
nio = self._ethernet_adapters[adapter_number].get_nio(0)
if nio:
yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1))
yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1))
yield from self._modify_vm("--nic{} null".format(adapter_number + 1))
@asyncio.coroutine
def suspend(self):
"""
Suspends this VirtualBox VM.
"""
vm_state = yield from self._get_vm_state()
if vm_state == "running":
yield from self._control_vm("pause")
log.info("VirtualBox VM '{name}' [{id}] suspended".format(name=self.name, id=self.id))
else:
log.warn("VirtualBox VM '{name}' [{id}] cannot be suspended, current state: {state}".format(name=self.name,
id=self.id,
state=vm_state))
@asyncio.coroutine
def resume(self):
"""
Resumes this VirtualBox VM.
"""
yield from self._control_vm("resume")
log.info("VirtualBox VM '{name}' [{id}] resumed".format(name=self.name, id=self.id))
@asyncio.coroutine
def reload(self):
"""
Reloads this VirtualBox VM.
"""
result = yield from self._control_vm("reset")
log.info("VirtualBox VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id))
log.debug("Reload result: {}".format(result))
@asyncio.coroutine
def _get_all_hdd_files(self):
hdds = []
properties = yield from self.manager.execute("list", ["hdds"])
for prop in properties:
try:
name, value = prop.split(':', 1)
except ValueError:
continue
if name.strip() == "Location":
hdds.append(value.strip())
return hdds
@asyncio.coroutine
def _reattach_hdds(self):
hdd_info_file = os.path.join(self.working_dir, self._vmname, "hdd_info.json")
try:
with open(hdd_info_file, "r", encoding="utf-8") as f:
hdd_table = json.load(f)
except (ValueError, OSError) as e:
raise VirtualBoxError("Could not read HDD info file: {}".format(e))
for hdd_info in hdd_table:
hdd_file = os.path.join(self.working_dir, self._vmname, "Snapshots", hdd_info["hdd"])
if os.path.exists(hdd_file):
log.info("VirtualBox VM '{name}' [{id}] attaching HDD {controller} {port} {device} {medium}".format(name=self.name,
id=self.id,
controller=hdd_info["controller"],
port=hdd_info["port"],
device=hdd_info["device"],
medium=hdd_file))
yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"],
hdd_info["port"],
hdd_info["device"],
hdd_file))
@asyncio.coroutine
def close(self):
"""
Closes this VirtualBox VM.
"""
if not (yield from super().close()):
return False
for adapter in self._ethernet_adapters.values():
if adapter is not None:
for nio in adapter.ports.values():
if nio and isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
self.acpi_shutdown = False
yield from self.stop()
if self._linked_clone:
hdd_table = []
if os.path.exists(self.working_dir):
hdd_files = yield from self._get_all_hdd_files()
vm_info = yield from self._get_vm_info()
for entry, value in vm_info.items():
match = re.search("^([\s\w]+)\-(\d)\-(\d)$", entry)
if match:
controller = match.group(1)
port = match.group(2)
device = match.group(3)
if value in hdd_files:
log.info("VirtualBox VM '{name}' [{id}] detaching HDD {controller} {port} {device}".format(name=self.name,
id=self.id,
controller=controller,
port=port,
device=device))
yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller,
port,
device))
hdd_table.append(
{
"hdd": os.path.basename(value),
"controller": controller,
"port": port,
"device": device,
}
)
log.info("VirtualBox VM '{name}' [{id}] unregistering".format(name=self.name, id=self.id))
yield from self.manager.execute("unregistervm", [self._name])
if hdd_table:
try:
hdd_info_file = os.path.join(self.working_dir, self._vmname, "hdd_info.json")
with open(hdd_info_file, "w", encoding="utf-8") as f:
json.dump(hdd_table, f, indent=4)
except OSError as e:
log.warning("VirtualBox VM '{name}' [{id}] could not write HHD info file: {error}".format(name=self.name,
id=self.id,
error=e.strerror))
@property
def headless(self):
"""
Returns either the VM will start in headless mode
:returns: boolean
"""
return self._headless
@headless.setter
def headless(self, headless):
"""
Sets either the VM will start in headless mode
:param headless: boolean
"""
if headless:
log.info("VirtualBox VM '{name}' [{id}] has enabled the headless mode".format(name=self.name, id=self.id))
else:
log.info("VirtualBox VM '{name}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id))
self._headless = headless
@property
def acpi_shutdown(self):
"""
Returns either the VM will use ACPI shutdown
:returns: boolean
"""
return self._acpi_shutdown
@acpi_shutdown.setter
def acpi_shutdown(self, acpi_shutdown):
"""
Sets either the VM will use ACPI shutdown
:param acpi_shutdown: boolean
"""
if acpi_shutdown:
log.info("VirtualBox VM '{name}' [{id}] has enabled the ACPI shutdown mode".format(name=self.name, id=self.id))
else:
log.info("VirtualBox VM '{name}' [{id}] has disabled the ACPI shutdown mode".format(name=self.name, id=self.id))
self._acpi_shutdown = acpi_shutdown
@property
def enable_remote_console(self):
"""
Returns either the remote console is enabled or not
:returns: boolean
"""
return self._enable_remote_console
@asyncio.coroutine
def set_enable_remote_console(self, enable_remote_console):
"""
Sets either the console is enabled or not
:param enable_remote_console: boolean
"""
if enable_remote_console:
log.info("VirtualBox VM '{name}' [{id}] has enabled the console".format(name=self.name, id=self.id))
vm_state = yield from self._get_vm_state()
if vm_state == "running":
self._start_remote_console()
else:
log.info("VirtualBox VM '{name}' [{id}] has disabled the console".format(name=self.name, id=self.id))
self._stop_remote_console()
self._enable_remote_console = enable_remote_console
@property
def ram(self):
"""
Returns the amount of RAM allocated to this VirtualBox VM.
:returns: amount RAM in MB (integer)
"""
return self._ram
@asyncio.coroutine
def set_ram(self, ram):
"""
Set the amount of RAM allocated to this VirtualBox VM.
:param ram: amount RAM in MB (integer)
"""
if ram == 0:
return
yield from self._modify_vm('--memory {}'.format(ram))
log.info("VirtualBox VM '{name}' [{id}] has set amount of RAM to {ram}".format(name=self.name, id=self.id, ram=ram))
self._ram = ram
@property
def vmname(self):
"""
Returns the VirtualBox VM name.
:returns: VirtualBox VM name
"""
return self._vmname
@asyncio.coroutine
def set_vmname(self, vmname):
"""
Renames the VirtualBox VM.
:param vmname: VirtualBox VM name
"""
if self._linked_clone:
yield from self._modify_vm('--name "{}"'.format(vmname))
log.info("VirtualBox VM '{name}' [{id}] has set the VM name to '{vmname}'".format(name=self.name, id=self.id, vmname=vmname))
self._vmname = vmname
@property
def adapters(self):
"""
Returns the number of adapters configured for this VirtualBox VM.
:returns: number of adapters
"""
return self._adapters
@asyncio.coroutine
def set_adapters(self, adapters):
"""
Sets the number of Ethernet adapters for this VirtualBox VM instance.
:param adapters: number of adapters
"""
# check for the maximum adapters supported by the VM
self._maximum_adapters = yield from self._get_maximum_supported_adapters()
if adapters > self._maximum_adapters:
raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters))
self._ethernet_adapters.clear()
for adapter_number in range(0, adapters):
self._ethernet_adapters[adapter_number] = EthernetAdapter()
self._adapters = len(self._ethernet_adapters)
log.info("VirtualBox VM '{name}' [{id}] has changed the number of Ethernet adapters to {adapters}".format(name=self.name,
id=self.id,
adapters=adapters))
@property
def use_any_adapter(self):
"""
Returns either GNS3 can use any VirtualBox adapter on this instance.
:returns: boolean
"""
return self._use_any_adapter
@use_any_adapter.setter
def use_any_adapter(self, use_any_adapter):
"""
Allows GNS3 to use any VirtualBox adapter on this instance.
:param use_any_adapter: boolean
"""
if use_any_adapter:
log.info("VirtualBox VM '{name}' [{id}] is allowed to use any adapter".format(name=self.name, id=self.id))
else:
log.info("VirtualBox VM '{name}' [{id}] is not allowed to use any adapter".format(name=self.name, id=self.id))
self._use_any_adapter = use_any_adapter
@property
def adapter_type(self):
"""
Returns the adapter type for this VirtualBox VM instance.
:returns: adapter type (string)
"""
return self._adapter_type
@adapter_type.setter
def adapter_type(self, adapter_type):
"""
Sets the adapter type for this VirtualBox VM instance.
:param adapter_type: adapter type (string)
"""
self._adapter_type = adapter_type
log.info("VirtualBox VM '{name}' [{id}]: adapter type changed to {adapter_type}".format(name=self.name,
id=self.id,
adapter_type=adapter_type))
@asyncio.coroutine
def _get_vm_info(self):
"""
Returns this VM info.
:returns: dict of info
"""
vm_info = {}
results = yield from self.manager.execute("showvminfo", [self._vmname, "--machinereadable"])
for info in results:
try:
name, value = info.split('=', 1)
except ValueError:
continue
vm_info[name.strip('"')] = value.strip('"')
return vm_info
@asyncio.coroutine
def _get_maximum_supported_adapters(self):
"""
Returns the maximum adapters supported by this VM.
:returns: maximum number of supported adapters (int)
"""
# check the maximum number of adapters supported by the VM
vm_info = yield from self._get_vm_info()
maximum_adapters = 8
if "chipset" in vm_info:
chipset = vm_info["chipset"]
if chipset == "ich9":
maximum_adapters = int(self._system_properties["Maximum ICH9 Network Adapter count"])
return maximum_adapters
def _get_pipe_name(self):
"""
Returns the pipe name to create a serial connection.
:returns: pipe path (string)
"""
if sys.platform.startswith("win"):
pipe_name = r"\\.\pipe\gns3_vbox\{}".format(self.id)
else:
pipe_name = os.path.join(tempfile.gettempdir(), "gns3_vbox", "{}".format(self.id))
try:
os.makedirs(os.path.dirname(pipe_name), exist_ok=True)
except OSError as e:
raise VirtualBoxError("Could not create the VirtualBox pipe directory: {}".format(e))
return pipe_name
@asyncio.coroutine
def _set_serial_console(self):
"""
Configures the first serial port to allow a serial console connection.
"""
# activate the first serial port
yield from self._modify_vm("--uart1 0x3F8 4")
# set server mode with a pipe on the first serial port
pipe_name = self._get_pipe_name()
args = [self._vmname, "--uartmode1", "server", pipe_name]
yield from self.manager.execute("modifyvm", args)
@asyncio.coroutine
def _storage_attach(self, params):
"""
Change storage medium in this VM.
:param params: params to use with sub-command storageattach
"""
args = shlex.split(params)
yield from self.manager.execute("storageattach", [self._vmname] + args)
@asyncio.coroutine
def _get_nic_attachements(self, maximum_adapters):
"""
Returns NIC attachements.
:param maximum_adapters: maximum number of supported adapters
:returns: list of adapters with their Attachment setting (NAT, bridged etc.)
"""
nics = []
vm_info = yield from self._get_vm_info()
for adapter_number in range(0, maximum_adapters):
entry = "nic{}".format(adapter_number + 1)
if entry in vm_info:
value = vm_info[entry]
nics.append(value.lower())
else:
nics.append(None)
return nics
@asyncio.coroutine
def _set_network_options(self):
"""
Configures network options.
"""
nic_attachments = yield from self._get_nic_attachements(self._maximum_adapters)
for adapter_number in range(0, self._adapters):
attachment = nic_attachments[adapter_number]
if attachment == "null":
# disconnect the cable if no backend is attached.
yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1))
if attachment == "none":
# set the backend to null to avoid a difference in the number of interfaces in the Guest.
yield from self._modify_vm("--nic{} null".format(adapter_number + 1))
yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1))
nio = self._ethernet_adapters[adapter_number].get_nio(0)
if nio:
if not isinstance(nio, NIONAT) and not self._use_any_adapter and attachment not in ("none", "null", "generic"):
raise VirtualBoxError("Attachment ({}) already configured on adapter {}. "
"Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment,
adapter_number + 1))
yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1))
vbox_adapter_type = "82540EM"
if self._adapter_type == "PCnet-PCI II (Am79C970A)":
vbox_adapter_type = "Am79C970A"
if self._adapter_type == "PCNet-FAST III (Am79C973)":
vbox_adapter_type = "Am79C973"
if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)":
vbox_adapter_type = "82540EM"
if self._adapter_type == "Intel PRO/1000 T Server (82543GC)":
vbox_adapter_type = "82543GC"
if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)":
vbox_adapter_type = "82545EM"
if self._adapter_type == "Paravirtualized Network (virtio-net)":
vbox_adapter_type = "virtio"
args = [self._vmname, "--nictype{}".format(adapter_number + 1), vbox_adapter_type]
yield from self.manager.execute("modifyvm", args)
if isinstance(nio, NIOUDP):
log.debug("setting UDP params on adapter {}".format(adapter_number))
yield from self._modify_vm("--nic{} generic".format(adapter_number + 1))
yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1))
yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
elif isinstance(nio, NIONAT):
yield from self._modify_vm("--nic{} nat".format(adapter_number + 1))
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
if nio.capturing:
yield from self._modify_vm("--nictrace{} on".format(adapter_number + 1))
yield from self._modify_vm('--nictracefile{} "{}"'.format(adapter_number + 1, nio.pcap_output_file))
for adapter_number in range(self._adapters, self._maximum_adapters):
log.debug("disabling remaining adapter {}".format(adapter_number))
yield from self._modify_vm("--nic{} none".format(adapter_number + 1))
@asyncio.coroutine
def _create_linked_clone(self):
"""
Creates a new linked clone.
"""
gns3_snapshot_exists = False
vm_info = yield from self._get_vm_info()
for entry, value in vm_info.items():
if entry.startswith("SnapshotName") and value == "GNS3 Linked Base for clones":
gns3_snapshot_exists = True
if not gns3_snapshot_exists:
result = yield from self.manager.execute("snapshot", [self._vmname, "take", "GNS3 Linked Base for clones"])
log.debug("GNS3 snapshot created: {}".format(result))
args = [self._vmname,
"--snapshot",
"GNS3 Linked Base for clones",
"--options",
"link",
"--name",
self.name,
"--basefolder",
self.working_dir,
"--register"]
result = yield from self.manager.execute("clonevm", args)
log.debug("VirtualBox VM: {} cloned".format(result))
self._vmname = self._name
yield from self.manager.execute("setextradata", [self._vmname, "GNS3/Clone", "yes"])
args = [self._vmname, "take", "reset"]
result = yield from self.manager.execute("snapshot", args)
log.debug("Snapshot 'reset' created: {}".format(result))
def _start_remote_console(self):
"""
Starts remote console support for this VM.
"""
# starts the Telnet to pipe thread
pipe_name = self._get_pipe_name()
if sys.platform.startswith("win"):
try:
self._serial_pipe = open(pipe_name, "a+b")
except OSError as e:
raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e))
try:
self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._manager.port_manager.console_host, self._console)
except OSError as e:
raise VirtualBoxError("Unable to create Telnet server: {}".format(e))
self._telnet_server_thread.start()
else:
try:
self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._serial_pipe.connect(pipe_name)
except OSError as e:
raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e))
try:
self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._manager.port_manager.console_host, self._console)
except OSError as e:
raise VirtualBoxError("Unable to create Telnet server: {}".format(e))
self._telnet_server_thread.start()
def _stop_remote_console(self):
"""
Stops remote console support for this VM.
"""
if self._telnet_server_thread:
if self._telnet_server_thread.is_alive():
self._telnet_server_thread.stop()
self._telnet_server_thread.join(timeout=3)
if self._telnet_server_thread.is_alive():
log.warn("Serial pipe thread is still alive!")
self._telnet_server_thread = None
if self._serial_pipe:
if sys.platform.startswith("win"):
win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno()))
else:
self._serial_pipe.close()
self._serial_pipe = None
@asyncio.coroutine
def adapter_add_nio_binding(self, adapter_number, nio):
"""
Adds an adapter NIO binding.
:param adapter_number: adapter number
:param nio: NIO instance to add to the slot/port
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
vm_state = yield from self._get_vm_state()
if vm_state == "running":
if isinstance(nio, NIOUDP):
# dynamically configure an UDP tunnel on the VirtualBox adapter
yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_number + 1))
yield from self._control_vm("nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
yield from self._control_vm("nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
yield from self._control_vm("nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
# check if the UDP tunnel has been correctly set
vm_info = yield from self._get_vm_info()
generic_driver_number = "generic{}".format(adapter_number + 1)
if generic_driver_number not in vm_info and vm_info[generic_driver_number] != "UDPTunnel":
log.warning("UDP tunnel has not been set on nic: {}".format(adapter_number + 1))
self.project.emit("log.warning", {"message": "UDP tunnel has not been set on nic: {}".format(adapter_number + 1)})
elif isinstance(nio, NIONAT):
yield from self._control_vm("nic{} nat".format(adapter_number + 1))
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
adapter.add_nio(0, nio)
log.info("VirtualBox VM '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name,
id=self.id,
nio=nio,
adapter_number=adapter_number))
@asyncio.coroutine
def adapter_remove_nio_binding(self, adapter_number):
"""
Removes an adapter NIO binding.
:param adapter_number: adapter number
:returns: NIO instance
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
vm_state = yield from self._get_vm_state()
if vm_state == "running":
# dynamically disable the VirtualBox adapter
yield from self._control_vm("setlinkstate{} off".format(adapter_number + 1))
yield from self._control_vm("nic{} null".format(adapter_number + 1))
nio = adapter.get_nio(0)
if isinstance(nio, NIOUDP):
self.manager.port_manager.release_udp_port(nio.lport, self._project)
adapter.remove_nio(0)
log.info("VirtualBox VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(name=self.name,
id=self.id,
nio=nio,
adapter_number=adapter_number))
return nio
@asyncio.coroutine
def start_capture(self, adapter_number, output_file):
"""
Starts a packet capture.
:param adapter_number: adapter number
:param output_file: PCAP destination file for the capture
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
vm_state = yield from self._get_vm_state()
if vm_state == "running" or vm_state == "paused" or vm_state == "stuck":
raise VirtualBoxError("Sorry, packet capturing on a started VirtualBox VM is not supported.")
nio = adapter.get_nio(0)
if not nio:
raise VirtualBoxError("Adapter {} is not connected".format(adapter_number))
if nio.capturing:
raise VirtualBoxError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number))
nio.startPacketCapture(output_file)
log.info("VirtualBox VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(name=self.name,
id=self.id,
adapter_number=adapter_number))
def stop_capture(self, adapter_number):
"""
Stops a packet capture.
:param adapter_number: adapter number
"""
try:
adapter = self._ethernet_adapters[adapter_number]
except KeyError:
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
adapter_number=adapter_number))
nio = adapter.get_nio(0)
if not nio:
raise VirtualBoxError("Adapter {} is not connected".format(adapter_number))
nio.stopPacketCapture()
log.info("VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name,
id=self.id,
adapter_number=adapter_number))
+35
View File
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class VMError(Exception):
def __init__(self, message, original_exception=None):
super().__init__(message)
if isinstance(message, Exception):
message = str(message)
self._message = message
self._original_exception = original_exception
def __repr__(self):
return self._message
def __str__(self):
return self._message
+670
View File
@@ -0,0 +1,670 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
VMware player/workstation server module.
"""
import os
import sys
import re
import shutil
import asyncio
import subprocess
import logging
import codecs
from collections import OrderedDict
from gns3server.utils.interfaces import interfaces
from gns3server.utils.asyncio import subprocess_check_output
from pkg_resources import parse_version
log = logging.getLogger(__name__)
from gns3server.compute.base_manager import BaseManager
from gns3server.compute.vmware.vmware_vm import VMwareVM
from gns3server.compute.vmware.vmware_error import VMwareError
from gns3server.compute.vmware.nio_vmnet import NIOVMNET
class VMware(BaseManager):
_VM_CLASS = VMwareVM
def __init__(self):
super().__init__()
self._execute_lock = asyncio.Lock()
self._vmware_inventory_lock = asyncio.Lock()
self._vmrun_path = None
self._vmnets = []
self._vmnet_start_range = 2
if sys.platform.startswith("win"):
self._vmnet_end_range = 19
else:
self._vmnet_end_range = 255
@property
def vmrun_path(self):
"""
Returns the path vmrun utility.
:returns: path
"""
return self._vmrun_path
@staticmethod
def _find_vmrun_registry(regkey):
import winreg
try:
# default path not used, let's look in the registry
hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, regkey)
install_path, _ = winreg.QueryValueEx(hkey, "InstallPath")
vmrun_path = os.path.join(install_path, "vmrun.exe")
winreg.CloseKey(hkey)
if os.path.exists(vmrun_path):
return vmrun_path
except OSError:
pass
return None
def find_vmrun(self):
"""
Searches for vmrun.
:returns: path to vmrun
"""
# look for vmrun
vmrun_path = self.config.get_section_config("VMware").get("vmrun_path")
if not vmrun_path:
if sys.platform.startswith("win"):
vmrun_path = shutil.which("vmrun")
if vmrun_path is None:
# look for vmrun.exe using the VMware Workstation directory listed in the registry
vmrun_path = self._find_vmrun_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation")
if vmrun_path is None:
# look for vmrun.exe using the VIX directory listed in the registry
vmrun_path = self._find_vmrun_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware VIX")
elif sys.platform.startswith("darwin"):
vmrun_path = "/Applications/VMware Fusion.app/Contents/Library/vmrun"
else:
vmrun_path = shutil.which("vmrun")
if not vmrun_path:
raise VMwareError("Could not find vmrun")
if not os.path.isfile(vmrun_path):
raise VMwareError("vmrun {} is not accessible".format(vmrun_path))
if not os.access(vmrun_path, os.X_OK):
raise VMwareError("vmrun is not executable")
if os.path.basename(vmrun_path) not in ["vmrun", "vmrun.exe"]:
raise VMwareError("Invalid vmrun executable name {}".format(os.path.basename(vmrun_path)))
self._vmrun_path = vmrun_path
return vmrun_path
@staticmethod
def _find_vmware_version_registry(regkey):
import winreg
version = None
try:
# default path not used, let's look in the registry
hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, regkey)
version, _ = winreg.QueryValueEx(hkey, "ProductVersion")
winreg.CloseKey(hkey)
except OSError:
pass
if version is not None:
match = re.search("([0-9]+)\.", version)
if match:
version = match.group(1)
return version
@asyncio.coroutine
def check_vmware_version(self):
"""
Check VMware version
"""
if sys.platform.startswith("win"):
# look for vmrun.exe using the directory listed in the registry
ws_version = self._find_vmware_version_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation")
if ws_version is None:
player_version = self._find_vmware_version_registry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Player")
if player_version:
log.debug("VMware Player version {} detected".format(player_version))
if int(player_version) < 6:
raise VMwareError("Using VMware Player requires version 6 or above")
else:
log.warning("Could not find VMware version")
else:
log.debug("VMware Workstation version {} detected".format(ws_version))
if int(ws_version) < 10:
raise VMwareError("Using VMware Workstation requires version 10 or above")
return
else:
if sys.platform.startswith("darwin"):
if not os.path.isdir("/Applications/VMware Fusion.app"):
raise VMwareError("VMware Fusion is not installed in the standard location /Applications/VMware Fusion.app")
return # FIXME: no version checking on Mac OS X but we support all versions of fusion
vmware_path = VMware._get_linux_vmware_binary()
if vmware_path is None:
raise VMwareError("VMware is not installed (vmware or vmplayer executable could not be found in $PATH)")
try:
output = yield from subprocess_check_output(vmware_path, "-v")
match = re.search("VMware Workstation ([0-9]+)\.", output)
version = None
if match:
version = match.group(1)
log.debug("VMware Workstation version {} detected".format(version))
if int(version) < 10:
raise VMwareError("Using VMware Workstation requires version 10 or above")
match = re.search("VMware Player ([0-9]+)\.", output)
if match:
version = match.group(1)
log.debug("VMware Player version {} detected".format(version))
if int(version) < 6:
raise VMwareError("Using VMware Player requires version 6 or above")
if version is None:
log.warning("Could not find VMware version. Output of VMware: {}".format(output))
raise VMwareError("Could not find VMware version. Output of VMware: {}".format(output))
except (OSError, subprocess.SubprocessError) as e:
log.error("Error while looking for the VMware version: {}".format(e))
raise VMwareError("Error while looking for the VMware version: {}".format(e))
@staticmethod
def _get_vmnet_interfaces_registry():
import winreg
vmnet_interfaces = []
regkey = r"SOFTWARE\Wow6432Node\VMware, Inc.\VMnetLib\VMnetConfig"
try:
hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, regkey)
for index in range(winreg.QueryInfoKey(hkey)[0]):
vmnet = winreg.EnumKey(hkey, index)
hkeyvmnet = winreg.OpenKey(hkey, vmnet)
if winreg.QueryInfoKey(hkeyvmnet)[1]:
# the vmnet has not been configure if the key has no values
vmnet = vmnet.replace("vm", "VM")
if vmnet not in ("VMnet1", "VMnet8"):
vmnet_interfaces.append(vmnet)
winreg.CloseKey(hkeyvmnet)
winreg.CloseKey(hkey)
except OSError as e:
raise VMwareError("Could not read registry key {}: {}".format(regkey, e))
return vmnet_interfaces
@staticmethod
def _get_vmnet_interfaces():
if sys.platform.startswith("win"):
return VMware._get_vmnet_interfaces_registry()
elif sys.platform.startswith("darwin"):
vmware_networking_file = "/Library/Preferences/VMware Fusion/networking"
else:
# location on Linux
vmware_networking_file = "/etc/vmware/networking"
vmnet_interfaces = []
try:
with open(vmware_networking_file, "r", encoding="utf-8") as f:
for line in f.read().splitlines():
match = re.search("VNET_([0-9]+)_VIRTUAL_ADAPTER", line)
if match:
vmnet = "vmnet{}".format(match.group(1))
if vmnet not in ("vmnet1", "vmnet8"):
vmnet_interfaces.append(vmnet)
except OSError as e:
raise VMwareError("Cannot open {}: {}".format(vmware_networking_file, e))
return vmnet_interfaces
@staticmethod
def _get_vmnet_interfaces_ubridge():
vmnet_interfaces = []
for interface in interfaces():
if sys.platform.startswith("win"):
if "netcard" in interface:
windows_name = interface["netcard"]
else:
windows_name = interface["name"]
match = re.search("(VMnet[0-9]+)", windows_name)
if match:
vmnet = match.group(1)
if vmnet not in ("VMnet1", "VMnet8"):
vmnet_interfaces.append(vmnet)
elif interface["name"].startswith("vmnet"):
vmnet = interface["name"]
if vmnet not in ("vmnet1", "vmnet8"):
vmnet_interfaces.append(interface["name"])
return vmnet_interfaces
def is_managed_vmnet(self, vmnet):
self._vmnet_start_range = self.config.get_section_config("VMware").getint("vmnet_start_range", self._vmnet_start_range)
self._vmnet_end_range = self.config.get_section_config("VMware").getint("vmnet_end_range", self._vmnet_end_range)
match = re.search("vmnet([0-9]+)$", vmnet, re.IGNORECASE)
if match:
vmnet_number = match.group(1)
if self._vmnet_start_range <= int(vmnet_number) <= self._vmnet_end_range:
return True
return False
def allocate_vmnet(self):
if not self._vmnets:
raise VMwareError("No VMnet interface available between vmnet{} and vmnet{}. Go to preferences VMware / Network / Configure to add more interfaces.".format(self._vmnet_start_range, self._vmnet_end_range))
return self._vmnets.pop(0)
def refresh_vmnet_list(self, ubridge=True):
if ubridge:
# VMnet host adapters must be present when uBridge is used
vmnet_interfaces = self._get_vmnet_interfaces_ubridge()
else:
vmnet_interfaces = self._get_vmnet_interfaces()
# remove vmnets already in use
for vm in self._vms.values():
for used_vmnet in vm.vmnets:
if used_vmnet in vmnet_interfaces:
log.debug("{} is already in use".format(used_vmnet))
vmnet_interfaces.remove(used_vmnet)
# remove vmnets that are not managed
for vmnet in vmnet_interfaces.copy():
if vmnet in vmnet_interfaces and self.is_managed_vmnet(vmnet) is False:
vmnet_interfaces.remove(vmnet)
self._vmnets = vmnet_interfaces
def create_nio(self, executable, nio_settings):
if nio_settings["type"] == "nio_vmnet":
return NIOVMNET(nio_settings["vmnet"])
else:
return super().create_nio(None, nio_settings)
@property
def host_type(self):
"""
Returns the VMware host type.
player = VMware player
ws = VMware Workstation
fusion = VMware Fusion
:returns: host type (string)
"""
if sys.platform.startswith("darwin"):
host_type = "fusion"
else:
host_type = self.config.get_section_config("VMware").get("host_type", "ws")
return host_type
@asyncio.coroutine
def execute(self, subcommand, args, timeout=120, host_type=None):
with (yield from self._execute_lock):
vmrun_path = self.vmrun_path
if not vmrun_path:
vmrun_path = self.find_vmrun()
if host_type is None:
host_type = self.host_type
command = [vmrun_path, "-T", host_type, subcommand]
command.extend(args)
command_string = " ".join(command)
log.info("Executing vmrun with command: {}".format(command_string))
try:
process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
except (OSError, subprocess.SubprocessError) as e:
raise VMwareError("Could not execute vmrun: {}".format(e))
try:
stdout_data, _ = yield from asyncio.wait_for(process.communicate(), timeout=timeout)
except asyncio.TimeoutError:
raise VMwareError("vmrun has timed out after {} seconds!".format(timeout))
if process.returncode:
# vmrun print errors on stdout
vmrun_error = stdout_data.decode("utf-8", errors="ignore")
raise VMwareError("vmrun has returned an error: {}".format(vmrun_error))
return stdout_data.decode("utf-8", errors="ignore").splitlines()
@asyncio.coroutine
def check_vmrun_version(self):
with (yield from self._execute_lock):
vmrun_path = self.vmrun_path
if not vmrun_path:
vmrun_path = self.find_vmrun()
try:
output = yield from subprocess_check_output(vmrun_path)
match = re.search("vmrun version ([0-9\.]+)", output)
version = None
if match:
version = match.group(1)
log.debug("VMware vmrun version {} detected".format(version))
if parse_version(version) < parse_version("1.13"):
# VMware VIX library version must be at least >= 1.13
raise VMwareError("VMware vmrun executable version must be >= version 1.13")
if version is None:
log.warning("Could not find VMware vmrun version. Output: {}".format(output))
raise VMwareError("Could not find VMware vmrun version. Output: {}".format(output))
except (OSError, subprocess.SubprocessError) as e:
log.error("Error while looking for the VMware vmrun version: {}".format(e))
raise VMwareError("Error while looking for the VMware vmrun version: {}".format(e))
@asyncio.coroutine
def remove_from_vmware_inventory(self, vmx_path):
"""
Removes a linked clone from the VMware inventory file.
:param vmx_path: path of the linked clone VMX file
"""
with (yield from self._vmware_inventory_lock):
inventory_path = self.get_vmware_inventory_path()
if os.path.exists(inventory_path):
try:
inventory_pairs = self.parse_vmware_file(inventory_path)
except OSError as e:
log.warning('Could not read VMware inventory file "{}": {}'.format(inventory_path, e))
return
vmlist_entry = None
for name, value in inventory_pairs.items():
if value == vmx_path:
vmlist_entry = name.split(".", 1)[0]
break
if vmlist_entry is not None:
for name in inventory_pairs.copy().keys():
if name.startswith(vmlist_entry):
del inventory_pairs[name]
try:
self.write_vmware_file(inventory_path, inventory_pairs)
except OSError as e:
raise VMwareError('Could not write VMware inventory file "{}": {}'.format(inventory_path, e))
@staticmethod
def parse_vmware_file(path):
"""
Parses a VMware file (VMX, preferences or inventory).
:param path: path to the VMware file
:returns: dict
"""
pairs = OrderedDict()
encoding = "utf-8"
# get the first line to read the .encoding value
with open(path, "rb") as f:
line = f.readline().decode(encoding, errors="ignore")
if line.startswith("#!"):
# skip the shebang
line = f.readline().decode(encoding, errors="ignore")
try:
key, value = line.split('=', 1)
if key.strip().lower() == ".encoding":
file_encoding = value.strip('" ')
try:
codecs.lookup(file_encoding)
encoding = file_encoding
except LookupError:
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
except ValueError:
log.warning("Couldn't find file encoding in {}, using {}...".format(path, encoding))
# read the file with the correct encoding
with open(path, encoding=encoding, errors="ignore") as f:
for line in f.read().splitlines():
try:
key, value = line.split('=', 1)
pairs[key.strip().lower()] = value.strip('" ')
except ValueError:
continue
return pairs
@staticmethod
def write_vmware_file(path, pairs):
"""
Write a VMware file (excepting VMX file).
:param path: path to the VMware file
:param pairs: settings to write
"""
encoding = "utf-8"
if ".encoding" in pairs:
file_encoding = pairs[".encoding"]
try:
codecs.lookup(file_encoding)
encoding = file_encoding
except LookupError:
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
with open(path, "w", encoding=encoding, errors="ignore") as f:
for key, value in pairs.items():
entry = '{} = "{}"\n'.format(key, value)
f.write(entry)
@staticmethod
def write_vmx_file(path, pairs):
"""
Write a VMware VMX file.
:param path: path to the VMX file
:param pairs: settings to write
"""
encoding = "utf-8"
if ".encoding" in pairs:
file_encoding = pairs[".encoding"]
try:
codecs.lookup(file_encoding)
encoding = file_encoding
except LookupError:
log.warning("Invalid file encoding detected in '{}': {}".format(path, file_encoding))
with open(path, "w", encoding=encoding, errors="ignore") as f:
if sys.platform.startswith("linux"):
# write the shebang on the first line on Linux
vmware_path = VMware._get_linux_vmware_binary()
if vmware_path:
f.write("#!{}\n".format(vmware_path))
for key, value in pairs.items():
entry = '{} = "{}"\n'.format(key, value)
f.write(entry)
def _get_vms_from_inventory(self, inventory_path):
"""
Searches for VMs by parsing a VMware inventory file.
:param inventory_path: path to the inventory file
:returns: list of VMs
"""
vm_entries = {}
vms = []
try:
log.debug('Reading VMware inventory file "{}"'.format(inventory_path))
pairs = self.parse_vmware_file(inventory_path)
for key, value in pairs.items():
if key.startswith("vmlist"):
try:
vm_entry, variable_name = key.split('.', 1)
except ValueError:
continue
if vm_entry not in vm_entries:
vm_entries[vm_entry] = {}
vm_entries[vm_entry][variable_name.strip()] = value
except OSError as e:
log.warning("Could not read VMware inventory file {}: {}".format(inventory_path, e))
for vm_settings in vm_entries.values():
if "displayname" in vm_settings and "config" in vm_settings:
log.debug('Found VM named "{}" with VMX file "{}"'.format(vm_settings["displayname"], vm_settings["config"]))
vms.append({"vmname": vm_settings["displayname"], "vmx_path": vm_settings["config"]})
return vms
def _get_vms_from_directory(self, directory):
"""
Searches for VMs in a given directory.
:param directory: path to the directory
:returns: list of VMs
"""
vms = []
for path, _, filenames in os.walk(directory):
for filename in filenames:
if os.path.splitext(filename)[1] == ".vmx":
vmx_path = os.path.join(path, filename)
log.debug('Reading VMware VMX file "{}"'.format(vmx_path))
try:
pairs = self.parse_vmware_file(vmx_path)
if "displayname" in pairs:
log.debug('Found VM named "{}"'.format(pairs["displayname"]))
vms.append({"vmname": pairs["displayname"], "vmx_path": vmx_path})
except OSError as e:
log.warning('Could not read VMware VMX file "{}": {}'.format(vmx_path, e))
continue
return vms
@staticmethod
def get_vmware_inventory_path():
"""
Returns VMware inventory file path.
:returns: path to the inventory file
"""
if sys.platform.startswith("win"):
return os.path.expandvars(r"%APPDATA%\Vmware\Inventory.vmls")
elif sys.platform.startswith("darwin"):
return os.path.expanduser("~/Library/Application Support/VMware Fusion/vmInventory")
else:
return os.path.expanduser("~/.vmware/inventory.vmls")
@staticmethod
def get_vmware_preferences_path():
"""
Returns VMware preferences file path.
:returns: path to the preferences file
"""
if sys.platform.startswith("win"):
return os.path.expandvars(r"%APPDATA%\VMware\preferences.ini")
elif sys.platform.startswith("darwin"):
return os.path.expanduser("~/Library/Preferences/VMware Fusion/preferences")
else:
return os.path.expanduser("~/.vmware/preferences")
@staticmethod
def get_vmware_default_vm_path():
"""
Returns VMware default VM directory path.
:returns: path to the default VM directory
"""
if sys.platform.startswith("win"):
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)
else:
return '{}\Virtual Machines'.format(documents_folder)
elif sys.platform.startswith("darwin"):
return os.path.expanduser("~/Documents/Virtual Machines.localized")
else:
return os.path.expanduser("~/vmware")
@asyncio.coroutine
def list_vms(self):
"""
Gets VMware VM list.
"""
# check for the right VMware version
yield from self.check_vmware_version()
inventory_path = self.get_vmware_inventory_path()
if os.path.exists(inventory_path) and self.host_type == "ws":
# inventory may exist for VMware player if VMware workstation has been previously installed
return self._get_vms_from_inventory(inventory_path)
else:
# VMware player has no inventory file, let's search the default location for VMs
vmware_preferences_path = self.get_vmware_preferences_path()
default_vm_path = self.get_vmware_default_vm_path()
pairs = {}
if os.path.exists(vmware_preferences_path):
# the default vm path may be present in VMware preferences file.
try:
pairs = self.parse_vmware_file(vmware_preferences_path)
except OSError as e:
log.warning('Could not read VMware preferences file "{}": {}'.format(vmware_preferences_path, e))
if "prefvmx.defaultvmpath" in pairs:
default_vm_path = pairs["prefvmx.defaultvmpath"]
if not os.path.isdir(default_vm_path):
raise VMwareError('Could not find the default VM directory: "{}"'.format(default_vm_path))
vms = self._get_vms_from_directory(default_vm_path)
# looks for VMX paths in the preferences file in case not all VMs are in the default directory
for key, value in pairs.items():
m = re.match(r'pref.mruVM(\d+)\.filename', key)
if m:
display_name = "pref.mruVM{}.displayName".format(m.group(1))
if display_name in pairs:
found = False
for vm in vms:
if vm["vmname"] == display_name:
found = True
if found is False:
vms.append({"vmname": pairs[display_name], "vmx_path": value})
return vms
@staticmethod
def _get_linux_vmware_binary():
"""
Return the path of the vmware binary on Linux or None
"""
path = shutil.which("vmware")
if path is None:
path = shutil.which("vmplayer")
return path
if __name__ == '__main__':
loop = asyncio.get_event_loop()
vmware = VMware.instance()
print("=> Check version")
loop.run_until_complete(asyncio.async(vmware.check_vmware_version()))
+53
View File
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Interface for VMnet NIOs.
"""
from ..nios.nio import NIO
class NIOVMNET(NIO):
"""
VMnet NIO.
"""
def __init__(self, vmnet):
super().__init__()
self._vmnet = vmnet
@property
def vmnet(self):
"""
Returns vmnet interface used by this NIO.
:returns: vmnet interface name
"""
return self._vmnet
def __str__(self):
return "NIO VMNET"
def __json__(self):
return {"type": "nio_vmnet",
"vmnet": self._vmnet}
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Custom exceptions for the VMware module.
"""
from ..vm_error import VMError
class VMwareError(VMError):
pass
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more