From 6cb1c820330f131a5eba0912b697ad896b082d0d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Sep 2010 19:28:10 -0700 Subject: [PATCH 01/18] add in support for ajaxterm console access --- nova/adminclient.py | 22 ++++++++++++++++++++++ nova/endpoint/admin.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/nova/adminclient.py b/nova/adminclient.py index 0ca32b1e..9670b718 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -24,6 +24,19 @@ import base64 import boto from boto.ec2.regioninfo import RegionInfo +class ConsoleInfo(object): + def __init__(self, connection=None, endpoint=None): + self.connection = connection + self.endpoint = endpoint + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'url': + self.url = str(value) + if name == 'kind': + self.url = str(value) class UserInfo(object): """ @@ -349,3 +362,12 @@ class NovaAdminClient(object): def get_hosts(self): return self.apiconn.get_list('DescribeHosts', {}, [('item', HostInfo)]) + def create_console(self, instance_id, kind='ajax'): + """ + Create a console + """ + console = self.apiconn.get_object('CreateConsole', {'Kind': kind, 'InstanceId': instance_id}, ConsoleInfo) + + if console.url != None: + return console + diff --git a/nova/endpoint/admin.py b/nova/endpoint/admin.py index 3d91c66d..686e462b 100644 --- a/nova/endpoint/admin.py +++ b/nova/endpoint/admin.py @@ -21,10 +21,14 @@ Admin API controller, exposed through http via the api worker. """ import base64 +import uuid +import subprocess +import random from nova import db from nova import exception from nova.auth import manager +from utils import novadir def user_dict(user, base64_file=None): @@ -211,3 +215,28 @@ class AdminController(object): def describe_host(self, _context, name, **_kwargs): """Returns status info for single node.""" return host_dict(db.host_get(name)) + + @admin_only + def create_console(self, _context, kind, instance_id, **_kwargs): + """Create a Console""" + #instance = db.instance_get(_context, instance_id) + host = '127.0.0.1' + + def get_port(): + for i in range(0,100): # don't loop forever + port = int(random.uniform(10000, 12000)) + cmd = "netcat 0.0.0.0 " + str(port) + " -w 2 < /dev/null" + # this Popen will exit with 0 only if the port is in use, + # so a nonzero return value implies it is unused + port_is_unused = subprocess.Popen(cmd, shell=True).wait() + if port_is_unused: + return port + raise 'Unable to find an open port' + + port = str(get_port()) + token = str(uuid.uuid4()) + cmd = novadir() + "tools/ajaxterm//ajaxterm.py --command 'ssh root@" + host + "' -t " \ + + token + " -p " + port + port_is_unused = subprocess.Popen(cmd, shell=True) + return {'url': 'http://tonbuntu:' + port + '/?token=' + token } + From e20fe7ba923e080ecac9eb337ec3471fdeb2a7b2 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Sep 2010 20:36:13 -0700 Subject: [PATCH 02/18] add in a few comments --- nova/endpoint/admin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nova/endpoint/admin.py b/nova/endpoint/admin.py index 686e462b..8d184f10 100644 --- a/nova/endpoint/admin.py +++ b/nova/endpoint/admin.py @@ -220,10 +220,9 @@ class AdminController(object): def create_console(self, _context, kind, instance_id, **_kwargs): """Create a Console""" #instance = db.instance_get(_context, instance_id) - host = '127.0.0.1' def get_port(): - for i in range(0,100): # don't loop forever + for i in xrange(0,100): # don't loop forever port = int(random.uniform(10000, 12000)) cmd = "netcat 0.0.0.0 " + str(port) + " -w 2 < /dev/null" # this Popen will exit with 0 only if the port is in use, @@ -235,8 +234,10 @@ class AdminController(object): port = str(get_port()) token = str(uuid.uuid4()) + + host = '127.0.0.1' #TODO add actual host cmd = novadir() + "tools/ajaxterm//ajaxterm.py --command 'ssh root@" + host + "' -t " \ + token + " -p " + port - port_is_unused = subprocess.Popen(cmd, shell=True) - return {'url': 'http://tonbuntu:' + port + '/?token=' + token } + port_is_unused = subprocess.Popen(cmd, shell=True) #TODO error check + return {'url': 'http://tonbuntu:' + port + '/?token=' + token } #TODO - s/tonbuntu/api_server_public_ip From 1c51f4e8ab050b250701f276b1a7629f9be063d8 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 1 Nov 2010 16:25:56 -0700 Subject: [PATCH 03/18] basics to get proxied ajaxterm working with virsh --- bin/nova-ajax-proxy | 31 +++++++++++++++++++++++++++++++ nova/boto_extensions.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100755 bin/nova-ajax-proxy create mode 100644 nova/boto_extensions.py diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy new file mode 100755 index 00000000..1a0c896e --- /dev/null +++ b/bin/nova-ajax-proxy @@ -0,0 +1,31 @@ +#!/usr/bin/python +from twisted.internet import reactor +from twisted.web import http +from twisted.web.proxy import Proxy, ProxyRequest +import urlparse, exceptions + +class AjaxProxyRequest(ProxyRequest): + def process(self): + if 'referer' in self.received_headers: + auth_uri = self.received_headers['referer'] + else: + auth_uri = self.uri + + try: + auth_params = urlparse.parse_qs(urlparse.urlparse(auth_uri).query) + parsed_uri = urlparse.urlparse(self.uri) + + self.uri = "http://%s:%s%s?%s"% (auth_params['host'][0], auth_params['port'][0], parsed_uri.path, parsed_uri.query) + + ProxyRequest.process(self) + except (exceptions.KeyError): + pass + +class AjaxProxy(Proxy): + requestFactory = AjaxProxyRequest + +factory = http.HTTPFactory() +factory.protocol = AjaxProxy + +reactor.listenTCP(8000, factory) +reactor.run() diff --git a/nova/boto_extensions.py b/nova/boto_extensions.py new file mode 100644 index 00000000..6d55b801 --- /dev/null +++ b/nova/boto_extensions.py @@ -0,0 +1,40 @@ +import base64 +import boto +from boto.ec2.connection import EC2Connection + +class AjaxConsole: + def __init__(self, parent=None): + self.parent = parent + self.instance_id = None + self.url = None + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'instanceId': + self.instance_id = value + elif name == 'url': + self.url = value + else: + setattr(self, name, value) + +class NovaEC2Connection(EC2Connection): + def get_ajax_console(self, instance_id): + """ + Retrieves a console connection for the specified instance. + + :type instance_id: string + :param instance_id: The instance ID of a running instance on the cloud. + + :rtype: :class:`AjaxConsole` + """ + params = {} + self.build_list_params(params, [instance_id], 'InstanceId') + return self.get_object('GetAjaxConsole', params, AjaxConsole) + pass + +def override_connect_ec2(aws_access_key_id=None, aws_secret_access_key=None, **kwargs): + return NovaEC2Connection(aws_access_key_id, aws_secret_access_key, **kwargs) + +boto.connect_ec2 = override_connect_ec2 From b1cc833a27e8c2303bc64b30092c11d995dec98a Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 22 Dec 2010 02:19:38 -0800 Subject: [PATCH 04/18] connecting ajax proxy to rabbit to allow token based security --- bin/nova-ajax-proxy | 71 ++++++++++++++++++++++++++++++++++++++++----- nova/flags.py | 3 +- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index 1a0c896e..cad496b2 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -1,8 +1,30 @@ #!/usr/bin/python + +import datetime +import os +import sys + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +from nova import utils +from nova import flags +from nova import rpc + +import exceptions +import logging +import urlparse + +FLAGS = flags.FLAGS from twisted.internet import reactor +from twisted.internet import task from twisted.web import http from twisted.web.proxy import Proxy, ProxyRequest -import urlparse, exceptions class AjaxProxyRequest(ProxyRequest): def process(self): @@ -20,12 +42,47 @@ class AjaxProxyRequest(ProxyRequest): ProxyRequest.process(self) except (exceptions.KeyError): pass - + class AjaxProxy(Proxy): + tokens = {} requestFactory = AjaxProxyRequest + + def start(self): + conn = rpc.Connection.instance(new=True) + self.consumer = rpc.TopicConsumer( + connection=conn, + topic=FLAGS.ajax_proxy_topic) + self.consumer.register_callback(self) + + task.LoopingCall(self.age).start(1.0) + task.LoopingCall(self.pollq).start(0.1) + + factory = http.HTTPFactory() + factory.protocol = AjaxProxy + + reactor.listenTCP(8000, factory) + reactor.run() + + def age(self): + pass + + def pollq(self): + self.consumer.fetch(auto_ack=True, enable_callbacks=True) -factory = http.HTTPFactory() -factory.protocol = AjaxProxy - -reactor.listenTCP(8000, factory) -reactor.run() + def __call__(self, data, message): + if data['method'] == 'authorize': + AjaxProxy.tokens['token'] = {'args': data['args'], 'born_at': datetime.datetime.now()} + + +if __name__ == '__main__': + utils.default_flagfile() + FLAGS(sys.argv) + + formatter = logging.Formatter('(%(name)s): %(levelname)s %(message)s') + handler = logging.StreamHandler() + handler.setFormatter(formatter) + logging.getLogger().addHandler(handler) + + ajaxproxy = AjaxProxy() + ajaxproxy.start() + diff --git a/nova/flags.py b/nova/flags.py index 8fa0beb7..53ae9be4 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -217,7 +217,8 @@ DEFINE_string('scheduler_topic', 'scheduler', 'the topic scheduler nodes listen on') DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') - +DEFINE_string('ajax_proxy_topic', 'ajax_proxy', + 'the topic ajax proxy nodes listen on') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, From 08b9061af05cd9d11a49c68948eb6c2aa743c791 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 22 Dec 2010 12:36:37 -0800 Subject: [PATCH 05/18] working connection security --- bin/nova-ajax-proxy | 57 ++++++++++++++++++++++++++++++++------------- nova/flags.py | 5 +++- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index cad496b2..76a70d24 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -1,8 +1,8 @@ #!/usr/bin/python -import datetime import os import sys +import time # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... @@ -12,8 +12,9 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -from nova import utils +from nova import exception from nova import flags +from nova import utils from nova import rpc import exceptions @@ -23,10 +24,13 @@ import urlparse FLAGS = flags.FLAGS from twisted.internet import reactor from twisted.internet import task -from twisted.web import http +from twisted.web import error, http from twisted.web.proxy import Proxy, ProxyRequest + +flags.DEFINE_integer('ajax_console_idle_timeout', 300, + 'Seconds before idle connection destroyed') -class AjaxProxyRequest(ProxyRequest): +class AjaxConsoleProxyRequest(ProxyRequest): def process(self): if 'referer' in self.received_headers: auth_uri = self.received_headers['referer'] @@ -36,42 +40,63 @@ class AjaxProxyRequest(ProxyRequest): try: auth_params = urlparse.parse_qs(urlparse.urlparse(auth_uri).query) parsed_uri = urlparse.urlparse(self.uri) + + auth_info = auth_params['token'][0] + auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]] + args = auth_info['args'] + auth_info['last_activity_at'] = time.time() + - self.uri = "http://%s:%s%s?%s"% (auth_params['host'][0], auth_params['port'][0], parsed_uri.path, parsed_uri.query) + self.uri = ("http://%s:%s%s?token=%s"% ( + str(args['host']), + str(args['port']), + parsed_uri.path, + str(args['token']))) ProxyRequest.process(self) except (exceptions.KeyError): - pass + raise exception.NotAuthorized("Unauthorized Request") -class AjaxProxy(Proxy): - tokens = {} - requestFactory = AjaxProxyRequest +class AjaxConsoleProxy(Proxy): + #tokens = {} + tokens = {'key': {'args':'','last_activity_at':time.time()}} + requestFactory = AjaxConsoleProxyRequest def start(self): conn = rpc.Connection.instance(new=True) self.consumer = rpc.TopicConsumer( connection=conn, - topic=FLAGS.ajax_proxy_topic) + topic=FLAGS.ajax_console_proxy_topic) self.consumer.register_callback(self) task.LoopingCall(self.age).start(1.0) task.LoopingCall(self.pollq).start(0.1) factory = http.HTTPFactory() - factory.protocol = AjaxProxy + factory.protocol = AjaxConsoleProxy - reactor.listenTCP(8000, factory) + port = urlparse.urlparse(FLAGS.ajax_console_proxy_url).port + reactor.listenTCP(port, factory) reactor.run() def age(self): - pass + now = time.time() + print now + to_delete = [] + for k, v in AjaxConsoleProxy.tokens.items(): + if now - v['last_activity_at'] > FLAGS.ajax_console_idle_timeout: + to_delete.append(k) + + for k in to_delete: + print "del" + del AjaxConsoleProxy.tokens[k] def pollq(self): self.consumer.fetch(auto_ack=True, enable_callbacks=True) def __call__(self, data, message): - if data['method'] == 'authorize': - AjaxProxy.tokens['token'] = {'args': data['args'], 'born_at': datetime.datetime.now()} + if data['method'] == 'authorize_ajax_console': + AjaxConsoleProxy.tokens[data['args']['token']] = {'args': data['args'], 'born_at': time.time()} if __name__ == '__main__': @@ -83,6 +108,6 @@ if __name__ == '__main__': handler.setFormatter(formatter) logging.getLogger().addHandler(handler) - ajaxproxy = AjaxProxy() + ajaxproxy = AjaxConsoleProxy() ajaxproxy.start() diff --git a/nova/flags.py b/nova/flags.py index 53ae9be4..c6e56fcc 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -217,8 +217,11 @@ DEFINE_string('scheduler_topic', 'scheduler', 'the topic scheduler nodes listen on') DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') -DEFINE_string('ajax_proxy_topic', 'ajax_proxy', +DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy', 'the topic ajax proxy nodes listen on') +DEFINE_string('ajax_console_proxy_url', + 'http://tonbuntu:8000', + 'location of ajax console proxy, in the form "http://tonbuntu:8000"') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, From 221b4e64840af54d9009b1e95c3f82d8b71df090 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 22 Dec 2010 18:52:43 -0800 Subject: [PATCH 06/18] minor notes, commit before rewriting proxy with eventlet --- bin/nova-ajax-proxy | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index 76a70d24..4948897c 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -46,7 +46,6 @@ class AjaxConsoleProxyRequest(ProxyRequest): args = auth_info['args'] auth_info['last_activity_at'] = time.time() - self.uri = ("http://%s:%s%s?token=%s"% ( str(args['host']), str(args['port']), From 5c315ed9e86fb934671b7d8c1dc2420ba6f2af72 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 22 Dec 2010 23:41:07 -0800 Subject: [PATCH 07/18] rewrite proxy to not use twisted --- bin/nova-ajax-proxy | 124 ++++++++++++++++++++++---------------------- nova/flags.py | 3 ++ 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index 4948897c..52d7ee3d 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -12,101 +12,99 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) +import webob.dec +import webob.exc + from nova import exception from nova import flags from nova import utils from nova import rpc +from nova import wsgi import exceptions import logging import urlparse FLAGS = flags.FLAGS -from twisted.internet import reactor -from twisted.internet import task -from twisted.web import error, http -from twisted.web.proxy import Proxy, ProxyRequest flags.DEFINE_integer('ajax_console_idle_timeout', 300, 'Seconds before idle connection destroyed') -class AjaxConsoleProxyRequest(ProxyRequest): - def process(self): - if 'referer' in self.received_headers: - auth_uri = self.received_headers['referer'] - else: - auth_uri = self.uri +import eventlet +from eventlet import greenthread +from eventlet.green import urllib2 +class AjaxConsoleProxy(object): + tokens = {} + + def __call__(self, environ, start_response): try: - auth_params = urlparse.parse_qs(urlparse.urlparse(auth_uri).query) - parsed_uri = urlparse.urlparse(self.uri) + req_url = '%s://%s%s?%s' % (environ['wsgi.url_scheme'], environ['HTTP_HOST'], environ['PATH_INFO'], environ['QUERY_STRING']) + if 'HTTP_REFERER' in environ: + auth_url = environ['HTTP_REFERER'] + else: + auth_url = req_url - auth_info = auth_params['token'][0] - auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]] + auth_params = urlparse.parse_qs(urlparse.urlparse(auth_url).query) + parsed_url = urlparse.urlparse(req_url) + + auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]] args = auth_info['args'] auth_info['last_activity_at'] = time.time() - self.uri = ("http://%s:%s%s?token=%s"% ( - str(args['host']), - str(args['port']), - parsed_uri.path, - str(args['token']))) + remote_url = ("http://%s:%s%s?token=%s"% ( + str(args['host']), + str(args['port']), + parsed_url.path, + str(args['token']))) - ProxyRequest.process(self) + opener = urllib2.urlopen(remote_url, environ['wsgi.input'].read()) + body = opener.read() + info = opener.info() + + start_response("200 OK", info.dict.items()) + return body except (exceptions.KeyError): - raise exception.NotAuthorized("Unauthorized Request") + start_response("401 NOT AUTHORIZED",[]) + return "Not Authorized" + except Exception: + start_response("500 ERROR",[]) + return "Server Error" -class AjaxConsoleProxy(Proxy): - #tokens = {} - tokens = {'key': {'args':'','last_activity_at':time.time()}} - requestFactory = AjaxConsoleProxyRequest - def start(self): + def register_listeners(self): + class Callback: + def __call__(self, data, message): + if data['method'] == 'authorize_ajax_console': + AjaxConsoleProxy.tokens[data['args']['token']] = \ + {'args': data['args'], 'last_activity_at': time.time()} + conn = rpc.Connection.instance(new=True) - self.consumer = rpc.TopicConsumer( + consumer = rpc.TopicConsumer( connection=conn, topic=FLAGS.ajax_console_proxy_topic) - self.consumer.register_callback(self) + consumer.register_callback(Callback()) - task.LoopingCall(self.age).start(1.0) - task.LoopingCall(self.pollq).start(0.1) + def delete_expired_tokens(): + now = time.time() + to_delete = [] + for k, v in AjaxConsoleProxy.tokens.items(): + if now - v['last_activity_at'] > FLAGS.ajax_console_idle_timeout: + to_delete.append(k) - factory = http.HTTPFactory() - factory.protocol = AjaxConsoleProxy - - port = urlparse.urlparse(FLAGS.ajax_console_proxy_url).port - reactor.listenTCP(port, factory) - reactor.run() - - def age(self): - now = time.time() - print now - to_delete = [] - for k, v in AjaxConsoleProxy.tokens.items(): - if now - v['last_activity_at'] > FLAGS.ajax_console_idle_timeout: - to_delete.append(k) - - for k in to_delete: - print "del" - del AjaxConsoleProxy.tokens[k] - - def pollq(self): - self.consumer.fetch(auto_ack=True, enable_callbacks=True) - - def __call__(self, data, message): - if data['method'] == 'authorize_ajax_console': - AjaxConsoleProxy.tokens[data['args']['token']] = {'args': data['args'], 'born_at': time.time()} + for k in to_delete: + del AjaxConsoleProxy.tokens[k] + utils.LoopingCall(consumer.fetch, auto_ack=True, + enable_callbacks=True).start(0.1) + utils.LoopingCall(delete_expired_tokens).start(1) if __name__ == '__main__': utils.default_flagfile() FLAGS(sys.argv) + server = wsgi.Server() + acp = AjaxConsoleProxy() + acp.register_listeners() + server.start(acp, FLAGS.ajax_console_proxy_port, host='0.0.0.0') + server.wait() - formatter = logging.Formatter('(%(name)s): %(levelname)s %(message)s') - handler = logging.StreamHandler() - handler.setFormatter(formatter) - logging.getLogger().addHandler(handler) - - ajaxproxy = AjaxConsoleProxy() - ajaxproxy.start() - diff --git a/nova/flags.py b/nova/flags.py index c6e56fcc..c4404a12 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -222,6 +222,9 @@ DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy', DEFINE_string('ajax_console_proxy_url', 'http://tonbuntu:8000', 'location of ajax console proxy, in the form "http://tonbuntu:8000"') +DEFINE_string('ajax_console_proxy_port', + 8000, + 'port that ajax_console_proxy binds') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, From 5f06747330bc69aa8af3771029effc3e4b462422 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 22 Dec 2010 23:46:21 -0800 Subject: [PATCH 08/18] some cleanup --- bin/nova-ajax-proxy | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index 52d7ee3d..df73b0ad 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -1,8 +1,15 @@ #!/usr/bin/python +from eventlet import greenthread +from eventlet.green import urllib2 + +import exceptions +import logging import os import sys import time +import urlparse + # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... @@ -12,36 +19,28 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -import webob.dec -import webob.exc - -from nova import exception from nova import flags from nova import utils from nova import rpc from nova import wsgi -import exceptions -import logging -import urlparse - FLAGS = flags.FLAGS flags.DEFINE_integer('ajax_console_idle_timeout', 300, 'Seconds before idle connection destroyed') -import eventlet -from eventlet import greenthread -from eventlet.green import urllib2 class AjaxConsoleProxy(object): tokens = {} - def __call__(self, environ, start_response): + def __call__(self, env, start_response): try: - req_url = '%s://%s%s?%s' % (environ['wsgi.url_scheme'], environ['HTTP_HOST'], environ['PATH_INFO'], environ['QUERY_STRING']) - if 'HTTP_REFERER' in environ: - auth_url = environ['HTTP_REFERER'] + req_url = '%s://%s%s?%s' % (env['wsgi.url_scheme'], + env['HTTP_HOST'], + env['PATH_INFO'], + env['QUERY_STRING']) + if 'HTTP_REFERER' in env: + auth_url = env['HTTP_REFERER'] else: auth_url = req_url @@ -50,7 +49,7 @@ class AjaxConsoleProxy(object): auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]] args = auth_info['args'] - auth_info['last_activity_at'] = time.time() + auth_info['last_activity'] = time.time() remote_url = ("http://%s:%s%s?token=%s"% ( str(args['host']), @@ -58,7 +57,7 @@ class AjaxConsoleProxy(object): parsed_url.path, str(args['token']))) - opener = urllib2.urlopen(remote_url, environ['wsgi.input'].read()) + opener = urllib2.urlopen(remote_url, env['wsgi.input'].read()) body = opener.read() info = opener.info() @@ -77,7 +76,7 @@ class AjaxConsoleProxy(object): def __call__(self, data, message): if data['method'] == 'authorize_ajax_console': AjaxConsoleProxy.tokens[data['args']['token']] = \ - {'args': data['args'], 'last_activity_at': time.time()} + {'args': data['args'], 'last_activity': time.time()} conn = rpc.Connection.instance(new=True) consumer = rpc.TopicConsumer( @@ -89,7 +88,7 @@ class AjaxConsoleProxy(object): now = time.time() to_delete = [] for k, v in AjaxConsoleProxy.tokens.items(): - if now - v['last_activity_at'] > FLAGS.ajax_console_idle_timeout: + if now - v['last_activity'] > FLAGS.ajax_console_idle_timeout: to_delete.append(k) for k in to_delete: From 6d7d6bcf48394b3599968ec57d7c3ea8ee015d6c Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 22 Dec 2010 23:47:46 -0800 Subject: [PATCH 09/18] add in license --- bin/nova-ajax-proxy | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index df73b0ad..3adc1018 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -1,4 +1,25 @@ -#!/usr/bin/python +#!/usr/bin/env python +# pylint: disable-msg=C0103 +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Ajax Console Proxy Server""" + from eventlet import greenthread from eventlet.green import urllib2 From a31a5c54b5f04507d67d18e189911d3248a86bfa Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 22 Dec 2010 23:49:10 -0800 Subject: [PATCH 10/18] more tweaks --- bin/nova-ajax-proxy | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index 3adc1018..bc828c5b 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -20,7 +20,6 @@ """Ajax Console Proxy Server""" - from eventlet import greenthread from eventlet.green import urllib2 @@ -31,7 +30,6 @@ import sys import time import urlparse - # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), @@ -91,7 +89,6 @@ class AjaxConsoleProxy(object): start_response("500 ERROR",[]) return "Server Error" - def register_listeners(self): class Callback: def __call__(self, data, message): From 80e951a84051160774272e7638f23476f755c385 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 23 Dec 2010 01:22:54 -0800 Subject: [PATCH 11/18] some pep8 fixes --- nova/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/flags.py b/nova/flags.py index 2d5aec84..406f159e 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -220,7 +220,7 @@ DEFINE_string('scheduler_topic', 'scheduler', 'the topic scheduler nodes listen on') DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') -DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy', +DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy', 'the topic ajax proxy nodes listen on') DEFINE_string('ajax_console_proxy_url', 'http://tonbuntu:8000', From 392c8e898ba0dbf0a5662ba0886f28f387f70861 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 23 Dec 2010 01:26:03 -0800 Subject: [PATCH 12/18] pep8 fixes --- bin/nova-ajax-proxy | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-proxy index bc828c5b..53b77971 100755 --- a/bin/nova-ajax-proxy +++ b/bin/nova-ajax-proxy @@ -21,7 +21,7 @@ """Ajax Console Proxy Server""" from eventlet import greenthread -from eventlet.green import urllib2 +from eventlet.green import urllib2 import exceptions import logging @@ -45,9 +45,9 @@ from nova import wsgi FLAGS = flags.FLAGS -flags.DEFINE_integer('ajax_console_idle_timeout', 300, +flags.DEFINE_integer('ajax_console_idle_timeout', 300, 'Seconds before idle connection destroyed') - + class AjaxConsoleProxy(object): tokens = {} @@ -67,10 +67,10 @@ class AjaxConsoleProxy(object): parsed_url = urlparse.urlparse(req_url) auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]] - args = auth_info['args'] - auth_info['last_activity'] = time.time() + args = auth_info['args'] + auth_info['last_activity'] = time.time() - remote_url = ("http://%s:%s%s?token=%s"% ( + remote_url = ("http://%s:%s%s?token=%s" % ( str(args['host']), str(args['port']), parsed_url.path, @@ -83,10 +83,10 @@ class AjaxConsoleProxy(object): start_response("200 OK", info.dict.items()) return body except (exceptions.KeyError): - start_response("401 NOT AUTHORIZED",[]) + start_response("401 NOT AUTHORIZED", []) return "Not Authorized" except Exception: - start_response("500 ERROR",[]) + start_response("500 ERROR", []) return "Server Error" def register_listeners(self): @@ -112,7 +112,7 @@ class AjaxConsoleProxy(object): for k in to_delete: del AjaxConsoleProxy.tokens[k] - utils.LoopingCall(consumer.fetch, auto_ack=True, + utils.LoopingCall(consumer.fetch, auto_ack=True, enable_callbacks=True).start(0.1) utils.LoopingCall(delete_expired_tokens).start(1) @@ -124,4 +124,3 @@ if __name__ == '__main__': acp.register_listeners() server.start(acp, FLAGS.ajax_console_proxy_port, host='0.0.0.0') server.wait() - From cc844b765896d64748f2fd6b6adf654000b5c734 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 23 Dec 2010 01:32:15 -0800 Subject: [PATCH 13/18] better bin name, and pep8 --- bin/{nova-ajax-proxy => nova-ajax-console-proxy} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bin/{nova-ajax-proxy => nova-ajax-console-proxy} (100%) diff --git a/bin/nova-ajax-proxy b/bin/nova-ajax-console-proxy similarity index 100% rename from bin/nova-ajax-proxy rename to bin/nova-ajax-console-proxy From 53e78b5fdf19ccc5a752c28b49535381e40e21da Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 28 Dec 2010 17:42:33 -0800 Subject: [PATCH 14/18] add in unit tests --- nova/tests/cloud_unittest.py | 13 +++++++++++++ nova/tests/compute_unittest.py | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 70d2c44d..8e339122 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -150,6 +150,19 @@ class CloudTestCase(test.TestCase): greenthread.sleep(0.3) rv = yield self.cloud.terminate_instances(self.context, [instance_id]) + def test_ajax_console(self): + kwargs = {'image_id': image_id } + rv = yield self.cloud.run_instances(self.context, **kwargs) + instance_id = rv['instancesSet'][0]['instanceId'] + output = yield self.cloud.get_console_output(context=self.context, + instance_id=[instance_id]) + self.assertEquals(b64decode(output['output']), + 'http://fakeajaxconsole.com/?token=FAKETOKEN') + # TODO(soren): We need this until we can stop polling in the rpc code + # for unit tests. + greenthread.sleep(0.3) + rv = yield self.cloud.terminate_instances(self.context, [instance_id]) + def test_key_generation(self): result = self._create_key('test') private_key = result['private_key'] diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index 348bb335..52984797 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -153,6 +153,16 @@ class ComputeTestCase(test.TestCase): self.assert_(console) self.compute.terminate_instance(self.context, instance_id) + def test_ajax_console(self): + """Make sure we can get console output from instance""" + instance_id = self._create_instance() + self.compute.run_instance(self.context, instance_id) + + console = self.compute.get_ajax_console(self.context, + instance_id) + self.assert_(console) + self.compute.terminate_instance(self.context, instance_id) + def test_run_instance_existing(self): """Ensure failure when running an instance that already exists""" instance_id = self._create_instance() From c437c396d12d28b1d50d51cd12b683280af0ea27 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 29 Dec 2010 16:11:02 -0800 Subject: [PATCH 15/18] pep8 fix, and add in flags that don't refernece my laptop --- nova/flags.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/flags.py b/nova/flags.py index 406f159e..6f2747fc 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -223,11 +223,11 @@ DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy', 'the topic ajax proxy nodes listen on') DEFINE_string('ajax_console_proxy_url', - 'http://tonbuntu:8000', - 'location of ajax console proxy, in the form "http://tonbuntu:8000"') + 'http://127.0.0.1:8000', + 'location of ajax console proxy, \ + in the form "http://127.0.0.1:8000"') DEFINE_string('ajax_console_proxy_port', - 8000, - 'port that ajax_console_proxy binds') + 8000, 'port that ajax_console_proxy binds') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, From e32da431bf49d9e82166dbac780f370bd39a0ddf Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 4 Jan 2011 16:22:47 -0800 Subject: [PATCH 16/18] some more cleanup --- nova/adminclient.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/nova/adminclient.py b/nova/adminclient.py index c4e72b93..eabfce80 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -26,20 +26,6 @@ import httplib from nova import flags from boto.ec2.regioninfo import RegionInfo -class ConsoleInfo(object): - def __init__(self, connection=None, endpoint=None): - self.connection = connection - self.endpoint = endpoint - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == 'url': - self.url = str(value) - if name == 'kind': - self.url = str(value) - FLAGS = flags.FLAGS DEFAULT_CLC_URL = 'http://127.0.0.1:8773' @@ -389,13 +375,3 @@ class NovaAdminClient(object): def get_hosts(self): return self.apiconn.get_list('DescribeHosts', {}, [('item', HostInfo)]) - - def create_console(self, instance_id, kind='ajax'): - """ - Create a console - """ - console = self.apiconn.get_object('CreateConsole', {'Kind': kind, 'InstanceId': instance_id}, ConsoleInfo) - - if console.url != None: - return console - From a85452258040966c560d35c767d971e8d0e7e36b Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Mon, 10 Jan 2011 13:44:45 -0800 Subject: [PATCH 17/18] consolidate boto_extensions.py and euca-get-ajax-console, fix bugs from previous trunk merge --- bin/nova-ajax-console-proxy | 13 +++++++++++- nova/boto_extensions.py | 40 ------------------------------------- 2 files changed, 12 insertions(+), 41 deletions(-) delete mode 100644 nova/boto_extensions.py diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy index 53b77971..2bc40765 100755 --- a/bin/nova-ajax-console-proxy +++ b/bin/nova-ajax-console-proxy @@ -24,6 +24,7 @@ from eventlet import greenthread from eventlet.green import urllib2 import exceptions +import gettext import logging import os import sys @@ -38,9 +39,12 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) +gettext.install('nova', unicode=1) + from nova import flags -from nova import utils +from nova import log as logging from nova import rpc +from nova import utils from nova import wsgi FLAGS = flags.FLAGS @@ -48,6 +52,10 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('ajax_console_idle_timeout', 300, 'Seconds before idle connection destroyed') +LOG = logging.getLogger('nova.ajax_console_proxy') +LOG.setLevel(logging.DEBUG) +LOG.addHandler(logging.StreamHandler()) + class AjaxConsoleProxy(object): tokens = {} @@ -83,6 +91,9 @@ class AjaxConsoleProxy(object): start_response("200 OK", info.dict.items()) return body except (exceptions.KeyError): + if env['PATH_INFO'] != '/favicon.ico': + LOG.audit("Unauthorized request %s, %s" + % (req_url, str(env))) start_response("401 NOT AUTHORIZED", []) return "Not Authorized" except Exception: diff --git a/nova/boto_extensions.py b/nova/boto_extensions.py deleted file mode 100644 index 6d55b801..00000000 --- a/nova/boto_extensions.py +++ /dev/null @@ -1,40 +0,0 @@ -import base64 -import boto -from boto.ec2.connection import EC2Connection - -class AjaxConsole: - def __init__(self, parent=None): - self.parent = parent - self.instance_id = None - self.url = None - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == 'instanceId': - self.instance_id = value - elif name == 'url': - self.url = value - else: - setattr(self, name, value) - -class NovaEC2Connection(EC2Connection): - def get_ajax_console(self, instance_id): - """ - Retrieves a console connection for the specified instance. - - :type instance_id: string - :param instance_id: The instance ID of a running instance on the cloud. - - :rtype: :class:`AjaxConsole` - """ - params = {} - self.build_list_params(params, [instance_id], 'InstanceId') - return self.get_object('GetAjaxConsole', params, AjaxConsole) - pass - -def override_connect_ec2(aws_access_key_id=None, aws_secret_access_key=None, **kwargs): - return NovaEC2Connection(aws_access_key_id, aws_secret_access_key, **kwargs) - -boto.connect_ec2 = override_connect_ec2 From c149bc8dde12a496bc09da11e7451eedc3cf07f1 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 11 Jan 2011 14:33:20 -0800 Subject: [PATCH 18/18] bah - pep8 errors --- nova/tests/test_cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 76a62040..8e43eec0 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -168,7 +168,7 @@ class CloudTestCase(test.TestCase): rv = self.cloud.terminate_instances(self.context, [instance_id]) def test_ajax_console(self): - kwargs = {'image_id': image_id } + kwargs = {'image_id': image_id} rv = yield self.cloud.run_instances(self.context, **kwargs) instance_id = rv['instancesSet'][0]['instanceId'] output = yield self.cloud.get_console_output(context=self.context,