From 5e72fcbe14c45c7772ddc31cafbb966146dbace6 Mon Sep 17 00:00:00 2001 From: Jerry Seutter Date: Fri, 29 Aug 2014 18:05:56 +0000 Subject: [PATCH] GNS-42 - Move deadman switch into gns3server codebase --- gns3dms/__init__.py | 26 ++ gns3dms/cloud/__init__.py | 0 gns3dms/cloud/base_cloud_ctrl.py | 179 +++++++++++++ gns3dms/cloud/exceptions.py | 45 ++++ gns3dms/cloud/rackspace_ctrl.py | 225 +++++++++++++++++ gns3dms/main.py | 390 +++++++++++++++++++++++++++++ gns3dms/modules/__init__.py | 24 ++ gns3dms/modules/daemon.py | 123 +++++++++ gns3dms/modules/rackspace_cloud.py | 68 +++++ gns3dms/version.py | 27 ++ requirements.txt | 4 + setup.py | 1 + 12 files changed, 1112 insertions(+) create mode 100644 gns3dms/__init__.py create mode 100644 gns3dms/cloud/__init__.py create mode 100644 gns3dms/cloud/base_cloud_ctrl.py create mode 100644 gns3dms/cloud/exceptions.py create mode 100644 gns3dms/cloud/rackspace_ctrl.py create mode 100644 gns3dms/main.py create mode 100644 gns3dms/modules/__init__.py create mode 100644 gns3dms/modules/daemon.py create mode 100644 gns3dms/modules/rackspace_cloud.py create mode 100644 gns3dms/version.py diff --git a/gns3dms/__init__.py b/gns3dms/__init__.py new file mode 100644 index 00000000..cf426f79 --- /dev/null +++ b/gns3dms/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# __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__ diff --git a/gns3dms/cloud/__init__.py b/gns3dms/cloud/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3dms/cloud/base_cloud_ctrl.py b/gns3dms/cloud/base_cloud_ctrl.py new file mode 100644 index 00000000..3fb7ec61 --- /dev/null +++ b/gns3dms/cloud/base_cloud_ctrl.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Base cloud controller class. + +Base class for interacting with Cloud APIs to create and manage cloud +instances. + +""" + +from libcloud.compute.base import NodeAuthSSHKey +from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed +from .exceptions import OverLimit, BadRequest, ServiceUnavailable +from .exceptions import Unauthorized, ApiError + + +def parse_exception(exception): + """ + Parse the exception to separate the HTTP status code from the text. + + Libcloud raises many exceptions of the form: + Exception(" ") + + in lieu of raising specific incident-based exceptions. + + """ + + e_str = str(exception) + + try: + status = int(e_str[0:3]) + error_text = e_str[3:] + + except ValueError: + status = None + error_text = e_str + + return status, error_text + + +class BaseCloudCtrl(object): + + """ Base class for interacting with a cloud provider API. """ + + http_status_to_exception = { + 400: BadRequest, + 401: Unauthorized, + 404: ItemNotFound, + 405: MethodNotAllowed, + 413: OverLimit, + 500: ApiError, + 503: ServiceUnavailable + } + + def __init__(self, username, api_key): + self.username = username + self.api_key = api_key + + def _handle_exception(self, status, error_text, response_overrides=None): + """ Raise an exception based on the HTTP status. """ + + if response_overrides: + if status in response_overrides: + raise response_overrides[status](error_text) + + raise self.http_status_to_exception[status](error_text) + + def authenticate(self): + """ Validate cloud account credentials. Return boolean. """ + raise NotImplementedError + + def list_sizes(self): + """ Return a list of NodeSize objects. """ + + return self.driver.list_sizes() + + def create_instance(self, name, size, image, keypair): + """ + Create a new instance with the supplied attributes. + + Return a Node object. + + """ + + auth_key = NodeAuthSSHKey(keypair.public_key) + + try: + return self.driver.create_node( + name=name, + size=size, + image=image, + auth=auth_key + ) + + except Exception as e: + status, error_text = parse_exception(e) + + if status: + self._handle_exception(status, error_text) + else: + raise e + + def delete_instance(self, instance): + """ Delete the specified instance. Returns True or False. """ + + try: + return self.driver.destroy_node(instance) + + except Exception as e: + + status, error_text = parse_exception(e) + + if status: + self._handle_exception(status, error_text) + else: + raise e + + def get_instance(self, instance): + """ Return a Node object representing the requested instance. """ + + for i in self.driver.list_nodes(): + if i.id == instance.id: + return i + + raise ItemNotFound("Instance not found") + + def list_instances(self): + """ Return a list of instances in the current region. """ + + return self.driver.list_nodes() + + def create_key_pair(self, name): + """ Create and return a new Key Pair. """ + + response_overrides = { + 409: KeyPairExists + } + try: + return self.driver.create_key_pair(name) + + except Exception as e: + status, error_text = parse_exception(e) + if status: + self._handle_exception(status, error_text, response_overrides) + else: + raise e + + def delete_key_pair(self, keypair): + """ Delete the keypair. Returns True or False. """ + + try: + return self.driver.delete_key_pair(keypair) + + except Exception as e: + status, error_text = parse_exception(e) + if status: + self._handle_exception(status, error_text) + else: + raise e + + def list_key_pairs(self): + """ Return a list of Key Pairs. """ + + return self.driver.list_key_pairs() diff --git a/gns3dms/cloud/exceptions.py b/gns3dms/cloud/exceptions.py new file mode 100644 index 00000000..beeb598d --- /dev/null +++ b/gns3dms/cloud/exceptions.py @@ -0,0 +1,45 @@ +""" Exception classes for CloudCtrl classes. """ + +class ApiError(Exception): + """ Raised when the server returns 500 Compute Error. """ + pass + +class BadRequest(Exception): + """ Raised when the server returns 400 Bad Request. """ + pass + +class ComputeFault(Exception): + """ Raised when the server returns 400|500 Compute Fault. """ + pass + +class Forbidden(Exception): + """ Raised when the server returns 403 Forbidden. """ + pass + +class ItemNotFound(Exception): + """ Raised when the server returns 404 Not Found. """ + pass + +class KeyPairExists(Exception): + """ Raised when the server returns 409 Conflict Key pair exists. """ + pass + +class MethodNotAllowed(Exception): + """ Raised when the server returns 405 Method Not Allowed. """ + pass + +class OverLimit(Exception): + """ Raised when the server returns 413 Over Limit. """ + pass + +class ServerCapacityUnavailable(Exception): + """ Raised when the server returns 503 Server Capacity Uavailable. """ + pass + +class ServiceUnavailable(Exception): + """ Raised when the server returns 503 Service Unavailable. """ + pass + +class Unauthorized(Exception): + """ Raised when the server returns 401 Unauthorized. """ + pass diff --git a/gns3dms/cloud/rackspace_ctrl.py b/gns3dms/cloud/rackspace_ctrl.py new file mode 100644 index 00000000..ad23598b --- /dev/null +++ b/gns3dms/cloud/rackspace_ctrl.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" Interacts with Rackspace API to create and manage cloud instances. """ + +from .base_cloud_ctrl import BaseCloudCtrl +import json +import requests +from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP +from libcloud.compute.providers import get_driver +from libcloud.compute.types import Provider + +from .exceptions import ItemNotFound, ApiError +from ..version import __version__ + +import logging +log = logging.getLogger(__name__) + +RACKSPACE_REGIONS = [{ENDPOINT_ARGS_MAP[k]['region']: k} for k in + ENDPOINT_ARGS_MAP] + +GNS3IAS_URL = 'http://localhost:8888' # TODO find a place for this value + + +class RackspaceCtrl(BaseCloudCtrl): + + """ Controller class for interacting with Rackspace API. """ + + def __init__(self, username, api_key): + super(RackspaceCtrl, self).__init__(username, api_key) + + # set this up so it can be swapped out with a mock for testing + self.post_fn = requests.post + self.driver_cls = get_driver(Provider.RACKSPACE) + + self.driver = None + self.region = None + self.instances = {} + + self.authenticated = False + self.identity_ep = \ + "https://identity.api.rackspacecloud.com/v2.0/tokens" + + self.regions = [] + self.token = None + + def authenticate(self): + """ + Submit username and api key to API service. + + If authentication is successful, set self.regions and self.token. + Return boolean. + + """ + + self.authenticated = False + + if len(self.username) < 1: + return False + + if len(self.api_key) < 1: + return False + + data = json.dumps({ + "auth": { + "RAX-KSKEY:apiKeyCredentials": { + "username": self.username, + "apiKey": self.api_key + } + } + }) + + headers = { + 'Content-type': 'application/json', + 'Accept': 'application/json' + } + + response = self.post_fn(self.identity_ep, data=data, headers=headers) + + if response.status_code == 200: + + api_data = response.json() + self.token = self._parse_token(api_data) + + if self.token: + self.authenticated = True + user_regions = self._parse_endpoints(api_data) + self.regions = self._make_region_list(user_regions) + + else: + self.regions = [] + self.token = None + + response.connection.close() + + return self.authenticated + + def list_regions(self): + """ Return a list the regions available to the user. """ + + return self.regions + + def _parse_endpoints(self, api_data): + """ + Parse the JSON-encoded data returned by the Identity Service API. + + Return a list of regions available for Compute v2. + + """ + + region_codes = [] + + for ep_type in api_data['access']['serviceCatalog']: + if ep_type['name'] == "cloudServersOpenStack" \ + and ep_type['type'] == "compute": + + for ep in ep_type['endpoints']: + if ep['versionId'] == "2": + region_codes.append(ep['region']) + + return region_codes + + def _parse_token(self, api_data): + """ Parse the token from the JSON-encoded data returned by the API. """ + + try: + token = api_data['access']['token']['id'] + except KeyError: + return None + + return token + + def _make_region_list(self, region_codes): + """ + Make a list of regions for use in the GUI. + + Returns a list of key-value pairs in the form: + : + eg, + [ + {'DFW': 'dfw'} + {'ORD': 'ord'}, + ... + ] + + """ + + region_list = [] + + for ep in ENDPOINT_ARGS_MAP: + if ENDPOINT_ARGS_MAP[ep]['region'] in region_codes: + region_list.append({ENDPOINT_ARGS_MAP[ep]['region']: ep}) + + return region_list + + def set_region(self, region): + """ Set self.region and self.driver. Returns True or False. """ + + try: + self.driver = self.driver_cls(self.username, self.api_key, + region=region) + + except ValueError: + return False + + self.region = region + return True + + def _get_shared_images(self, username, region, gns3_version): + """ + Given a GNS3 version, ask gns3-ias to share compatible images + + Response: + [{"created_at": "", "schema": "", "status": "", "member_id": "", "image_id": "", "updated_at": ""},] + or, if access was already asked + [{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},] + """ + endpoint = GNS3IAS_URL+"/images/grant_access" + params = { + "user_id": username, + "user_region": region, + "gns3_version": gns3_version, + } + response = requests.get(endpoint, params=params) + status = response.status_code + if status == 200: + return response.json() + elif status == 404: + raise ItemNotFound() + else: + raise ApiError("IAS status code: %d" % status) + + def list_images(self): + """ + Return a dictionary containing RackSpace server images + retrieved from gns3-ias server + """ + if not (self.username and self.region): + return [] + + try: + response = self._get_shared_images(self.username, self.region, __version__) + shared_images = json.loads(response) + images = {} + for i in shared_images: + images[i['image_id']] = i['image_name'] + return images + except ItemNotFound: + return [] + except ApiError as e: + log.error('Error while retrieving image list: %s' % e) diff --git a/gns3dms/main.py b/gns3dms/main.py new file mode 100644 index 00000000..bad64a44 --- /dev/null +++ b/gns3dms/main.py @@ -0,0 +1,390 @@ +# -*- 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 . + +# __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 Rackspace API key + --cloud_user_name + + --instance_id ID of the Rackspace instance to terminate + + --deadtime How long in seconds can the communication lose exist before we + shutdown this instance. + Default: + Example --deadtime=3600 (60 minutes) + + --check-interval Defaults to --deadtime, used for debugging + + --init-wait Inital wait time, how long before we start pulling the file. + Default: 300 (5 min) + Example --init-wait=300 + + --file The file we monitor for updates + + -k Kill previous instance running in background + --background Run in background + +""" % (SCRIPT_NAME) + +# Parse cmd line options +def parse_cmd_line(argv): + """ + Parse command line arguments + + argv: Pass in cmd line arguments + """ + + short_args = "dvhk" + long_args = ("debug", + "verbose", + "help", + "cloud_user_name=", + "cloud_api_key=", + "instance_id=", + "deadtime=", + "init-wait=", + "check-interval=", + "file=", + "background", + ) + try: + opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args) + except getopt.GetoptError as e: + print("Unrecognized command line option or missing required argument: %s" %(e)) + print(usage) + sys.exit(2) + + cmd_line_option_list = {} + cmd_line_option_list["debug"] = False + cmd_line_option_list["verbose"] = True + cmd_line_option_list["cloud_user_name"] = None + cmd_line_option_list["cloud_api_key"] = None + cmd_line_option_list["instance_id"] = None + cmd_line_option_list["deadtime"] = 60 * 60 #minutes + cmd_line_option_list["check-interval"] = None + cmd_line_option_list["init-wait"] = 5 * 60 + cmd_line_option_list["file"] = None + cmd_line_option_list["shutdown"] = False + cmd_line_option_list["daemon"] = False + cmd_line_option_list['starttime'] = datetime.datetime.now() + + if sys.platform == "linux": + cmd_line_option_list['syslog'] = "/dev/log" + elif sys.platform == "osx": + cmd_line_option_list['syslog'] = "/var/run/syslog" + else: + cmd_line_option_list['syslog'] = ('localhost',514) + + + get_gns3secrets(cmd_line_option_list) + + for opt, val in opts: + if (opt in ("-h", "--help")): + print(usage) + sys.exit(0) + elif (opt in ("-d", "--debug")): + cmd_line_option_list["debug"] = True + elif (opt in ("-v", "--verbose")): + cmd_line_option_list["verbose"] = True + elif (opt in ("--cloud_user_name")): + cmd_line_option_list["cloud_user_name"] = val + elif (opt in ("--cloud_api_key")): + cmd_line_option_list["cloud_api_key"] = val + elif (opt in ("--instance_id")): + cmd_line_option_list["instance_id"] = val + elif (opt in ("--deadtime")): + cmd_line_option_list["deadtime"] = int(val) + elif (opt in ("--check-interval")): + cmd_line_option_list["check-interval"] = int(val) + elif (opt in ("--init-wait")): + cmd_line_option_list["init-wait"] = int(val) + elif (opt in ("--file")): + cmd_line_option_list["file"] = val + elif (opt in ("-k")): + cmd_line_option_list["shutdown"] = True + elif (opt in ("--background")): + cmd_line_option_list["daemon"] = True + + if cmd_line_option_list["shutdown"] == False: + + if cmd_line_option_list["check-interval"] is None: + cmd_line_option_list["check-interval"] = cmd_line_option_list["deadtime"] + 120 + + if cmd_line_option_list["cloud_user_name"] is None: + print("You need to specify a username!!!!") + print(usage) + sys.exit(2) + + if cmd_line_option_list["cloud_api_key"] is None: + print("You need to specify an apikey!!!!") + print(usage) + sys.exit(2) + + if cmd_line_option_list["file"] is None: + print("You need to specify a file to watch!!!!") + print(usage) + sys.exit(2) + + if cmd_line_option_list["instance_id"] is None: + print("You need to specify an instance_id") + print(usage) + sys.exit(2) + + return cmd_line_option_list + +def get_gns3secrets(cmd_line_option_list): + """ + Load cloud credentials from .gns3secrets + """ + + gns3secret_paths = [ + os.path.expanduser("~/"), + SCRIPT_PATH, + ] + + config = configparser.ConfigParser() + + for gns3secret_path in gns3secret_paths: + gns3secret_file = "%s/.gns3secrets.conf" % (gns3secret_path) + if os.path.isfile(gns3secret_file): + config.read(gns3secret_file) + + try: + for key, value in config.items("Cloud"): + cmd_line_option_list[key] = value.strip() + except configparser.NoSectionError: + pass + + +def set_logging(cmd_options): + """ + Setup logging and format output for console and syslog + + Syslog is using the KERN facility + """ + log = logging.getLogger("%s" % (LOG_NAME)) + log_level = logging.INFO + log_level_console = logging.WARNING + + if cmd_options['verbose'] == True: + log_level_console = logging.INFO + + if cmd_options['debug'] == True: + log_level_console = logging.DEBUG + log_level = logging.DEBUG + + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') + + console_log = logging.StreamHandler() + console_log.setLevel(log_level_console) + console_log.setFormatter(formatter) + + syslog_hndlr = SysLogHandler( + address=cmd_options['syslog'], + facility=SysLogHandler.LOG_KERN + ) + + syslog_hndlr.setFormatter(sys_formatter) + + log.setLevel(log_level) + log.addHandler(console_log) + log.addHandler(syslog_hndlr) + + return log + +def send_shutdown(pid_file): + """ + Sends the daemon process a kill signal + """ + try: + with open(pid_file, 'r') as pidf: + pid = int(pidf.readline().strip()) + pidf.close() + os.kill(pid, 15) + except: + log.info("No running instance found!!!") + log.info("Missing PID file: %s" % (pid_file)) + + +def _get_file_age(filename): + return datetime.datetime.fromtimestamp( + os.path.getmtime(filename) + ) + +def monitor_loop(options): + """ + Checks the options["file"] modification time against an interval. If the + modification time is too old we terminate the instance. + """ + + log.debug("Waiting for init-wait to pass: %s" % (options["init-wait"])) + time.sleep(options["init-wait"]) + + log.info("Starting monitor_loop") + + terminate_attempts = 0 + + while options['shutdown'] == False: + log.debug("In monitor_loop for : %s" % ( + datetime.datetime.now() - options['starttime']) + ) + + file_last_modified = _get_file_age(options["file"]) + now = datetime.datetime.now() + + delta = now - file_last_modified + log.debug("File last updated: %s seconds ago" % (delta.seconds)) + + if delta.seconds > options["deadtime"]: + log.warning("Deadtime exceeded, terminating instance ...") + #Terminate involes many layers of HTTP / API calls, lots of + #different errors types could occur here. + try: + rksp = Rackspace(options) + rksp.terminate() + except Exception as e: + log.critical("Exception during terminate: %s" % (e)) + + terminate_attempts+=1 + log.warning("Termination sent, attempt: %s" % (terminate_attempts)) + time.sleep(600) + else: + time.sleep(options["check-interval"]) + + log.info("Leaving monitor_loop") + log.info("Shutting down") + + +def main(): + + global log + global my_daemon + options = parse_cmd_line(sys.argv) + log = set_logging(options) + + def _shutdown(signalnum=None, frame=None): + """ + Handles the SIGINT and SIGTERM event, inside of main so it has access to + the log vars. + """ + + log.info("Received shutdown signal") + options["shutdown"] = True + + pid_file = "%s/.gns3ias.pid" % (expanduser("~")) + + if options["shutdown"]: + send_shutdown(pid_file) + sys.exit(0) + + if options["daemon"]: + my_daemon = MyDaemon(pid_file, options) + + # Setup signal to catch Control-C / SIGINT and SIGTERM + signal.signal(signal.SIGINT, _shutdown) + signal.signal(signal.SIGTERM, _shutdown) + + log.info("Starting ...") + log.debug("Using settings:") + for key, value in iter(sorted(options.items())): + log.debug("%s : %s" % (key, value)) + + + log.debug("Checking file ....") + if os.path.isfile(options["file"]) == False: + log.critical("File does not exist!!!") + sys.exit(1) + + test_acess = _get_file_age(options["file"]) + if type(test_acess) is not datetime.datetime: + log.critical("Can't get file modification time!!!") + sys.exit(1) + + if my_daemon: + my_daemon.start() + else: + monitor_loop(options) + + +class MyDaemon(daemon.daemon): + def run(self): + monitor_loop(self.options) + + + +if __name__ == "__main__": + result = main() + sys.exit(result) + + diff --git a/gns3dms/modules/__init__.py b/gns3dms/modules/__init__.py new file mode 100644 index 00000000..885d6fa0 --- /dev/null +++ b/gns3dms/modules/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# __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) \ No newline at end of file diff --git a/gns3dms/modules/daemon.py b/gns3dms/modules/daemon.py new file mode 100644 index 00000000..d10d8d2e --- /dev/null +++ b/gns3dms/modules/daemon.py @@ -0,0 +1,123 @@ +"""Generic linux daemon base class for python 3.x.""" + +import sys, os, time, atexit, signal + +class daemon: + """A generic daemon class. + + Usage: subclass the daemon class and override the run() method.""" + + def __init__(self, pidfile, options): + self.pidfile = pidfile + self.options = options + + def daemonize(self): + """Deamonize class. UNIX double fork mechanism.""" + + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #1 failed: {0}\n'.format(err)) + sys.exit(1) + + # decouple from parent environment + os.chdir('/') + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + + # exit from second parent + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #2 failed: {0}\n'.format(err)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + + pid = str(os.getpid()) + with open(self.pidfile,'w+') as f: + f.write(pid + '\n') + + def delpid(self): + os.remove(self.pidfile) + + def 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: + message = "pidfile {0} already exist. " + \ + "Daemon already running?\n" + sys.stderr.write(message.format(self.pidfile)) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run() + + def stop(self): + """Stop the daemon.""" + + # Get the pid from the pidfile + try: + with open(self.pidfile,'r') as pf: + pid = int(pf.read().strip()) + except IOError: + pid = None + + if not pid: + message = "pidfile {0} does not exist. " + \ + "Daemon not running?\n" + sys.stderr.write(message.format(self.pidfile)) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, signal.SIGTERM) + time.sleep(0.1) + except OSError as err: + e = str(err.args) + if e.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print (str(err.args)) + sys.exit(1) + + def restart(self): + """Restart the daemon.""" + self.stop() + self.start() + + def run(self): + """You should override this method when you subclass Daemon. + + It will be called after the process has been daemonized by + start() or restart().""" diff --git a/gns3dms/modules/rackspace_cloud.py b/gns3dms/modules/rackspace_cloud.py new file mode 100644 index 00000000..4b1d6c0f --- /dev/null +++ b/gns3dms/modules/rackspace_cloud.py @@ -0,0 +1,68 @@ +# -*- 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 . + +# __version__ is a human-readable version number. + +# __version_info__ is a four-tuple for programmatic comparison. The first +# three numbers are the components of the version number. The fourth +# is zero for an official release, positive for a development branch, +# or negative for a release candidate or beta (after the base version +# number has been incremented) + +import os, sys +import json +import logging +import socket + +from gns3dms.cloud.rackspace_ctrl import RackspaceCtrl + + +LOG_NAME = "gns3dms.rksp" +log = logging.getLogger("%s" % (LOG_NAME)) + +class Rackspace(object): + def __init__(self, options): + self.username = options["cloud_user_name"] + self.apikey = options["cloud_api_key"] + self.authenticated = False + self.hostname = socket.gethostname() + self.instance_id = options["instance_id"] + + log.debug("Authenticating with Rackspace") + log.debug("My hostname: %s" % (self.hostname)) + self.rksp = RackspaceCtrl(self.username, self.apikey) + self.authenticated = self.rksp.authenticate() + + def _find_my_instance(self): + if self.authenticated == False: + log.critical("Not authenticated against rackspace!!!!") + + for region_dict in self.rksp.list_regions(): + region_k, region_v = region_dict.popitem() + log.debug("Checking region: %s" % (region_k)) + self.rksp.set_region(region_v) + for server in self.rksp.list_instances(): + log.debug("Checking server: %s" % (server.name)) + if server.name.lower() == self.hostname.lower() and server.id == self.instance_id: + log.info("Found matching instance: %s" % (server.id)) + log.info("Startup id: %s" % (self.instance_id)) + return server + + def terminate(self): + server = self._find_my_instance() + log.warning("Sending termination") + self.rksp.delete_instance(server) diff --git a/gns3dms/version.py b/gns3dms/version.py new file mode 100644 index 00000000..545a0060 --- /dev/null +++ b/gns3dms/version.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# __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) diff --git a/requirements.txt b/requirements.txt index 5f53f5ff..2cf31cd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,7 @@ netifaces tornado==3.2.2 pyzmq jsonschema +pycurl +python-dateutil +apache-libcloud + diff --git a/setup.py b/setup.py index e64cfa3d..5da49293 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ setup( entry_points={ "console_scripts": [ "gns3server = gns3server.main:main", + "gns3dms = gns3dms.main:main", ] }, packages=find_packages(),