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

Merge pull request #7 from planctechnologies/gns-110

Support launching devices from cloud file images
This commit is contained in:
Nasrullah Taha 2014-10-30 14:23:43 -06:00
commit bf0b6ee534
9 changed files with 270 additions and 41 deletions

View File

@ -22,13 +22,24 @@ Base class for interacting with Cloud APIs to create and manage cloud
instances. instances.
""" """
from collections import namedtuple
import hashlib
import os
import logging
from io import StringIO, BytesIO
from libcloud.compute.base import NodeAuthSSHKey from libcloud.compute.base import NodeAuthSSHKey
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
from .exceptions import OverLimit, BadRequest, ServiceUnavailable from .exceptions import OverLimit, BadRequest, ServiceUnavailable
from .exceptions import Unauthorized, ApiError from .exceptions import Unauthorized, ApiError
KeyPair = namedtuple("KeyPair", ['name'], verbose=False)
log = logging.getLogger(__name__)
def parse_exception(exception): def parse_exception(exception):
""" """
Parse the exception to separate the HTTP status code from the text. Parse the exception to separate the HTTP status code from the text.
@ -67,6 +78,8 @@ class BaseCloudCtrl(object):
503: ServiceUnavailable 503: ServiceUnavailable
} }
GNS3_CONTAINER_NAME = 'GNS3'
def __init__(self, username, api_key): def __init__(self, username, api_key):
self.username = username self.username = username
self.api_key = api_key self.api_key = api_key
@ -89,23 +102,37 @@ class BaseCloudCtrl(object):
return self.driver.list_sizes() return self.driver.list_sizes()
def create_instance(self, name, size, image, keypair): def list_flavors(self):
""" Return an iterable of flavors """
raise NotImplementedError
def create_instance(self, name, size_id, image_id, keypair):
""" """
Create a new instance with the supplied attributes. Create a new instance with the supplied attributes.
Return a Node object. Return a Node object.
""" """
auth_key = NodeAuthSSHKey(keypair.public_key)
try: try:
return self.driver.create_node( image = self.get_image(image_id)
name=name, if image is None:
size=size, raise ItemNotFound("Image not found")
image=image,
auth=auth_key size = self.driver.ex_get_size(size_id)
)
args = {
"name": name,
"size": size,
"image": image,
}
if keypair is not None:
auth_key = NodeAuthSSHKey(keypair.public_key)
args["auth"] = auth_key
args["ex_keyname"] = name
return self.driver.create_node(**args)
except Exception as e: except Exception as e:
status, error_text = parse_exception(e) status, error_text = parse_exception(e)
@ -113,7 +140,8 @@ class BaseCloudCtrl(object):
if status: if status:
self._handle_exception(status, error_text) self._handle_exception(status, error_text)
else: else:
raise e log.error("create_instance method raised an exception: {}".format(e))
log.error('image id {}'.format(image))
def delete_instance(self, instance): def delete_instance(self, instance):
""" Delete the specified instance. Returns True or False. """ """ Delete the specified instance. Returns True or False. """
@ -142,7 +170,11 @@ class BaseCloudCtrl(object):
def list_instances(self): def list_instances(self):
""" Return a list of instances in the current region. """ """ Return a list of instances in the current region. """
return self.driver.list_nodes() try:
return self.driver.list_nodes()
except Exception as e:
log.error("list_instances returned an error: {}".format(e))
def create_key_pair(self, name): def create_key_pair(self, name):
""" Create and return a new Key Pair. """ """ Create and return a new Key Pair. """
@ -173,7 +205,85 @@ class BaseCloudCtrl(object):
else: else:
raise e raise e
def delete_key_pair_by_name(self, keypair_name):
""" Utility method to incapsulate boilerplate code """
kp = KeyPair(name=keypair_name)
return self.delete_key_pair(kp)
def list_key_pairs(self): def list_key_pairs(self):
""" Return a list of Key Pairs. """ """ Return a list of Key Pairs. """
return self.driver.list_key_pairs() return self.driver.list_key_pairs()
def upload_file(self, file_path, folder):
"""
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
:param file_path: path to file to upload
:param folder: folder in cloud storage to save file in
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
"""
try:
gns3_container = self.storage_driver.create_container(self.GNS3_CONTAINER_NAME)
except ContainerAlreadyExistsError:
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
with open(file_path, 'rb') as file:
local_file_hash = hashlib.md5(file.read()).hexdigest()
cloud_object_name = folder + '/' + os.path.basename(file_path)
cloud_hash_name = cloud_object_name + '.md5'
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
# if the file and its hash are in object storage, and the local and storage file hashes match
# do not upload the file, otherwise upload it
if cloud_object_name in cloud_objects and cloud_hash_name in cloud_objects:
hash_object = gns3_container.get_object(cloud_hash_name)
cloud_object_hash = ''
for chunk in hash_object.as_stream():
cloud_object_hash += chunk.decode('utf8')
if cloud_object_hash == local_file_hash:
return False
file.seek(0)
self.storage_driver.upload_object_via_stream(file, gns3_container, cloud_object_name)
self.storage_driver.upload_object_via_stream(StringIO(local_file_hash), gns3_container, cloud_hash_name)
return True
def list_projects(self):
"""
Lists projects in cloud storage
:return: List of (project name, object name in storage)
"""
try:
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
projects = [
(obj.name.replace('projects/', '').replace('.zip', ''), obj.name)
for obj in gns3_container.list_objects()
if obj.name.startswith('projects/') and obj.name[-4:] == '.zip'
]
return projects
except ContainerDoesNotExistError:
return []
def download_file(self, file_name, destination=None):
"""
Downloads file from cloud storage
:param file_name: name of file in cloud storage to download
:param destination: local path to save file to (if None, returns file contents as a file-like object)
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
"""
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
storage_object = gns3_container.get_object(file_name)
if destination is not None:
storage_object.download(destination)
else:
contents = b''
for chunk in storage_object.as_stream():
contents += chunk
return BytesIO(contents)

View File

@ -23,31 +23,37 @@ import requests
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
from libcloud.compute.providers import get_driver from libcloud.compute.providers import get_driver
from libcloud.compute.types import Provider from libcloud.compute.types import Provider
from libcloud.storage.providers import get_driver as get_storage_driver
from libcloud.storage.types import Provider as StorageProvider
from .exceptions import ItemNotFound, ApiError from .exceptions import ItemNotFound, ApiError
from ..version import __version__ from ..version import __version__
from collections import OrderedDict
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
ENDPOINT_ARGS_MAP] ENDPOINT_ARGS_MAP]
GNS3IAS_URL = 'http://localhost:8888' # TODO find a place for this value
class RackspaceCtrl(BaseCloudCtrl): class RackspaceCtrl(BaseCloudCtrl):
""" Controller class for interacting with Rackspace API. """ """ Controller class for interacting with Rackspace API. """
def __init__(self, username, api_key): def __init__(self, username, api_key, gns3_ias_url):
super(RackspaceCtrl, self).__init__(username, api_key) super(RackspaceCtrl, self).__init__(username, api_key)
self.gns3_ias_url = gns3_ias_url
# set this up so it can be swapped out with a mock for testing # set this up so it can be swapped out with a mock for testing
self.post_fn = requests.post self.post_fn = requests.post
self.driver_cls = get_driver(Provider.RACKSPACE) self.driver_cls = get_driver(Provider.RACKSPACE)
self.storage_driver_cls = get_storage_driver(StorageProvider.CLOUDFILES)
self.driver = None self.driver = None
self.storage_driver = None
self.region = None self.region = None
self.instances = {} self.instances = {}
@ -57,6 +63,26 @@ class RackspaceCtrl(BaseCloudCtrl):
self.regions = [] self.regions = []
self.token = None self.token = None
self.tenant_id = None
self.flavor_ep = "https://dfw.servers.api.rackspacecloud.com/v2/{username}/flavors"
self._flavors = OrderedDict([
('2', '512MB, 1 VCPU'),
('3', '1GB, 1 VCPU'),
('4', '2GB, 2 VCPUs'),
('5', '4GB, 2 VCPUs'),
('6', '8GB, 4 VCPUs'),
('7', '15GB, 6 VCPUs'),
('8', '30GB, 8 VCPUs'),
('performance1-1', '1GB Performance, 1 VCPU'),
('performance1-2', '2GB Performance, 2 VCPUs'),
('performance1-4', '4GB Performance, 4 VCPUs'),
('performance1-8', '8GB Performance, 8 VCPUs'),
('performance2-15', '15GB Performance, 4 VCPUs'),
('performance2-30', '30GB Performance, 8 VCPUs'),
('performance2-60', '60GB Performance, 16 VCPUs'),
('performance2-90', '90GB Performance, 24 VCPUs'),
('performance2-120', '120GB Performance, 32 VCPUs',)
])
def authenticate(self): def authenticate(self):
""" """
@ -100,6 +126,7 @@ class RackspaceCtrl(BaseCloudCtrl):
self.authenticated = True self.authenticated = True
user_regions = self._parse_endpoints(api_data) user_regions = self._parse_endpoints(api_data)
self.regions = self._make_region_list(user_regions) self.regions = self._make_region_list(user_regions)
self.tenant_id = self._parse_tenant_id(api_data)
else: else:
self.regions = [] self.regions = []
@ -114,6 +141,11 @@ class RackspaceCtrl(BaseCloudCtrl):
return self.regions return self.regions
def list_flavors(self):
""" Return the dictionary containing flavors id and names """
return self._flavors
def _parse_endpoints(self, api_data): def _parse_endpoints(self, api_data):
""" """
Parse the JSON-encoded data returned by the Identity Service API. Parse the JSON-encoded data returned by the Identity Service API.
@ -144,6 +176,17 @@ class RackspaceCtrl(BaseCloudCtrl):
return token return token
def _parse_tenant_id(self, api_data):
""" """
try:
roles = api_data['access']['user']['roles']
for role in roles:
if 'tenantId' in role and role['name'] == 'compute:default':
return role['tenantId']
return None
except KeyError:
return None
def _make_region_list(self, region_codes): def _make_region_list(self, region_codes):
""" """
Make a list of regions for use in the GUI. Make a list of regions for use in the GUI.
@ -173,6 +216,8 @@ class RackspaceCtrl(BaseCloudCtrl):
try: try:
self.driver = self.driver_cls(self.username, self.api_key, self.driver = self.driver_cls(self.username, self.api_key,
region=region) region=region)
self.storage_driver = self.storage_driver_cls(self.username, self.api_key,
region=region)
except ValueError: except ValueError:
return False return False
@ -189,14 +234,19 @@ class RackspaceCtrl(BaseCloudCtrl):
or, if access was already asked or, if access was already asked
[{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},] [{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},]
""" """
endpoint = GNS3IAS_URL+"/images/grant_access" endpoint = self.gns3_ias_url+"/images/grant_access"
params = { params = {
"user_id": username, "user_id": username,
"user_region": region, "user_region": region.upper(),
"gns3_version": gns3_version, "gns3_version": gns3_version,
} }
response = requests.get(endpoint, params=params) try:
response = requests.get(endpoint, params=params)
except requests.ConnectionError:
raise ApiError("Unable to connect to IAS")
status = response.status_code status = response.status_code
if status == 200: if status == 200:
return response.json() return response.json()
elif status == 404: elif status == 404:
@ -209,17 +259,53 @@ class RackspaceCtrl(BaseCloudCtrl):
Return a dictionary containing RackSpace server images Return a dictionary containing RackSpace server images
retrieved from gns3-ias server retrieved from gns3-ias server
""" """
if not (self.username and self.region): if not (self.tenant_id and self.region):
return [] return {}
try: try:
response = self._get_shared_images(self.username, self.region, __version__) shared_images = self._get_shared_images(self.tenant_id, self.region, __version__)
shared_images = json.loads(response)
images = {} images = {}
for i in shared_images: for i in shared_images:
images[i['image_id']] = i['image_name'] images[i['image_id']] = i['image_name']
return images return images
except ItemNotFound: except ItemNotFound:
return [] return {}
except ApiError as e: except ApiError as e:
log.error('Error while retrieving image list: %s' % e) log.error('Error while retrieving image list: %s' % e)
return {}
def get_image(self, image_id):
return self.driver.get_image(image_id)
def get_provider(cloud_settings):
"""
Utility function to retrieve a cloud provider instance already authenticated and with the
region set
:param cloud_settings: cloud settings dictionary
:return: a provider instance or None on errors
"""
try:
username = cloud_settings['cloud_user_name']
apikey = cloud_settings['cloud_api_key']
region = cloud_settings['cloud_region']
ias_url = cloud_settings.get('gns3_ias_url', '')
except KeyError as e:
log.error("Unable to create cloud provider: {}".format(e))
return
provider = RackspaceCtrl(username, apikey, ias_url)
if not provider.authenticate():
log.error("Authentication failed for cloud provider")
return
if not region:
region = provider.list_regions().values()[0]
if not provider.set_region(region):
log.error("Unable to set cloud provider region")
return
return provider

View File

@ -77,7 +77,7 @@ Options:
--cloud_user_name --cloud_user_name
--instance_id ID of the Rackspace instance to terminate --instance_id ID of the Rackspace instance to terminate
--region Region of instance --cloud_region Region of instance
--deadtime How long in seconds can the communication lose exist before we --deadtime How long in seconds can the communication lose exist before we
shutdown this instance. shutdown this instance.
@ -205,8 +205,8 @@ def parse_cmd_line(argv):
print(usage) print(usage)
sys.exit(2) sys.exit(2)
if cmd_line_option_list["region"] is None: if cmd_line_option_list["cloud_region"] is None:
print("You need to specify a region") print("You need to specify a cloud_region")
print(usage) print(usage)
sys.exit(2) sys.exit(2)

View File

@ -26,6 +26,8 @@ import configparser
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
CLOUD_SERVER = 'CLOUD_SERVER'
class Config(object): class Config(object):
""" """
@ -62,20 +64,30 @@ class Config(object):
# 5: server.conf in the current working directory # 5: server.conf in the current working directory
home = os.path.expanduser("~") home = os.path.expanduser("~")
self._cloud_config = os.path.join(home, ".config", appname, "cloud.conf") self._cloud_file = os.path.join(home, ".config", appname, "cloud.conf")
filename = "server.conf" filename = "server.conf"
self._files = [os.path.join(home, ".config", appname, filename), self._files = [os.path.join(home, ".config", appname, filename),
os.path.join(home, ".config", appname + ".conf"), os.path.join(home, ".config", appname + ".conf"),
os.path.join("/etc/xdg", appname, filename), os.path.join("/etc/xdg", appname, filename),
os.path.join("/etc/xdg", appname + ".conf"), os.path.join("/etc/xdg", appname + ".conf"),
filename, filename,
self._cloud_config] self._cloud_file]
self._config = configparser.ConfigParser() self._config = configparser.ConfigParser()
self.read_config() self.read_config()
self._cloud_config = configparser.ConfigParser()
self.read_cloud_config()
def list_cloud_config_file(self): def list_cloud_config_file(self):
return self._cloud_config return self._cloud_file
def read_cloud_config(self):
parsed_file = self._cloud_config.read(self._cloud_file)
if not self._cloud_config.has_section(CLOUD_SERVER):
self._cloud_config.add_section(CLOUD_SERVER)
def cloud_settings(self):
return self._cloud_config[CLOUD_SERVER]
def read_config(self): def read_config(self):
""" """

View File

@ -78,11 +78,15 @@ class LoginHandler(tornado.web.RequestHandler):
self.set_secure_cookie("user", "None") self.set_secure_cookie("user", "None")
auth_status = "failure" auth_status = "failure"
log.info("Authentication attempt %s: %s" %(auth_status, user)) log.info("Authentication attempt {}: {}, {}".format(auth_status, user, password))
try: try:
redirect_to = self.get_secure_cookie("login_success_redirect_to") redirect_to = self.get_secure_cookie("login_success_redirect_to")
except tornado.web.MissingArgumentError: except tornado.web.MissingArgumentError:
redirect_to = "/" redirect_to = "/"
self.redirect(redirect_to) if redirect_to is None:
self.write({'result': auth_status})
else:
log.info('Redirecting to {}'.format(redirect_to))
self.redirect(redirect_to)

View File

@ -61,6 +61,7 @@ class IModule(multiprocessing.Process):
self._current_destination = None self._current_destination = None
self._current_call_id = None self._current_call_id = None
self._stopping = False self._stopping = False
self._cloud_settings = config.cloud_settings()
def _setup(self): def _setup(self):
""" """
@ -177,7 +178,6 @@ class IModule(multiprocessing.Process):
# add session to the response # add session to the response
response = [self._current_session, jsonrpc_response] response = [self._current_session, jsonrpc_response]
log.debug("ZeroMQ client ({}) sending: {}".format(self.name, response))
self._stream.send_json(response) self._stream.send_json(response)
def send_param_error(self): def send_param_error(self):

View File

@ -19,6 +19,7 @@ import os
import base64 import base64
import time import time
from gns3server.modules import IModule from gns3server.modules import IModule
from gns3dms.cloud.rackspace_ctrl import get_provider
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
from ..nodes.c1700 import C1700 from ..nodes.c1700 import C1700
@ -140,12 +141,22 @@ class VM(object):
chassis = request.get("chassis") chassis = request.get("chassis")
router_id = request.get("router_id") router_id = request.get("router_id")
# Locate the image
updated_image_path = os.path.join(self.images_directory, image) updated_image_path = os.path.join(self.images_directory, image)
if os.path.isfile(updated_image_path): if os.path.isfile(updated_image_path):
image = updated_image_path image = updated_image_path
else:
if not os.path.exists(self.images_directory):
os.mkdir(self.images_directory)
if request.get("cloud_path", None):
# Download the image from cloud files
cloud_path = request.get("cloud_path")
full_cloud_path = "/".join((cloud_path, image))
provider = get_provider(self._cloud_settings)
provider.download_file(full_cloud_path, updated_image_path)
try: try:
if platform not in PLATFORMS: if platform not in PLATFORMS:
raise DynamipsError("Unknown router platform: {}".format(platform)) raise DynamipsError("Unknown router platform: {}".format(platform))

View File

@ -67,6 +67,10 @@ VM_CREATE_SCHEMA = {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
},
"cloud_path": {
"description": "Path to the image in the cloud object store",
"type": "string",
} }
}, },
"additionalProperties": False, "additionalProperties": False,

View File

@ -61,6 +61,7 @@ USAGE: %s
Options: Options:
-d, --debug Enable debugging -d, --debug Enable debugging
-i --ip The ip address of the server, for cert generation
-v, --verbose Enable verbose logging -v, --verbose Enable verbose logging
-h, --help Display this menu :) -h, --help Display this menu :)
@ -79,6 +80,7 @@ def parse_cmd_line(argv):
short_args = "dvh" short_args = "dvh"
long_args = ("debug", long_args = ("debug",
"ip=",
"verbose", "verbose",
"help", "help",
"data=", "data=",
@ -105,6 +107,8 @@ def parse_cmd_line(argv):
sys.exit(0) sys.exit(0)
elif opt in ("-d", "--debug"): elif opt in ("-d", "--debug"):
cmd_line_option_list["debug"] = True cmd_line_option_list["debug"] = True
elif opt in ("--ip",):
cmd_line_option_list["ip"] = val
elif opt in ("-v", "--verbose"): elif opt in ("-v", "--verbose"):
cmd_line_option_list["verbose"] = True cmd_line_option_list["verbose"] = True
elif opt in ("--data",): elif opt in ("--data",):
@ -151,7 +155,7 @@ def set_logging(cmd_options):
return log return log
def _generate_certs(): def _generate_certs(options):
""" """
Generate a self-signed certificate for SSL-enabling the WebSocket Generate a self-signed certificate for SSL-enabling the WebSocket
connection. The certificate is sent back to the client so it can connection. The certificate is sent back to the client so it can
@ -159,7 +163,7 @@ def _generate_certs():
:return: A 2-tuple of strings containing (server_key, server_cert) :return: A 2-tuple of strings containing (server_key, server_cert)
""" """
cmd = ["{}/cert_utils/create_cert.sh".format(SCRIPT_PATH)] cmd = ["{}/cert_utils/create_cert.sh".format(SCRIPT_PATH), options['ip']]
log.debug("Generating certs with cmd: {}".format(' '.join(cmd))) log.debug("Generating certs with cmd: {}".format(' '.join(cmd)))
output_raw = subprocess.check_output(cmd, shell=False, output_raw = subprocess.check_output(cmd, shell=False,
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
@ -176,9 +180,9 @@ def _start_gns3server():
:return: None :return: None
""" """
cmd = ['gns3server', '--quiet'] cmd = 'gns3server --quiet > /tmp/gns3.log 2>&1 &'
log.info("Starting gns3server with cmd {}".format(cmd)) log.info("Starting gns3server with cmd {}".format(cmd))
subprocess.Popen(cmd, shell=False) os.system(cmd)
def main(): def main():
@ -211,7 +215,7 @@ def main():
except FileExistsError: except FileExistsError:
pass pass
(server_key, server_crt) = _generate_certs() (server_key, server_crt) = _generate_certs(options)
cloud_config = configparser.ConfigParser() cloud_config = configparser.ConfigParser()
cloud_config['CLOUD_SERVER'] = {} cloud_config['CLOUD_SERVER'] = {}
@ -221,15 +225,13 @@ def main():
cloud_config['CLOUD_SERVER']['SSL_KEY'] = server_key cloud_config['CLOUD_SERVER']['SSL_KEY'] = server_key
cloud_config['CLOUD_SERVER']['SSL_CRT'] = server_crt cloud_config['CLOUD_SERVER']['SSL_CRT'] = server_crt
cloud_config['CLOUD_SERVER']['SSL_ENABLED'] = 'yes' cloud_config['CLOUD_SERVER']['SSL_ENABLED'] = 'no'
cloud_config['CLOUD_SERVER']['WEB_USERNAME'] = str(uuid.uuid4()).upper()[0:8] cloud_config['CLOUD_SERVER']['WEB_USERNAME'] = str(uuid.uuid4()).upper()[0:8]
cloud_config['CLOUD_SERVER']['WEB_PASSWORD'] = 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: with open(cfg, 'w') as cloud_config_file:
cloud_config.write(cloud_config_file) cloud_config.write(cloud_config_file)
cloud_config_file.close()
_start_gns3server() _start_gns3server()
with open(server_crt, 'r') as cert_file: with open(server_crt, 'r') as cert_file: