mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-07 07:33:43 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c47081f363 | ||
|
|
3793789732 | ||
|
|
4c010b466d | ||
|
|
701a1626f7 | ||
|
|
e6e83ffa8a | ||
|
|
2bfe098866 | ||
|
|
2ef45781ad | ||
|
|
14cc6d414f | ||
|
|
cbbb89f69c | ||
|
|
29896151e7 | ||
|
|
7f6f7022b0 | ||
|
|
c77ebca036 | ||
|
|
881cc6f592 | ||
|
|
5265818365 | ||
|
|
d1e186317c | ||
|
|
a6fd457cff | ||
|
|
4eedc9c74c | ||
|
|
ccb4ccd612 | ||
|
|
c775b05d33 | ||
|
|
9cedb1cc99 | ||
|
|
1f615430ae | ||
|
|
2de1a97076 | ||
|
|
747c0ee7a3 | ||
|
|
d9f44edcaf | ||
|
|
e9991affc3 | ||
|
|
84511d7b39 | ||
|
|
3590985c07 | ||
|
|
7785c03eac | ||
|
|
7f37f649da | ||
|
|
acb5103119 | ||
|
|
823344bf44 | ||
|
|
ecf7ebc26b | ||
|
|
317a37147e | ||
|
|
0f238c5bb7 | ||
|
|
93934cf554 | ||
|
|
6a81390808 | ||
|
|
f1503ae655 | ||
|
|
20592f72c0 | ||
|
|
00f49e337d | ||
|
|
fc04a94dce | ||
|
|
ba357b0541 | ||
|
|
f58c7960c9 | ||
|
|
5a468888c8 | ||
|
|
8f53d51c05 | ||
|
|
1e01c85be9 | ||
|
|
fed02ee167 | ||
|
|
632134a02a | ||
|
|
183a6aed44 | ||
|
|
d97ba11728 | ||
|
|
9ec1373d36 | ||
|
|
b220cffcf4 | ||
|
|
7ef8dd794a | ||
|
|
b9a4531544 | ||
|
|
cfdff773c4 | ||
|
|
ef8c6b603c | ||
|
|
4918675cd5 | ||
|
|
6ef614103e | ||
|
|
09948a366f | ||
|
|
3bd88178a0 |
No files matched your search
@@ -29,7 +29,7 @@ import logging
|
||||
from io import StringIO, BytesIO
|
||||
|
||||
from libcloud.compute.base import NodeAuthSSHKey
|
||||
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError
|
||||
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError
|
||||
|
||||
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
|
||||
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
|
||||
@@ -216,11 +216,11 @@ class BaseCloudCtrl(object):
|
||||
|
||||
return self.driver.list_key_pairs()
|
||||
|
||||
def upload_file(self, file_path, folder):
|
||||
def upload_file(self, file_path, cloud_object_name):
|
||||
"""
|
||||
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
|
||||
:param file_path: path to file to upload
|
||||
:param folder: folder in cloud storage to save file in
|
||||
:param cloud_object_name: name of file saved in cloud storage
|
||||
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
|
||||
"""
|
||||
try:
|
||||
@@ -231,7 +231,6 @@ class BaseCloudCtrl(object):
|
||||
with open(file_path, 'rb') as file:
|
||||
local_file_hash = hashlib.md5(file.read()).hexdigest()
|
||||
|
||||
cloud_object_name = folder + '/' + os.path.basename(file_path)
|
||||
cloud_hash_name = cloud_object_name + '.md5'
|
||||
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
|
||||
|
||||
@@ -254,23 +253,24 @@ class BaseCloudCtrl(object):
|
||||
def list_projects(self):
|
||||
"""
|
||||
Lists projects in cloud storage
|
||||
:return: List of (project name, object name in storage)
|
||||
:return: Dictionary where project names are keys and values are names of objects in storage
|
||||
"""
|
||||
|
||||
try:
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
projects = [
|
||||
(obj.name.replace('projects/', '').replace('.zip', ''), obj.name)
|
||||
projects = {
|
||||
obj.name.replace('projects/', '').replace('.zip', ''): obj.name
|
||||
for obj in gns3_container.list_objects()
|
||||
if obj.name.startswith('projects/') and obj.name[-4:] == '.zip'
|
||||
]
|
||||
}
|
||||
return projects
|
||||
except ContainerDoesNotExistError:
|
||||
return []
|
||||
|
||||
def download_file(self, file_name, destination=None):
|
||||
"""
|
||||
Downloads file from cloud storage
|
||||
Downloads file from cloud storage. If a file exists at destination, and it is identical to the file in cloud
|
||||
storage, it is not downloaded.
|
||||
:param file_name: name of file in cloud storage to download
|
||||
:param destination: local path to save file to (if None, returns file contents as a file-like object)
|
||||
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
|
||||
@@ -278,7 +278,22 @@ class BaseCloudCtrl(object):
|
||||
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
storage_object = gns3_container.get_object(file_name)
|
||||
|
||||
if destination is not None:
|
||||
if os.path.isfile(destination):
|
||||
# if a file exists at destination and its hash matches that of the
|
||||
# file in cloud storage, don't download it
|
||||
with open(destination, 'rb') as f:
|
||||
local_file_hash = hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
cloud_object_hash = ''
|
||||
for chunk in hash_object.as_stream():
|
||||
cloud_object_hash += chunk.decode('utf8')
|
||||
|
||||
if local_file_hash == cloud_object_hash:
|
||||
return
|
||||
|
||||
storage_object.download(destination)
|
||||
else:
|
||||
contents = b''
|
||||
@@ -287,3 +302,40 @@ class BaseCloudCtrl(object):
|
||||
contents += chunk
|
||||
|
||||
return BytesIO(contents)
|
||||
|
||||
def find_storage_image_names(self, images_to_find):
|
||||
"""
|
||||
Maps names of image files to their full name in cloud storage
|
||||
:param images_to_find: list of image names to find
|
||||
:return: A dictionary where keys are image names, and values are the corresponding names of
|
||||
the files in cloud storage
|
||||
"""
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
images_in_storage = [obj.name for obj in gns3_container.list_objects() if obj.name.startswith('images/')]
|
||||
|
||||
images = {}
|
||||
for image_name in images_to_find:
|
||||
images_with_same_name =\
|
||||
list(filter(lambda storage_image_name: storage_image_name.endswith(image_name), images_in_storage))
|
||||
|
||||
if len(images_with_same_name) == 1:
|
||||
images[image_name] = images_with_same_name[0]
|
||||
else:
|
||||
raise Exception('Image does not exist in cloud storage or is duplicated')
|
||||
|
||||
return images
|
||||
|
||||
def delete_file(self, file_name):
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
|
||||
try:
|
||||
object_to_delete = gns3_container.get_object(file_name)
|
||||
object_to_delete.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
|
||||
try:
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
hash_object.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
@@ -42,11 +42,9 @@ class RackspaceCtrl(BaseCloudCtrl):
|
||||
|
||||
""" Controller class for interacting with Rackspace API. """
|
||||
|
||||
def __init__(self, username, api_key, gns3_ias_url):
|
||||
def __init__(self, username, api_key, *args, **kwargs):
|
||||
super(RackspaceCtrl, self).__init__(username, api_key)
|
||||
|
||||
self.gns3_ias_url = gns3_ias_url
|
||||
|
||||
# set this up so it can be swapped out with a mock for testing
|
||||
self.post_fn = requests.post
|
||||
self.driver_cls = get_driver(Provider.RACKSPACE)
|
||||
@@ -225,55 +223,6 @@ class RackspaceCtrl(BaseCloudCtrl):
|
||||
self.region = region
|
||||
return True
|
||||
|
||||
def _get_shared_images(self, username, region, gns3_version):
|
||||
"""
|
||||
Given a GNS3 version, ask gns3-ias to share compatible images
|
||||
|
||||
Response:
|
||||
[{"created_at": "", "schema": "", "status": "", "member_id": "", "image_id": "", "updated_at": ""},]
|
||||
or, if access was already asked
|
||||
[{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},]
|
||||
"""
|
||||
endpoint = self.gns3_ias_url+"/images/grant_access"
|
||||
params = {
|
||||
"user_id": username,
|
||||
"user_region": region.upper(),
|
||||
"gns3_version": gns3_version,
|
||||
}
|
||||
try:
|
||||
response = requests.get(endpoint, params=params)
|
||||
except requests.ConnectionError:
|
||||
raise ApiError("Unable to connect to IAS")
|
||||
|
||||
status = response.status_code
|
||||
|
||||
if status == 200:
|
||||
return response.json()
|
||||
elif status == 404:
|
||||
raise ItemNotFound()
|
||||
else:
|
||||
raise ApiError("IAS status code: %d" % status)
|
||||
|
||||
def list_images(self):
|
||||
"""
|
||||
Return a dictionary containing RackSpace server images
|
||||
retrieved from gns3-ias server
|
||||
"""
|
||||
if not (self.tenant_id and self.region):
|
||||
return {}
|
||||
|
||||
try:
|
||||
shared_images = self._get_shared_images(self.tenant_id, self.region, __version__)
|
||||
images = {}
|
||||
for i in shared_images:
|
||||
images[i['image_id']] = i['image_name']
|
||||
return images
|
||||
except ItemNotFound:
|
||||
return {}
|
||||
except ApiError as e:
|
||||
log.error('Error while retrieving image list: %s' % e)
|
||||
return {}
|
||||
|
||||
def get_image(self, image_id):
|
||||
return self.driver.get_image(image_id)
|
||||
|
||||
@@ -290,12 +239,11 @@ def get_provider(cloud_settings):
|
||||
username = cloud_settings['cloud_user_name']
|
||||
apikey = cloud_settings['cloud_api_key']
|
||||
region = cloud_settings['cloud_region']
|
||||
ias_url = cloud_settings.get('gns3_ias_url', '')
|
||||
except KeyError as e:
|
||||
log.error("Unable to create cloud provider: {}".format(e))
|
||||
return
|
||||
|
||||
provider = RackspaceCtrl(username, apikey, ias_url)
|
||||
provider = RackspaceCtrl(username, apikey)
|
||||
|
||||
if not provider.authenticate():
|
||||
log.error("Authentication failed for cloud provider")
|
||||
|
||||
+12
-11
@@ -79,12 +79,12 @@ Options:
|
||||
--instance_id ID of the Rackspace instance to terminate
|
||||
--cloud_region Region of instance
|
||||
|
||||
--deadtime How long in seconds can the communication lose exist before we
|
||||
--dead_time How long in seconds can the communication lose exist before we
|
||||
shutdown this instance.
|
||||
Default:
|
||||
Example --deadtime=3600 (60 minutes)
|
||||
Example --dead_time=3600 (60 minutes)
|
||||
|
||||
--check-interval Defaults to --deadtime, used for debugging
|
||||
--check-interval Defaults to --dead_time, used for debugging
|
||||
|
||||
--init-wait Inital wait time, how long before we start pulling the file.
|
||||
Default: 300 (5 min)
|
||||
@@ -113,7 +113,7 @@ def parse_cmd_line(argv):
|
||||
"cloud_api_key=",
|
||||
"instance_id=",
|
||||
"region=",
|
||||
"deadtime=",
|
||||
"dead_time=",
|
||||
"init-wait=",
|
||||
"check-interval=",
|
||||
"file=",
|
||||
@@ -133,7 +133,7 @@ def parse_cmd_line(argv):
|
||||
cmd_line_option_list["cloud_api_key"] = None
|
||||
cmd_line_option_list["instance_id"] = None
|
||||
cmd_line_option_list["region"] = None
|
||||
cmd_line_option_list["deadtime"] = 60 * 60 #minutes
|
||||
cmd_line_option_list["dead_time"] = 60 * 60 #minutes
|
||||
cmd_line_option_list["check-interval"] = None
|
||||
cmd_line_option_list["init-wait"] = 5 * 60
|
||||
cmd_line_option_list["file"] = None
|
||||
@@ -150,6 +150,7 @@ def parse_cmd_line(argv):
|
||||
|
||||
|
||||
get_gns3secrets(cmd_line_option_list)
|
||||
cmd_line_option_list["dead_time"] = int(cmd_line_option_list["dead_time"])
|
||||
|
||||
for opt, val in opts:
|
||||
if (opt in ("-h", "--help")):
|
||||
@@ -167,8 +168,8 @@ def parse_cmd_line(argv):
|
||||
cmd_line_option_list["instance_id"] = val
|
||||
elif (opt in ("--region")):
|
||||
cmd_line_option_list["region"] = val
|
||||
elif (opt in ("--deadtime")):
|
||||
cmd_line_option_list["deadtime"] = int(val)
|
||||
elif (opt in ("--dead_time")):
|
||||
cmd_line_option_list["dead_time"] = int(val)
|
||||
elif (opt in ("--check-interval")):
|
||||
cmd_line_option_list["check-interval"] = int(val)
|
||||
elif (opt in ("--init-wait")):
|
||||
@@ -183,7 +184,7 @@ def parse_cmd_line(argv):
|
||||
if cmd_line_option_list["shutdown"] == False:
|
||||
|
||||
if cmd_line_option_list["check-interval"] is None:
|
||||
cmd_line_option_list["check-interval"] = cmd_line_option_list["deadtime"] + 120
|
||||
cmd_line_option_list["check-interval"] = cmd_line_option_list["dead_time"] + 120
|
||||
|
||||
if cmd_line_option_list["cloud_user_name"] is None:
|
||||
print("You need to specify a username!!!!")
|
||||
@@ -317,9 +318,9 @@ def monitor_loop(options):
|
||||
delta = now - file_last_modified
|
||||
log.debug("File last updated: %s seconds ago" % (delta.seconds))
|
||||
|
||||
if delta.seconds > options["deadtime"]:
|
||||
log.warning("Deadtime exceeded, terminating instance ...")
|
||||
#Terminate involes many layers of HTTP / API calls, lots of
|
||||
if delta.seconds > options["dead_time"]:
|
||||
log.warning("Dead time exceeded, terminating instance ...")
|
||||
#Terminate involves many layers of HTTP / API calls, lots of
|
||||
#different errors types could occur here.
|
||||
try:
|
||||
rksp = Rackspace(options)
|
||||
|
||||
@@ -41,7 +41,7 @@ class Rackspace(object):
|
||||
self.authenticated = False
|
||||
self.hostname = socket.gethostname()
|
||||
self.instance_id = options["instance_id"]
|
||||
self.region = options["region"]
|
||||
self.region = options["cloud_region"]
|
||||
|
||||
log.debug("Authenticating with Rackspace")
|
||||
log.debug("My hostname: %s" % (self.hostname))
|
||||
@@ -59,7 +59,7 @@ class Rackspace(object):
|
||||
self.rksp.set_region(self.region)
|
||||
for server in self.rksp.list_instances():
|
||||
log.debug("Checking server: %s" % (server.name))
|
||||
if server.name.lower() == self.hostname.lower() and server.id == self.instance_id:
|
||||
if server.id == self.instance_id:
|
||||
log.info("Found matching instance: %s" % (server.id))
|
||||
log.info("Startup id: %s" % (self.instance_id))
|
||||
return server
|
||||
|
||||
@@ -35,16 +35,48 @@ def get_windows_interfaces():
|
||||
"""
|
||||
|
||||
import win32com.client
|
||||
import pywintypes
|
||||
locator = win32com.client.Dispatch("WbemScripting.SWbemLocator")
|
||||
service = locator.ConnectServer(".", "root\cimv2")
|
||||
interfaces = []
|
||||
# more info on Win32_NetworkAdapter: http://msdn.microsoft.com/en-us/library/aa394216%28v=vs.85%29.aspx
|
||||
for adapter in service.InstancesOf("Win32_NetworkAdapter"):
|
||||
if adapter.NetConnectionStatus == 2 or adapter.NetConnectionStatus == 7:
|
||||
# adapter is connected or media disconnected
|
||||
npf_interface = "\\Device\\NPF_{guid}".format(guid=adapter.GUID)
|
||||
try:
|
||||
# more info on Win32_NetworkAdapter: http://msdn.microsoft.com/en-us/library/aa394216%28v=vs.85%29.aspx
|
||||
for adapter in service.InstancesOf("Win32_NetworkAdapter"):
|
||||
if adapter.NetConnectionStatus == 2 or adapter.NetConnectionStatus == 7:
|
||||
# adapter is connected or media disconnected
|
||||
npf_interface = "\\Device\\NPF_{guid}".format(guid=adapter.GUID)
|
||||
interfaces.append({"id": npf_interface,
|
||||
"name": adapter.NetConnectionID})
|
||||
except pywintypes.com_error:
|
||||
log.warn("could not use the COM service to retrieve interface info, trying using the registry...")
|
||||
return get_windows_interfaces_from_registry()
|
||||
|
||||
return interfaces
|
||||
|
||||
|
||||
def get_windows_interfaces_from_registry():
|
||||
|
||||
import winreg
|
||||
|
||||
interfaces = []
|
||||
try:
|
||||
hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkCards")
|
||||
for index in range(winreg.QueryInfoKey(hkey)[0]):
|
||||
network_card_id = winreg.EnumKey(hkey, index)
|
||||
hkeycard = winreg.OpenKey(hkey, network_card_id)
|
||||
guid, _ = winreg.QueryValueEx(hkeycard, "ServiceName")
|
||||
connection = r"SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}" + "\{}\Connection".format(guid)
|
||||
hkeycon = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, connection)
|
||||
name, _ = winreg.QueryValueEx(hkeycon, "Name")
|
||||
npf_interface = "\\Device\\NPF_{guid}".format(guid=guid)
|
||||
interfaces.append({"id": npf_interface,
|
||||
"name": adapter.NetConnectionID})
|
||||
"name": name})
|
||||
winreg.CloseKey(hkeycon)
|
||||
winreg.CloseKey(hkeycard)
|
||||
winreg.CloseKey(hkey)
|
||||
except OSError as e:
|
||||
log.error("could not read registry information: {}".format(e))
|
||||
|
||||
return interfaces
|
||||
|
||||
|
||||
|
||||
@@ -129,8 +129,8 @@ def has_privileged_access(executable):
|
||||
if os.geteuid() == 0:
|
||||
# we are root, so we should have privileged access.
|
||||
return True
|
||||
if os.stat(executable).st_mode & stat.S_ISVTX == stat.S_ISVTX:
|
||||
# the executable has a sticky bit.
|
||||
if 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)
|
||||
|
||||
@@ -83,6 +83,7 @@ class DeadMan(IModule):
|
||||
cmd.append("--file")
|
||||
cmd.append("%s" % (self._heartbeat_file))
|
||||
cmd.append("--background")
|
||||
cmd.append("--debug")
|
||||
log.info("Deadman: Running command: %s"%(cmd))
|
||||
|
||||
process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, shell=False)
|
||||
@@ -94,7 +95,6 @@ class DeadMan(IModule):
|
||||
"""
|
||||
|
||||
cmd = []
|
||||
|
||||
cmd.append("gns3dms")
|
||||
cmd.append("-k")
|
||||
log.info("Deadman: Running command: %s"%(cmd))
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import base64
|
||||
import ntpath
|
||||
import time
|
||||
from gns3server.modules import IModule
|
||||
@@ -73,8 +72,8 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ADAPTER_MATRIX = {"C7200_IO_2FE": C7200_IO_2FE,
|
||||
"C7200_IO_FE": C7200_IO_FE,
|
||||
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,
|
||||
@@ -171,6 +170,8 @@ class VM(object):
|
||||
|
||||
if chassis:
|
||||
router = PLATFORMS[platform](hypervisor, name, router_id, chassis=chassis)
|
||||
elif platform == "c7200" and os.path.basename(image).lower().startswith("c7200p"):
|
||||
router = PLATFORMS[platform](hypervisor, name, router_id, npe="npe-g2")
|
||||
else:
|
||||
router = PLATFORMS[platform](hypervisor, name, router_id)
|
||||
router.ram = ram
|
||||
@@ -412,36 +413,40 @@ class VM(object):
|
||||
|
||||
response = {}
|
||||
try:
|
||||
startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id))
|
||||
private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id))
|
||||
default_startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id))
|
||||
default_private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id))
|
||||
|
||||
# a new startup-config has been pushed
|
||||
if "startup_config_base64" in request:
|
||||
# update the request with the new local startup-config path
|
||||
request["startup_config"] = self.create_config_from_base64(request["startup_config_base64"], router, startup_config_path)
|
||||
request["startup_config"] = self.create_config_from_base64(request["startup_config_base64"], router, default_startup_config_path)
|
||||
|
||||
# a new private-config has been pushed
|
||||
if "private_config_base64" in request:
|
||||
# update the request with the new local private-config path
|
||||
request["private_config"] = self.create_config_from_base64(request["private_config_base64"], router, private_config_path)
|
||||
request["private_config"] = self.create_config_from_base64(request["private_config_base64"], router, default_private_config_path)
|
||||
|
||||
if "startup_config" in request:
|
||||
if os.path.isfile(request["startup_config"]) and request["startup_config"] != startup_config_path:
|
||||
startup_config_path = request["startup_config"].replace("\\", '/')
|
||||
if os.path.isfile(startup_config_path) and startup_config_path != default_startup_config_path:
|
||||
# this is a local file set in the GUI
|
||||
request["startup_config"] = self.create_config_from_file(request["startup_config"], router, startup_config_path)
|
||||
router.set_config(request["startup_config"])
|
||||
startup_config_path = self.create_config_from_file(startup_config_path, router, default_startup_config_path)
|
||||
router.set_config(startup_config_path)
|
||||
else:
|
||||
router.set_config(request["startup_config"])
|
||||
response["startup_config"] = request["startup_config"]
|
||||
router.set_config(startup_config_path)
|
||||
response["startup_config"] = startup_config_path
|
||||
del request["startup_config"]
|
||||
|
||||
if "private_config" in request:
|
||||
if os.path.isfile(request["private_config"]) and request["private_config"] != private_config_path:
|
||||
private_config_path = request["private_config"].replace("\\", '/')
|
||||
if os.path.isfile(private_config_path) and private_config_path != default_private_config_path:
|
||||
# this is a local file set in the GUI
|
||||
request["private_config"] = self.create_config_from_file(request["private_config"], router, private_config_path)
|
||||
router.set_config(router.startup_config, request["private_config"])
|
||||
private_config_path = self.create_config_from_file(private_config_path, router, default_private_config_path)
|
||||
router.set_config(router.startup_config, private_config_path)
|
||||
else:
|
||||
router.set_config(router.startup_config, request["private_config"])
|
||||
response["private_config"] = request["private_config"]
|
||||
router.set_config(router.startup_config, private_config_path)
|
||||
response["private_config"] = private_config_path
|
||||
del request["private_config"]
|
||||
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
@@ -468,7 +473,7 @@ class VM(object):
|
||||
except DynamipsError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
elif name.startswith("slot") and value == None:
|
||||
elif name.startswith("slot") and value is None:
|
||||
slot_id = int(name[-1])
|
||||
if router.slots[slot_id]:
|
||||
try:
|
||||
@@ -760,7 +765,6 @@ class VM(object):
|
||||
if was_auto_started:
|
||||
router.stop()
|
||||
|
||||
validated_idlepc = "0x0"
|
||||
response = {"id": router.id,
|
||||
"logs": logs,
|
||||
"idlepc": validated_idlepc}
|
||||
|
||||
@@ -474,8 +474,8 @@ class DynamipsHypervisor(object):
|
||||
log.debug("sending {}".format(command))
|
||||
self.socket.sendall(command.encode('utf-8'))
|
||||
except OSError as e:
|
||||
raise DynamipsError("Lost communication with {host}:{port} :{error}"
|
||||
.format(host=self._host, port=self._port, error=e))
|
||||
raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}"
|
||||
.format(host=self._host, port=self._port, error=e, run=self.is_running()))
|
||||
|
||||
# Now retrieve the result
|
||||
data = []
|
||||
@@ -485,16 +485,16 @@ class DynamipsHypervisor(object):
|
||||
chunk = self.socket.recv(1024) # match to Dynamips' buffer size
|
||||
buf += chunk.decode("utf-8")
|
||||
except OSError as e:
|
||||
raise DynamipsError("Communication timed out with {host}:{port} :{error}"
|
||||
.format(host=self._host, port=self._port, error=e))
|
||||
raise DynamipsError("Communication timed out with {host}:{port} :{error}, Dynamips process running: {run}"
|
||||
.format(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}"
|
||||
.format(host=self._host, port=self._port))
|
||||
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] == '':
|
||||
@@ -502,8 +502,8 @@ class DynamipsHypervisor(object):
|
||||
buf = ''
|
||||
|
||||
if len(data) == 0:
|
||||
raise DynamipsError("no data returned from {host}:{port}"
|
||||
.format(host=self._host, port=self._port))
|
||||
raise DynamipsError("no data returned from {host}:{port}, Dynamips process running: {run}"
|
||||
.format(host=self._host, port=self._port, run=self.is_running()))
|
||||
|
||||
# Does it contain an error code?
|
||||
if self.error_re.search(data[-1]):
|
||||
|
||||
@@ -212,7 +212,7 @@ class Hypervisor(DynamipsHypervisor):
|
||||
cwd=self._working_dir)
|
||||
log.info("Dynamips started PID={}".format(self._process.pid))
|
||||
self._started = True
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.error("could not start Dynamips: {}".format(e))
|
||||
raise DynamipsError("could not start Dynamips: {}".format(e))
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class C1700(Router):
|
||||
Router.__init__(self, hypervisor, name, router_id, platform="c1700")
|
||||
|
||||
# Set default values for this platform
|
||||
self._ram = 64
|
||||
self._ram = 128
|
||||
self._nvram = 32
|
||||
self._disk0 = 0
|
||||
self._disk1 = 0
|
||||
|
||||
@@ -59,7 +59,7 @@ class C2600(Router):
|
||||
Router.__init__(self, hypervisor, name, router_id, platform="c2600")
|
||||
|
||||
# Set default values for this platform
|
||||
self._ram = 64
|
||||
self._ram = 128
|
||||
self._nvram = 128
|
||||
self._disk0 = 0
|
||||
self._disk1 = 0
|
||||
|
||||
@@ -40,7 +40,7 @@ class C2691(Router):
|
||||
Router.__init__(self, hypervisor, name, router_id, platform="c2691")
|
||||
|
||||
# Set default values for this platform
|
||||
self._ram = 128
|
||||
self._ram = 192
|
||||
self._nvram = 112
|
||||
self._disk0 = 16
|
||||
self._disk1 = 0
|
||||
|
||||
@@ -42,7 +42,7 @@ class C3600(Router):
|
||||
Router.__init__(self, hypervisor, name, router_id, platform="c3600")
|
||||
|
||||
# Set default values for this platform
|
||||
self._ram = 128
|
||||
self._ram = 192
|
||||
self._nvram = 128
|
||||
self._disk0 = 0
|
||||
self._disk1 = 0
|
||||
|
||||
@@ -40,7 +40,7 @@ class C3745(Router):
|
||||
Router.__init__(self, hypervisor, name, router_id, platform="c3745")
|
||||
|
||||
# Set default values for this platform
|
||||
self._ram = 128
|
||||
self._ram = 256
|
||||
self._nvram = 304
|
||||
self._disk0 = 16
|
||||
self._disk1 = 0
|
||||
|
||||
@@ -22,7 +22,7 @@ http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L294
|
||||
|
||||
from ..dynamips_error import DynamipsError
|
||||
from .router import Router
|
||||
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
|
||||
|
||||
import logging
|
||||
@@ -43,7 +43,7 @@ class C7200(Router):
|
||||
Router.__init__(self, hypervisor, name, router_id, platform="c7200")
|
||||
|
||||
# Set default values for this platform
|
||||
self._ram = 256
|
||||
self._ram = 512
|
||||
self._nvram = 128
|
||||
self._disk0 = 64
|
||||
self._disk1 = 0
|
||||
@@ -70,7 +70,7 @@ class C7200(Router):
|
||||
if npe == "npe-g2":
|
||||
self.slot_add_binding(0, C7200_IO_GE_E())
|
||||
else:
|
||||
self.slot_add_binding(0, C7200_IO_2FE())
|
||||
self.slot_add_binding(0, C7200_IO_FE())
|
||||
|
||||
def defaults(self):
|
||||
"""
|
||||
|
||||
@@ -354,8 +354,11 @@ class Router(object):
|
||||
self.resume()
|
||||
elif status == "inactive":
|
||||
|
||||
if not os.path.isfile(self._image):
|
||||
raise DynamipsError("IOS image '{}' is not accessible".format(self._image))
|
||||
if not os.path.isfile(self._image) or not os.path.exists(self._image):
|
||||
if os.path.islink(self._image):
|
||||
raise DynamipsError("IOS image '{}' linked to '{}' is not accessible".format(self._image, os.path.realpath(self._image)))
|
||||
else:
|
||||
raise DynamipsError("IOS image '{}' is not accessible".format(self._image))
|
||||
|
||||
try:
|
||||
with open(self._image, "rb") as f:
|
||||
@@ -1324,7 +1327,7 @@ class Router(object):
|
||||
adapter=current_adapter))
|
||||
|
||||
# Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running
|
||||
if self.is_running() and not (self._platform == 'c7200'
|
||||
if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200'))
|
||||
and not (self._platform == 'c3600' and self.chassis == '3660')
|
||||
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
|
||||
raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter,
|
||||
@@ -1369,7 +1372,7 @@ class Router(object):
|
||||
slot_id=slot_id))
|
||||
|
||||
# Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running
|
||||
if self.is_running() and not (self._platform == 'c7200'
|
||||
if self.is_running() and not ((self._platform == 'c7200' and not str(adapter).startswith('C7200'))
|
||||
and not (self._platform == 'c3600' and self.chassis == '3660')
|
||||
and not (self._platform == 'c3745' and adapter == 'NM-4T')):
|
||||
raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter,
|
||||
|
||||
@@ -99,6 +99,7 @@ class IOU(IModule):
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
self._server_iourc_path = iou_config.get("iourc", "")
|
||||
self._iourc = ""
|
||||
|
||||
# check every 5 seconds
|
||||
@@ -473,7 +474,11 @@ class IOU(IModule):
|
||||
|
||||
try:
|
||||
iou_instance.iouyap = self._iouyap
|
||||
iou_instance.iourc = self._iourc
|
||||
if self._iourc:
|
||||
iou_instance.iourc = self._iourc
|
||||
else:
|
||||
# if there is no IOURC file pushed by the client then use the server IOURC file
|
||||
iou_instance.iourc = self._server_iourc_path
|
||||
iou_instance.start()
|
||||
except IOUError as e:
|
||||
self.send_custom_error(str(e))
|
||||
|
||||
@@ -509,7 +509,7 @@ class IOUDevice(object):
|
||||
cwd=self._working_dir)
|
||||
|
||||
log.info("iouyap started PID={}".format(self._iouyap_process.pid))
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
iouyap_stdout = self.read_iouyap_stdout()
|
||||
log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout))
|
||||
raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout))
|
||||
@@ -521,7 +521,7 @@ class IOUDevice(object):
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(["ldd", self._path])
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
||||
except (FileNotFoundError, subprocess.SubprocessError) as e:
|
||||
log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e))
|
||||
return
|
||||
|
||||
@@ -539,7 +539,10 @@ class IOUDevice(object):
|
||||
if not self.is_running():
|
||||
|
||||
if not os.path.isfile(self._path) or not os.path.exists(self._path):
|
||||
raise IOUError("IOU image '{}' is not accessible".format(self._path))
|
||||
if os.path.islink(self._path):
|
||||
raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path)))
|
||||
else:
|
||||
raise IOUError("IOU image '{}' is not accessible".format(self._path))
|
||||
|
||||
try:
|
||||
with open(self._path, "rb") as f:
|
||||
@@ -559,7 +562,7 @@ class IOUDevice(object):
|
||||
self._library_check()
|
||||
|
||||
if not self._iourc or not os.path.isfile(self._iourc):
|
||||
raise IOUError("A iourc file is necessary to start IOU")
|
||||
raise IOUError("A valid iourc file is necessary to start IOU")
|
||||
|
||||
if not self._iouyap or not os.path.isfile(self._iouyap):
|
||||
raise IOUError("iouyap is necessary to start IOU")
|
||||
@@ -583,7 +586,7 @@ class IOUDevice(object):
|
||||
self._started = True
|
||||
except FileNotFoundError as e:
|
||||
raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e))
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
iou_stdout = self.read_iou_stdout()
|
||||
log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
|
||||
raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout))
|
||||
@@ -761,7 +764,7 @@ class IOUDevice(object):
|
||||
command.extend(["-l"])
|
||||
else:
|
||||
raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path)))
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e))
|
||||
|
||||
def _build_command(self):
|
||||
|
||||
@@ -67,11 +67,14 @@ class Qemu(IModule):
|
||||
qemu_config = config.get_section_config(name.upper())
|
||||
self._console_start_port_range = qemu_config.get("console_start_port_range", 5001)
|
||||
self._console_end_port_range = qemu_config.get("console_end_port_range", 5500)
|
||||
self._monitor_start_port_range = qemu_config.get("monitor_start_port_range", 5501)
|
||||
self._monitor_end_port_range = qemu_config.get("monitor_end_port_range", 6000)
|
||||
self._allocated_udp_ports = []
|
||||
self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001)
|
||||
self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500)
|
||||
self._host = qemu_config.get("host", kwargs["host"])
|
||||
self._console_host = qemu_config.get("console_host", kwargs["console_host"])
|
||||
self._monitor_host = qemu_config.get("monitor_host", "127.0.0.1")
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
@@ -137,6 +140,8 @@ class Qemu(IModule):
|
||||
- project_name
|
||||
- console_start_port_range
|
||||
- console_end_port_range
|
||||
- monitor_start_port_range
|
||||
- monitor_end_port_range
|
||||
- udp_start_port_range
|
||||
- udp_end_port_range
|
||||
|
||||
@@ -174,6 +179,10 @@ class Qemu(IModule):
|
||||
self._console_start_port_range = request["console_start_port_range"]
|
||||
self._console_end_port_range = request["console_end_port_range"]
|
||||
|
||||
if "monitor_start_port_range" in request and "monitor_end_port_range" in request:
|
||||
self._monitor_start_port_range = request["monitor_start_port_range"]
|
||||
self._monitor_end_port_range = request["monitor_end_port_range"]
|
||||
|
||||
if "udp_start_port_range" in request and "udp_end_port_range" in request:
|
||||
self._udp_start_port_range = request["udp_start_port_range"]
|
||||
self._udp_end_port_range = request["udp_end_port_range"]
|
||||
@@ -191,6 +200,7 @@ class Qemu(IModule):
|
||||
|
||||
Optional request parameters:
|
||||
- console (QEMU VM console port)
|
||||
- monitor (QEMU VM monitor port)
|
||||
|
||||
Response parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
@@ -207,6 +217,7 @@ class Qemu(IModule):
|
||||
name = request["name"]
|
||||
qemu_path = request["qemu_path"]
|
||||
console = request.get("console")
|
||||
monitor = request.get("monitor")
|
||||
qemu_id = request.get("qemu_id")
|
||||
|
||||
try:
|
||||
@@ -218,7 +229,11 @@ class Qemu(IModule):
|
||||
console,
|
||||
self._console_host,
|
||||
self._console_start_port_range,
|
||||
self._console_end_port_range)
|
||||
self._console_end_port_range,
|
||||
monitor,
|
||||
self._monitor_host,
|
||||
self._monitor_start_port_range,
|
||||
self._monitor_end_port_range)
|
||||
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
@@ -601,14 +616,14 @@ class Qemu(IModule):
|
||||
if sys.platform.startswith("win"):
|
||||
return ""
|
||||
try:
|
||||
output = subprocess.check_output([qemu_path, "--version"])
|
||||
match = re.search("QEMU emulator version ([0-9a-z\-\.]+)", output.decode("utf-8"))
|
||||
output = subprocess.check_output([qemu_path, "-version"])
|
||||
match = re.search("version\s+([0-9a-z\-\.]+)", output.decode("utf-8"))
|
||||
if match:
|
||||
version = match.group(1)
|
||||
return version
|
||||
else:
|
||||
raise QemuError("Could not determine the Qemu version for {}".format(qemu_path))
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
except subprocess.SubprocessError as e:
|
||||
raise QemuError("Error while looking for the Qemu version: {}".format(e))
|
||||
|
||||
@IModule.route("qemu.qemu_list")
|
||||
@@ -625,7 +640,13 @@ class Qemu(IModule):
|
||||
# look for Qemu binaries in the current working directory and $PATH
|
||||
if sys.platform.startswith("win"):
|
||||
# add specific Windows paths
|
||||
paths.append(os.path.join(os.getcwd(), "qemu"))
|
||||
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.append(os.path.join(exec_dir, f))
|
||||
|
||||
if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
|
||||
paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
|
||||
if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]):
|
||||
@@ -633,15 +654,18 @@ class Qemu(IModule):
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# add specific locations on Mac OS X regardless of what's in $PATH
|
||||
paths.extend(["/usr/local/bin", "/opt/local/bin"])
|
||||
if hasattr(sys, "frozen"):
|
||||
paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/")))
|
||||
for path in paths:
|
||||
try:
|
||||
for f in os.listdir(path):
|
||||
if (f.startswith("qemu-system") or f == "qemu" or f == "qemu.exe") and os.access(os.path.join(path, f), os.X_OK):
|
||||
if (f.startswith("qemu-system") 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)):
|
||||
qemu_path = os.path.join(path, f)
|
||||
version = self._get_qemu_version(qemu_path)
|
||||
qemus.append({"path": qemu_path, "version": version})
|
||||
except (OSError, QemuError) as e:
|
||||
log.warn("Could not find QEMU version for {}: {}".format(path, e))
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
response = {"qemus": qemus}
|
||||
|
||||
@@ -19,12 +19,16 @@
|
||||
QEMU VM instance.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
import subprocess
|
||||
import shlex
|
||||
import ntpath
|
||||
import telnetlib
|
||||
import time
|
||||
import re
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3dms.cloud.rackspace_ctrl import get_provider
|
||||
@@ -51,10 +55,15 @@ class QemuVM(object):
|
||||
:param console_host: IP address to bind for console connections
|
||||
:param console_start_port_range: TCP console port range start
|
||||
:param console_end_port_range: TCP console port range end
|
||||
:param monitor: TCP monitor port
|
||||
:param monitor_host: IP address to bind for monitor connections
|
||||
:param monitor_start_port_range: TCP monitor port range start
|
||||
:param monitor_end_port_range: TCP monitor port range end
|
||||
"""
|
||||
|
||||
_instances = []
|
||||
_allocated_console_ports = []
|
||||
_allocated_monitor_ports = []
|
||||
|
||||
def __init__(self,
|
||||
name,
|
||||
@@ -65,7 +74,11 @@ class QemuVM(object):
|
||||
console=None,
|
||||
console_host="0.0.0.0",
|
||||
console_start_port_range=5001,
|
||||
console_end_port_range=5500):
|
||||
console_end_port_range=5500,
|
||||
monitor=None,
|
||||
monitor_host="0.0.0.0",
|
||||
monitor_start_port_range=5501,
|
||||
monitor_end_port_range=6000):
|
||||
|
||||
if not qemu_id:
|
||||
self._id = 0
|
||||
@@ -89,10 +102,14 @@ class QemuVM(object):
|
||||
self._command = []
|
||||
self._started = False
|
||||
self._process = None
|
||||
self._cpulimit_process = None
|
||||
self._stdout_file = ""
|
||||
self._console_host = console_host
|
||||
self._console_start_port_range = console_start_port_range
|
||||
self._console_end_port_range = console_end_port_range
|
||||
self._monitor_host = monitor_host
|
||||
self._monitor_start_port_range = monitor_start_port_range
|
||||
self._monitor_end_port_range = monitor_end_port_range
|
||||
self._cloud_path = None
|
||||
|
||||
# QEMU settings
|
||||
@@ -102,11 +119,15 @@ class QemuVM(object):
|
||||
self._options = ""
|
||||
self._ram = 256
|
||||
self._console = console
|
||||
self._monitor = monitor
|
||||
self._ethernet_adapters = []
|
||||
self._adapter_type = "e1000"
|
||||
self._initrd = ""
|
||||
self._kernel_image = ""
|
||||
self._kernel_command_line = ""
|
||||
self._legacy_networking = False
|
||||
self._cpu_throttling = 0 # means no CPU throttling
|
||||
self._process_priority = "low"
|
||||
|
||||
working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id))
|
||||
|
||||
@@ -130,6 +151,20 @@ class QemuVM(object):
|
||||
raise QemuError("Console port {} is already used by another QEMU VM".format(console))
|
||||
self._allocated_console_ports.append(self._console)
|
||||
|
||||
if not self._monitor:
|
||||
# allocate a monitor port
|
||||
try:
|
||||
self._monitor = find_unused_port(self._monitor_start_port_range,
|
||||
self._monitor_end_port_range,
|
||||
self._monitor_host,
|
||||
ignore_ports=self._allocated_monitor_ports)
|
||||
except Exception as e:
|
||||
raise QemuError(e)
|
||||
|
||||
if self._monitor in self._allocated_monitor_ports:
|
||||
raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor))
|
||||
self._allocated_monitor_ports.append(self._monitor)
|
||||
|
||||
self.adapters = 1 # creates 1 adapter by default
|
||||
log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name,
|
||||
id=self._id))
|
||||
@@ -150,9 +185,14 @@ class QemuVM(object):
|
||||
"adapters": self.adapters,
|
||||
"adapter_type": self._adapter_type,
|
||||
"console": self._console,
|
||||
"monitor": self._monitor,
|
||||
"initrd": self._initrd,
|
||||
"kernel_image": self._kernel_image,
|
||||
"kernel_command_line": self._kernel_command_line}
|
||||
"kernel_command_line": self._kernel_command_line,
|
||||
"legacy_networking": self._legacy_networking,
|
||||
"cpu_throttling": self._cpu_throttling,
|
||||
"process_priority": self._process_priority
|
||||
}
|
||||
|
||||
return qemu_defaults
|
||||
|
||||
@@ -174,6 +214,7 @@ class QemuVM(object):
|
||||
|
||||
cls._instances.clear()
|
||||
cls._allocated_console_ports.clear()
|
||||
cls._allocated_monitor_ports.clear()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -258,6 +299,35 @@ class QemuVM(object):
|
||||
id=self._id,
|
||||
port=console))
|
||||
|
||||
@property
|
||||
def monitor(self):
|
||||
"""
|
||||
Returns the TCP monitor port.
|
||||
|
||||
:returns: monitor port (integer)
|
||||
"""
|
||||
|
||||
return self._monitor
|
||||
|
||||
@monitor.setter
|
||||
def monitor(self, monitor):
|
||||
"""
|
||||
Sets the TCP monitor port.
|
||||
|
||||
:param monitor: monitor port (integer)
|
||||
"""
|
||||
|
||||
if monitor in self._allocated_monitor_ports:
|
||||
raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor))
|
||||
|
||||
self._allocated_monitor_ports.remove(self._monitor)
|
||||
self._monitor = monitor
|
||||
self._allocated_monitor_ports.append(self._monitor)
|
||||
|
||||
log.info("QEMU VM {name} [id={id}]: monitor port set to {port}".format(name=self._name,
|
||||
id=self._id,
|
||||
port=monitor))
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this QEMU VM.
|
||||
@@ -267,8 +337,11 @@ class QemuVM(object):
|
||||
if self._id in self._instances:
|
||||
self._instances.remove(self._id)
|
||||
|
||||
if self.console and self.console in self._allocated_console_ports:
|
||||
self._allocated_console_ports.remove(self.console)
|
||||
if self._console and self._console in self._allocated_console_ports:
|
||||
self._allocated_console_ports.remove(self._console)
|
||||
|
||||
if self._monitor and self._monitor in self._allocated_monitor_ports:
|
||||
self._allocated_monitor_ports.remove(self._monitor)
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name,
|
||||
id=self._id))
|
||||
@@ -282,8 +355,11 @@ class QemuVM(object):
|
||||
if self._id in self._instances:
|
||||
self._instances.remove(self._id)
|
||||
|
||||
if self.console:
|
||||
self._allocated_console_ports.remove(self.console)
|
||||
if self._console:
|
||||
self._allocated_console_ports.remove(self._console)
|
||||
|
||||
if self._monitor:
|
||||
self._allocated_monitor_ports.remove(self._monitor)
|
||||
|
||||
try:
|
||||
shutil.rmtree(self._working_dir)
|
||||
@@ -437,6 +513,80 @@ class QemuVM(object):
|
||||
id=self._id,
|
||||
adapter_type=adapter_type))
|
||||
|
||||
@property
|
||||
def legacy_networking(self):
|
||||
"""
|
||||
Returns either QEMU legacy networking commands are used.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._legacy_networking
|
||||
|
||||
@legacy_networking.setter
|
||||
def legacy_networking(self, legacy_networking):
|
||||
"""
|
||||
Sets either QEMU legacy networking commands are used.
|
||||
|
||||
:param legacy_networking: boolean
|
||||
"""
|
||||
|
||||
if legacy_networking:
|
||||
log.info("QEMU VM {name} [id={id}] has enabled legacy networking".format(name=self._name, id=self._id))
|
||||
else:
|
||||
log.info("QEMU VM {name} [id={id}] has disabled legacy networking".format(name=self._name, id=self._id))
|
||||
self._legacy_networking = legacy_networking
|
||||
|
||||
@property
|
||||
def cpu_throttling(self):
|
||||
"""
|
||||
Returns the percentage of CPU allowed.
|
||||
|
||||
:returns: integer
|
||||
"""
|
||||
|
||||
return self._cpu_throttling
|
||||
|
||||
@cpu_throttling.setter
|
||||
def cpu_throttling(self, cpu_throttling):
|
||||
"""
|
||||
Sets the percentage of CPU allowed.
|
||||
|
||||
:param cpu_throttling: integer
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the percentage of CPU allowed to {cpu}".format(name=self._name,
|
||||
id=self._id,
|
||||
cpu=cpu_throttling))
|
||||
self._cpu_throttling = cpu_throttling
|
||||
self._stop_cpulimit()
|
||||
if cpu_throttling:
|
||||
self._set_cpu_throttling()
|
||||
|
||||
@property
|
||||
def process_priority(self):
|
||||
"""
|
||||
Returns the process priority.
|
||||
|
||||
:returns: string
|
||||
"""
|
||||
|
||||
return self._process_priority
|
||||
|
||||
@process_priority.setter
|
||||
def process_priority(self, process_priority):
|
||||
"""
|
||||
Sets the process priority.
|
||||
|
||||
:param process_priority: string
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the process priority to {priority}".format(name=self._name,
|
||||
id=self._id,
|
||||
priority=process_priority))
|
||||
self._process_priority = process_priority
|
||||
|
||||
|
||||
@property
|
||||
def ram(self):
|
||||
"""
|
||||
@@ -552,12 +702,96 @@ class QemuVM(object):
|
||||
kernel_command_line=kernel_command_line))
|
||||
self._kernel_command_line = kernel_command_line
|
||||
|
||||
def _set_process_priority(self):
|
||||
"""
|
||||
Changes the process priority
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
try:
|
||||
import win32api
|
||||
import win32con
|
||||
import win32process
|
||||
except ImportError:
|
||||
log.error("pywin32 must be installed to change the priority class for QEMU VM {}".format(self._name))
|
||||
else:
|
||||
log.info("setting QEMU VM {} priority class to BELOW_NORMAL".format(self._name))
|
||||
handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, self._process.pid)
|
||||
if self._process_priority == "realtime":
|
||||
priority = win32process.REALTIME_PRIORITY_CLASS
|
||||
elif self._process_priority == "very high":
|
||||
priority = win32process.HIGH_PRIORITY_CLASS
|
||||
elif self._process_priority == "high":
|
||||
priority = win32process.ABOVE_NORMAL_PRIORITY_CLASS
|
||||
elif self._process_priority == "low":
|
||||
priority = win32process.BELOW_NORMAL_PRIORITY_CLASS
|
||||
elif self._process_priority == "very low":
|
||||
priority = win32process.IDLE_PRIORITY_CLASS
|
||||
else:
|
||||
priority = win32process.NORMAL_PRIORITY_CLASS
|
||||
win32process.SetPriorityClass(handle, priority)
|
||||
else:
|
||||
if self._process_priority == "realtime":
|
||||
priority = -20
|
||||
elif self._process_priority == "very high":
|
||||
priority = -15
|
||||
elif self._process_priority == "high":
|
||||
priority = -5
|
||||
elif self._process_priority == "low":
|
||||
priority = 5
|
||||
elif self._process_priority == "very low":
|
||||
priority = 19
|
||||
else:
|
||||
priority = 0
|
||||
try:
|
||||
subprocess.call(['renice', '-n', str(priority), '-p', str(self._process.pid)])
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.error("could not change process priority for QEMU VM {}: {}".format(self._name, e))
|
||||
|
||||
def _stop_cpulimit(self):
|
||||
"""
|
||||
Stops the cpulimit process.
|
||||
"""
|
||||
|
||||
if self._cpulimit_process and self._cpulimit_process.poll() is None:
|
||||
self._cpulimit_process.kill()
|
||||
try:
|
||||
self._process.wait(3)
|
||||
except subprocess.TimeoutExpired:
|
||||
log.error("could not kill cpulimit process {}".format(self._cpulimit_process.pid))
|
||||
|
||||
def _set_cpu_throttling(self):
|
||||
"""
|
||||
Limits the CPU usage for current QEMU process.
|
||||
"""
|
||||
|
||||
if not self.is_running():
|
||||
return
|
||||
|
||||
try:
|
||||
if sys.platform.startswith("win") and hasattr(sys, "frozen"):
|
||||
cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe")
|
||||
else:
|
||||
cpulimit_exec = "cpulimit"
|
||||
subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self._working_dir)
|
||||
log.info("CPU throttled to {}%".format(self._cpu_throttling))
|
||||
except FileNotFoundError:
|
||||
raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling")
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise QemuError("Could not throttle CPU: {}".format(e))
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts this QEMU VM.
|
||||
"""
|
||||
|
||||
if not self.is_running():
|
||||
if self.is_running():
|
||||
|
||||
# resume the VM if it is paused
|
||||
self.resume()
|
||||
return
|
||||
|
||||
else:
|
||||
|
||||
if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path):
|
||||
found = False
|
||||
@@ -600,6 +834,29 @@ class QemuVM(object):
|
||||
log.debug("Download of {} complete.".format(src))
|
||||
self.hdb_disk_image = dst
|
||||
|
||||
if self.initrd != "":
|
||||
_, filename = ntpath.split(self.initrd)
|
||||
src = '{}/{}'.format(self.cloud_path, filename)
|
||||
dst = os.path.join(self.working_dir, filename)
|
||||
if not os.path.isfile(dst):
|
||||
cloud_settings = Config.instance().cloud_settings()
|
||||
provider = get_provider(cloud_settings)
|
||||
log.debug("Downloading file from {} to {}...".format(src, dst))
|
||||
provider.download_file(src, dst)
|
||||
log.debug("Download of {} complete.".format(src))
|
||||
self.initrd = dst
|
||||
if self.kernel_image != "":
|
||||
_, filename = ntpath.split(self.kernel_image)
|
||||
src = '{}/{}'.format(self.cloud_path, filename)
|
||||
dst = os.path.join(self.working_dir, filename)
|
||||
if not os.path.isfile(dst):
|
||||
cloud_settings = Config.instance().cloud_settings()
|
||||
provider = get_provider(cloud_settings)
|
||||
log.debug("Downloading file from {} to {}...".format(src, dst))
|
||||
provider.download_file(src, dst)
|
||||
log.debug("Download of {} complete.".format(src))
|
||||
self.kernel_image = dst
|
||||
|
||||
self._command = self._build_command()
|
||||
try:
|
||||
log.info("starting QEMU: {}".format(self._command))
|
||||
@@ -612,11 +869,15 @@ class QemuVM(object):
|
||||
cwd=self._working_dir)
|
||||
log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid))
|
||||
self._started = True
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
stdout = self.read_stdout()
|
||||
log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
|
||||
raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
|
||||
|
||||
self._set_process_priority()
|
||||
if self._cpu_throttling:
|
||||
self._set_cpu_throttling()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops this QEMU VM.
|
||||
@@ -635,27 +896,88 @@ class QemuVM(object):
|
||||
self._process.pid))
|
||||
self._process = None
|
||||
self._started = False
|
||||
self._stop_cpulimit()
|
||||
|
||||
def _control_vm(self, command, expected=None, timeout=30):
|
||||
"""
|
||||
Executes a command with QEMU monitor when this VM is running.
|
||||
|
||||
:param command: QEMU monitor command (e.g. info status, stop etc.)
|
||||
:param timeout: how long to wait for QEMU monitor
|
||||
|
||||
:returns: result of the command (Match object or None)
|
||||
"""
|
||||
|
||||
result = None
|
||||
if self.is_running() and self._monitor:
|
||||
log.debug("Execute QEMU monitor command: {}".format(command))
|
||||
try:
|
||||
tn = telnetlib.Telnet(self._monitor_host, self._monitor, timeout=timeout)
|
||||
except OSError as e:
|
||||
log.warn("Could not connect to QEMU monitor: {}".format(e))
|
||||
return result
|
||||
try:
|
||||
tn.write(command.encode('ascii') + b"\n")
|
||||
time.sleep(0.1)
|
||||
except OSError as e:
|
||||
log.warn("Could not write to QEMU monitor: {}".format(e))
|
||||
tn.close()
|
||||
return result
|
||||
if expected:
|
||||
try:
|
||||
ind, match, dat = tn.expect(list=expected, timeout=timeout)
|
||||
if match:
|
||||
result = match
|
||||
except EOFError as e:
|
||||
log.warn("Could not read from QEMU monitor: {}".format(e))
|
||||
tn.close()
|
||||
return result
|
||||
|
||||
def _get_vm_status(self):
|
||||
"""
|
||||
Returns this VM suspend status (running|paused)
|
||||
|
||||
:returns: status (string)
|
||||
"""
|
||||
|
||||
result = None
|
||||
|
||||
match = self._control_vm("info status", [b"running", b"paused"])
|
||||
if match:
|
||||
result = match.group(0).decode('ascii')
|
||||
return result
|
||||
|
||||
def suspend(self):
|
||||
"""
|
||||
Suspends this QEMU VM.
|
||||
"""
|
||||
|
||||
pass
|
||||
vm_status = self._get_vm_status()
|
||||
if vm_status == "running":
|
||||
self._control_vm("stop")
|
||||
log.debug("QEMU VM has been suspended")
|
||||
else:
|
||||
log.info("QEMU VM is not running to be suspended, current status is {}".format(vm_status))
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
Reloads this QEMU VM.
|
||||
"""
|
||||
|
||||
pass
|
||||
self._control_vm("system_reset")
|
||||
log.debug("QEMU VM has been reset")
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resumes this QEMU VM.
|
||||
"""
|
||||
|
||||
pass
|
||||
vm_status = self._get_vm_status()
|
||||
if vm_status == "paused":
|
||||
self._control_vm("cont")
|
||||
log.debug("QEMU VM has been resumed")
|
||||
else:
|
||||
log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status))
|
||||
|
||||
def port_add_nio_binding(self, adapter_id, nio):
|
||||
"""
|
||||
@@ -671,6 +993,25 @@ class QemuVM(object):
|
||||
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
|
||||
adapter_id=adapter_id))
|
||||
|
||||
if self.is_running():
|
||||
# dynamically configure an UDP tunnel on the QEMU VM adapter
|
||||
if nio and isinstance(nio, NIO_UDP):
|
||||
if self._legacy_networking:
|
||||
self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id))
|
||||
self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id,
|
||||
adapter_id,
|
||||
nio.lport,
|
||||
nio.rport,
|
||||
nio.rhost))
|
||||
else:
|
||||
#FIXME: does it work? very undocumented feature...
|
||||
self._control_vm("netdev_del gns3-{}".format(adapter_id))
|
||||
self._control_vm("netdev_add socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id,
|
||||
nio.rhost,
|
||||
nio.rport,
|
||||
self._host,
|
||||
nio.lport))
|
||||
|
||||
adapter.add_nio(0, nio)
|
||||
log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name,
|
||||
id=self._id,
|
||||
@@ -692,6 +1033,16 @@ class QemuVM(object):
|
||||
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
|
||||
adapter_id=adapter_id))
|
||||
|
||||
if self.is_running():
|
||||
# dynamically disable the QEMU VM adapter
|
||||
if self._legacy_networking:
|
||||
self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id))
|
||||
self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id))
|
||||
else:
|
||||
#FIXME: does it work? very undocumented feature...
|
||||
self._control_vm("netdev_del gns3-{}".format(adapter_id))
|
||||
self._control_vm("netdev_add user,id=gns3-{}".format(adapter_id))
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
adapter.remove_nio(0)
|
||||
log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name,
|
||||
@@ -752,6 +1103,13 @@ class QemuVM(object):
|
||||
else:
|
||||
return []
|
||||
|
||||
def _monitor_options(self):
|
||||
|
||||
if self._monitor:
|
||||
return ["-monitor", "telnet:{}:{},server,nowait".format(self._monitor_host, self._monitor)]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _disk_options(self):
|
||||
|
||||
options = []
|
||||
@@ -769,6 +1127,11 @@ class QemuVM(object):
|
||||
|
||||
try:
|
||||
if self._hda_disk_image:
|
||||
if not os.path.isfile(self._hda_disk_image) or not os.path.exists(self._hda_disk_image):
|
||||
if os.path.islink(self._hda_disk_image):
|
||||
raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image)))
|
||||
else:
|
||||
raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image))
|
||||
hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2")
|
||||
if not os.path.exists(hda_disk):
|
||||
retcode = subprocess.call([qemu_img_path, "create", "-o",
|
||||
@@ -782,11 +1145,16 @@ class QemuVM(object):
|
||||
retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "128M"])
|
||||
log.info("{} returned with {}".format(qemu_img_path, retcode))
|
||||
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise QemuError("Could not create disk image {}".format(e))
|
||||
|
||||
options.extend(["-hda", hda_disk])
|
||||
if self._hdb_disk_image:
|
||||
if not os.path.isfile(self._hdb_disk_image) or not os.path.exists(self._hdb_disk_image):
|
||||
if os.path.islink(self._hdb_disk_image):
|
||||
raise QemuError("hdb disk image '{}' linked to '{}' is not accessible".format(self._hdb_disk_image, os.path.realpath(self._hdb_disk_image)))
|
||||
else:
|
||||
raise QemuError("hdb disk image '{}' is not accessible".format(self._hdb_disk_image))
|
||||
hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2")
|
||||
if not os.path.exists(hdb_disk):
|
||||
try:
|
||||
@@ -794,7 +1162,7 @@ class QemuVM(object):
|
||||
"backing_file={}".format(self._hdb_disk_image),
|
||||
"-f", "qcow2", hdb_disk])
|
||||
log.info("{} returned with {}".format(qemu_img_path, retcode))
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise QemuError("Could not create disk image {}".format(e))
|
||||
options.extend(["-hdb", hdb_disk])
|
||||
|
||||
@@ -804,8 +1172,18 @@ class QemuVM(object):
|
||||
|
||||
options = []
|
||||
if self._initrd:
|
||||
if not os.path.isfile(self._initrd) or not os.path.exists(self._initrd):
|
||||
if os.path.islink(self._initrd):
|
||||
raise QemuError("initrd file '{}' linked to '{}' is not accessible".format(self._initrd, os.path.realpath(self._initrd)))
|
||||
else:
|
||||
raise QemuError("initrd file '{}' is not accessible".format(self._initrd))
|
||||
options.extend(["-initrd", self._initrd])
|
||||
if self._kernel_image:
|
||||
if not os.path.isfile(self._kernel_image) or not os.path.exists(self._kernel_image):
|
||||
if os.path.islink(self._kernel_image):
|
||||
raise QemuError("kernel image '{}' linked to '{}' is not accessible".format(self._kernel_image, os.path.realpath(self._kernel_image)))
|
||||
else:
|
||||
raise QemuError("kernel image '{}' is not accessible".format(self._kernel_image))
|
||||
options.extend(["-kernel", self._kernel_image])
|
||||
if self._kernel_command_line:
|
||||
options.extend(["-append", self._kernel_command_line])
|
||||
@@ -819,16 +1197,30 @@ class QemuVM(object):
|
||||
for adapter in self._ethernet_adapters:
|
||||
#TODO: let users specify a base mac address
|
||||
mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id)
|
||||
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)])
|
||||
if self._legacy_networking:
|
||||
network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)])
|
||||
else:
|
||||
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)])
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
if nio and isinstance(nio, NIO_UDP):
|
||||
network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id,
|
||||
nio.rhost,
|
||||
nio.rport,
|
||||
self._host,
|
||||
nio.lport)])
|
||||
if self._legacy_networking:
|
||||
network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id,
|
||||
adapter_id,
|
||||
nio.lport,
|
||||
nio.rport,
|
||||
nio.rhost)])
|
||||
else:
|
||||
network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id,
|
||||
nio.rhost,
|
||||
nio.rport,
|
||||
self._host,
|
||||
nio.lport)])
|
||||
else:
|
||||
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)])
|
||||
if self._legacy_networking:
|
||||
network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)])
|
||||
else:
|
||||
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)])
|
||||
adapter_id += 1
|
||||
|
||||
return network_options
|
||||
@@ -845,6 +1237,7 @@ class QemuVM(object):
|
||||
command.extend(self._disk_options())
|
||||
command.extend(self._linux_boot_options())
|
||||
command.extend(self._serial_options())
|
||||
command.extend(self._monitor_options())
|
||||
additional_options = self._options.strip()
|
||||
if additional_options:
|
||||
command.extend(shlex.split(additional_options))
|
||||
|
||||
@@ -41,6 +41,12 @@ QEMU_CREATE_SCHEMA = {
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
"monitor": {
|
||||
"description": "monitor TCP port",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name", "qemu_path"],
|
||||
@@ -95,7 +101,7 @@ QEMU_UPDATE_SCHEMA = {
|
||||
"description": "number of adapters",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8,
|
||||
"maximum": 32,
|
||||
},
|
||||
"adapter_type": {
|
||||
"description": "QEMU adapter type",
|
||||
@@ -108,6 +114,12 @@ QEMU_UPDATE_SCHEMA = {
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
"monitor": {
|
||||
"description": "monitor TCP port",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
"initrd": {
|
||||
"description": "QEMU initrd path",
|
||||
"type": "string",
|
||||
@@ -124,8 +136,27 @@ QEMU_UPDATE_SCHEMA = {
|
||||
"description": "Path to the image in the cloud object store",
|
||||
"type": "string",
|
||||
},
|
||||
"legacy_networking": {
|
||||
"description": "Use QEMU legagy networking commands (-net syntax)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"cpu_throttling": {
|
||||
"description": "Percentage of CPU allowed for QEMU",
|
||||
"minimum": 0,
|
||||
"maximum": 800,
|
||||
"type": "integer",
|
||||
},
|
||||
"process_priority": {
|
||||
"description": "Process priority for QEMU",
|
||||
"enum": ["realtime",
|
||||
"very high",
|
||||
"high",
|
||||
"normal",
|
||||
"low",
|
||||
"very low"]
|
||||
},
|
||||
"options": {
|
||||
"description": "additional QEMU options",
|
||||
"description": "Additional QEMU options",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
@@ -350,7 +381,7 @@ QEMU_ADD_NIO_SCHEMA = {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8
|
||||
"maximum": 32
|
||||
},
|
||||
"nio": {
|
||||
"type": "object",
|
||||
@@ -384,7 +415,7 @@ QEMU_DELETE_NIO_SCHEMA = {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8
|
||||
"maximum": 32
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
|
||||
@@ -62,8 +62,13 @@ class VirtualBox(IModule):
|
||||
|
||||
# get the vboxmanage location
|
||||
self._vboxmanage_path = None
|
||||
if sys.platform.startswith("win") and "VBOX_INSTALL_PATH" in os.environ:
|
||||
self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
|
||||
if sys.platform.startswith("win"):
|
||||
if "VBOX_INSTALL_PATH" in os.environ:
|
||||
self._vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
|
||||
elif "VBOX_MSI_INSTALL_PATH" in os.environ:
|
||||
self._vboxmanage_path = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe")
|
||||
elif sys.platform.startswith("darwin"):
|
||||
self._vboxmanage_path = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
|
||||
else:
|
||||
config = Config.instance()
|
||||
vbox_config = config.get_section_config(name.upper())
|
||||
@@ -84,6 +89,8 @@ class VirtualBox(IModule):
|
||||
elif not os.access(self._vboxmanage_path, os.X_OK):
|
||||
log.warning("vboxmanage is not executable")
|
||||
|
||||
self._vbox_user = None
|
||||
|
||||
# a new process start when calling IModule
|
||||
IModule.__init__(self, name, *args, **kwargs)
|
||||
self._vbox_instances = {}
|
||||
@@ -202,6 +209,9 @@ class VirtualBox(IModule):
|
||||
if "vboxmanage_path" in request:
|
||||
self._vboxmanage_path = request["vboxmanage_path"]
|
||||
|
||||
if "vbox_user" in request:
|
||||
self._vbox_user = request["vbox_user"]
|
||||
|
||||
if "console_start_port_range" in request and "console_end_port_range" in request:
|
||||
self._console_start_port_range = request["console_start_port_range"]
|
||||
self._console_end_port_range = request["console_end_port_range"]
|
||||
@@ -249,6 +259,7 @@ class VirtualBox(IModule):
|
||||
raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?")
|
||||
|
||||
vbox_instance = VirtualBoxVM(self._vboxmanage_path,
|
||||
self._vbox_user,
|
||||
name,
|
||||
vmname,
|
||||
linked_clone,
|
||||
@@ -706,7 +717,7 @@ class VirtualBox(IModule):
|
||||
response = {"port_id": request["port_id"]}
|
||||
self.send_response(response)
|
||||
|
||||
def _execute_vboxmanage(self, command):
|
||||
def _execute_vboxmanage(self, user, command):
|
||||
"""
|
||||
Executes VBoxManage and return its result.
|
||||
|
||||
@@ -716,12 +727,14 @@ class VirtualBox(IModule):
|
||||
"""
|
||||
|
||||
try:
|
||||
result = subprocess.check_output(command, stderr=subprocess.STDOUT, universal_newlines=True, timeout=30)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if not user.strip() or sys.platform.startswith("win"):
|
||||
result = subprocess.check_output(command, stderr=subprocess.STDOUT, timeout=60)
|
||||
else:
|
||||
sudo_command = "sudo -i -u " + user.strip() + " " + " ".join(command)
|
||||
result = subprocess.check_output(sudo_command, stderr=subprocess.STDOUT, shell=True, timeout=60)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise VirtualBoxError("Could not execute VBoxManage {}".format(e))
|
||||
except subprocess.TimeoutExpired:
|
||||
raise VirtualBoxError("VBoxManage has timed out")
|
||||
return result
|
||||
return result.decode("utf-8", errors="ignore")
|
||||
|
||||
@IModule.route("virtualbox.vm_list")
|
||||
def vm_list(self, request):
|
||||
@@ -740,11 +753,16 @@ class VirtualBox(IModule):
|
||||
else:
|
||||
vboxmanage_path = self._vboxmanage_path
|
||||
|
||||
if request and "vbox_user" in request:
|
||||
vbox_user = request["vbox_user"]
|
||||
else:
|
||||
vbox_user = self._vbox_user
|
||||
|
||||
if not vboxmanage_path or not os.path.exists(vboxmanage_path):
|
||||
raise VirtualBoxError("Could not find VBoxManage, is VirtualBox correctly installed?")
|
||||
|
||||
command = [vboxmanage_path, "--nologo", "list", "vms"]
|
||||
result = self._execute_vboxmanage(command)
|
||||
result = self._execute_vboxmanage(vbox_user, command)
|
||||
except VirtualBoxError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
@@ -753,7 +771,13 @@ class VirtualBox(IModule):
|
||||
for line in result.splitlines():
|
||||
vmname, uuid = line.rsplit(' ', 1)
|
||||
vmname = vmname.strip('"')
|
||||
extra_data = self._execute_vboxmanage([vboxmanage_path, "getextradata", vmname, "GNS3/Clone"]).strip()
|
||||
if vmname == "<inaccessible>":
|
||||
continue # ignore inaccessible VMs
|
||||
try:
|
||||
extra_data = self._execute_vboxmanage(vbox_user, [vboxmanage_path, "getextradata", vmname, "GNS3/Clone"]).strip()
|
||||
except VirtualBoxError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
if not extra_data == "Value: yes":
|
||||
vms.append(vmname)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import re
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
@@ -64,6 +63,7 @@ class VirtualBoxVM(object):
|
||||
|
||||
def __init__(self,
|
||||
vboxmanage_path,
|
||||
vbox_user,
|
||||
name,
|
||||
vmname,
|
||||
linked_clone,
|
||||
@@ -95,6 +95,7 @@ class VirtualBoxVM(object):
|
||||
self._working_dir = None
|
||||
self._command = []
|
||||
self._vboxmanage_path = vboxmanage_path
|
||||
self._vbox_user = vbox_user
|
||||
self._started = False
|
||||
self._console_host = console_host
|
||||
self._console_start_port_range = console_start_port_range
|
||||
@@ -304,10 +305,10 @@ class VirtualBoxVM(object):
|
||||
hdd_file = os.path.join(self._working_dir, self._vmname, "Snapshots", hdd_info["hdd"])
|
||||
if os.path.exists(hdd_file):
|
||||
log.debug("reattaching hdd {}".format(hdd_file))
|
||||
self._storage_attach('--storagectl {} --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"],
|
||||
hdd_info["port"],
|
||||
hdd_info["device"],
|
||||
hdd_file))
|
||||
self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"],
|
||||
hdd_info["port"],
|
||||
hdd_info["device"],
|
||||
hdd_file))
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
@@ -327,13 +328,13 @@ class VirtualBoxVM(object):
|
||||
hdd_files = self._get_all_hdd_files()
|
||||
vm_info = self._get_vm_info()
|
||||
for entry, value in vm_info.items():
|
||||
match = re.search("^(\w+)\-(\d)\-(\d)$", entry)
|
||||
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:
|
||||
self._storage_attach("--storagectl {} --port {} --device {} --type hdd --medium none".format(controller, port, device))
|
||||
self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device))
|
||||
hdd_table.append(
|
||||
{
|
||||
"hdd": os.path.basename(value),
|
||||
@@ -428,8 +429,10 @@ class VirtualBoxVM(object):
|
||||
|
||||
if enable_remote_console:
|
||||
log.info("VirtualBox VM {name} [id={id}] has enabled the console".format(name=self._name, id=self._id))
|
||||
self._start_remote_console()
|
||||
else:
|
||||
log.info("VirtualBox VM {name} [id={id}] has disabled the console".format(name=self._name, id=self._id))
|
||||
self._stop_remote_console()
|
||||
self._enable_remote_console = enable_remote_console
|
||||
|
||||
@property
|
||||
@@ -451,6 +454,8 @@ class VirtualBoxVM(object):
|
||||
"""
|
||||
|
||||
log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname))
|
||||
if self._linked_clone:
|
||||
self._modify_vm('--name "{}"'.format(vmname))
|
||||
self._vmname = vmname
|
||||
|
||||
@property
|
||||
@@ -535,7 +540,7 @@ class VirtualBoxVM(object):
|
||||
id=self._id,
|
||||
adapter_type=adapter_type))
|
||||
|
||||
def _execute(self, subcommand, args, timeout=30):
|
||||
def _execute(self, subcommand, args, timeout=60):
|
||||
"""
|
||||
Executes a command with VBoxManage.
|
||||
|
||||
@@ -549,18 +554,23 @@ class VirtualBoxVM(object):
|
||||
command = [self._vboxmanage_path, "--nologo", subcommand]
|
||||
command.extend(args)
|
||||
log.debug("Execute vboxmanage command: {}".format(command))
|
||||
user = self._vbox_user
|
||||
try:
|
||||
result = subprocess.check_output(command, stderr=subprocess.STDOUT, universal_newlines=True, timeout=timeout)
|
||||
if not user.strip() or sys.platform.startswith("win") or sys.platform.startswith("darwin"):
|
||||
result = subprocess.check_output(command, stderr=subprocess.STDOUT, timeout=timeout)
|
||||
else:
|
||||
sudo_command = "sudo -i -u " + user.strip() + " " + " ".join(command)
|
||||
result = subprocess.check_output(sudo_command, stderr=subprocess.STDOUT, shell=True, timeout=timeout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.output:
|
||||
# only the first line of the output is useful
|
||||
virtualbox_error = e.output.splitlines()[0]
|
||||
virtualbox_error = e.output.decode("utf-8").splitlines()[0]
|
||||
raise VirtualBoxError("{}".format(virtualbox_error))
|
||||
else:
|
||||
raise VirtualBoxError("{}".format(e))
|
||||
except subprocess.TimeoutExpired:
|
||||
raise VirtualBoxError("VBoxManage has timed out")
|
||||
return result.splitlines()
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise VirtualBoxError("Could not execute VBoxManage: {}".format(e))
|
||||
return result.decode("utf-8", errors="ignore").splitlines()
|
||||
|
||||
def _get_vm_info(self):
|
||||
"""
|
||||
@@ -770,9 +780,56 @@ class VirtualBoxVM(object):
|
||||
"--register"]
|
||||
|
||||
result = self._execute("clonevm", args)
|
||||
log.debug("cloned VirtualBox VM: {}".format(result))
|
||||
|
||||
self._vmname = self._name
|
||||
self._execute("setextradata", [self._vmname, "GNS3/Clone", "yes"])
|
||||
log.debug("cloned VirtualBox VM: {}".format(result))
|
||||
|
||||
args = [self._name, "take", "reset"]
|
||||
result = self._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))
|
||||
self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._console_host, self._console)
|
||||
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))
|
||||
self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._console_host, self._console)
|
||||
self._telnet_server_thread.start()
|
||||
|
||||
def _stop_remote_console(self):
|
||||
"""
|
||||
Stops remote console support for this VM.
|
||||
"""
|
||||
|
||||
if self._telnet_server_thread:
|
||||
self._telnet_server_thread.stop()
|
||||
self._telnet_server_thread.join(timeout=3)
|
||||
if self._telnet_server_thread.isAlive():
|
||||
log.warn("Serial pire 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
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
@@ -805,43 +862,14 @@ class VirtualBoxVM(object):
|
||||
self._execute("guestproperty", ["set", self._vmname, "ProjectDirInGNS3", self._working_dir])
|
||||
|
||||
if self._enable_remote_console:
|
||||
# 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))
|
||||
self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._console_host, self._console)
|
||||
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))
|
||||
self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._console_host, self._console)
|
||||
self._telnet_server_thread.start()
|
||||
self._start_remote_console()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops this VirtualBox VM.
|
||||
"""
|
||||
|
||||
if self._telnet_server_thread:
|
||||
self._telnet_server_thread.stop()
|
||||
self._telnet_server_thread.join(timeout=3)
|
||||
if self._telnet_server_thread.isAlive():
|
||||
log.warn("Serial pire 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
|
||||
|
||||
self._stop_remote_console()
|
||||
vm_state = self._get_vm_state()
|
||||
if vm_state == "running" or vm_state == "paused" or vm_state == "stuck":
|
||||
# power off the VM
|
||||
|
||||
@@ -346,7 +346,7 @@ class VPCSDevice(object):
|
||||
raise VPCSError("VPCS executable version must be >= 0.5b1")
|
||||
else:
|
||||
raise VPCSError("Could not determine the VPCS version for {}".format(self._path))
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise VPCSError("Error while looking for the VPCS version: {}".format(e))
|
||||
|
||||
def start(self):
|
||||
@@ -386,7 +386,7 @@ class VPCSDevice(object):
|
||||
creationflags=flags)
|
||||
log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid))
|
||||
self._started = True
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
vpcs_stdout = self.read_vpcs_stdout()
|
||||
log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
|
||||
raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
|
||||
|
||||
@@ -184,7 +184,6 @@ class Server(object):
|
||||
except KeyError:
|
||||
log.info("Missing cloud.conf - disabling HTTP auth and SSL")
|
||||
|
||||
|
||||
router = self._create_zmq_router()
|
||||
# Add our JSON-RPC Websocket handler to Tornado
|
||||
self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))])
|
||||
@@ -208,6 +207,7 @@ class Server(object):
|
||||
|
||||
if parse_version(tornado.version) >= parse_version("3.1"):
|
||||
kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit
|
||||
|
||||
tornado_app.listen(self._port, **kwargs)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EADDRINUSE: # socket already in use
|
||||
|
||||
@@ -44,7 +44,7 @@ import uuid
|
||||
|
||||
SCRIPT_NAME = os.path.basename(__file__)
|
||||
|
||||
#Is the full path when used as an import
|
||||
# This is the full path when used as an import
|
||||
SCRIPT_PATH = os.path.dirname(__file__)
|
||||
|
||||
if not SCRIPT_PATH:
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "1.2"
|
||||
__version_info__ = (1, 2, 0, 0)
|
||||
__version__ = "1.2.3"
|
||||
__version_info__ = (1, 2, 3, 0)
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- 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 ws4py.client.threadedclient import WebSocketClient
|
||||
|
||||
|
||||
class WSClient(WebSocketClient):
|
||||
|
||||
def opened(self):
|
||||
|
||||
print("Connection successful with {}:{}".format(self.host, self.port))
|
||||
|
||||
self.send('{"jsonrpc": 2.0, "method": "dynamips.settings", "params": {"path": "/usr/local/bin/dynamips", "allocate_hypervisor_per_device": true, "working_dir": "/tmp/gns3-1b4grwm3-files", "udp_end_port_range": 20000, "sparse_memory_support": true, "allocate_hypervisor_per_ios_image": true, "aux_start_port_range": 2501, "use_local_server": true, "hypervisor_end_port_range": 7700, "aux_end_port_range": 3000, "mmap_support": true, "console_start_port_range": 2001, "console_end_port_range": 2500, "hypervisor_start_port_range": 7200, "ghost_ios_support": true, "memory_usage_limit_per_hypervisor": 1024, "jit_sharing_support": false, "udp_start_port_range": 10001}}')
|
||||
self.send('{"jsonrpc": 2.0, "method": "dynamips.vm.create", "id": "e8caf5be-de3d-40dd-80b9-ab6df8029570", "params": {"image": "/home/grossmj/GNS3/images/IOS/c3725-advipservicesk9-mz.124-15.T14.image", "name": "R1", "platform": "c3725", "ram": 256}}')
|
||||
|
||||
def closed(self, code, reason=None):
|
||||
|
||||
print("Closed down. Code: {} Reason: {}".format(code, reason))
|
||||
|
||||
def received_message(self, m):
|
||||
|
||||
print(m)
|
||||
if len(m) == 175:
|
||||
self.close(reason='Bye bye')
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
ws = WSClient('ws://localhost:8000/', protocols=['http-only', 'chat'])
|
||||
ws.connect()
|
||||
ws.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
ws.close()
|
||||
@@ -14,7 +14,7 @@ def test_port(hypervisor):
|
||||
|
||||
def test_host(hypervisor):
|
||||
|
||||
assert hypervisor.host == "127.0.0.1"
|
||||
assert hypervisor.host == "0.0.0.0"
|
||||
|
||||
|
||||
def test_working_dir(hypervisor):
|
||||
|
||||
Reference in new issue
Block a user