From 3845cab84b4c740d17b25b25d11b3f146da73e05 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 28 Aug 2014 22:09:38 -0600 Subject: [PATCH 01/11] Adding initial module --- gns3server/modules/deadman/__init__.py | 95 ++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 gns3server/modules/deadman/__init__.py diff --git a/gns3server/modules/deadman/__init__.py b/gns3server/modules/deadman/__init__.py new file mode 100644 index 00000000..59cf106c --- /dev/null +++ b/gns3server/modules/deadman/__init__.py @@ -0,0 +1,95 @@ +# -*- 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 . + +""" +IOU server module. +""" + +import os +import base64 +import tempfile + +from gns3server.modules import IModule +from gns3server.config import Config + + +import logging +log = logging.getLogger(__name__) + +class DeadMan(): + """ + DeadMan module. + + :param name: module name + :param args: arguments for the module + :param kwargs: named arguments for the module + """ + + def __init__(self, name, *args, **kwargs): + config = Config.instance() + + # a new process start when calling IModule + IModule.__init__(self, name, *args, **kwargs) + self._host = kwargs["host"] + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] + self._working_dir = self._projects_dir + + # check every 5 seconds + #self._deadman_callback = self.add_periodic_callback(self._check_deadman_is_alive, 5000) + self._deadman_callback.start() + + def stop(self, signum=None): + """ + Properly stops the module. + + :param signum: signal number (if called by the signal handler) + """ + + self._iou_callback.stop() + + # delete all IOU instances + for iou_id in self._iou_instances: + iou_instance = self._iou_instances[iou_id] + iou_instance.delete() + + self.delete_iourc_file() + + IModule.stop(self, signum) # this will stop the I/O loop + + + @IModule.route("iou.reset") + def reset(self, request=None): + """ + Resets the module (JSON-RPC notification). + + :param request: JSON request (not used) + """ + + # delete all IOU instances + for iou_id in self._iou_instances: + iou_instance = self._iou_instances[iou_id] + iou_instance.delete() + + # resets the instance IDs + IOUDevice.reset() + + self._iou_instances.clear() + self._allocated_udp_ports.clear() + self.delete_iourc_file() + + log.info("IOU module has been reset") \ No newline at end of file From e688d96c36441cbfa2c7eae9c84377131ed69593 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 28 Aug 2014 23:06:28 -0600 Subject: [PATCH 02/11] Add start, stop, restart and heartbeat handling to DeadMan module --- gns3server/modules/deadman/__init__.py | 104 +++++++++++++++++++------ 1 file changed, 79 insertions(+), 25 deletions(-) diff --git a/gns3server/modules/deadman/__init__.py b/gns3server/modules/deadman/__init__.py index 59cf106c..ab81e971 100644 --- a/gns3server/modules/deadman/__init__.py +++ b/gns3server/modules/deadman/__init__.py @@ -16,12 +16,12 @@ # along with this program. If not, see . """ -IOU server module. +DeadMan server module. """ import os -import base64 -import tempfile +import time +import subprocess from gns3server.modules import IModule from gns3server.config import Config @@ -48,10 +48,45 @@ class DeadMan(): self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir - - # check every 5 seconds - #self._deadman_callback = self.add_periodic_callback(self._check_deadman_is_alive, 5000) - self._deadman_callback.start() + self._heartbeat_file = "%s/heartbeat_file_for_gnsdms" % ( + self._tempdir) + + if 'heartbeat_file' in kwargs: + self._heartbeat_file = kwargs['heartbeat_file'] + + + self._deadman_process = None + self.start() + + def _start_deadman_process(self): + """ + Start a subprocess and return the object + """ + + cmd = [] + + cmd.append("gns3dms") + cmd.append("--file %s" % (self._heartbeat_file)) + cmd.append("--background") + log.debug("Deadman: Running %s"%(cmd)) + + process = subprocess.Popen(cmd, shell=False) + return process + + def _stop_deadman_process(self): + """ + Start a subprocess and return the object + """ + + cmd = [] + + cmd.append("gns3dms") + cmd.append("-k") + log.debug("Deadman: Running %s"%(cmd)) + + process = subprocess.Popen(cmd, shell=False) + return process + def stop(self, signum=None): """ @@ -60,19 +95,26 @@ class DeadMan(): :param signum: signal number (if called by the signal handler) """ - self._iou_callback.stop() + if self._deadman_process == None: + log.info("Deadman: Can't stop, is not currently running") - # delete all IOU instances - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.delete() - - self.delete_iourc_file() + log.debug("Deadman: Stopping process") + self._deadman_process = self._stop_deadman_process() + self._deadman_process = None + #Jerry or Jeremy why do we do this? Won't this stop the I/O loop for + #for everyone? IModule.stop(self, signum) # this will stop the I/O loop + def start(self, request=None): + """ + Start the deadman process on the server + """ - @IModule.route("iou.reset") + self._deadman_process = self._start_deadman_process() + log.debug("Deadman: Process is starting") + + @IModule.route("deadman.reset") def reset(self, request=None): """ Resets the module (JSON-RPC notification). @@ -80,16 +122,28 @@ class DeadMan(): :param request: JSON request (not used) """ - # delete all IOU instances - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.delete() + self.stop() + self.start() - # resets the instance IDs - IOUDevice.reset() + log.info("Deadman: Module has been reset") - self._iou_instances.clear() - self._allocated_udp_ports.clear() - self.delete_iourc_file() - log.info("IOU module has been reset") \ No newline at end of file + @IModule.route("deadman.heartbeat") + def heartbeat(self, request=None): + """ + Update a file on the server that the deadman switch will monitor + """ + + now = time.time() + + with open(self._heartbeat_file, 'w') as heartbeat_file: + heartbeat_file.write(now) + heartbeat_file.close() + + log.debug("Deadman: heartbeat_file updated: %s %s" % ( + self._heartbeat_file, + now, + )) + + + self.start() \ No newline at end of file From 99a8f5f21a886a15fdfd4ae41a4edcbd3b47e7b8 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 30 Aug 2014 01:32:48 -0600 Subject: [PATCH 03/11] Added create_cert.sh and ssl_options to enable SSL --- gns3server/cert_utils/create_cert.sh | 82 ++++++++++++++++++++++++++++ gns3server/server.py | 8 +++ 2 files changed, 90 insertions(+) create mode 100755 gns3server/cert_utils/create_cert.sh diff --git a/gns3server/cert_utils/create_cert.sh b/gns3server/cert_utils/create_cert.sh new file mode 100755 index 00000000..a4e20a81 --- /dev/null +++ b/gns3server/cert_utils/create_cert.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# -*- 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 . + +# Bash shell script for generating self-signed certs. Run this in a folder, as it +# generates a few files. Large portions of this script were taken from the +# following artcile: +# +# http://usrportage.de/archives/919-Batch-generating-SSL-certificates.html +# +# Additional alterations by: Brad Landers +# Date: 2012-01-27 +# https://gist.github.com/bradland/1690807 + +# Script accepts a single argument, the fqdn for the cert + +DOMAIN="$1" +if [ -z "$DOMAIN" ]; then + DOMAIN="gns3server.localdomain.com" +fi + +fail_if_error() { + [ $1 != 0 ] && { + unset PASSPHRASE + exit 10 + } +} + +# Generate a passphrase +export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo) + +# Certificate details; replace items in angle brackets with your own info +subj=" +C=CA +ST=Alberta +O=GNS3 +localityName=Calgary +commonName=gns3server.localdomain.com +organizationalUnitName=GNS3Server +emailAddress=gns3cert@gns3.com +" + +# Generate the server private key +openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048 +fail_if_error $? + +#openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE + +# Generate the CSR +openssl req \ + -new \ + -batch \ + -subj "$(echo -n "$subj" | tr "\n" "/")" \ + -key $DOMAIN.key \ + -out $DOMAIN.csr \ + -passin env:PASSPHRASE +fail_if_error $? +cp $DOMAIN.key $DOMAIN.key.org +fail_if_error $? + +# Strip the password so we don't have to type it every time we restart Apache +openssl rsa -in $DOMAIN.key.org -out $DOMAIN.key -passin env:PASSPHRASE +fail_if_error $? + +# Generate the cert (good for 10 years) +openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt +fail_if_error $? \ No newline at end of file diff --git a/gns3server/server.py b/gns3server/server.py index d4869e53..f23f6abb 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -159,6 +159,14 @@ class Server(object): zmq.__version__, zmq.zmq_version())) kwargs = {"address": self._host} + + ssl_options={ + "certfile": "/home/michaelgale/nas/workspace/gns3-server/gns3server/certs/gns3server.localdomain.com.crt", + "keyfile": "/home/michaelgale/nas/workspace/gns3-server/gns3server/certs/gns3server.localdomain.com.key", + } + + kwargs['ssl_options'] = ssl_options + if parse_version(tornado.version) >= parse_version("3.1"): kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit tornado_app.listen(self._port, **kwargs) From bcf0aae531d4b09787d1bc9a2ed6f467e32afa4a Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Sep 2014 22:17:06 -0600 Subject: [PATCH 04/11] Added HOME support and cert dir to create_cert script --- gns3server/cert_utils/create_cert.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/gns3server/cert_utils/create_cert.sh b/gns3server/cert_utils/create_cert.sh index a4e20a81..89a5bc67 100755 --- a/gns3server/cert_utils/create_cert.sh +++ b/gns3server/cert_utils/create_cert.sh @@ -29,6 +29,11 @@ # Script accepts a single argument, the fqdn for the cert +DST_DIR="$HOME/.conf/GNS3Certs/" +OLD_DIR=`pwd` + +#GNS3 Server expects to find certs with the default FQDN below. If you create +#different certs you will need to update server.py DOMAIN="$1" if [ -z "$DOMAIN" ]; then DOMAIN="gns3server.localdomain.com" @@ -37,10 +42,16 @@ fi fail_if_error() { [ $1 != 0 ] && { unset PASSPHRASE + cd $OLD_DIR exit 10 } } +mkdir -p $DST_DIR +fail_if_error $? +cd $DST_DIR + + # Generate a passphrase export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo) @@ -56,7 +67,7 @@ emailAddress=gns3cert@gns3.com " # Generate the server private key -openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048 +openssl genrsa -aes256 -out $DST_DIR/$DOMAIN.key -passout env:PASSPHRASE 2048 fail_if_error $? #openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE @@ -79,4 +90,6 @@ fail_if_error $? # Generate the cert (good for 10 years) openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt -fail_if_error $? \ No newline at end of file +fail_if_error $? + +cd $OLD_DIR \ No newline at end of file From a95cc678e91e301a27eab4c33885b364ada36d67 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Sep 2014 22:33:45 -0600 Subject: [PATCH 05/11] Added server.py ssl mode dependant on cert existence --- gns3server/cert_utils/create_cert.sh | 1 + gns3server/server.py | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/gns3server/cert_utils/create_cert.sh b/gns3server/cert_utils/create_cert.sh index 89a5bc67..e08e2714 100755 --- a/gns3server/cert_utils/create_cert.sh +++ b/gns3server/cert_utils/create_cert.sh @@ -47,6 +47,7 @@ fail_if_error() { } } + mkdir -p $DST_DIR fail_if_error $? cd $DST_DIR diff --git a/gns3server/server.py b/gns3server/server.py index f23f6abb..2bc4a893 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -33,6 +33,7 @@ import tornado.ioloop import tornado.web import tornado.autoreload import pkg_resources +from os.path import expanduser from pkg_resources import parse_version from .config import Config @@ -136,6 +137,24 @@ class Server(object): JSONRPCWebSocket.register_destination(destination, instance.name) instance.start() # starts the new process + + def _get_cert_info(self): + """ + Finds the cert and key file needed for SSL + """ + + home = expanduser("~") + ssl_dir = "%s/.conf/GNS3Certs/" % (home) + log.debug("Looking for SSL certs in: %s" % (ssl_dir)) + + keyfile = "%s/gns3server.localdomain.com.key" % (ssl_dir) + certfile = "%s/gns3server.localdomain.com.crt" % (ssl_dir) + + if os.path.isfile(keyfile) and os.path.isfile(certfile): + return { "certfile" : certfile, + "keyfile" : keyfile, + } + def run(self): """ Starts the Tornado web server and ZeroMQ server. @@ -160,12 +179,11 @@ class Server(object): zmq.zmq_version())) kwargs = {"address": self._host} - ssl_options={ - "certfile": "/home/michaelgale/nas/workspace/gns3-server/gns3server/certs/gns3server.localdomain.com.crt", - "keyfile": "/home/michaelgale/nas/workspace/gns3-server/gns3server/certs/gns3server.localdomain.com.key", - } + ssl_options = self._get_cert_info() - kwargs['ssl_options'] = ssl_options + if ssl_options: + log.info("Certs found - starting in SSL mode") + kwargs['ssl_options'] = ssl_options if parse_version(tornado.version) >= parse_version("3.1"): kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit From 382e693fc85bd015725bacdab36be9d5cf7287b1 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2014 00:05:06 -0600 Subject: [PATCH 06/11] Added authentication handler for basic auth check --- gns3server/handlers/auth_handler.py | 71 ++++++++++++++++++++++++++ gns3server/handlers/version_handler.py | 4 +- gns3server/server.py | 18 +++++-- 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 gns3server/handlers/auth_handler.py diff --git a/gns3server/handlers/auth_handler.py b/gns3server/handlers/auth_handler.py new file mode 100644 index 00000000..0bedb40b --- /dev/null +++ b/gns3server/handlers/auth_handler.py @@ -0,0 +1,71 @@ +# -*- 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 . + +""" +Simple file upload & listing handler. +""" + + +import os +import tornado.web + +import logging +log = logging.getLogger(__name__) + +class GNS3BaseHandler(tornado.web.RequestHandler): + def get_current_user(self): + user = self.get_secure_cookie("user") + if not user: + return None + + if self.settings['required_user'] == user.decode("utf-8"): + return user + +class LoginHandler(tornado.web.RequestHandler): + def get(self): + self.write('
' + 'Name: ' + 'Password: ' + '' + '
') + + try: + redirect_to = self.get_argument("next") + self.set_secure_cookie("login_success_redirect_to", redirect_to) + except tornado.web.MissingArgumentError: + pass + + def post(self): + + user = self.get_argument("name") + password = self.get_argument("password") + + if self.settings['required_user'] == user and self.settings['required_pass'] == password: + self.set_secure_cookie("user", user) + auth_status = "successful" + else: + self.set_secure_cookie("user", "None") + auth_status = "failure" + + log.info("Authentication attempt %s: %s" %(auth_status, user)) + + try: + redirect_to = self.get_secure_cookie("login_success_redirect_to") + except tornado.web.MissingArgumentError: + redirect_to = "/" + + self.redirect(redirect_to) \ No newline at end of file diff --git a/gns3server/handlers/version_handler.py b/gns3server/handlers/version_handler.py index c85aa31c..3b338bd2 100644 --- a/gns3server/handlers/version_handler.py +++ b/gns3server/handlers/version_handler.py @@ -16,11 +16,13 @@ # along with this program. If not, see . import tornado.web +from .auth_handler import GNS3BaseHandler from ..version import __version__ -class VersionHandler(tornado.web.RequestHandler): +class VersionHandler(GNS3BaseHandler): + @tornado.web.authenticated def get(self): response = {'version': __version__} self.write(response) diff --git a/gns3server/server.py b/gns3server/server.py index 2bc4a893..275123ad 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -34,12 +34,15 @@ import tornado.web import tornado.autoreload import pkg_resources from os.path import expanduser +import base64 +import uuid from pkg_resources import parse_version from .config import Config from .handlers.jsonrpc_websocket import JSONRPCWebSocket from .handlers.version_handler import VersionHandler from .handlers.file_upload_handler import FileUploadHandler +from .handlers.auth_handler import LoginHandler from .builtins.server_version import server_version from .builtins.interfaces import interfaces from .modules import MODULES @@ -47,12 +50,12 @@ from .modules import MODULES import logging log = logging.getLogger(__name__) - class Server(object): # built-in handlers handlers = [(r"/version", VersionHandler), - (r"/upload", FileUploadHandler)] + (r"/upload", FileUploadHandler), + (r"/login", LoginHandler)] def __init__(self, host, port, ipc=False): @@ -160,6 +163,15 @@ class Server(object): Starts the Tornado web server and ZeroMQ server. """ + # FIXME: debug mode! + settings = { + "debug":True, + "cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), + "login_url": "/login", + "required_user" : "test123", + "required_pass" : "test456", + } + router = self._create_zmq_router() # Add our JSON-RPC Websocket handler to Tornado self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))]) @@ -169,7 +181,7 @@ class Server(object): templates_dir = pkg_resources.resource_filename("gns3server", "templates") tornado_app = tornado.web.Application(self.handlers, template_path=templates_dir, - debug=True) # FIXME: debug mode! + **settings) # FIXME: debug mode! try: print("Starting server on {}:{} (Tornado v{}, PyZMQ v{}, ZMQ v{})".format(self._host, From b84dda3c8ec309423fd4f4d6400cf9da082bf5a7 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2014 22:12:34 -0600 Subject: [PATCH 07/11] HTTP auth added to file_upload and jsonrpc --- gns3server/handlers/auth_handler.py | 11 +++++++++++ gns3server/handlers/file_upload_handler.py | 5 ++++- gns3server/handlers/jsonrpc_websocket.py | 13 +++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/auth_handler.py b/gns3server/handlers/auth_handler.py index 0bedb40b..f136ab02 100644 --- a/gns3server/handlers/auth_handler.py +++ b/gns3server/handlers/auth_handler.py @@ -22,6 +22,7 @@ Simple file upload & listing handler. import os import tornado.web +import tornado.websocket import logging log = logging.getLogger(__name__) @@ -35,6 +36,16 @@ class GNS3BaseHandler(tornado.web.RequestHandler): if self.settings['required_user'] == user.decode("utf-8"): return user +class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler): + def get_current_user(self): + user = self.get_secure_cookie("user") + if not user: + return None + + if self.settings['required_user'] == user.decode("utf-8"): + return user + + class LoginHandler(tornado.web.RequestHandler): def get(self): self.write('
' diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py index c819a401..15673604 100644 --- a/gns3server/handlers/file_upload_handler.py +++ b/gns3server/handlers/file_upload_handler.py @@ -23,6 +23,7 @@ Simple file upload & listing handler. import os import stat import tornado.web +from .auth_handler import GNS3BaseHandler from ..version import __version__ from ..config import Config @@ -30,7 +31,7 @@ import logging log = logging.getLogger(__name__) -class FileUploadHandler(tornado.web.RequestHandler): +class FileUploadHandler(GNS3BaseHandler): """ File upload handler. @@ -54,6 +55,7 @@ class FileUploadHandler(tornado.web.RequestHandler): except OSError as e: log.error("could not create the upload directory {}: {}".format(self._upload_dir, e)) + @tornado.web.authenticated def get(self): """ Invoked on GET request. @@ -70,6 +72,7 @@ class FileUploadHandler(tornado.web.RequestHandler): path=path, items=items) + @tornado.web.authenticated def post(self): """ Invoked on POST request. diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py index 5b18496c..a226be78 100644 --- a/gns3server/handlers/jsonrpc_websocket.py +++ b/gns3server/handlers/jsonrpc_websocket.py @@ -22,6 +22,7 @@ JSON-RPC protocol over Websockets. import zmq import uuid import tornado.websocket +from .auth_handler import GNS3WebSocketBaseHandler from tornado.escape import json_decode from ..jsonrpc import JSONRPCParseError from ..jsonrpc import JSONRPCInvalidRequest @@ -33,7 +34,7 @@ import logging log = logging.getLogger(__name__) -class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): +class JSONRPCWebSocket(GNS3WebSocketBaseHandler): """ STOMP protocol over Tornado Websockets with message routing to ZeroMQ dealer clients. @@ -116,7 +117,15 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): """ log.info("Websocket client {} connected".format(self.session_id)) - self.clients.add(self) + + authenticated_user = self.get_current_user() + + if authenticated_user: + self.clients.add(self) + log.info("Websocket authenticated user: %s" % (authenticated_user)) + else: + self.close() + log.info("Websocket non-authenticated user attempt: %s" % (authenticated_user)) def on_message(self, message): """ From 6f9e0f6d2e2cc2544d460567cba74f601d91ae0a Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2014 22:19:59 -0600 Subject: [PATCH 08/11] Moved certs to .config --- gns3server/cert_utils/create_cert.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/cert_utils/create_cert.sh b/gns3server/cert_utils/create_cert.sh index e08e2714..57427088 100755 --- a/gns3server/cert_utils/create_cert.sh +++ b/gns3server/cert_utils/create_cert.sh @@ -29,7 +29,7 @@ # Script accepts a single argument, the fqdn for the cert -DST_DIR="$HOME/.conf/GNS3Certs/" +DST_DIR="$HOME/.config/GNS3Certs/" OLD_DIR=`pwd` #GNS3 Server expects to find certs with the default FQDN below. If you create From 36e539382c05b722f93dffbd99bc54c4bd8fdc44 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 6 Sep 2014 00:51:43 -0600 Subject: [PATCH 09/11] Added support for cloud.conf file and startup script --- gns3server/start_server.py | 240 +++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 gns3server/start_server.py diff --git a/gns3server/start_server.py b/gns3server/start_server.py new file mode 100644 index 00000000..b27f3af8 --- /dev/null +++ b/gns3server/start_server.py @@ -0,0 +1,240 @@ +# -*- 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) + +""" +Startup script for GNS3 Server Cloud Instance +""" + +import os +import sys +import configparser +import getopt +import datetime +import signal +from logging.handlers import * +from os.path import expanduser +from gns3server.config import Config +import ast +import subprocess +import uuid + +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]))) + + +LOG_NAME = "gns3-startup" +log = None + +usage = """ +USAGE: %s + +Options: + + -d, --debug Enable debugging + -v, --verbose Enable verbose logging + -h, --help Display this menu :) + + --data Python dict of data to be written to the config file: + " { 'gns3' : 'Is AWESOME' } " + +""" % (SCRIPT_NAME) + +# Parse cmd line options +def parse_cmd_line(argv): + """ + Parse command line arguments + + argv: Pass in cmd line arguments + """ + + short_args = "dvh" + long_args = ("debug", + "verbose", + "help", + "data=", + ) + 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["data"] = None + + 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) + + 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 ("--data")): + cmd_line_option_list["data"] = ast.literal_eval(val) + + return cmd_line_option_list + + +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 _generate_certs(): + cmd = [] + cmd.append("%s/cert_utils/create_cert.sh" % (SCRIPT_PATH)) + + log.debug("Generating certs ...") + output_raw = subprocess.check_output(cmd, shell=False, + stderr=subprocess.STDOUT) + + output_str = output_raw.decode("utf-8") + output = output_str.strip().split("\n") + log.debug(output) + return (output[-2], output[-1]) + +def _start_gns3server(): + cmd = [] + cmd.append("gns3server") + + log.debug("Starting gns3server ...") + subprocess.Popen(cmd, shell=False) + + +def main(): + + global log + 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") + sys.exit(0) + + + # Setup signal to catch Control-C / SIGINT and SIGTERM + signal.signal(signal.SIGINT, _shutdown) + signal.signal(signal.SIGTERM, _shutdown) + + client_data = {} + + config = Config.instance() + cfg = config.list_cloud_config_file() + cfg_path = os.path.dirname(cfg) + + try: + os.makedirs(cfg_path) + except FileExistsError: + pass + + (server_key, server_crt ) = _generate_certs() + + cloud_config = configparser.ConfigParser() + cloud_config['CLOUD_SERVER'] = {} + + if options['data']: + cloud_config['CLOUD_SERVER'] = options['data'] + + cloud_config['CLOUD_SERVER']['SSL_KEY'] = server_key + cloud_config['CLOUD_SERVER']['SSL_CRT'] = server_crt + cloud_config['CLOUD_SERVER']['SSL_ENABLED'] = 'yes' + cloud_config['CLOUD_SERVER']['WEB_USERNAME'] = str(uuid.uuid4()).upper()[0:8] + cloud_config['CLOUD_SERVER']['WEB_PASSWORD'] = str(uuid.uuid4()).upper()[0:8] + + with open(cfg, 'w') as cloud_config_file: + cloud_config.write(cloud_config_file) + + cloud_config_file.close() + + _start_gns3server() + + with open(server_crt, 'r') as cert_file: + cert_data = cert_file.readlines() + + cert_file.close() + + client_data['SSL_CRT_FILE'] = server_crt + client_data['SSL_CRT'] = cert_data + + print(client_data) + + +if __name__ == "__main__": + result = main() + sys.exit(result) From ef492d4690c518395ddb90c7cbfaff03e22f2616 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 6 Sep 2014 20:46:06 -0600 Subject: [PATCH 10/11] Update gns3dms to support cloud.conf --- gns3dms/main.py | 34 +++++++++++++++++--------- gns3server/cert_utils/create_cert.sh | 5 +++- gns3server/config.py | 7 +++++- gns3server/modules/__init__.py | 3 ++- gns3server/modules/deadman/__init__.py | 12 ++++++--- gns3server/server.py | 32 ++++++++---------------- requirements.txt | 1 + 7 files changed, 54 insertions(+), 40 deletions(-) diff --git a/gns3dms/main.py b/gns3dms/main.py index bad64a44..e8d245d6 100644 --- a/gns3dms/main.py +++ b/gns3dms/main.py @@ -24,7 +24,7 @@ # number has been incremented) """ -Monitors communication with the GNS3 client via tmp file. Will terminate the instance if +Monitors communication with the GNS3 client via tmp file. Will terminate the instance if communication is lost. """ @@ -62,7 +62,7 @@ sys.path.append(EXTRA_LIB) import daemon -my_daemon = None +my_daemon = None usage = """ USAGE: %s @@ -73,14 +73,14 @@ Options: -v, --verbose Enable verbose logging -h, --help Display this menu :) - --cloud_api_key Rackspace API key + --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: + + --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 @@ -146,7 +146,7 @@ def parse_cmd_line(argv): cmd_line_option_list['syslog'] = ('localhost',514) - get_gns3secrets(cmd_line_option_list) + get_gns3config(cmd_line_option_list) for opt, val in opts: if (opt in ("-h", "--help")): @@ -202,7 +202,7 @@ def parse_cmd_line(argv): return cmd_line_option_list -def get_gns3secrets(cmd_line_option_list): +def get_gns3config(cmd_line_option_list): """ Load cloud credentials from .gns3secrets """ @@ -225,6 +225,15 @@ def get_gns3secrets(cmd_line_option_list): except configparser.NoSectionError: pass + cloud_config_file = "%s/.config/GNS3/cloud.conf" + if os.path.isfile(cloud_config_file) + config.read(cloud_config_file) + + try: + for key, value in config.items("CLOUD_SERVER"): + cmd_line_option_list[key] = value.strip() + except configparser.NoSectionError: + pass def set_logging(cmd_options): """ @@ -256,7 +265,7 @@ def set_logging(cmd_options): ) syslog_hndlr.setFormatter(sys_formatter) - + log.setLevel(log_level) log.addHandler(console_log) log.addHandler(syslog_hndlr) @@ -308,7 +317,7 @@ def monitor_loop(options): if delta.seconds > options["deadtime"]: log.warning("Deadtime exceeded, terminating instance ...") - #Terminate involes many layers of HTTP / API calls, lots of + #Terminate involes many layers of HTTP / API calls, lots of #different errors types could occur here. try: rksp = Rackspace(options) @@ -341,7 +350,8 @@ def main(): log.info("Received shutdown signal") options["shutdown"] = True - + sys.exit(0) + pid_file = "%s/.gns3ias.pid" % (expanduser("~")) if options["shutdown"]: diff --git a/gns3server/cert_utils/create_cert.sh b/gns3server/cert_utils/create_cert.sh index 57427088..5b2c8e28 100755 --- a/gns3server/cert_utils/create_cert.sh +++ b/gns3server/cert_utils/create_cert.sh @@ -68,7 +68,7 @@ emailAddress=gns3cert@gns3.com " # Generate the server private key -openssl genrsa -aes256 -out $DST_DIR/$DOMAIN.key -passout env:PASSPHRASE 2048 +openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048 fail_if_error $? #openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE @@ -93,4 +93,7 @@ fail_if_error $? openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt fail_if_error $? +echo "${DST_DIR}${DOMAIN}.key" +echo "${DST_DIR}${DOMAIN}.crt" + cd $OLD_DIR \ No newline at end of file diff --git a/gns3server/config.py b/gns3server/config.py index cd2d07a1..caa9c0d4 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -62,16 +62,21 @@ class Config(object): # 5: server.conf in the current working directory home = os.path.expanduser("~") + self._cloud_config = os.path.join(home, ".config", appname, "cloud.conf") filename = "server.conf" self._files = [os.path.join(home, ".config", appname, filename), os.path.join(home, ".config", appname + ".conf"), os.path.join("/etc/xdg", appname, filename), os.path.join("/etc/xdg", appname + ".conf"), - filename] + filename, + self._cloud_config] self._config = configparser.ConfigParser() self.read_config() + def list_cloud_config_file(self): + return self._cloud_config + def read_config(self): """ Read the configuration files. diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 5bd4c110..f38af25b 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -20,8 +20,9 @@ from .base import IModule from .dynamips import Dynamips from .vpcs import VPCS from .virtualbox import VirtualBox +from .deadman import DeadMan -MODULES = [Dynamips, VPCS, VirtualBox] +MODULES = [Dynamips, VPCS, VirtualBox, DeadMan] if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/deadman/__init__.py b/gns3server/modules/deadman/__init__.py index ab81e971..288d5b2a 100644 --- a/gns3server/modules/deadman/__init__.py +++ b/gns3server/modules/deadman/__init__.py @@ -30,7 +30,7 @@ from gns3server.config import Config import logging log = logging.getLogger(__name__) -class DeadMan(): +class DeadMan(IModule): """ DeadMan module. @@ -51,10 +51,12 @@ class DeadMan(): self._heartbeat_file = "%s/heartbeat_file_for_gnsdms" % ( self._tempdir) + self.cloud_config = Config.instance().get_section_config("CLOUD_SERVER") + self._heartbeat_file = self.cloud_config["heartbeat_file"] + if 'heartbeat_file' in kwargs: self._heartbeat_file = kwargs['heartbeat_file'] - self._deadman_process = None self.start() @@ -63,8 +65,12 @@ class DeadMan(): Start a subprocess and return the object """ - cmd = [] + #gnsserver gets configuration options from cloud.conf. This is where + #the client adds specific cloud information. + #gns3dms also reads in cloud.conf. That is why we don't need to specific + #all the command line arguments here. + cmd = [] cmd.append("gns3dms") cmd.append("--file %s" % (self._heartbeat_file)) cmd.append("--background") diff --git a/gns3server/server.py b/gns3server/server.py index 275123ad..49223790 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -141,35 +141,20 @@ class Server(object): instance.start() # starts the new process - def _get_cert_info(self): - """ - Finds the cert and key file needed for SSL - """ - - home = expanduser("~") - ssl_dir = "%s/.conf/GNS3Certs/" % (home) - log.debug("Looking for SSL certs in: %s" % (ssl_dir)) - - keyfile = "%s/gns3server.localdomain.com.key" % (ssl_dir) - certfile = "%s/gns3server.localdomain.com.crt" % (ssl_dir) - - if os.path.isfile(keyfile) and os.path.isfile(certfile): - return { "certfile" : certfile, - "keyfile" : keyfile, - } - def run(self): """ Starts the Tornado web server and ZeroMQ server. """ # FIXME: debug mode! + cloud_config = Config.instance().get_section_config("CLOUD_SERVER") + settings = { "debug":True, "cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), "login_url": "/login", - "required_user" : "test123", - "required_pass" : "test456", + "required_user" : cloud_config['WEB_USERNAME'], + "required_pass" : cloud_config['WEB_PASSWORD'], } router = self._create_zmq_router() @@ -191,11 +176,14 @@ class Server(object): zmq.zmq_version())) kwargs = {"address": self._host} - ssl_options = self._get_cert_info() + if cloud_config["SSL_ENABLED"] == "yes": + ssl_options = { + "certfile" : cloud_config["SSL_CRT"], + "keyfile" : cloud_config["SSL_KEY"], + } - if ssl_options: log.info("Certs found - starting in SSL mode") - kwargs['ssl_options'] = ssl_options + kwargs["ssl_options"] = ssl_options if parse_version(tornado.version) >= parse_version("3.1"): kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit diff --git a/requirements.txt b/requirements.txt index 2cf31cd5..3e267f9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ jsonschema pycurl python-dateutil apache-libcloud +requests From f876a862c41c16757dda56189eff895eda2e8c84 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 6 Sep 2014 21:13:09 -0600 Subject: [PATCH 11/11] GNS3 server will now create the heardbeat file durining initialization --- gns3dms/main.py | 7 ++++--- gns3server/modules/deadman/__init__.py | 12 +++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/gns3dms/main.py b/gns3dms/main.py index e8d245d6..94e412d7 100644 --- a/gns3dms/main.py +++ b/gns3dms/main.py @@ -145,7 +145,6 @@ def parse_cmd_line(argv): else: cmd_line_option_list['syslog'] = ('localhost',514) - get_gns3config(cmd_line_option_list) for opt, val in opts: @@ -225,8 +224,10 @@ def get_gns3config(cmd_line_option_list): except configparser.NoSectionError: pass - cloud_config_file = "%s/.config/GNS3/cloud.conf" - if os.path.isfile(cloud_config_file) + cloud_config_file = "%s/.config/GNS3/cloud.conf" % ( + os.path.expanduser("~/")) + + if os.path.isfile(cloud_config_file): config.read(cloud_config_file) try: diff --git a/gns3server/modules/deadman/__init__.py b/gns3server/modules/deadman/__init__.py index 288d5b2a..86f97363 100644 --- a/gns3server/modules/deadman/__init__.py +++ b/gns3server/modules/deadman/__init__.py @@ -51,13 +51,11 @@ class DeadMan(IModule): self._heartbeat_file = "%s/heartbeat_file_for_gnsdms" % ( self._tempdir) - self.cloud_config = Config.instance().get_section_config("CLOUD_SERVER") - self._heartbeat_file = self.cloud_config["heartbeat_file"] - if 'heartbeat_file' in kwargs: self._heartbeat_file = kwargs['heartbeat_file'] self._deadman_process = None + self.heartbeat() self.start() def _start_deadman_process(self): @@ -72,11 +70,12 @@ class DeadMan(IModule): cmd = [] cmd.append("gns3dms") - cmd.append("--file %s" % (self._heartbeat_file)) + cmd.append("--file") + cmd.append("%s" % (self._heartbeat_file)) cmd.append("--background") log.debug("Deadman: Running %s"%(cmd)) - process = subprocess.Popen(cmd, shell=False) + process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, shell=False) return process def _stop_deadman_process(self): @@ -143,7 +142,7 @@ class DeadMan(IModule): now = time.time() with open(self._heartbeat_file, 'w') as heartbeat_file: - heartbeat_file.write(now) + heartbeat_file.write(str(now)) heartbeat_file.close() log.debug("Deadman: heartbeat_file updated: %s %s" % ( @@ -151,5 +150,4 @@ class DeadMan(IModule): now, )) - self.start() \ No newline at end of file