summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
Diffstat (limited to 'nova')
-rw-r--r--nova/api/openstack/servers.py3
-rw-r--r--nova/compute/api.py19
-rw-r--r--nova/compute/manager.py61
-rw-r--r--nova/tests/test_compute.py8
-rw-r--r--nova/utils.py23
-rw-r--r--nova/virt/fake.py15
-rw-r--r--nova/virt/xenapi/vmops.py50
-rw-r--r--nova/virt/xenapi_conn.py6
8 files changed, 155 insertions, 30 deletions
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 49611703a..0018b96f3 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -195,7 +195,8 @@ class Controller(wsgi.Controller):
display_description=env['server']['name'],
key_name=key_pair['name'],
key_data=key_pair['public_key'],
- metadata=metadata)
+ metadata=metadata,
+ onset_files=env.get('onset_files', []))
return _translate_keys(instances[0])
def update(self, req, id):
diff --git a/nova/compute/api.py b/nova/compute/api.py
index cad167f4d..8b11d41d3 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -85,10 +85,11 @@ class API(base.Base):
min_count=1, max_count=1,
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
- availability_zone=None, user_data=None, metadata=[]):
+ availability_zone=None, user_data=None, metadata=[],
+ onset_files=None):
"""Create the number of instances requested if quota and
- other arguments check out ok."""
-
+ other arguments check out ok.
+ """
type_data = instance_types.INSTANCE_TYPES[instance_type]
num_instances = quota.allowed_instances(context, max_count, type_data)
if num_instances < min_count:
@@ -179,9 +180,8 @@ class API(base.Base):
'key_name': key_name,
'key_data': key_data,
'locked': False,
- 'availability_zone': availability_zone,
- 'metadata': metadata}
-
+ 'metadata': metadata,
+ 'availability_zone': availability_zone}
elevated = context.elevated()
instances = []
LOG.debug(_("Going to run %s instances..."), num_instances)
@@ -218,7 +218,8 @@ class API(base.Base):
{"method": "run_instance",
"args": {"topic": FLAGS.compute_topic,
"instance_id": instance_id,
- "availability_zone": availability_zone}})
+ "availability_zone": availability_zone,
+ "onset_files": onset_files}})
for group_id in security_groups:
self.trigger_security_group_members_refresh(elevated, group_id)
@@ -459,6 +460,10 @@ class API(base.Base):
"""Set the root/admin password for the given instance."""
self._cast_compute_message('set_admin_password', context, instance_id)
+ def inject_file(self, context, instance_id):
+ """Write a file to the given instance."""
+ self._cast_compute_message('inject_file', context, instance_id)
+
def get_ajax_console(self, context, instance_id):
"""Get a url to an AJAX Console"""
instance = self.get(context, instance_id)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 6fab1a41c..b8d4b7ee9 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -34,6 +34,7 @@ terminating it.
:func:`nova.utils.import_object`
"""
+import base64
import datetime
import random
import string
@@ -130,7 +131,7 @@ class ComputeManager(manager.Manager):
state = power_state.FAILED
self.db.instance_set_state(context, instance_id, state)
- def get_console_topic(self, context, **_kwargs):
+ def get_console_topic(self, context, **kwargs):
"""Retrieves the console host for a project on this host
Currently this is just set in the flags for each compute
host."""
@@ -139,7 +140,7 @@ class ComputeManager(manager.Manager):
FLAGS.console_topic,
FLAGS.console_host)
- def get_network_topic(self, context, **_kwargs):
+ def get_network_topic(self, context, **kwargs):
"""Retrieves the network host for a project on this host"""
# TODO(vish): This method should be memoized. This will make
# the call to get_network_host cheaper, so that
@@ -158,21 +159,22 @@ class ComputeManager(manager.Manager):
@exception.wrap_exception
def refresh_security_group_rules(self, context,
- security_group_id, **_kwargs):
+ security_group_id, **kwargs):
"""This call passes straight through to the virtualization driver."""
return self.driver.refresh_security_group_rules(security_group_id)
@exception.wrap_exception
def refresh_security_group_members(self, context,
- security_group_id, **_kwargs):
+ security_group_id, **kwargs):
"""This call passes straight through to the virtualization driver."""
return self.driver.refresh_security_group_members(security_group_id)
@exception.wrap_exception
- def run_instance(self, context, instance_id, **_kwargs):
+ def run_instance(self, context, instance_id, **kwargs):
"""Launch a new instance with specified options."""
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
+ instance_ref.onset_files = kwargs.get('onset_files', [])
if instance_ref['name'] in self.driver.list_instances():
raise exception.Error(_("Instance has already been created"))
LOG.audit(_("instance %s: starting..."), instance_id,
@@ -323,28 +325,43 @@ class ComputeManager(manager.Manager):
"""Set the root/admin password for an instance on this server."""
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- if instance_ref['state'] != power_state.RUNNING:
- logging.warn('trying to reset the password on a non-running '
- 'instance: %s (state: %s expected: %s)',
- instance_ref['id'],
- instance_ref['state'],
- power_state.RUNNING)
-
- logging.debug('instance %s: setting admin password',
+ instance_id = instance_ref['id']
+ instance_state = instance_ref['state']
+ expected_state = power_state.RUNNING
+ if instance_state != expected_state:
+ LOG.warn(_('trying to reset the password on a non-running '
+ 'instance: %(instance_id)s (state: %(instance_state)s '
+ 'expected: %(expected_state)s)') % locals())
+ LOG.audit(_('instance %s: setting admin password'),
instance_ref['name'])
if new_pass is None:
# Generate a random password
- new_pass = self._generate_password(FLAGS.password_length)
-
+ new_pass = utils.generate_password(FLAGS.password_length)
self.driver.set_admin_password(instance_ref, new_pass)
self._update_state(context, instance_id)
- def _generate_password(self, length=20):
- """Generate a random sequence of letters and digits
- to be used as a password.
- """
- chrs = string.letters + string.digits
- return "".join([random.choice(chrs) for i in xrange(length)])
+ @exception.wrap_exception
+ @checks_instance_lock
+ def inject_file(self, context, instance_id, path, file_contents):
+ """Write a file to the specified path on an instance on this server"""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+ instance_id = instance_ref['id']
+ instance_state = instance_ref['state']
+ expected_state = power_state.RUNNING
+ if instance_state != expected_state:
+ LOG.warn(_('trying to inject a file into a non-running '
+ 'instance: %(instance_id)s (state: %(instance_state)s '
+ 'expected: %(expected_state)s)') % locals())
+ # Files/paths *should* be base64-encoded at this point, but
+ # double-check to make sure.
+ b64_path = utils.ensure_b64_encoding(path)
+ b64_contents = utils.ensure_b64_encoding(file_contents)
+ plain_path = base64.b64decode(b64_path)
+ nm = instance_ref['name']
+ msg = _('instance %(nm)s: injecting file to %(plain_path)s') % locals()
+ LOG.audit(msg)
+ self.driver.inject_file(instance_ref, b64_path, b64_contents)
@exception.wrap_exception
@checks_instance_lock
@@ -523,7 +540,7 @@ class ComputeManager(manager.Manager):
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)
+ LOG.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)
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 2aa0690e7..b049ac943 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -202,6 +202,14 @@ class ComputeTestCase(test.TestCase):
self.compute.set_admin_password(self.context, instance_id)
self.compute.terminate_instance(self.context, instance_id)
+ def test_inject_file(self):
+ """Ensure we can write a file to an instance"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.inject_file(self.context, instance_id, "/tmp/test",
+ "File Contents")
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_snapshot(self):
"""Ensure instance can be snapshotted"""
instance_id = self._create_instance()
diff --git a/nova/utils.py b/nova/utils.py
index ba71ebf39..42efa0008 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -20,12 +20,14 @@
System-level utilities and helper functions.
"""
+import base64
import datetime
import inspect
import json
import os
import random
import socket
+import string
import struct
import sys
import time
@@ -235,6 +237,15 @@ def generate_mac():
return ':'.join(map(lambda x: "%02x" % x, mac))
+def generate_password(length=20):
+ """Generate a random sequence of letters and digits
+ to be used as a password. Note that this is not intended
+ to represent the ultimate in security.
+ """
+ chrs = string.letters + string.digits
+ return "".join([random.choice(chrs) for i in xrange(length)])
+
+
def last_octet(address):
return int(address.split(".")[-1])
@@ -476,3 +487,15 @@ def dumps(value):
def loads(s):
return json.loads(s)
+
+
+def ensure_b64_encoding(val):
+ """Safety method to ensure that values expected to be base64-encoded
+ actually are. If they are, the value is returned unchanged. Otherwise,
+ the encoded value is returned.
+ """
+ try:
+ dummy = base64.decode(val)
+ return val
+ except TypeError:
+ return base64.b64encode(val)
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 161445b86..92749f38a 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -152,6 +152,21 @@ class FakeConnection(object):
"""
pass
+ def inject_file(self, instance, b64_path, b64_contents):
+ """
+ Writes a file on the specified instance.
+
+ The first parameter is an instance of nova.compute.service.Instance,
+ and so the instance is being specified as instance.name. The second
+ parameter is the base64-encoded path to which the file is to be
+ written on the instance; the third is the contents of the file, also
+ base64-encoded.
+
+ The work will be done asynchronously. This function returns a
+ task that allows the caller to detect when it is complete.
+ """
+ pass
+
def rescue(self, instance):
"""
Rescue the specified instance.
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 842e08f22..0168681f6 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -79,6 +79,7 @@ class VMOps(object):
user = AuthManager().get_user(instance.user_id)
project = AuthManager().get_project(instance.project_id)
+
#if kernel is not present we must download a raw disk
if instance.kernel_id:
disk_image_type = ImageType.DISK
@@ -150,6 +151,21 @@ class VMOps(object):
LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.')
% locals())
+ def _inject_onset_files():
+ onset_files = instance.onset_files
+ if onset_files:
+ # Check if this is a JSON-encoded string and convert if needed.
+ if isinstance(onset_files, basestring):
+ try:
+ onset_files = json.loads(onset_files)
+ except ValueError:
+ LOG.exception(_("Invalid value for onset_files: '%s'")
+ % onset_files)
+ onset_files = []
+ # Inject any files, if specified
+ for path, contents in instance.onset_files:
+ LOG.debug(_("Injecting file path: '%s'") % path)
+ self.inject_file(instance, path, contents)
# NOTE(armando): Do we really need to do this in virt?
# NOTE(tr3buchet): not sure but wherever we do it, we need to call
# reset_network afterwards
@@ -163,6 +179,8 @@ class VMOps(object):
if state == power_state.RUNNING:
LOG.debug(_('Instance %s: booted'), instance['name'])
timer.stop()
+ _inject_onset_files()
+ return True
except Exception, exc:
LOG.warn(exc)
LOG.exception(_('instance %s: failed to boot'),
@@ -171,6 +189,7 @@ class VMOps(object):
instance['id'],
power_state.SHUTDOWN)
timer.stop()
+ return False
timer.f = _wait_for_boot
@@ -304,6 +323,32 @@ class VMOps(object):
raise RuntimeError(resp_dict['message'])
return resp_dict['message']
+ def inject_file(self, instance, b64_path, b64_contents):
+ """Write a file to the VM instance. The path to which it is to be
+ written and the contents of the file need to be supplied; both should
+ be base64-encoded to prevent errors with non-ASCII characters being
+ transmitted. If the agent does not support file injection, or the user
+ has disabled it, a NotImplementedError will be raised.
+ """
+ # Files/paths *should* be base64-encoded at this point, but
+ # double-check to make sure.
+ b64_path = utils.ensure_b64_encoding(b64_path)
+ b64_contents = utils.ensure_b64_encoding(b64_contents)
+
+ # Need to uniquely identify this request.
+ transaction_id = str(uuid.uuid4())
+ args = {'id': transaction_id, 'b64_path': b64_path,
+ 'b64_contents': b64_contents}
+ # If the agent doesn't support file injection, a NotImplementedError
+ # will be raised with the appropriate message.
+ resp = self._make_agent_call('inject_file', instance, '', args)
+ resp_dict = json.loads(resp)
+ if resp_dict['returncode'] != '0':
+ # There was some other sort of error; the message will contain
+ # a description of the error.
+ raise RuntimeError(resp_dict['message'])
+ return resp_dict['message']
+
def _shutdown(self, instance, vm):
"""Shutdown an instance """
state = self.get_info(instance['name'])['state']
@@ -515,6 +560,11 @@ class VMOps(object):
if 'TIMEOUT:' in err_msg:
LOG.error(_('TIMEOUT: The call to %(method)s timed out. '
'VM id=%(instance_id)s; args=%(strargs)s') % locals())
+ elif 'NOT IMPLEMENTED:' in err_msg:
+ LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not'
+ ' supported by the agent. VM id=%(instance_id)s;'
+ ' args=%(strargs)s') % locals())
+ raise NotImplementedError(err_msg)
else:
LOG.error(_('The call to %(method)s returned an error: %(e)s. '
'VM id=%(instance_id)s; args=%(strargs)s') % locals())
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 2720d175f..c2f65699f 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -168,6 +168,12 @@ class XenAPIConnection(object):
"""Set the root/admin password on the VM instance"""
self._vmops.set_admin_password(instance, new_pass)
+ def inject_file(self, instance, b64_path, b64_contents):
+ """Create a file on the VM instance. The file path and contents
+ should be base64-encoded.
+ """
+ self._vmops.inject_file(instance, b64_path, b64_contents)
+
def destroy(self, instance):
"""Destroy VM instance"""
self._vmops.destroy(instance)