mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-28 11:18:11 +00:00
Drop unused cloud code, this cleanup the dependencies
This commit is contained in:
parent
3407ba802e
commit
83c1ada63e
@ -1,26 +0,0 @@
|
|||||||
# -*- 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__
|
|
@ -1,340 +0,0 @@
|
|||||||
# -*- 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 collections import namedtuple
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from io import StringIO, BytesIO
|
|
||||||
|
|
||||||
from libcloud.compute.base import NodeAuthSSHKey
|
|
||||||
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError
|
|
||||||
|
|
||||||
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
|
|
||||||
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
|
|
||||||
from .exceptions import Unauthorized, ApiError
|
|
||||||
|
|
||||||
|
|
||||||
KeyPair = namedtuple("KeyPair", ['name'], verbose=False)
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
GNS3_CONTAINER_NAME = 'GNS3'
|
|
||||||
|
|
||||||
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 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.
|
|
||||||
|
|
||||||
Return a Node object.
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
image = self.get_image(image_id)
|
|
||||||
if image is None:
|
|
||||||
raise ItemNotFound("Image not found")
|
|
||||||
|
|
||||||
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:
|
|
||||||
status, error_text = parse_exception(e)
|
|
||||||
|
|
||||||
if status:
|
|
||||||
self._handle_exception(status, error_text)
|
|
||||||
else:
|
|
||||||
log.error("create_instance method raised an exception: {}".format(e))
|
|
||||||
log.error('image id {}'.format(image))
|
|
||||||
|
|
||||||
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. """
|
|
||||||
|
|
||||||
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):
|
|
||||||
""" 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 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):
|
|
||||||
""" Return a list of Key Pairs. """
|
|
||||||
|
|
||||||
return self.driver.list_key_pairs()
|
|
||||||
|
|
||||||
def upload_file(self, file_path, cloud_object_name):
|
|
||||||
"""
|
|
||||||
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
|
|
||||||
:param file_path: path to file to upload
|
|
||||||
:param cloud_object_name: name of file saved in cloud storage
|
|
||||||
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
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_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: Dictionary where project names are keys and values are names of objects in storage
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
|
||||||
projects = {
|
|
||||||
obj.name.replace('projects/', '').replace('.zip', ''): obj.name
|
|
||||||
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. If a file exists at destination, and it is identical to the file in cloud
|
|
||||||
storage, it is not downloaded.
|
|
||||||
:param file_name: name of file in cloud storage to download
|
|
||||||
:param destination: local path to save file to (if None, returns file contents as a file-like object)
|
|
||||||
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
|
|
||||||
"""
|
|
||||||
|
|
||||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
|
||||||
storage_object = gns3_container.get_object(file_name)
|
|
||||||
|
|
||||||
if destination is not None:
|
|
||||||
if os.path.isfile(destination):
|
|
||||||
# if a file exists at destination and its hash matches that of the
|
|
||||||
# file in cloud storage, don't download it
|
|
||||||
with open(destination, 'rb') as f:
|
|
||||||
local_file_hash = hashlib.md5(f.read()).hexdigest()
|
|
||||||
|
|
||||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
|
||||||
cloud_object_hash = ''
|
|
||||||
for chunk in hash_object.as_stream():
|
|
||||||
cloud_object_hash += chunk.decode('utf8')
|
|
||||||
|
|
||||||
if local_file_hash == cloud_object_hash:
|
|
||||||
return
|
|
||||||
|
|
||||||
storage_object.download(destination)
|
|
||||||
else:
|
|
||||||
contents = b''
|
|
||||||
|
|
||||||
for chunk in storage_object.as_stream():
|
|
||||||
contents += chunk
|
|
||||||
|
|
||||||
return BytesIO(contents)
|
|
||||||
|
|
||||||
def find_storage_image_names(self, images_to_find):
|
|
||||||
"""
|
|
||||||
Maps names of image files to their full name in cloud storage
|
|
||||||
:param images_to_find: list of image names to find
|
|
||||||
:return: A dictionary where keys are image names, and values are the corresponding names of
|
|
||||||
the files in cloud storage
|
|
||||||
"""
|
|
||||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
|
||||||
images_in_storage = [obj.name for obj in gns3_container.list_objects() if obj.name.startswith('images/')]
|
|
||||||
|
|
||||||
images = {}
|
|
||||||
for image_name in images_to_find:
|
|
||||||
images_with_same_name =\
|
|
||||||
list(filter(lambda storage_image_name: storage_image_name.endswith(image_name), images_in_storage))
|
|
||||||
|
|
||||||
if len(images_with_same_name) == 1:
|
|
||||||
images[image_name] = images_with_same_name[0]
|
|
||||||
else:
|
|
||||||
raise Exception('Image does not exist in cloud storage or is duplicated')
|
|
||||||
|
|
||||||
return images
|
|
||||||
|
|
||||||
def delete_file(self, file_name):
|
|
||||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
|
||||||
|
|
||||||
try:
|
|
||||||
object_to_delete = gns3_container.get_object(file_name)
|
|
||||||
object_to_delete.delete()
|
|
||||||
except ObjectDoesNotExistError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
|
||||||
hash_object.delete()
|
|
||||||
except ObjectDoesNotExistError:
|
|
||||||
pass
|
|
@ -1,67 +0,0 @@
|
|||||||
""" 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
|
|
@ -1,259 +0,0 @@
|
|||||||
# -*- 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 libcloud.storage.providers import get_driver as get_storage_driver
|
|
||||||
from libcloud.storage.types import Provider as StorageProvider
|
|
||||||
|
|
||||||
from .exceptions import ItemNotFound, ApiError
|
|
||||||
from ..version import __version__
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import logging
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in
|
|
||||||
ENDPOINT_ARGS_MAP]
|
|
||||||
|
|
||||||
|
|
||||||
class RackspaceCtrl(BaseCloudCtrl):
|
|
||||||
|
|
||||||
""" Controller class for interacting with Rackspace API. """
|
|
||||||
|
|
||||||
def __init__(self, username, api_key, *args, **kwargs):
|
|
||||||
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.storage_driver_cls = get_storage_driver(StorageProvider.CLOUDFILES)
|
|
||||||
|
|
||||||
self.driver = None
|
|
||||||
self.storage_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
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
self.tenant_id = self._parse_tenant_id(api_data)
|
|
||||||
|
|
||||||
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 list_flavors(self):
|
|
||||||
""" Return the dictionary containing flavors id and names """
|
|
||||||
|
|
||||||
return self._flavors
|
|
||||||
|
|
||||||
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 _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):
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
self.storage_driver = self.storage_driver_cls(self.username, self.api_key,
|
|
||||||
region=region)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.region = region
|
|
||||||
return True
|
|
||||||
|
|
||||||
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']
|
|
||||||
except KeyError as e:
|
|
||||||
log.error("Unable to create cloud provider: {}".format(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
provider = RackspaceCtrl(username, apikey)
|
|
||||||
|
|
||||||
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
|
|
402
gns3dms/main.py
402
gns3dms/main.py
@ -1,402 +0,0 @@
|
|||||||
# -*- 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
|
|
||||||
--cloud_region Region of instance
|
|
||||||
|
|
||||||
--dead_time How long in seconds can the communication lose exist before we
|
|
||||||
shutdown this instance.
|
|
||||||
Default:
|
|
||||||
Example --dead_time=3600 (60 minutes)
|
|
||||||
|
|
||||||
--check-interval Defaults to --dead_time, used for debugging
|
|
||||||
|
|
||||||
--init-wait Inital wait time, how long before we start pulling the file.
|
|
||||||
Default: 300 (5 min)
|
|
||||||
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=",
|
|
||||||
"dead_time=",
|
|
||||||
"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["dead_time"] = 60 * 60 # minutes
|
|
||||||
cmd_line_option_list["check-interval"] = None
|
|
||||||
cmd_line_option_list["init-wait"] = 5 * 60
|
|
||||||
cmd_line_option_list["file"] = None
|
|
||||||
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)
|
|
||||||
cmd_line_option_list["dead_time"] = int(cmd_line_option_list["dead_time"])
|
|
||||||
|
|
||||||
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 ("--dead_time")):
|
|
||||||
cmd_line_option_list["dead_time"] = int(val)
|
|
||||||
elif (opt in ("--check-interval")):
|
|
||||||
cmd_line_option_list["check-interval"] = int(val)
|
|
||||||
elif (opt in ("--init-wait")):
|
|
||||||
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"] is False:
|
|
||||||
|
|
||||||
if cmd_line_option_list["check-interval"] is None:
|
|
||||||
cmd_line_option_list["check-interval"] = cmd_line_option_list["dead_time"] + 120
|
|
||||||
|
|
||||||
if cmd_line_option_list["cloud_user_name"] is None:
|
|
||||||
print("You need to specify a username!!!!")
|
|
||||||
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["cloud_region"] is None:
|
|
||||||
print("You need to specify a cloud_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']:
|
|
||||||
log_level_console = logging.INFO
|
|
||||||
|
|
||||||
if cmd_options['debug']:
|
|
||||||
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'] is 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["dead_time"]:
|
|
||||||
log.warning("Dead time exceeded, terminating instance ...")
|
|
||||||
# Terminate involves many layers of HTTP / API calls, lots of
|
|
||||||
# different errors types could occur here.
|
|
||||||
try:
|
|
||||||
rksp = Rackspace(options)
|
|
||||||
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"]) is False:
|
|
||||||
log.critical("File does not exist!!!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
test_acess = _get_file_age(options["file"])
|
|
||||||
if not isinstance(test_acess, 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)
|
|
@ -1,24 +0,0 @@
|
|||||||
# -*- 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)
|
|
@ -1,144 +0,0 @@
|
|||||||
"""Generic linux daemon base class for python 3.x."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import atexit
|
|
||||||
import 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 True:
|
|
||||||
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()."""
|
|
@ -1,73 +0,0 @@
|
|||||||
# -*- 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
|
|
||||||
import 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["cloud_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 is not 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.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)
|
|
@ -1,27 +0,0 @@
|
|||||||
# -*- 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)
|
|
@ -1,8 +1,6 @@
|
|||||||
netifaces==0.10.4
|
netifaces==0.10.4
|
||||||
jsonschema==2.4.0
|
jsonschema==2.4.0
|
||||||
python-dateutil==2.3
|
python-dateutil==2.3
|
||||||
apache-libcloud==0.16.0
|
|
||||||
requests==2.5.0
|
|
||||||
aiohttp==0.14.4
|
aiohttp==0.14.4
|
||||||
Jinja2==2.7.3
|
Jinja2==2.7.3
|
||||||
raven==5.2.0
|
raven==5.2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user