From 5e72fcbe14c45c7772ddc31cafbb966146dbace6 Mon Sep 17 00:00:00 2001
From: Jerry Seutter <jseutter@gmail.com>
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 <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__
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 <http://www.gnu.org/licenses/>.
+
+"""
+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("<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
+    }
+
+    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 <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 .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:
+            <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)
+
+        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 <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
+  
+  --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 <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)
\ 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 <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, 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 <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)
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(),