diff options
| author | root <root@tonbuntu> | 2011-01-12 09:24:57 +0000 |
|---|---|---|
| committer | Tarmac <> | 2011-01-12 09:24:57 +0000 |
| commit | 76fdd667f2efe7e2dc710fe0254437d176efb45c (patch) | |
| tree | dc3640acddec70efd4fef418a298ca53a1e6aa55 /nova | |
| parent | 78882d496b94915b8a6e2f2edce13e8129299982 (diff) | |
| parent | 7cfca5208766539ae368a9f0b8daba6103041f7f (diff) | |
| download | nova-76fdd667f2efe7e2dc710fe0254437d176efb45c.tar.gz nova-76fdd667f2efe7e2dc710fe0254437d176efb45c.tar.xz nova-76fdd667f2efe7e2dc710fe0254437d176efb45c.zip | |
This branch adds web based serial console access. Here is an overview of how it works (for libvirt):
1. User requests an ajax console for an instance_id (either through OS api, or tools/euca-get-ajax-console)
a. api server calls compute worker to complete request
b. compute worker parses an instance's xml to locate its pseudo terminal (/dev/pts/x)
c. compute worker spawns an ajaxterm daemon, bound to a random port in a specified range. socat is used to connect to /dev/pts/x. Note that ajaxterm was modified in the following ways:
i. dies after 5 minutes of inactivity
ii. now requires token authentication. Previously it was trivial to hijack an ajaxterm
d. compute worker returns ajaxterm connect information to the api server: port, host, token
e. api server casts connect information to the nova-ajax-console-proxy (a new service)
f. api server returns a url for the ajaxterm (eg. http://nova-ajax-console-proxy/?token=123)
2. User now has a url, and can paste it in a browser
a. Browser sends request to https://nova-ajax-console-proxy/?token=123
b. nova-ajax-console-proxy maps token to connect information
c. nova-ajax-console-proxy constructs a proxy to the ajaxterm that is running on the host machine. This is now done with eventlet, though previously it was done using twisted
3. User interacts with console through web browser
NOTE: For this to work as expected, serial console login must be enabled in the instance. Instructions for how to do this on ubuntu can be found here: https://help.ubuntu.com/community/SerialConsoleHowto. Note that you must actively log out of the serial console when you are finished, otherwise the console will remain open even after the ajaxterm term session has ended.
Also note that nova.sh has been modified in this branch to launch nova-ajax-console-proxy.
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/api/ec2/cloud.py | 5 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 9 | ||||
| -rw-r--r-- | nova/compute/api.py | 21 | ||||
| -rw-r--r-- | nova/compute/manager.py | 8 | ||||
| -rw-r--r-- | nova/flags.py | 9 | ||||
| -rw-r--r-- | nova/tests/test_cloud.py | 13 | ||||
| -rw-r--r-- | nova/tests/test_compute.py | 10 | ||||
| -rw-r--r-- | nova/utils.py | 5 | ||||
| -rw-r--r-- | nova/virt/fake.py | 3 | ||||
| -rw-r--r-- | nova/virt/libvirt.xml.template | 13 | ||||
| -rw-r--r-- | nova/virt/libvirt_conn.py | 45 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 5 | ||||
| -rw-r--r-- | nova/virt/xenapi_conn.py | 4 |
13 files changed, 148 insertions, 2 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 135836348..39174d554 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -501,6 +501,11 @@ class CloudController(object): "Timestamp": now, "output": base64.b64encode(output)} + def get_ajax_console(self, context, instance_id, **kwargs): + ec2_id = instance_id[0] + internal_id = ec2_id_to_id(ec2_id) + return self.compute_api.get_ajax_console(context, internal_id) + def describe_volumes(self, context, volume_id=None, **kwargs): volumes = self.volume_api.get_all(context) # NOTE(vish): volume_id is an optional list of volume ids to filter by. diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c8a9947f3..29af82533 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -283,6 +283,15 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + def get_ajax_console(self, req, id): + """ Returns a url to an instance's ajaxterm console. """ + try: + self.compute_api.get_ajax_console(req.environ['nova.context'], + int(id)) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + def diagnostics(self, req, id): """Permit Admins to retrieve server diagnostics.""" ctxt = req.environ["nova.context"] diff --git a/nova/compute/api.py b/nova/compute/api.py index fbe3c6e75..56402c11b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -414,7 +414,26 @@ class API(base.Base): rpc.cast(context, self.db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "unrescue_instance", - "args": {"instance_id": instance_id}}) + "args": {"instance_id": instance['id']}}) + + def get_ajax_console(self, context, instance_id): + """Get a url to an AJAX Console""" + + instance = self.get(context, instance_id) + + output = rpc.call(context, + '%s.%s' % (FLAGS.compute_topic, + instance['host']), + {'method': 'get_ajax_console', + 'args': {'instance_id': instance['id']}}) + + rpc.cast(context, '%s' % FLAGS.ajax_console_proxy_topic, + {'method': 'authorize_ajax_console', + 'args': {'token': output['token'], 'host': output['host'], + 'port': output['port']}}) + + return {'url': '%s?token=%s' % (FLAGS.ajax_console_proxy_url, + output['token'])} def lock(self, context, instance_id): """ diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 018e0bbbe..6b2fc4adb 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -471,6 +471,14 @@ class ComputeManager(manager.Manager): return self.driver.get_console_output(instance_ref) @exception.wrap_exception + def get_ajax_console(self, context, instance_id): + """Return connection information for an ajax console""" + context = context.elevated() + logging.debug(_("instance %s: getting ajax console"), instance_id) + instance_ref = self.db.instance_get(context, instance_id) + + return self.driver.get_ajax_console(instance_ref) + @checks_instance_lock def attach_volume(self, context, instance_id, volume_id, mountpoint): """Attach a volume to an instance.""" diff --git a/nova/flags.py b/nova/flags.py index 25362f883..76ab2f788 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -234,7 +234,14 @@ 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', + 'the topic ajax proxy nodes listen on') +DEFINE_string('ajax_console_proxy_url', + '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') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index b8a15c7b2..8e43eec00 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -167,6 +167,19 @@ class CloudTestCase(test.TestCase): greenthread.sleep(0.3) rv = 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/test_compute.py b/nova/tests/test_compute.py index 1d407c5a3..52660ee74 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -169,6 +169,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() diff --git a/nova/utils.py b/nova/utils.py index aadbec532..45adb7b38 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -153,6 +153,11 @@ def abspath(s): return os.path.join(os.path.dirname(__file__), s) +def novadir(): + import nova + return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0] + + def default_flagfile(filename='nova.conf'): for arg in sys.argv: if arg.find('flagfile') != -1: diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 849261f07..9186d885e 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -289,6 +289,9 @@ class FakeConnection(object): def get_console_output(self, instance): return 'FAKE CONSOLE OUTPUT' + def get_ajax_console(self, instance): + return 'http://fakeajaxconsole.com/?token=FAKETOKEN' + def get_console_pool_info(self, console_type): return {'address': '127.0.0.1', 'username': 'fakeuser', diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 3fb2243da..2eb7d9488 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -71,9 +71,22 @@ #end if </filterref> </interface> + + <!-- The order is significant here. File must be defined first --> <serial type="file"> <source path='${basepath}/console.log'/> <target port='1'/> </serial> + + <console type='pty' tty='/dev/pts/2'> + <source path='/dev/pts/2'/> + <target port='0'/> + </console> + + <serial type='pty'> + <source path='/dev/pts/2'/> + <target port='0'/> + </serial> + </devices> </domain> diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 1bd4d250e..655c55fa1 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -38,6 +38,11 @@ Supports KVM, QEMU, UML, and XEN. import os import shutil +import random +import subprocess +import uuid +from xml.dom import minidom + from eventlet import greenthread from eventlet import event @@ -86,6 +91,9 @@ flags.DEFINE_string('libvirt_uri', flags.DEFINE_bool('allow_project_net_traffic', True, 'Whether to allow in project network traffic') +flags.DEFINE_string('ajaxterm_portrange', + '10000-12000', + 'Range of ports that ajaxterm should randomly try to bind') flags.DEFINE_string('firewall_driver', 'nova.virt.libvirt_conn.IptablesFirewallDriver', 'Firewall driver (defaults to iptables)') @@ -433,6 +441,43 @@ class LibvirtConnection(object): return self._dump_file(fpath) + @exception.wrap_exception + def get_ajax_console(self, instance): + def get_open_port(): + start_port, end_port = FLAGS.ajaxterm_portrange.split("-") + for i in xrange(0, 100): # don't loop forever + port = random.randint(int(start_port), int(end_port)) + # netcat will exit with 0 only if the port is in use, + # so a nonzero return value implies it is unused + cmd = 'netcat 0.0.0.0 %s -w 1 </dev/null || echo free' % (port) + stdout, stderr = utils.execute(cmd) + if stdout.strip() == 'free': + return port + raise Exception(_('Unable to find an open port')) + + def get_pty_for_instance(instance_name): + virt_dom = self._conn.lookupByName(instance_name) + xml = virt_dom.XMLDesc(0) + dom = minidom.parseString(xml) + + for serial in dom.getElementsByTagName('serial'): + if serial.getAttribute('type') == 'pty': + source = serial.getElementsByTagName('source')[0] + return source.getAttribute('path') + + port = get_open_port() + token = str(uuid.uuid4()) + host = instance['host'] + + ajaxterm_cmd = 'sudo socat - %s' \ + % get_pty_for_instance(instance['name']) + + cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \ + % (utils.novadir(), ajaxterm_cmd, token, port) + + subprocess.Popen(cmd, shell=True) + return {'token': token, 'host': host, 'port': port} + def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None): # syntactic nicety basepath = lambda fname = '', prefix = prefix: os.path.join( diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index e20930fe8..7e3585991 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -284,6 +284,11 @@ class VMOps(object): # TODO: implement this to fix pylint! return 'FAKE CONSOLE OUTPUT of instance' + def get_ajax_console(self, instance): + """Return link to instance's ajax console""" + # TODO: implement this! + return 'http://fakeajaxconsole/fake_url' + def list_from_xenstore(self, vm, path): """Runs the xenstore-ls command to get a listing of all records from 'path' downward. Returns a dict with the sub-paths as keys, diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 11473ddf8..45d0738a5 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -181,6 +181,10 @@ class XenAPIConnection(object): """Return snapshot of console""" return self._vmops.get_console_output(instance) + def get_ajax_console(self, instance): + """Return link to instance's ajax console""" + return self._vmops.get_ajax_console(instance) + def attach_volume(self, instance_name, device_path, mountpoint): """Attach volume storage to VM instance""" return self._volumeops.attach_volume(instance_name, |
