mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-30 20:28:08 +00:00
Copied fresh cloud files from gns gui repo
This commit is contained in:
parent
f287f5141a
commit
a833925497
@ -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)
|
||||||
|
@ -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['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
|
||||||
|
Loading…
Reference in New Issue
Block a user