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 76f03952..1255b7ad 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(),