Compare commits

..

50 Commits

Author SHA1 Message Date
f854752c84 Bump to version 1.0-beta4. 2014-10-14 17:20:28 -06:00
4195bdc7dd Auto idle-pc feature and improvements/bug fixes for GNS3 preferences. 2014-10-13 19:53:17 -06:00
b68c11e33e Bump version to 1.0-beta4.dev2 2014-10-09 21:26:07 -06:00
b3e86be182 Merge pull request #37 from planctechnologies/server_security2
Add secure communication between gui and server 2/2
2014-09-30 11:24:28 -06:00
5802c2b9f5 Merge pull request #36 from planctechnologies/server_security
Add secure communication between gui and server 1/2
2014-09-30 11:23:04 -06:00
83cef60c0f Merge pull request #35 from planctechnologies/deadman2
Deadman switch support 2/2
2014-09-30 11:20:39 -06:00
e39c93c91a Merge pull request #34 from planctechnologies/deadman
Deadman switch support 1/2
2014-09-30 11:20:01 -06:00
1a96a150bc Fix shemas for QEMU. 2014-09-30 11:15:15 -06:00
c66fbbdb36 Merge improved ssl and auth support 2014-09-29 18:24:28 -06:00
03fb75437b Add cert and auth support to gns3server. 2014-09-29 18:19:35 -06:00
3833803244 Copy deadman switch code from old repository into gns3server. 2014-09-29 18:14:07 -06:00
7c446796fe gns3server now controls the deadman switch. 2014-09-29 18:09:16 -06:00
ee88d6f808 Merge branch 'master' into dev 2014-09-29 17:24:52 -06:00
46495b9265 Merge remote-tracking branch 'origin/master' 2014-09-28 18:23:51 -06:00
a8193fa063 Split the PATH environment variable using os.pathsep 2014-09-28 18:23:27 -06:00
e3eecb6584 Merge pull request #33 from dlintott/fix_broken_test
Fix test for dynamips c7200 NPE (Default is now NPE-400)
2014-09-27 15:07:20 -06:00
35f3434b2f Merge pull request #32 from noplay/osx_installation
Instruction for development on MacOS X
2014-09-27 13:56:28 -06:00
20dc779fd8 Fix test for dynamips c7200 NPE (Default is now NPE-400) 2014-09-27 19:27:26 +01:00
04f670cb50 Instruction for development on MacOS X 2014-09-27 19:59:58 +02:00
6dce005594 Bump to version 1.0-beta1.dev1. 2014-09-24 11:14:28 -06:00
a49f107af2 Bump to version 1.0-beta3. 2014-09-24 11:01:33 -06:00
e7141685cc Tweaks to support Qemu on Windows. 2014-09-23 21:38:51 -06:00
aca9e0de56 Qemu integration stage 2, support for ASA and IDS. 2014-09-22 21:24:55 -06:00
3b465890b6 Increase sleep to work around Rackspace slowness 2014-09-22 09:10:30 -06:00
cf59240bef Bugfixes with cloud server communication 2014-09-21 21:41:51 -06:00
d1715baae1 Base QEMU support. 2014-09-18 15:47:43 -06:00
b132c901c9 Disabling auth from version string 2014-09-18 20:39:12 +00:00
a0e2fe551a Added web user and password to start_server output 2014-09-15 21:25:09 -06:00
800d4d91f9 Merge pull request #5 from planctechnologies/gns3-87-bugfixes
Gns-87 bugfixes
2014-09-11 14:48:26 -06:00
6c6c9200e4 Add CN support to cert as command line arg 2014-09-08 22:07:33 -06:00
4fa87005bc Enabled HTTP Auth, SSL and DMS disabling based on cloud.conf availability 2014-09-08 21:51:56 -06:00
17e4b51d18 Testing out dummy config 2014-09-08 20:45:36 -06:00
6421367259 Importing changeset from gns3dms repo 2014-09-08 15:35:22 +00:00
6ff2c654d9 Merge pull request #4 from planctechnologies/gns3-87
Gns3 87 - add server security and startup scripts
2014-09-08 09:25:28 -06:00
f876a862c4 GNS3 server will now create the heardbeat file durining initialization 2014-09-06 21:13:09 -06:00
ef492d4690 Update gns3dms to support cloud.conf 2014-09-06 20:46:06 -06:00
36e539382c Added support for cloud.conf file and startup script 2014-09-06 00:51:43 -06:00
6f9e0f6d2e Moved certs to .config 2014-09-03 22:19:59 -06:00
b84dda3c8e HTTP auth added to file_upload and jsonrpc 2014-09-03 22:12:34 -06:00
e2f3d2aca8 Pull from dev 2014-09-03 20:39:02 +00:00
382e693fc8 Added authentication handler for basic auth check 2014-09-03 00:05:06 -06:00
a95cc678e9 Added server.py ssl mode dependant on cert existence 2014-09-02 22:33:45 -06:00
bcf0aae531 Added HOME support and cert dir to create_cert script 2014-09-02 22:17:06 -06:00
b483f87c2f Bump version to 1.0-beta3.dev1. 2014-09-02 15:49:39 -06:00
174013da80 Merge pull request #2 from planctechnologies/deadman
GNS-42 - Move deadman switch into gns3server codebase
2014-08-31 23:50:41 -06:00
99a8f5f21a Added create_cert.sh and ssl_options to enable SSL 2014-08-30 01:32:48 -06:00
5e72fcbe14 GNS-42 - Move deadman switch into gns3server codebase 2014-08-29 18:05:56 +00:00
e688d96c36 Add start, stop, restart and heartbeat handling to DeadMan module 2014-08-28 23:06:28 -06:00
3845cab84b Adding initial module 2014-08-28 22:09:38 -06:00
98e3a2e088 Updated requirements.txt 2014-08-28 22:01:23 -06:00
53 changed files with 4086 additions and 40 deletions

View File

@ -34,4 +34,19 @@ Please use our all-in-one installer.
Mac OS X
--------
Please use our DMG package.
Please use our DMG package for a simple installation.
If you want to test the current git version or contribute to the project.
You can follow this instructions with virtualenwrapper: http://virtualenvwrapper.readthedocs.org/
and homebrew: http://brew.sh/.
.. code:: bash
brew install python3
mkvirtualenv gns3-server --python=/usr/local/bin/python3.4
python3 setup.py install
gns3server

View File

@ -101,8 +101,9 @@ def main():
startup_script)
passwd = uuid.uuid4().hex
instance.change_password(passwd)
# wait for the password change to be processed
sleep(POLL_SEC)
# wait for the password change to be processed. Continuing while
# a password change is processing will cause image creation to fail.
sleep(POLL_SEC*6)
env.host_string = str(instance.accessIPv4)
env.user = "root"

View File

@ -11,9 +11,7 @@ mkdir -p /opt/gns3
pushd /opt/gns3
git clone --branch ${git_branch} ${git_url}
cd gns3-server
pip3 install tornado
pip3 install pyzmq
pip3 install jsonschema
pip3 install -r dev-requirements.txt
python3 ./setup.py install
${rc_local}

26
gns3dms/__init__.py Normal file
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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
from .version import __version__

View File

View File

@ -0,0 +1,179 @@
# -*- 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/>.
"""
Base cloud controller class.
Base class for interacting with Cloud APIs to create and manage cloud
instances.
"""
from libcloud.compute.base import NodeAuthSSHKey
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
from .exceptions import Unauthorized, ApiError
def parse_exception(exception):
"""
Parse the exception to separate the HTTP status code from the text.
Libcloud raises many exceptions of the form:
Exception("<http status code> <http error> <reponse body>")
in lieu of raising specific incident-based exceptions.
"""
e_str = str(exception)
try:
status = int(e_str[0:3])
error_text = e_str[3:]
except ValueError:
status = None
error_text = e_str
return status, error_text
class BaseCloudCtrl(object):
""" Base class for interacting with a cloud provider API. """
http_status_to_exception = {
400: BadRequest,
401: Unauthorized,
404: ItemNotFound,
405: MethodNotAllowed,
413: OverLimit,
500: ApiError,
503: ServiceUnavailable
}
def __init__(self, username, api_key):
self.username = username
self.api_key = api_key
def _handle_exception(self, status, error_text, response_overrides=None):
""" Raise an exception based on the HTTP status. """
if response_overrides:
if status in response_overrides:
raise response_overrides[status](error_text)
raise self.http_status_to_exception[status](error_text)
def authenticate(self):
""" Validate cloud account credentials. Return boolean. """
raise NotImplementedError
def list_sizes(self):
""" Return a list of NodeSize objects. """
return self.driver.list_sizes()
def create_instance(self, name, size, image, keypair):
"""
Create a new instance with the supplied attributes.
Return a Node object.
"""
auth_key = NodeAuthSSHKey(keypair.public_key)
try:
return self.driver.create_node(
name=name,
size=size,
image=image,
auth=auth_key
)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
def delete_instance(self, instance):
""" Delete the specified instance. Returns True or False. """
try:
return self.driver.destroy_node(instance)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
def get_instance(self, instance):
""" Return a Node object representing the requested instance. """
for i in self.driver.list_nodes():
if i.id == instance.id:
return i
raise ItemNotFound("Instance not found")
def list_instances(self):
""" Return a list of instances in the current region. """
return self.driver.list_nodes()
def create_key_pair(self, name):
""" Create and return a new Key Pair. """
response_overrides = {
409: KeyPairExists
}
try:
return self.driver.create_key_pair(name)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text, response_overrides)
else:
raise e
def delete_key_pair(self, keypair):
""" Delete the keypair. Returns True or False. """
try:
return self.driver.delete_key_pair(keypair)
except Exception as e:
status, error_text = parse_exception(e)
if status:
self._handle_exception(status, error_text)
else:
raise e
def list_key_pairs(self):
""" Return a list of Key Pairs. """
return self.driver.list_key_pairs()

View File

@ -0,0 +1,45 @@
""" Exception classes for CloudCtrl classes. """
class ApiError(Exception):
""" Raised when the server returns 500 Compute Error. """
pass
class BadRequest(Exception):
""" Raised when the server returns 400 Bad Request. """
pass
class ComputeFault(Exception):
""" Raised when the server returns 400|500 Compute Fault. """
pass
class Forbidden(Exception):
""" Raised when the server returns 403 Forbidden. """
pass
class ItemNotFound(Exception):
""" Raised when the server returns 404 Not Found. """
pass
class KeyPairExists(Exception):
""" Raised when the server returns 409 Conflict Key pair exists. """
pass
class MethodNotAllowed(Exception):
""" Raised when the server returns 405 Method Not Allowed. """
pass
class OverLimit(Exception):
""" Raised when the server returns 413 Over Limit. """
pass
class ServerCapacityUnavailable(Exception):
""" Raised when the server returns 503 Server Capacity Uavailable. """
pass
class ServiceUnavailable(Exception):
""" Raised when the server returns 503 Service Unavailable. """
pass
class Unauthorized(Exception):
""" Raised when the server returns 401 Unauthorized. """
pass

View File

@ -0,0 +1,225 @@
# -*- 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/>.
""" Interacts with Rackspace API to create and manage cloud instances. """
from .base_cloud_ctrl import BaseCloudCtrl
import json
import requests
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
from libcloud.compute.providers import get_driver
from libcloud.compute.types import Provider
from .exceptions import ItemNotFound, ApiError
from ..version import __version__
import logging
log = logging.getLogger(__name__)
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
ENDPOINT_ARGS_MAP]
GNS3IAS_URL = 'http://localhost:8888' # TODO find a place for this value
class RackspaceCtrl(BaseCloudCtrl):
""" Controller class for interacting with Rackspace API. """
def __init__(self, username, api_key):
super(RackspaceCtrl, self).__init__(username, api_key)
# 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)
self.driver = None
self.region = None
self.instances = {}
self.authenticated = False
self.identity_ep = \
"https://identity.api.rackspacecloud.com/v2.0/tokens"
self.regions = []
self.token = None
def authenticate(self):
"""
Submit username and api key to API service.
If authentication is successful, set self.regions and self.token.
Return boolean.
"""
self.authenticated = False
if len(self.username) < 1:
return False
if len(self.api_key) < 1:
return False
data = json.dumps({
"auth": {
"RAX-KSKEY:apiKeyCredentials": {
"username": self.username,
"apiKey": self.api_key
}
}
})
headers = {
'Content-type': 'application/json',
'Accept': 'application/json'
}
response = self.post_fn(self.identity_ep, data=data, headers=headers)
if response.status_code == 200:
api_data = response.json()
self.token = self._parse_token(api_data)
if self.token:
self.authenticated = True
user_regions = self._parse_endpoints(api_data)
self.regions = self._make_region_list(user_regions)
else:
self.regions = []
self.token = None
response.connection.close()
return self.authenticated
def list_regions(self):
""" Return a list the regions available to the user. """
return self.regions
def _parse_endpoints(self, api_data):
"""
Parse the JSON-encoded data returned by the Identity Service API.
Return a list of regions available for Compute v2.
"""
region_codes = []
for ep_type in api_data['access']['serviceCatalog']:
if ep_type['name'] == "cloudServersOpenStack" \
and ep_type['type'] == "compute":
for ep in ep_type['endpoints']:
if ep['versionId'] == "2":
region_codes.append(ep['region'])
return region_codes
def _parse_token(self, api_data):
""" Parse the token from the JSON-encoded data returned by the API. """
try:
token = api_data['access']['token']['id']
except KeyError:
return None
return token
def _make_region_list(self, region_codes):
"""
Make a list of regions for use in the GUI.
Returns a list of key-value pairs in the form:
<API's Region Name>: <libcloud's Region Name>
eg,
[
{'DFW': 'dfw'}
{'ORD': 'ord'},
...
]
"""
region_list = []
for ep in ENDPOINT_ARGS_MAP:
if ENDPOINT_ARGS_MAP[ep]['region'] in region_codes:
region_list.append({ENDPOINT_ARGS_MAP[ep]['region']: ep})
return region_list
def set_region(self, region):
""" Set self.region and self.driver. Returns True or False. """
try:
self.driver = self.driver_cls(self.username, self.api_key,
region=region)
except ValueError:
return False
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 = GNS3IAS_URL+"/images/grant_access"
params = {
"user_id": username,
"user_region": region,
"gns3_version": gns3_version,
}
response = requests.get(endpoint, params=params)
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.username and self.region):
return []
try:
response = self._get_shared_images(self.username, self.region, __version__)
shared_images = json.loads(response)
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)

401
gns3dms/main.py Normal file
View File

@ -0,0 +1,401 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
"""
Monitors communication with the GNS3 client via tmp file. Will terminate the instance if
communication is lost.
"""
import os
import sys
import time
import getopt
import datetime
import logging
import signal
import configparser
from logging.handlers import *
from os.path import expanduser
SCRIPT_NAME = os.path.basename(__file__)
#Is the full path when used as an import
SCRIPT_PATH = os.path.dirname(__file__)
if not SCRIPT_PATH:
SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(
sys.argv[0])))
EXTRA_LIB = "%s/modules" % (SCRIPT_PATH)
sys.path.append(EXTRA_LIB)
from . import cloud
from rackspace_cloud import Rackspace
LOG_NAME = "gns3dms"
log = None
sys.path.append(EXTRA_LIB)
import daemon
my_daemon = None
usage = """
USAGE: %s
Options:
-d, --debug Enable debugging
-v, --verbose Enable verbose logging
-h, --help Display this menu :)
--cloud_api_key <api_key> Rackspace API key
--cloud_user_name
--instance_id ID of the Rackspace instance to terminate
--region Region of instance
--deadtime How long in seconds can the communication lose exist before we
shutdown this instance.
Default:
Example --deadtime=3600 (60 minutes)
--check-interval Defaults to --deadtime, used for debugging
--init-wait Inital wait time, how long before we start pulling the file.
Default: 300 (5 min)
Example --init-wait=300
--file The file we monitor for updates
-k Kill previous instance running in background
--background Run in background
""" % (SCRIPT_NAME)
# Parse cmd line options
def parse_cmd_line(argv):
"""
Parse command line arguments
argv: Pass in cmd line arguments
"""
short_args = "dvhk"
long_args = ("debug",
"verbose",
"help",
"cloud_user_name=",
"cloud_api_key=",
"instance_id=",
"region=",
"deadtime=",
"init-wait=",
"check-interval=",
"file=",
"background",
)
try:
opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args)
except getopt.GetoptError as e:
print("Unrecognized command line option or missing required argument: %s" %(e))
print(usage)
sys.exit(2)
cmd_line_option_list = {}
cmd_line_option_list["debug"] = False
cmd_line_option_list["verbose"] = True
cmd_line_option_list["cloud_user_name"] = None
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["check-interval"] = None
cmd_line_option_list["init-wait"] = 5 * 60
cmd_line_option_list["file"] = None
cmd_line_option_list["shutdown"] = False
cmd_line_option_list["daemon"] = False
cmd_line_option_list['starttime'] = datetime.datetime.now()
if sys.platform == "linux":
cmd_line_option_list['syslog'] = "/dev/log"
elif sys.platform == "osx":
cmd_line_option_list['syslog'] = "/var/run/syslog"
else:
cmd_line_option_list['syslog'] = ('localhost',514)
get_gns3secrets(cmd_line_option_list)
for opt, val in opts:
if (opt in ("-h", "--help")):
print(usage)
sys.exit(0)
elif (opt in ("-d", "--debug")):
cmd_line_option_list["debug"] = True
elif (opt in ("-v", "--verbose")):
cmd_line_option_list["verbose"] = True
elif (opt in ("--cloud_user_name")):
cmd_line_option_list["cloud_user_name"] = val
elif (opt in ("--cloud_api_key")):
cmd_line_option_list["cloud_api_key"] = val
elif (opt in ("--instance_id")):
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 ("--check-interval")):
cmd_line_option_list["check-interval"] = int(val)
elif (opt in ("--init-wait")):
cmd_line_option_list["init-wait"] = int(val)
elif (opt in ("--file")):
cmd_line_option_list["file"] = val
elif (opt in ("-k")):
cmd_line_option_list["shutdown"] = True
elif (opt in ("--background")):
cmd_line_option_list["daemon"] = True
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
if cmd_line_option_list["cloud_user_name"] is None:
print("You need to specify a username!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["cloud_api_key"] is None:
print("You need to specify an apikey!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["file"] is None:
print("You need to specify a file to watch!!!!")
print(usage)
sys.exit(2)
if cmd_line_option_list["instance_id"] is None:
print("You need to specify an instance_id")
print(usage)
sys.exit(2)
if cmd_line_option_list["region"] is None:
print("You need to specify a region")
print(usage)
sys.exit(2)
return cmd_line_option_list
def get_gns3secrets(cmd_line_option_list):
"""
Load cloud credentials from .gns3secrets
"""
gns3secret_paths = [
os.path.join(os.path.expanduser("~"), '.config', 'GNS3'),
SCRIPT_PATH,
]
config = configparser.ConfigParser()
for gns3secret_path in gns3secret_paths:
gns3secret_file = "%s/cloud.conf" % (gns3secret_path)
if os.path.isfile(gns3secret_file):
config.read(gns3secret_file)
try:
for key, value in config.items("CLOUD_SERVER"):
cmd_line_option_list[key] = value.strip()
except configparser.NoSectionError:
pass
def set_logging(cmd_options):
"""
Setup logging and format output for console and syslog
Syslog is using the KERN facility
"""
log = logging.getLogger("%s" % (LOG_NAME))
log_level = logging.INFO
log_level_console = logging.WARNING
if cmd_options['verbose'] == True:
log_level_console = logging.INFO
if cmd_options['debug'] == True:
log_level_console = logging.DEBUG
log_level = logging.DEBUG
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console_log = logging.StreamHandler()
console_log.setLevel(log_level_console)
console_log.setFormatter(formatter)
syslog_hndlr = SysLogHandler(
address=cmd_options['syslog'],
facility=SysLogHandler.LOG_KERN
)
syslog_hndlr.setFormatter(sys_formatter)
log.setLevel(log_level)
log.addHandler(console_log)
log.addHandler(syslog_hndlr)
return log
def send_shutdown(pid_file):
"""
Sends the daemon process a kill signal
"""
try:
with open(pid_file, 'r') as pidf:
pid = int(pidf.readline().strip())
pidf.close()
os.kill(pid, 15)
except:
log.info("No running instance found!!!")
log.info("Missing PID file: %s" % (pid_file))
def _get_file_age(filename):
return datetime.datetime.fromtimestamp(
os.path.getmtime(filename)
)
def monitor_loop(options):
"""
Checks the options["file"] modification time against an interval. If the
modification time is too old we terminate the instance.
"""
log.debug("Waiting for init-wait to pass: %s" % (options["init-wait"]))
time.sleep(options["init-wait"])
log.info("Starting monitor_loop")
terminate_attempts = 0
while options['shutdown'] == False:
log.debug("In monitor_loop for : %s" % (
datetime.datetime.now() - options['starttime'])
)
file_last_modified = _get_file_age(options["file"])
now = datetime.datetime.now()
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
#different errors types could occur here.
try:
rksp = Rackspace(options)
rksp.terminate()
except Exception as e:
log.critical("Exception during terminate: %s" % (e))
terminate_attempts+=1
log.warning("Termination sent, attempt: %s" % (terminate_attempts))
time.sleep(600)
else:
time.sleep(options["check-interval"])
log.info("Leaving monitor_loop")
log.info("Shutting down")
def main():
global log
global my_daemon
options = parse_cmd_line(sys.argv)
log = set_logging(options)
def _shutdown(signalnum=None, frame=None):
"""
Handles the SIGINT and SIGTERM event, inside of main so it has access to
the log vars.
"""
log.info("Received shutdown signal")
options["shutdown"] = True
pid_file = "%s/.gns3dms.pid" % (expanduser("~"))
if options["shutdown"]:
send_shutdown(pid_file)
sys.exit(0)
if options["daemon"]:
my_daemon = MyDaemon(pid_file, options)
# Setup signal to catch Control-C / SIGINT and SIGTERM
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
log.info("Starting ...")
log.debug("Using settings:")
for key, value in iter(sorted(options.items())):
log.debug("%s : %s" % (key, value))
log.debug("Checking file ....")
if os.path.isfile(options["file"]) == False:
log.critical("File does not exist!!!")
sys.exit(1)
test_acess = _get_file_age(options["file"])
if type(test_acess) is not datetime.datetime:
log.critical("Can't get file modification time!!!")
sys.exit(1)
if my_daemon:
my_daemon.start()
else:
monitor_loop(options)
class MyDaemon(daemon.daemon):
def run(self):
monitor_loop(self.options)
if __name__ == "__main__":
result = main()
sys.exit(result)

View File

@ -0,0 +1,24 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)

138
gns3dms/modules/daemon.py Normal file
View File

@ -0,0 +1,138 @@
"""Generic linux daemon base class for python 3.x."""
import sys, os, time, atexit, signal
class daemon:
"""A generic daemon class.
Usage: subclass the daemon class and override the run() method."""
def __init__(self, pidfile, options):
self.pidfile = pidfile
self.options = options
def daemonize(self):
"""Deamonize class. UNIX double fork mechanism."""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile,'w+') as f:
f.write(pid + '\n')
def delpid(self):
os.remove(self.pidfile)
def check_pid(self, pid):
""" Check For the existence of a unix pid. """
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
def start(self):
"""Start the daemon."""
# Check for a pidfile to see if the daemon already runs
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if pid:
pid_exist = self.check_pid(pid)
if pid_exist:
message = "Already running: %s\n" % (pid)
sys.stderr.write(message)
sys.exit(1)
else:
message = "pidfile {0} already exist. " + \
"but process is dead\n"
sys.stderr.write(message.format(self.pidfile))
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""Stop the daemon."""
# Get the pid from the pidfile
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if not pid:
message = "pidfile {0} does not exist. " + \
"Daemon not running?\n"
sys.stderr.write(message.format(self.pidfile))
return # not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
e = str(err.args)
if e.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print (str(err.args))
sys.exit(1)
def restart(self):
"""Restart the daemon."""
self.stop()
self.start()
def run(self):
"""You should override this method when you subclass Daemon.
It will be called after the process has been daemonized by
start() or restart()."""

View File

@ -0,0 +1,70 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
import os, sys
import json
import logging
import socket
from gns3dms.cloud.rackspace_ctrl import RackspaceCtrl
LOG_NAME = "gns3dms.rksp"
log = logging.getLogger("%s" % (LOG_NAME))
class Rackspace(object):
def __init__(self, options):
self.username = options["cloud_user_name"]
self.apikey = options["cloud_api_key"]
self.authenticated = False
self.hostname = socket.gethostname()
self.instance_id = options["instance_id"]
self.region = options["region"]
log.debug("Authenticating with Rackspace")
log.debug("My hostname: %s" % (self.hostname))
self.rksp = RackspaceCtrl(self.username, self.apikey)
self.authenticated = self.rksp.authenticate()
def _find_my_instance(self):
if self.authenticated == False:
log.critical("Not authenticated against rackspace!!!!")
for region in self.rksp.list_regions():
log.debug("Rackspace regions: %s" % (region))
log.debug("Checking region: %s" % (self.region))
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:
log.info("Found matching instance: %s" % (server.id))
log.info("Startup id: %s" % (self.instance_id))
return server
def terminate(self):
server = self._find_my_instance()
log.warning("Sending termination")
self.rksp.delete_instance(server)

27
gns3dms/version.py Normal file
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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "0.1"
__version_info__ = (0, 0, 1, -99)

View File

@ -0,0 +1,99 @@
#!/bin/bash
# -*- 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/>.
# Bash shell script for generating self-signed certs. Run this in a folder, as it
# generates a few files. Large portions of this script were taken from the
# following artcile:
#
# http://usrportage.de/archives/919-Batch-generating-SSL-certificates.html
#
# Additional alterations by: Brad Landers
# Date: 2012-01-27
# https://gist.github.com/bradland/1690807
# Script accepts a single argument, the fqdn for the cert
DST_DIR="$HOME/.config/GNS3Certs/"
OLD_DIR=`pwd`
#GNS3 Server expects to find certs with the default FQDN below. If you create
#different certs you will need to update server.py
DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
DOMAIN="gns3server.localdomain.com"
fi
fail_if_error() {
[ $1 != 0 ] && {
unset PASSPHRASE
cd $OLD_DIR
exit 10
}
}
mkdir -p $DST_DIR
fail_if_error $?
cd $DST_DIR
# Generate a passphrase
export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo)
# Certificate details; replace items in angle brackets with your own info
subj="
C=CA
ST=Alberta
O=GNS3
localityName=Calgary
commonName=$DOMAIN
organizationalUnitName=GNS3Server
emailAddress=gns3cert@gns3.com
"
# Generate the server private key
openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048
fail_if_error $?
#openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE
# Generate the CSR
openssl req \
-new \
-batch \
-subj "$(echo -n "$subj" | tr "\n" "/")" \
-key $DOMAIN.key \
-out $DOMAIN.csr \
-passin env:PASSPHRASE
fail_if_error $?
cp $DOMAIN.key $DOMAIN.key.org
fail_if_error $?
# Strip the password so we don't have to type it every time we restart Apache
openssl rsa -in $DOMAIN.key.org -out $DOMAIN.key -passin env:PASSPHRASE
fail_if_error $?
# Generate the cert (good for 10 years)
openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt
fail_if_error $?
echo "${DST_DIR}${DOMAIN}.key"
echo "${DST_DIR}${DOMAIN}.crt"
cd $OLD_DIR

View File

@ -62,16 +62,21 @@ class Config(object):
# 5: server.conf in the current working directory
home = os.path.expanduser("~")
self._cloud_config = os.path.join(home, ".config", appname, "cloud.conf")
filename = "server.conf"
self._files = [os.path.join(home, ".config", appname, filename),
os.path.join(home, ".config", appname + ".conf"),
os.path.join("/etc/xdg", appname, filename),
os.path.join("/etc/xdg", appname + ".conf"),
filename]
filename,
self._cloud_config]
self._config = configparser.ConfigParser()
self.read_config()
def list_cloud_config_file(self):
return self._cloud_config
def read_config(self):
"""
Read the configuration files.

View File

@ -0,0 +1,88 @@
# -*- 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/>.
"""
Simple file upload & listing handler.
"""
import os
import tornado.web
import tornado.websocket
import logging
log = logging.getLogger(__name__)
class GNS3BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user")
if not user:
return None
if self.settings['required_user'] == user.decode("utf-8"):
return user
class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler):
def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user")
if not user:
return None
if self.settings['required_user'] == user.decode("utf-8"):
return user
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.write('<html><body><form action="/login" method="post">'
'Name: <input type="text" name="name">'
'Password: <input type="text" name="password">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
try:
redirect_to = self.get_argument("next")
self.set_secure_cookie("login_success_redirect_to", redirect_to)
except tornado.web.MissingArgumentError:
pass
def post(self):
user = self.get_argument("name")
password = self.get_argument("password")
if self.settings['required_user'] == user and self.settings['required_pass'] == password:
self.set_secure_cookie("user", user)
auth_status = "successful"
else:
self.set_secure_cookie("user", "None")
auth_status = "failure"
log.info("Authentication attempt %s: %s" %(auth_status, user))
try:
redirect_to = self.get_secure_cookie("login_success_redirect_to")
except tornado.web.MissingArgumentError:
redirect_to = "/"
self.redirect(redirect_to)

View File

@ -23,6 +23,7 @@ Simple file upload & listing handler.
import os
import stat
import tornado.web
from .auth_handler import GNS3BaseHandler
from ..version import __version__
from ..config import Config
@ -30,7 +31,7 @@ import logging
log = logging.getLogger(__name__)
class FileUploadHandler(tornado.web.RequestHandler):
class FileUploadHandler(GNS3BaseHandler):
"""
File upload handler.
@ -54,6 +55,7 @@ class FileUploadHandler(tornado.web.RequestHandler):
except OSError as e:
log.error("could not create the upload directory {}: {}".format(self._upload_dir, e))
@tornado.web.authenticated
def get(self):
"""
Invoked on GET request.
@ -70,6 +72,7 @@ class FileUploadHandler(tornado.web.RequestHandler):
path=path,
items=items)
@tornado.web.authenticated
def post(self):
"""
Invoked on POST request.

View File

@ -22,6 +22,7 @@ JSON-RPC protocol over Websockets.
import zmq
import uuid
import tornado.websocket
from .auth_handler import GNS3WebSocketBaseHandler
from tornado.escape import json_decode
from ..jsonrpc import JSONRPCParseError
from ..jsonrpc import JSONRPCInvalidRequest
@ -33,7 +34,7 @@ import logging
log = logging.getLogger(__name__)
class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
class JSONRPCWebSocket(GNS3WebSocketBaseHandler):
"""
STOMP protocol over Tornado Websockets with message
routing to ZeroMQ dealer clients.
@ -119,7 +120,15 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
"""
log.info("Websocket client {} connected".format(self.session_id))
self.clients.add(self)
authenticated_user = self.get_current_user()
if authenticated_user:
self.clients.add(self)
log.info("Websocket authenticated user: %s" % (authenticated_user))
else:
self.close()
log.info("Websocket non-authenticated user attempt: %s" % (authenticated_user))
def on_message(self, message):
"""

View File

@ -16,10 +16,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import tornado.web
from .auth_handler import GNS3BaseHandler
from ..version import __version__
class VersionHandler(tornado.web.RequestHandler):
class VersionHandler(GNS3BaseHandler):
def get(self):
response = {'version': __version__}

View File

@ -17,11 +17,13 @@
import sys
from .base import IModule
from .deadman import DeadMan
from .dynamips import Dynamips
from .qemu import Qemu
from .vpcs import VPCS
from .virtualbox import VirtualBox
MODULES = [Dynamips, VPCS, VirtualBox]
MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
if sys.platform.startswith("linux"):
# IOU runs only on Linux

View File

@ -0,0 +1,164 @@
# -*- 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/>.
"""
DeadMan server module.
"""
import os
import time
import subprocess
from gns3server.modules import IModule
from gns3server.config import Config
import logging
log = logging.getLogger(__name__)
class DeadMan(IModule):
"""
DeadMan module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
config = Config.instance()
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._host = kwargs["host"]
self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
self._heartbeat_file = "%s/heartbeat_file_for_gnsdms" % (
self._tempdir)
if 'heartbeat_file' in kwargs:
self._heartbeat_file = kwargs['heartbeat_file']
self._is_enabled = False
try:
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
instance_id = cloud_config["instance_id"]
cloud_user_name = cloud_config["cloud_user_name"]
cloud_api_key = cloud_config["cloud_api_key"]
self._is_enabled = True
except KeyError:
log.critical("Missing cloud.conf - disabling Deadman Switch")
self._deadman_process = None
self.heartbeat()
self.start()
def _start_deadman_process(self):
"""
Start a subprocess and return the object
"""
#gnsserver gets configuration options from cloud.conf. This is where
#the client adds specific cloud information.
#gns3dms also reads in cloud.conf. That is why we don't need to specific
#all the command line arguments here.
cmd = []
cmd.append("gns3dms")
cmd.append("--file")
cmd.append("%s" % (self._heartbeat_file))
cmd.append("--background")
log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, shell=False)
return process
def _stop_deadman_process(self):
"""
Start a subprocess and return the object
"""
cmd = []
cmd.append("gns3dms")
cmd.append("-k")
log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, shell=False)
return process
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
if self._deadman_process == None:
log.info("Deadman: Can't stop, is not currently running")
log.debug("Deadman: Stopping process")
self._deadman_process = self._stop_deadman_process()
self._deadman_process = None
#Jerry or Jeremy why do we do this? Won't this stop the I/O loop for
#for everyone?
IModule.stop(self, signum) # this will stop the I/O loop
def start(self, request=None):
"""
Start the deadman process on the server
"""
if self._is_enabled:
self._deadman_process = self._start_deadman_process()
log.debug("Deadman: Process is starting")
@IModule.route("deadman.reset")
def reset(self, request=None):
"""
Resets the module (JSON-RPC notification).
:param request: JSON request (not used)
"""
self.stop()
self.start()
log.info("Deadman: Module has been reset")
@IModule.route("deadman.heartbeat")
def heartbeat(self, request=None):
"""
Update a file on the server that the deadman switch will monitor
"""
now = time.time()
with open(self._heartbeat_file, 'w') as heartbeat_file:
heartbeat_file.write(str(now))
heartbeat_file.close()
log.debug("Deadman: heartbeat_file updated: %s %s" % (
self._heartbeat_file,
now,
))
self.start()

View File

@ -111,7 +111,7 @@ class Dynamips(IModule):
dynamips_config = config.get_section_config(name.upper())
self._dynamips = dynamips_config.get("dynamips_path")
if not self._dynamips or not os.path.isfile(self._dynamips):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for Dynamips in the current working directory and $PATH
for path in paths:
try:

View File

@ -17,6 +17,7 @@
import os
import base64
import time
from gns3server.modules import IModule
from ..dynamips_error import DynamipsError
@ -61,6 +62,7 @@ from ..schemas.vm import VM_STOP_CAPTURE_SCHEMA
from ..schemas.vm import VM_SAVE_CONFIG_SCHEMA
from ..schemas.vm import VM_EXPORT_CONFIG_SCHEMA
from ..schemas.vm import VM_IDLEPCS_SCHEMA
from ..schemas.vm import VM_AUTO_IDLEPC_SCHEMA
from ..schemas.vm import VM_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.vm import VM_ADD_NIO_SCHEMA
from ..schemas.vm import VM_DELETE_NIO_SCHEMA
@ -704,6 +706,76 @@ class VM(object):
"idlepcs": idlepcs}
self.send_response(response)
@IModule.route("dynamips.vm.auto_idlepc")
def vm_auto_idlepc(self, request):
"""
Auto idle-pc calculation.
Mandatory request parameters:
- id (vm identifier)
Response parameters:
- id (vm identifier)
- logs (logs for the calculation)
- idlepc (idle-pc value)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_AUTO_IDLEPC_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
try:
router.idlepc = "0x0" # reset the current idle-pc value before calculating a new one
was_auto_started = False
if router.get_status() != "running":
router.start()
was_auto_started = True
time.sleep(20) # leave time to the router to boot
logs = []
validated_idlepc = "0x0"
idlepcs = router.get_idle_pc_prop()
if not idlepcs:
logs.append("No idle-pc values found")
for idlepc in idlepcs:
router.idlepc = idlepc.split()[0]
logs.append("Trying idle-pc value {}".format(router.idlepc))
start_time = time.time()
initial_cpu_usage = router.get_cpu_usage()
logs.append("Initial CPU usage = {}%".format(initial_cpu_usage))
time.sleep(4) # wait 4 seconds to probe the cpu again
elapsed_time = time.time() - start_time
cpu_elapsed_usage = router.get_cpu_usage() - initial_cpu_usage
cpu_usage = abs(cpu_elapsed_usage * 100.0 / elapsed_time)
logs.append("CPU usage after {:.2} seconds = {:.2}%".format(elapsed_time, cpu_usage))
if cpu_usage > 100:
cpu_usage = 100
if cpu_usage < 70:
validated_idlepc = router.idlepc
logs.append("Idle-PC value {} has been validated".format(validated_idlepc))
break
except DynamipsError as e:
self.send_custom_error(str(e))
return
finally:
if was_auto_started:
router.stop()
validated_idlepc = "0x0"
response = {"id": router.id,
"logs": logs,
"idlepc": validated_idlepc}
self.send_response(response)
@IModule.route("dynamips.vm.allocate_udp_port")
def vm_allocate_udp_port(self, request):
"""

View File

@ -1255,6 +1255,15 @@ class Router(object):
return (self._hypervisor.send("{platform} show_hardware {name}".format(platform=self._platform,
name=self._name)))
def get_cpu_usage(self):
"""
Returns the CPU usage.
:return: CPU usage in percent
"""
return int(self._hypervisor.send("vm cpu_usage {name} 0".format(name=self._name))[0])
def get_slot_bindings(self):
"""
Returns slot bindings.

View File

@ -487,6 +487,20 @@ VM_IDLEPCS_SCHEMA = {
"required": ["id"]
}
VM_AUTO_IDLEPC_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request an auto idle-pc calculation for this VM instance",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
VM_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a VM instance",

View File

@ -68,7 +68,7 @@ class IOU(IModule):
iou_config = config.get_section_config(name.upper())
self._iouyap = iou_config.get("iouyap_path")
if not self._iouyap or not os.path.isfile(self._iouyap):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for iouyap in the current working directory and $PATH
for path in paths:
try:

View File

@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
IOU NIO.
Network Input/Output.
"""
def __init__(self):

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_GenericEthernet(NIO):
"""
NIO generic Ethernet NIO.
Generic Ethernet NIO.
:param ethernet_device: Ethernet device name (e.g. eth0)
"""

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_TAP(NIO):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -0,0 +1,657 @@
# -*- 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/>.
"""
QEMU server module.
"""
import sys
import os
import socket
import shutil
import subprocess
import re
from gns3server.modules import IModule
from gns3server.config import Config
from .qemu_vm import QemuVM
from .qemu_error import QemuError
from .nios.nio_udp import NIO_UDP
from ..attic import find_unused_port
from .schemas import QEMU_CREATE_SCHEMA
from .schemas import QEMU_DELETE_SCHEMA
from .schemas import QEMU_UPDATE_SCHEMA
from .schemas import QEMU_START_SCHEMA
from .schemas import QEMU_STOP_SCHEMA
from .schemas import QEMU_SUSPEND_SCHEMA
from .schemas import QEMU_RELOAD_SCHEMA
from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA
from .schemas import QEMU_ADD_NIO_SCHEMA
from .schemas import QEMU_DELETE_NIO_SCHEMA
import logging
log = logging.getLogger(__name__)
class Qemu(IModule):
"""
QEMU module.
:param name: module name
:param args: arguments for the module
:param kwargs: named arguments for the module
"""
def __init__(self, name, *args, **kwargs):
# a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs)
self._qemu_instances = {}
config = Config.instance()
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._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._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir
def stop(self, signum=None):
"""
Properly stops the module.
:param signum: signal number (if called by the signal handler)
"""
# delete all QEMU instances
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.delete()
IModule.stop(self, signum) # this will stop the I/O loop
def get_qemu_instance(self, qemu_id):
"""
Returns a QEMU VM instance.
:param qemu_id: QEMU VM identifier
:returns: QemuVM instance
"""
if qemu_id not in self._qemu_instances:
log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1)
self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id))
return None
return self._qemu_instances[qemu_id]
@IModule.route("qemu.reset")
def reset(self, request):
"""
Resets the module.
:param request: JSON request
"""
# delete all QEMU instances
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.delete()
# resets the instance IDs
QemuVM.reset()
self._qemu_instances.clear()
self._allocated_udp_ports.clear()
log.info("QEMU module has been reset")
@IModule.route("qemu.settings")
def settings(self, request):
"""
Set or update settings.
Optional request parameters:
- working_dir (path to a working directory)
- project_name
- console_start_port_range
- console_end_port_range
- udp_start_port_range
- udp_end_port_range
:param request: JSON request
"""
if request is None:
self.send_param_error()
return
if "working_dir" in request:
new_working_dir = request["working_dir"]
log.info("this server is local with working directory path to {}".format(new_working_dir))
else:
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir):
try:
shutil.move(self._working_dir, new_working_dir)
except OSError as e:
log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
new_working_dir,
e))
return
# update the working directory if it has changed
if self._working_dir != new_working_dir:
self._working_dir = new_working_dir
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_instance.id))
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"]
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"]
log.debug("received request {}".format(request))
@IModule.route("qemu.create")
def qemu_create(self, request):
"""
Creates a new QEMU VM instance.
Mandatory request parameters:
- name (QEMU VM name)
- qemu_path (path to the Qemu binary)
Optional request parameters:
- console (QEMU VM console port)
Response parameters:
- id (QEMU VM instance identifier)
- name (QEMU VM name)
- default settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_CREATE_SCHEMA):
return
name = request["name"]
qemu_path = request["qemu_path"]
console = request.get("console")
qemu_id = request.get("qemu_id")
try:
qemu_instance = QemuVM(name,
qemu_path,
self._working_dir,
self._host,
qemu_id,
console,
self._console_start_port_range,
self._console_end_port_range)
except QemuError as e:
self.send_custom_error(str(e))
return
response = {"name": qemu_instance.name,
"id": qemu_instance.id}
defaults = qemu_instance.defaults()
response.update(defaults)
self._qemu_instances[qemu_instance.id] = qemu_instance
self.send_response(response)
@IModule.route("qemu.delete")
def qemu_delete(self, request):
"""
Deletes a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameter:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_DELETE_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.clean_delete()
del self._qemu_instances[request["id"]]
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.update")
def qemu_update(self, request):
"""
Updates a QEMU VM instance
Mandatory request parameters:
- id (QEMU VM instance identifier)
Optional request parameters:
- any setting to update
Response parameters:
- updated settings
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_UPDATE_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
# update the QEMU VM settings
response = {}
for name, value in request.items():
if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value:
try:
setattr(qemu_instance, name, value)
response[name] = value
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(response)
@IModule.route("qemu.start")
def qemu_start(self, request):
"""
Starts a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_START_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.start()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.stop")
def qemu_stop(self, request):
"""
Stops a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_STOP_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.stop()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.reload")
def qemu_reload(self, request):
"""
Reloads a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_RELOAD_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.reload()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.stop")
def qemu_stop(self, request):
"""
Stops a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_STOP_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.stop()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.suspend")
def qemu_suspend(self, request):
"""
Suspends a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_SUSPEND_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
qemu_instance.suspend()
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
@IModule.route("qemu.allocate_udp_port")
def allocate_udp_port(self, request):
"""
Allocates a UDP port in order to create an UDP NIO.
Mandatory request parameters:
- id (QEMU VM identifier)
- port_id (unique port identifier)
Response parameters:
- port_id (unique port identifier)
- lport (allocated local port)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_ALLOCATE_UDP_PORT_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
try:
port = find_unused_port(self._udp_start_port_range,
self._udp_end_port_range,
host=self._host,
socket_type="UDP",
ignore_ports=self._allocated_udp_ports)
except Exception as e:
self.send_custom_error(str(e))
return
self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(qemu_instance.name,
qemu_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("qemu.add_nio")
def add_nio(self, request):
"""
Adds an NIO (Network Input/Output) for a QEMU VM instance.
Mandatory request parameters:
- id (QEMU VM instance identifier)
- port (port number)
- port_id (unique port identifier)
- nio (one of the following)
- type "nio_udp"
- lport (local port)
- rhost (remote host)
- rport (remote port)
Response parameters:
- port_id (unique port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_ADD_NIO_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
port = request["port"]
try:
nio = None
if request["nio"]["type"] == "nio_udp":
lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport)
if not nio:
raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
except QemuError as e:
self.send_custom_error(str(e))
return
try:
qemu_instance.port_add_nio_binding(port, nio)
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response({"port_id": request["port_id"]})
@IModule.route("qemu.delete_nio")
def delete_nio(self, request):
"""
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (QEMU VM instance identifier)
- port (port identifier)
Response parameters:
- True on success
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA):
return
# get the instance
qemu_instance = self.get_qemu_instance(request["id"])
if not qemu_instance:
return
port = request["port"]
try:
nio = qemu_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except QemuError as e:
self.send_custom_error(str(e))
return
self.send_response(True)
def _get_qemu_version(self, qemu_path):
"""
Gets the Qemu version.
:param qemu_path: path to Qemu
"""
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"))
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:
raise QemuError("Error while looking for the Qemu version: {}".format(e))
@IModule.route("qemu.qemu_list")
def qemu_list(self, request):
"""
Gets QEMU binaries list.
Response parameters:
- Server address/host
- List of Qemu binaries
"""
qemus = []
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# 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 "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"]):
paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
for path in paths:
try:
for f in os.listdir(path):
if f.startswith("qemu-system") and os.access(os.path.join(path, f), os.X_OK):
qemu_path = os.path.join(path, f)
version = self._get_qemu_version(qemu_path)
qemus.append({"path": qemu_path, "version": version})
except OSError:
continue
response = {"server": self._host,
"qemus": qemus}
self.send_response(response)
@IModule.route("qemu.echo")
def echo(self, request):
"""
Echo end point for testing purposes.
:param request: JSON request
"""
if request is None:
self.send_param_error()
else:
log.debug("received request {}".format(request))
self.send_response(request)

View File

@ -0,0 +1,104 @@
# -*- 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_id in range(0, interfaces):
self._ports[port_id] = 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_id):
"""
Checks if a port exists on this adapter.
:returns: True is the port exists,
False otherwise.
"""
if port_id in self._ports:
return True
return False
def add_nio(self, port_id, nio):
"""
Adds a NIO to a port on this adapter.
:param port_id: port ID (integer)
:param nio: NIO instance
"""
self._ports[port_id] = nio
def remove_nio(self, port_id):
"""
Removes a NIO from a port on this adapter.
:param port_id: port ID (integer)
"""
self._ports[port_id] = None
def get_nio(self, port_id):
"""
Returns the NIO assigned to a port.
:params port_id: port ID (integer)
:returns: NIO instance
"""
return self._ports[port_id]
@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

View File

@ -0,0 +1,31 @@
# -*- 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):
"""
QEMU Ethernet adapter.
"""
def __init__(self):
Adapter.__init__(self, interfaces=1)
def __str__(self):
return "QEMU Ethernet adapter"

View File

View File

@ -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/>.
"""
Base interface for NIOs.
"""
class NIO(object):
"""
Network Input/Output.
"""
def __init__(self):
self._capturing = False
self._pcap_output_file = ""
def startPacketCapture(self, pcap_output_file):
"""
:param pcap_output_file: PCAP destination file for the capture
"""
self._capturing = True
self._pcap_output_file = pcap_output_file
def stopPacketCapture(self):
self._capturing = False
self._pcap_output_file = ""
@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

View File

@ -0,0 +1,75 @@
# -*- 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 NIO_UDP(NIO):
"""
UDP NIO.
:param lport: local port number
:param rhost: remote address/host
:param rport: remote port number
"""
_instance_count = 0
def __init__(self, lport, rhost, rport):
NIO.__init__(self)
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"

View File

@ -0,0 +1,39 @@
# -*- 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/>.
"""
Custom exceptions for QEMU module.
"""
class QemuError(Exception):
def __init__(self, message, original_exception=None):
Exception.__init__(self, 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

View File

@ -0,0 +1,785 @@
# -*- 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/>.
"""
QEMU VM instance.
"""
import os
import shutil
import random
import subprocess
import shlex
from .qemu_error import QemuError
from .adapters.ethernet_adapter import EthernetAdapter
from .nios.nio_udp import NIO_UDP
from ..attic import find_unused_port
import logging
log = logging.getLogger(__name__)
class QemuVM(object):
"""
QEMU VM implementation.
:param name: name of this QEMU VM
:param qemu_path: path to the QEMU binary
:param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections
:param qemu_id: QEMU VM instance ID
:param console: TCP console port
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
"""
_instances = []
_allocated_console_ports = []
def __init__(self,
name,
qemu_path,
working_dir,
host="127.0.0.1",
qemu_id=None,
console=None,
console_start_port_range=5001,
console_end_port_range=5500):
if not qemu_id:
self._id = 0
for identifier in range(1, 1024):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0:
raise QemuError("Maximum number of QEMU VM instances reached")
else:
if qemu_id in self._instances:
raise QemuError("QEMU identifier {} is already used by another QEMU VM instance".format(qemu_id))
self._id = qemu_id
self._instances.append(self._id)
self._name = name
self._working_dir = None
self._host = host
self._command = []
self._started = False
self._process = None
self._stdout_file = ""
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
# QEMU settings
self._qemu_path = qemu_path
self._hda_disk_image = ""
self._hdb_disk_image = ""
self._options = ""
self._ram = 256
self._console = console
self._ethernet_adapters = []
self._adapter_type = "e1000"
self._initrd = ""
self._kernel_image = ""
self._kernel_command_line = ""
working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id))
if qemu_id and not os.path.isdir(working_dir_path):
raise QemuError("Working directory {} doesn't exist".format(working_dir_path))
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise QemuError(e)
if self._console in self._allocated_console_ports:
raise QemuError("Console port {} is already used by another QEMU VM".format(console))
self._allocated_console_ports.append(self._console)
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))
def defaults(self):
"""
Returns all the default attribute values for this QEMU VM.
:returns: default values (dictionary)
"""
qemu_defaults = {"name": self._name,
"qemu_path": self._qemu_path,
"ram": self._ram,
"hda_disk_image": self._hda_disk_image,
"hdb_disk_image": self._hdb_disk_image,
"options": self._options,
"adapters": self.adapters,
"adapter_type": self._adapter_type,
"console": self._console,
"initrd": self._initrd,
"kernel_image": self._kernel_image,
"kernel_command_line": self._kernel_command_line}
return qemu_defaults
@property
def id(self):
"""
Returns the unique ID for this QEMU VM.
:returns: id (integer)
"""
return self._id
@classmethod
def reset(cls):
"""
Resets allocated instance list.
"""
cls._instances.clear()
cls._allocated_console_ports.clear()
@property
def name(self):
"""
Returns the name of this QEMU VM.
:returns: name
"""
return self._name
@name.setter
def name(self, new_name):
"""
Sets the name of this QEMU VM.
:param new_name: name
"""
log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id,
new_name=new_name))
self._name = new_name
@property
def working_dir(self):
"""
Returns current working directory
:returns: path to the working directory
"""
return self._working_dir
@working_dir.setter
def working_dir(self, working_dir):
"""
Sets the working directory this QEMU VM.
:param working_dir: path to the working directory
"""
try:
os.makedirs(working_dir)
except FileExistsError:
pass
except OSError as e:
raise QemuError("Could not create working directory {}: {}".format(working_dir, e))
self._working_dir = working_dir
log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id,
wd=self._working_dir))
@property
def console(self):
"""
Returns the TCP console port.
:returns: console port (integer)
"""
return self._console
@console.setter
def console(self, console):
"""
Sets the TCP console port.
:param console: console port (integer)
"""
if console in self._allocated_console_ports:
raise QemuError("Console port {} is already used by another QEMU VM".format(console))
self._allocated_console_ports.remove(self._console)
self._console = console
self._allocated_console_ports.append(self._console)
log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id,
port=console))
def delete(self):
"""
Deletes this QEMU VM.
"""
self.stop()
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)
log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
def clean_delete(self):
"""
Deletes this QEMU VM & all files.
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
try:
shutil.rmtree(self._working_dir)
except OSError as e:
log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name,
id=self._id,
error=e))
return
log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
id=self._id))
@property
def qemu_path(self):
"""
Returns the QEMU binary path for this QEMU VM.
:returns: QEMU path
"""
return self._qemu_path
@qemu_path.setter
def qemu_path(self, qemu_path):
"""
Sets the QEMU binary path this QEMU VM.
:param qemu_path: QEMU path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name,
id=self._id,
qemu_path=qemu_path))
self._qemu_path = qemu_path
@property
def hda_disk_image(self):
"""
Returns the hda disk image path for this QEMU VM.
:returns: QEMU hda disk image path
"""
return self._hda_disk_image
@hda_disk_image.setter
def hda_disk_image(self, hda_disk_image):
"""
Sets the hda disk image for this QEMU VM.
:param hda_disk_image: QEMU hda disk image path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name,
id=self._id,
disk_image=hda_disk_image))
self._hda_disk_image = hda_disk_image
@property
def hdb_disk_image(self):
"""
Returns the hdb disk image path for this QEMU VM.
:returns: QEMU hdb disk image path
"""
return self._hdb_disk_image
@hdb_disk_image.setter
def hdb_disk_image(self, hdb_disk_image):
"""
Sets the hdb disk image for this QEMU VM.
:param hdb_disk_image: QEMU hdb disk image path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name,
id=self._id,
disk_image=hdb_disk_image))
self._hdb_disk_image = hdb_disk_image
@property
def adapters(self):
"""
Returns the number of Ethernet adapters for this QEMU VM instance.
:returns: number of adapters
"""
return len(self._ethernet_adapters)
@adapters.setter
def adapters(self, adapters):
"""
Sets the number of Ethernet adapters for this QEMU VM instance.
:param adapters: number of adapters
"""
self._ethernet_adapters.clear()
for adapter_id in range(0, adapters):
self._ethernet_adapters.append(EthernetAdapter())
log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
id=self._id,
adapters=adapters))
@property
def adapter_type(self):
"""
Returns the adapter type for this QEMU 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 QEMU VM instance.
:param adapter_type: adapter type (string)
"""
self._adapter_type = adapter_type
log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name,
id=self._id,
adapter_type=adapter_type))
@property
def ram(self):
"""
Returns the RAM amount for this QEMU VM.
:returns: RAM amount in MB
"""
return self._ram
@ram.setter
def ram(self, ram):
"""
Sets the amount of RAM for this QEMU VM.
:param ram: RAM amount in MB
"""
log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name,
id=self._id,
ram=ram))
self._ram = ram
@property
def options(self):
"""
Returns the options for this QEMU VM.
:returns: QEMU options
"""
return self._options
@options.setter
def options(self, options):
"""
Sets the options for this QEMU VM.
:param options: QEMU options
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name,
id=self._id,
options=options))
self._options = options
@property
def initrd(self):
"""
Returns the initrd path for this QEMU VM.
:returns: QEMU initrd path
"""
return self._initrd
@initrd.setter
def initrd(self, initrd):
"""
Sets the initrd path for this QEMU VM.
:param initrd: QEMU initrd path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name,
id=self._id,
initrd=initrd))
self._initrd = initrd
@property
def kernel_image(self):
"""
Returns the kernel image path for this QEMU VM.
:returns: QEMU kernel image path
"""
return self._kernel_image
@kernel_image.setter
def kernel_image(self, kernel_image):
"""
Sets the kernel image path for this QEMU VM.
:param kernel_image: QEMU kernel image path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name,
id=self._id,
kernel_image=kernel_image))
self._kernel_image = kernel_image
@property
def kernel_command_line(self):
"""
Returns the kernel command line for this QEMU VM.
:returns: QEMU kernel command line
"""
return self._kernel_command_line
@kernel_command_line.setter
def kernel_command_line(self, kernel_command_line):
"""
Sets the kernel command line for this QEMU VM.
:param kernel_command_line: QEMU kernel command line
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name,
id=self._id,
kernel_command_line=kernel_command_line))
self._kernel_command_line = kernel_command_line
def start(self):
"""
Starts this QEMU VM.
"""
if not self.is_running():
if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path):
raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path))
self._command = self._build_command()
try:
log.info("starting QEMU: {}".format(self._command))
self._stdout_file = os.path.join(self._working_dir, "qemu.log")
log.info("logging to {}".format(self._stdout_file))
with open(self._stdout_file, "w") as fd:
self._process = subprocess.Popen(self._command,
stdout=fd,
stderr=subprocess.STDOUT,
cwd=self._working_dir)
log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid))
self._started = True
except OSError 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))
def stop(self):
"""
Stops this QEMU VM.
"""
# stop the QEMU process
if self.is_running():
log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid))
try:
self._process.terminate()
self._process.wait(1)
except subprocess.TimeoutExpired:
self._process.kill()
if self._process.poll() is None:
log.warn("QEMU VM instance {} PID={} is still running".format(self._id,
self._process.pid))
self._process = None
self._started = False
def suspend(self):
"""
Suspends this QEMU VM.
"""
pass
def reload(self):
"""
Reloads this QEMU VM.
"""
pass
def resume(self):
"""
Resumes this QEMU VM.
"""
pass
def port_add_nio_binding(self, adapter_id, nio):
"""
Adds a port NIO binding.
:param adapter_id: adapter ID
:param nio: NIO instance to add to the slot/port
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
adapter_id=adapter_id))
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,
nio=nio,
adapter_id=adapter_id))
def port_remove_nio_binding(self, adapter_id):
"""
Removes a port NIO binding.
:param adapter_id: adapter ID
:returns: NIO instance
"""
try:
adapter = self._ethernet_adapters[adapter_id]
except IndexError:
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
adapter_id=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,
id=self._id,
nio=nio,
adapter_id=adapter_id))
return nio
@property
def started(self):
"""
Returns either this QEMU VM has been started or not.
:returns: boolean
"""
return self._started
def read_stdout(self):
"""
Reads the standard output of the QEMU process.
Only use when the process has been stopped or has crashed.
"""
output = ""
if self._stdout_file:
try:
with open(self._stdout_file, errors="replace") as file:
output = file.read()
except OSError as e:
log.warn("could not read {}: {}".format(self._stdout_file, e))
return output
def is_running(self):
"""
Checks if the QEMU process is running
:returns: True or False
"""
if self._process and self._process.poll() is None:
return True
return False
def command(self):
"""
Returns the QEMU command line.
:returns: QEMU command line (string)
"""
return " ".join(self._build_command())
def _serial_options(self):
if self._console:
return ["-serial", "telnet:{}:{},server,nowait".format(self._host, self._console)]
else:
return []
def _disk_options(self):
options = []
qemu_img_path = ""
qemu_path_dir = os.path.dirname(self._qemu_path)
try:
for f in os.listdir(qemu_path_dir):
if f.startswith("qemu-img"):
qemu_img_path = os.path.join(qemu_path_dir, f)
except OSError as e:
raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e))
if not qemu_img_path:
raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir))
try:
if 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",
"backing_file={}".format(self._hda_disk_image),
"-f", "qcow2", hda_disk])
log.info("{} returned with {}".format(qemu_img_path, retcode))
else:
# create a "FLASH" with 256MB if no disk image has been specified
hda_disk = os.path.join(self._working_dir, "flash.qcow2")
if not os.path.exists(hda_disk):
retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "256M"])
log.info("{} returned with {}".format(qemu_img_path, retcode))
except OSError as e:
raise QemuError("Could not create disk image {}".format(e))
options.extend(["-hda", hda_disk])
if self._hdb_disk_image:
hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2")
if not os.path.exists(hdb_disk):
try:
retcode = subprocess.call([qemu_img_path, "create", "-o",
"backing_file={}".format(self._hdb_disk_image),
"-f", "qcow2", hdb_disk])
log.info("{} returned with {}".format(qemu_img_path, retcode))
except OSError as e:
raise QemuError("Could not create disk image {}".format(e))
options.extend(["-hdb", hdb_disk])
return options
def _linux_boot_options(self):
options = []
if self._initrd:
options.extend(["-initrd", self._initrd])
if self._kernel_image:
options.extend(["-kernel", self._kernel_image])
if self._kernel_command_line:
options.extend(["-append", self._kernel_command_line])
return options
def _network_options(self):
network_options = []
adapter_id = 0
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)])
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)])
else:
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)])
adapter_id += 1
return network_options
def _build_command(self):
"""
Command to start the QEMU process.
(to be passed to subprocess.Popen())
"""
command = [self._qemu_path]
command.extend(["-name", self._name])
command.extend(["-m", str(self._ram)])
command.extend(self._disk_options())
command.extend(self._linux_boot_options())
command.extend(self._serial_options())
additional_options = self._options.strip()
if additional_options:
command.extend(shlex.split(additional_options))
command.extend(self._network_options())
return command

View File

@ -0,0 +1,388 @@
# -*- 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/>.
QEMU_CREATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to create a new QEMU VM instance",
"type": "object",
"properties": {
"name": {
"description": "QEMU VM instance name",
"type": "string",
"minLength": 1,
},
"qemu_path": {
"description": "Path to QEMU",
"type": "string",
"minLength": 1,
},
"qemu_id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
},
"additionalProperties": False,
"required": ["name", "qemu_path"],
}
QEMU_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"name": {
"description": "QEMU VM instance name",
"type": "string",
"minLength": 1,
},
"qemu_path": {
"description": "path to QEMU",
"type": "string",
"minLength": 1,
},
"hda_disk_image": {
"description": "QEMU hda disk image path",
"type": "string",
},
"hdb_disk_image": {
"description": "QEMU hdb disk image path",
"type": "string",
},
"ram": {
"description": "amount of RAM in MB",
"type": "integer"
},
"adapters": {
"description": "number of adapters",
"type": "integer",
"minimum": 1,
"maximum": 8,
},
"adapter_type": {
"description": "QEMU adapter type",
"type": "string",
"minLength": 1,
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"initrd": {
"description": "QEMU initrd path",
"type": "string",
},
"kernel_image": {
"description": "QEMU kernel image path",
"type": "string",
},
"kernel_command_line": {
"description": "QEMU kernel command line",
"type": "string",
},
"options": {
"description": "additional QEMU options",
"type": "string",
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_SUSPEND_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to suspend a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id"]
}
QEMU_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the QEMU VM instance",
"type": "integer"
},
},
"additionalProperties": False,
"required": ["id", "port_id"]
}
QEMU_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for a QEMU VM instance",
"type": "object",
"definitions": {
"UDP": {
"description": "UDP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_udp"]
},
"lport": {
"description": "Local port",
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"rhost": {
"description": "Remote host",
"type": "string",
"minLength": 1
},
"rport": {
"description": "Remote port",
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the QEMU VM instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 8
},
"nio": {
"type": "object",
"description": "Network Input/Output",
"oneOf": [
{"$ref": "#/definitions/UDP"},
{"$ref": "#/definitions/Ethernet"},
{"$ref": "#/definitions/LinuxEthernet"},
{"$ref": "#/definitions/TAP"},
{"$ref": "#/definitions/UNIX"},
{"$ref": "#/definitions/VDE"},
{"$ref": "#/definitions/NULL"},
]
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "nio"]
}
QEMU_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for a QEMU VM instance",
"type": "object",
"properties": {
"id": {
"description": "QEMU VM instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 8
},
},
"additionalProperties": False,
"required": ["id", "port"]
}

View File

@ -67,8 +67,8 @@ class VirtualBox(IModule):
vbox_config = config.get_section_config(name.upper())
self._vboxwrapper_path = vbox_config.get("vboxwrapper_path")
if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
# look for iouyap in the current working directory and $PATH
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for vboxwrapper in the current working directory and $PATH
for path in paths:
try:
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
@ -172,9 +172,9 @@ class VirtualBox(IModule):
"""
Returns a VirtualBox VM instance.
:param vbox_id: VirtualBox device identifier
:param vbox_id: VirtualBox VM identifier
:returns: VBoxDevice instance
:returns: VirtualBoxVM instance
"""
if vbox_id not in self._vbox_instances:
@ -271,6 +271,7 @@ class VirtualBox(IModule):
Mandatory request parameters:
- name (VirtualBox VM name)
- vmname (VirtualBox VM name in VirtualBox)
Optional request parameters:
- console (VirtualBox VM console port)
@ -653,7 +654,7 @@ class VirtualBox(IModule):
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (VPCS instance identifier)
- id (VirtualBox instance identifier)
- port (port identifier)
Response parameters:
@ -688,7 +689,7 @@ class VirtualBox(IModule):
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
- capture_file_name
@ -729,7 +730,7 @@ class VirtualBox(IModule):
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)

View File

@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
IOU NIO.
Network Input/Output.
"""
def __init__(self):

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -63,7 +63,7 @@ class VPCS(IModule):
vpcs_config = config.get_section_config(name.upper())
self._vpcs = vpcs_config.get("vpcs_path")
if not self._vpcs or not os.path.isfile(self._vpcs):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
# look for VPCS in the current working directory and $PATH
for path in paths:
try:

View File

@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only).
class NIO_TAP(object):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""

View File

@ -22,7 +22,7 @@ Interface for UDP NIOs.
class NIO_UDP(object):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -338,11 +338,10 @@ class VPCSDevice(object):
"""
try:
output = subprocess.check_output([self._path, "-v"], stderr=subprocess.STDOUT, cwd=self._working_dir)
output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir)
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
if match:
version = match.group(1)
print(version)
if parse_version(version) < parse_version("0.5b1"):
raise VPCSError("VPCS executable version must be >= 0.5b1")
else:

View File

@ -33,12 +33,16 @@ import tornado.ioloop
import tornado.web
import tornado.autoreload
import pkg_resources
from os.path import expanduser
import base64
import uuid
from pkg_resources import parse_version
from .config import Config
from .handlers.jsonrpc_websocket import JSONRPCWebSocket
from .handlers.version_handler import VersionHandler
from .handlers.file_upload_handler import FileUploadHandler
from .handlers.auth_handler import LoginHandler
from .builtins.server_version import server_version
from .builtins.interfaces import interfaces
from .modules import MODULES
@ -46,12 +50,12 @@ from .modules import MODULES
import logging
log = logging.getLogger(__name__)
class Server(object):
# built-in handlers
handlers = [(r"/version", VersionHandler),
(r"/upload", FileUploadHandler)]
(r"/upload", FileUploadHandler),
(r"/login", LoginHandler)]
def __init__(self, host, port, ipc=False):
@ -136,11 +140,42 @@ class Server(object):
JSONRPCWebSocket.register_destination(destination, instance.name)
instance.start() # starts the new process
def run(self):
"""
Starts the Tornado web server and ZeroMQ server.
"""
settings = {
"debug":True,
"cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes),
"login_url": "/login",
}
ssl_options = {}
try:
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
cloud_settings = {
"required_user" : cloud_config['WEB_USERNAME'],
"required_pass" : cloud_config['WEB_PASSWORD'],
}
settings.update(cloud_settings)
if cloud_config["SSL_ENABLED"] == "yes":
ssl_options = {
"certfile" : cloud_config["SSL_CRT"],
"keyfile" : cloud_config["SSL_KEY"],
}
log.info("Certs found - starting in SSL mode")
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))])
@ -150,7 +185,7 @@ class Server(object):
templates_dir = pkg_resources.resource_filename("gns3server", "templates")
tornado_app = tornado.web.Application(self.handlers,
template_path=templates_dir,
debug=True) # FIXME: debug mode!
**settings) # FIXME: debug mode!
try:
print("Starting server on {}:{} (Tornado v{}, PyZMQ v{}, ZMQ v{})".format(self._host,
@ -159,6 +194,10 @@ class Server(object):
zmq.__version__,
zmq.zmq_version()))
kwargs = {"address": self._host}
if ssl_options:
kwargs["ssl_options"] = ssl_options
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)

242
gns3server/start_server.py Normal file
View File

@ -0,0 +1,242 @@
# -*- 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/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
"""
Startup script for GNS3 Server Cloud Instance
"""
import os
import sys
import configparser
import getopt
import datetime
import signal
from logging.handlers import *
from os.path import expanduser
from gns3server.config import Config
import ast
import subprocess
import uuid
SCRIPT_NAME = os.path.basename(__file__)
#Is the full path when used as an import
SCRIPT_PATH = os.path.dirname(__file__)
if not SCRIPT_PATH:
SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(
sys.argv[0])))
LOG_NAME = "gns3-startup"
log = None
usage = """
USAGE: %s
Options:
-d, --debug Enable debugging
-v, --verbose Enable verbose logging
-h, --help Display this menu :)
--data Python dict of data to be written to the config file:
" { 'gns3' : 'Is AWESOME' } "
""" % (SCRIPT_NAME)
# Parse cmd line options
def parse_cmd_line(argv):
"""
Parse command line arguments
argv: Pass in cmd line arguments
"""
short_args = "dvh"
long_args = ("debug",
"verbose",
"help",
"data=",
)
try:
opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args)
except getopt.GetoptError as e:
print("Unrecognized command line option or missing required argument: %s" %(e))
print(usage)
sys.exit(2)
cmd_line_option_list = {}
cmd_line_option_list["debug"] = False
cmd_line_option_list["verbose"] = True
cmd_line_option_list["data"] = None
if sys.platform == "linux":
cmd_line_option_list['syslog'] = "/dev/log"
elif sys.platform == "osx":
cmd_line_option_list['syslog'] = "/var/run/syslog"
else:
cmd_line_option_list['syslog'] = ('localhost',514)
for opt, val in opts:
if (opt in ("-h", "--help")):
print(usage)
sys.exit(0)
elif (opt in ("-d", "--debug")):
cmd_line_option_list["debug"] = True
elif (opt in ("-v", "--verbose")):
cmd_line_option_list["verbose"] = True
elif (opt in ("--data")):
cmd_line_option_list["data"] = ast.literal_eval(val)
return cmd_line_option_list
def set_logging(cmd_options):
"""
Setup logging and format output for console and syslog
Syslog is using the KERN facility
"""
log = logging.getLogger("%s" % (LOG_NAME))
log_level = logging.INFO
log_level_console = logging.WARNING
if cmd_options['verbose'] == True:
log_level_console = logging.INFO
if cmd_options['debug'] == True:
log_level_console = logging.DEBUG
log_level = logging.DEBUG
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console_log = logging.StreamHandler()
console_log.setLevel(log_level_console)
console_log.setFormatter(formatter)
syslog_hndlr = SysLogHandler(
address=cmd_options['syslog'],
facility=SysLogHandler.LOG_KERN
)
syslog_hndlr.setFormatter(sys_formatter)
log.setLevel(log_level)
log.addHandler(console_log)
log.addHandler(syslog_hndlr)
return log
def _generate_certs():
cmd = []
cmd.append("%s/cert_utils/create_cert.sh" % (SCRIPT_PATH))
log.debug("Generating certs ...")
output_raw = subprocess.check_output(cmd, shell=False,
stderr=subprocess.STDOUT)
output_str = output_raw.decode("utf-8")
output = output_str.strip().split("\n")
log.debug(output)
return (output[-2], output[-1])
def _start_gns3server():
cmd = []
cmd.append("gns3server")
log.info("Starting gns3server ...")
subprocess.Popen(cmd, shell=False)
def main():
global log
options = parse_cmd_line(sys.argv)
log = set_logging(options)
def _shutdown(signalnum=None, frame=None):
"""
Handles the SIGINT and SIGTERM event, inside of main so it has access to
the log vars.
"""
log.info("Received shutdown signal")
sys.exit(0)
# Setup signal to catch Control-C / SIGINT and SIGTERM
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
client_data = {}
config = Config.instance()
cfg = config.list_cloud_config_file()
cfg_path = os.path.dirname(cfg)
try:
os.makedirs(cfg_path)
except FileExistsError:
pass
(server_key, server_crt) = _generate_certs()
cloud_config = configparser.ConfigParser()
cloud_config['CLOUD_SERVER'] = {}
if options['data']:
cloud_config['CLOUD_SERVER'] = options['data']
cloud_config['CLOUD_SERVER']['SSL_KEY'] = server_key
cloud_config['CLOUD_SERVER']['SSL_CRT'] = server_crt
cloud_config['CLOUD_SERVER']['SSL_ENABLED'] = 'yes'
cloud_config['CLOUD_SERVER']['WEB_USERNAME'] = str(uuid.uuid4()).upper()[0:8]
cloud_config['CLOUD_SERVER']['WEB_PASSWORD'] = str(uuid.uuid4()).upper()[0:8]
with open(cfg, 'w') as cloud_config_file:
cloud_config.write(cloud_config_file)
cloud_config_file.close()
_start_gns3server()
with open(server_crt, 'r') as cert_file:
cert_data = cert_file.readlines()
cert_file.close()
client_data['SSL_CRT_FILE'] = server_crt
client_data['SSL_CRT'] = cert_data
client_data['WEB_USERNAME'] = cloud_config['CLOUD_SERVER']['WEB_USERNAME']
client_data['WEB_PASSWORD'] = cloud_config['CLOUD_SERVER']['WEB_PASSWORD']
print(client_data)
if __name__ == "__main__":
result = main()
sys.exit(result)

View File

@ -23,5 +23,5 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "1.0beta2"
__version__ = "1.0beta4"
__version_info__ = (1, 0, 0, -99)

View File

@ -1,4 +1,9 @@
netifaces
tornado
tornado==3.2.2
pyzmq
jsonschema
pycurl
python-dateutil
apache-libcloud
requests

View File

@ -52,6 +52,7 @@ setup(
entry_points={
"console_scripts": [
"gns3server = gns3server.main:main",
"gns3dms = gns3dms.main:main",
]
},
packages=find_packages(),

View File

@ -29,9 +29,9 @@ def test_router_exists(router_c7200):
def test_npe(router_c7200):
assert router_c7200.npe == "npe-200" # default value
router_c7200.npe = "npe-400"
assert router_c7200.npe == "npe-400"
assert router_c7200.npe == "npe-400" # default value
router_c7200.npe = "npe-200"
assert router_c7200.npe == "npe-200"
def test_midplane(router_c7200):