diff options
author | Cerberus <matt.dietz@rackspace.com> | 2011-02-18 16:45:31 -0600 |
---|---|---|
committer | Cerberus <matt.dietz@rackspace.com> | 2011-02-18 16:45:31 -0600 |
commit | 201391007e58b2f92fd7b56ccbf308e5909da7c0 (patch) | |
tree | 7965568a97336d0817f47035504ec79bcf488a70 | |
parent | a43c5929de7ebf58eb9ecb8416ce3cf4194c176a (diff) | |
parent | 8de8d1d045ca9fe12596e53d2244f4f8703cc209 (diff) | |
download | nova-201391007e58b2f92fd7b56ccbf308e5909da7c0.tar.gz nova-201391007e58b2f92fd7b56ccbf308e5909da7c0.tar.xz nova-201391007e58b2f92fd7b56ccbf308e5909da7c0.zip |
Merge from trunk and merge conflict resolution
-rw-r--r-- | .mailmap | 46 | ||||
-rw-r--r-- | Authors | 10 | ||||
-rw-r--r-- | nova/api/openstack/__init__.py | 4 | ||||
-rw-r--r-- | nova/api/openstack/auth.py | 1 | ||||
-rw-r--r-- | nova/api/openstack/servers.py | 5 | ||||
-rw-r--r-- | nova/api/openstack/zones.py | 80 | ||||
-rw-r--r-- | nova/auth/novarc.template | 7 | ||||
-rw-r--r-- | nova/compute/api.py | 15 | ||||
-rw-r--r-- | nova/compute/manager.py | 61 | ||||
-rw-r--r-- | nova/db/api.py | 30 | ||||
-rw-r--r-- | nova/db/sqlalchemy/api.py | 44 | ||||
-rw-r--r-- | nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py | 61 | ||||
-rw-r--r-- | nova/db/sqlalchemy/migration.py | 4 | ||||
-rw-r--r-- | nova/db/sqlalchemy/models.py | 12 | ||||
-rw-r--r-- | nova/log.py | 4 | ||||
-rw-r--r-- | nova/network/linux_net.py | 2 | ||||
-rw-r--r-- | nova/network/manager.py | 29 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_zones.py | 141 | ||||
-rw-r--r-- | nova/tests/test_compute.py | 8 | ||||
-rw-r--r-- | nova/utils.py | 23 | ||||
-rw-r--r-- | nova/virt/fake.py | 15 | ||||
-rw-r--r-- | nova/virt/xenapi/vmops.py | 50 | ||||
-rw-r--r-- | nova/virt/xenapi_conn.py | 6 | ||||
-rwxr-xr-x | plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 5 | ||||
-rwxr-xr-x | plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py | 10 |
25 files changed, 589 insertions, 84 deletions
@@ -1,37 +1,43 @@ # Format is: -# <preferred e-mail> <other e-mail> -<code@term.ie> <github@anarkystic.com> -<code@term.ie> <termie@preciousroy.local> +# <preferred e-mail> <other e-mail 1> +# <preferred e-mail> <other e-mail 2> +<anotherjesse@gmail.com> <jesse@dancelamb> +<anotherjesse@gmail.com> <jesse@gigantor.local> +<anotherjesse@gmail.com> <jesse@ubuntu> +<ant@openstack.org> <amesserl@rackspace.com> <Armando.Migliaccio@eu.citrix.com> <armando.migliaccio@citrix.com> -<matt.dietz@rackspace.com> <matthewdietz@Matthew-Dietzs-MacBook-Pro.local> -<matt.dietz@rackspace.com> <mdietz@openstack> +<brian.lamar@rackspace.com> <brian.lamar@gmail.com> +<bschott@isi.edu> <bfschott@gmail.com> <cbehrens@codestud.com> <chris.behrens@rackspace.com> +<chiradeep@cloud.com> <chiradeep@chiradeep-lt2> +<code@term.ie> <github@anarkystic.com> +<code@term.ie> <termie@preciousroy.local> +<corywright@gmail.com> <cory.wright@rackspace.com> <devin.carlen@gmail.com> <devcamcar@illian.local> <ewan.mellor@citrix.com> <emellor@silver> <jaypipes@gmail.com> <jpipes@serialcoder> -<anotherjesse@gmail.com> <jesse@dancelamb> -<anotherjesse@gmail.com> <jesse@gigantor.local> -<anotherjesse@gmail.com> <jesse@ubuntu> -<jmckenty@gmail.com> <jmckenty@yyj-dhcp171.corp.flock.com> <jmckenty@gmail.com> <jmckenty@joshua-mckentys-macbook-pro.local> +<jmckenty@gmail.com> <jmckenty@yyj-dhcp171.corp.flock.com> <jmckenty@gmail.com> <joshua.mckenty@nasa.gov> <justin@fathomdb.com> <justinsb@justinsb-desktop> -<masumotok@nttdata.co.jp> <root@openstack2-api> +<justin@fathomdb.com> <superstack@superstack.org> <masumotok@nttdata.co.jp> Masumoto<masumotok@nttdata.co.jp> +<masumotok@nttdata.co.jp> <root@openstack2-api> +<matt.dietz@rackspace.com> <matthewdietz@Matthew-Dietzs-MacBook-Pro.local> +<matt.dietz@rackspace.com> <mdietz@openstack> <mordred@inaugust.com> <mordred@hudson> -<paul@openstack.org> <pvoccio@castor.local> <paul@openstack.org> <paul.voccio@rackspace.com> +<paul@openstack.org> <pvoccio@castor.local> +<rconradharris@gmail.com> <rick.harris@rackspace.com> +<rlane@wikimedia.org> <laner@controller> +<sleepsonthefloor@gmail.com> <root@tonbuntu> <soren.hansen@rackspace.com> <soren@linux2go.dk> <todd@ansolabs.com> <todd@lapex> <todd@ansolabs.com> <todd@rubidine.com> -<vishvananda@gmail.com> <vishvananda@yahoo.com> +<tushar.vitthal.patil@gmail.com> <tpatil@vertex.co.in> +<ueno.nachi@lab.ntt.co.jp> <nati.ueno@gmail.com> +<ueno.nachi@lab.ntt.co.jp> <nova@u4> +<ueno.nachi@lab.ntt.co.jp> <openstack@lab.ntt.co.jp> <vishvananda@gmail.com> <root@mirror.nasanebula.net> <vishvananda@gmail.com> <root@ubuntu> -<sleepsonthefloor@gmail.com> <root@tonbuntu> -<rlane@wikimedia.org> <laner@controller> -<rconradharris@gmail.com> <rick.harris@rackspace.com> -<corywright@gmail.com> <cory.wright@rackspace.com> -<ant@openstack.org> <amesserl@rackspace.com> -<chiradeep@cloud.com> <chiradeep@chiradeep-lt2> -<justin@fathomdb.com> <superstack@superstack.org> -<brian.lamar@rackspace.com> <brian.lamar@gmail.com> +<vishvananda@gmail.com> <vishvananda@yahoo.com> @@ -3,17 +3,17 @@ Anne Gentle <anne@openstack.org> Anthony Young <sleepsonthefloor@gmail.com> Antony Messerli <ant@openstack.org> Armando Migliaccio <Armando.Migliaccio@eu.citrix.com> -Brian Waldon <brian.waldon@rackspace.com> Bilal Akhtar <bilalakhtar@ubuntu.com> Brian Lamar <brian.lamar@rackspace.com> -Brian Schott <bschott@isi.edu> <bfschott@gmail.com> +Brian Schott <bschott@isi.edu> +Brian Waldon <brian.waldon@rackspace.com> Chiradeep Vittal <chiradeep@cloud.com> Chmouel Boudjnah <chmouel@chmouel.com> Chris Behrens <cbehrens@codestud.com> Christian Berendt <berendt@b1-systems.de> Cory Wright <corywright@gmail.com> -David Pravec <David.Pravec@danix.org> Dan Prince <dan.prince@rackspace.com> +David Pravec <David.Pravec@danix.org> Dean Troyer <dtroyer@gmail.com> Devin Carlen <devin.carlen@gmail.com> Ed Leafe <ed@leafe.com> @@ -44,7 +44,7 @@ Monsyne Dragon <mdragon@rackspace.com> Monty Taylor <mordred@inaugust.com> MORITA Kazutaka <morita.kazutaka@gmail.com> Muneyuki Noguchi <noguchimn@nttdata.co.jp> -Nachi Ueno <ueno.nachi@lab.ntt.co.jp> <openstack@lab.ntt.co.jp> <nati.ueno@gmail.com> <nova@u4> +Nachi Ueno <ueno.nachi@lab.ntt.co.jp> Naveed Massjouni <naveed.massjouni@rackspace.com> Paul Voccio <paul@openstack.org> Ricardo Carrillo Cruz <emaildericky@gmail.com> @@ -59,7 +59,7 @@ Soren Hansen <soren.hansen@rackspace.com> Thierry Carrez <thierry@openstack.org> Todd Willey <todd@ansolabs.com> Trey Morris <trey.morris@rackspace.com> -Tushar Patil <tushar.vitthal.patil@gmail.com> <tpatil@vertex.co.in> +Tushar Patil <tushar.vitthal.patil@gmail.com> Vasiliy Shlykov <vash@vasiliyshlykov.org> Vishvananda Ishaya <vishvananda@gmail.com> Youcef Laribi <Youcef.Laribi@eu.citrix.com> diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index dc3738d4a..d0b18eced 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -34,6 +34,7 @@ from nova.api.openstack import flavors from nova.api.openstack import images from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups +from nova.api.openstack import zones LOG = logging.getLogger('nova.api.openstack') @@ -81,6 +82,9 @@ class APIRouter(wsgi.Router): server_members['resume'] = 'POST' server_members['reset_network'] = 'POST' + mapper.resource("zone", "zones", controller=zones.Controller(), + collection={'detail': 'GET'}) + mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}, member=server_members) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 1dfdd5318..473071738 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -19,6 +19,7 @@ import datetime import hashlib import json import time +import logging import webob.exc import webob.dec diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f68c97323..59fc3bc69 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2010 OpenStack LLC. # All Rights Reserved. # @@ -179,7 +177,8 @@ class Controller(wsgi.Controller): display_name=env['server']['name'], display_description=env['server']['name'], key_name=key_pair['name'], - key_data=key_pair['public_key']) + key_data=key_pair['public_key'], + onset_files=env.get('onset_files', [])) return _translate_keys(instances[0]) def update(self, req, id): diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py new file mode 100644 index 000000000..830464ffd --- /dev/null +++ b/nova/api/openstack/zones.py @@ -0,0 +1,80 @@ +# Copyright 2010 OpenStack LLC. +# 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. + +import common +import logging + +from nova import flags +from nova import wsgi +from nova import db + + +FLAGS = flags.FLAGS + + +def _filter_keys(item, keys): + """ + Filters all model attributes except for keys + item is a dict + + """ + return dict((k, v) for k, v in item.iteritems() if k in keys) + + +def _scrub_zone(zone): + return _filter_keys(zone, ('id', 'api_url')) + + +class Controller(wsgi.Controller): + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "zone": ["id", "api_url"]}}} + + def index(self, req): + """Return all zones in brief""" + items = db.zone_get_all(req.environ['nova.context']) + items = common.limited(items, req) + items = [_scrub_zone(item) for item in items] + return dict(zones=items) + + def detail(self, req): + """Return all zones in detail""" + return self.index(req) + + def show(self, req, id): + """Return data about the given zone id""" + zone_id = int(id) + zone = db.zone_get(req.environ['nova.context'], zone_id) + return dict(zone=_scrub_zone(zone)) + + def delete(self, req, id): + zone_id = int(id) + db.zone_delete(req.environ['nova.context'], zone_id) + return {} + + def create(self, req): + context = req.environ['nova.context'] + env = self._deserialize(req.body, req) + zone = db.zone_create(context, env["zone"]) + return dict(zone=_scrub_zone(zone)) + + def update(self, req, id): + context = req.environ['nova.context'] + env = self._deserialize(req.body, req) + zone_id = int(id) + zone = db.zone_update(context, zone_id, env["zone"]) + return dict(zone=_scrub_zone(zone)) diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template index c53a4acdc..cda2ecc28 100644 --- a/nova/auth/novarc.template +++ b/nova/auth/novarc.template @@ -10,7 +10,6 @@ export NOVA_CERT=${NOVA_KEY_DIR}/%(nova)s export EUCALYPTUS_CERT=${NOVA_CERT} # euca-bundle-image seems to require this set alias ec2-bundle-image="ec2-bundle-image --cert ${EC2_CERT} --privatekey ${EC2_PRIVATE_KEY} --user 42 --ec2cert ${NOVA_CERT}" alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_KEY} --url ${S3_URL} --ec2cert ${NOVA_CERT}" -export CLOUD_SERVERS_API_KEY="%(access)s" -export CLOUD_SERVERS_USERNAME="%(user)s" -export CLOUD_SERVERS_URL="%(os)s" - +export NOVA_API_KEY="%(access)s" +export NOVA_USERNAME="%(user)s" +export NOVA_URL="%(os)s" diff --git a/nova/compute/api.py b/nova/compute/api.py index 2eb0e0743..60ae3876e 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): + availability_zone=None, user_data=None, + 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: @@ -156,7 +157,6 @@ class API(base.Base): 'key_data': key_data, 'locked': False, 'availability_zone': availability_zone} - elevated = context.elevated() instances = [] LOG.debug(_("Going to run %s instances..."), num_instances) @@ -193,7 +193,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) @@ -477,6 +478,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 3f6c359ba..b3b66b3e3 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 @@ -629,7 +646,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/db/api.py b/nova/db/api.py index 8706ef3d6..3cf4b9780 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -603,7 +603,7 @@ def project_get_network(context, project_id, associate=True): """ - return IMPL.project_get_network(context, project_id) + return IMPL.project_get_network(context, project_id, associate) def project_get_network_v6(context, project_id): @@ -1027,3 +1027,31 @@ def console_get_all_by_instance(context, instance_id): def console_get(context, console_id, instance_id=None): """Get a specific console (possibly on a given instance).""" return IMPL.console_get(context, console_id, instance_id) + + +#################### + + +def zone_create(context, values): + """Create a new child Zone entry.""" + return IMPL.zone_create(context, values) + + +def zone_update(context, zone_id, values): + """Update a child Zone entry.""" + return IMPL.zone_update(context, values) + + +def zone_delete(context, zone_id): + """Delete a child Zone.""" + return IMPL.zone_delete(context, zone_id) + + +def zone_get(context, zone_id): + """Get a specific child Zone.""" + return IMPL.zone_get(context, zone_id) + + +def zone_get_all(context): + """Get all child Zones.""" + return IMPL.zone_get_all(context) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index facb46b8b..44cef6d13 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -2114,3 +2114,47 @@ def console_get(context, console_id, instance_id=None): raise exception.NotFound(_("No console with id %(console_id)s" " %(idesc)s") % locals()) return result + + +#################### + + +@require_admin_context +def zone_create(context, values): + zone = models.Zone() + zone.update(values) + zone.save() + return zone + + +@require_admin_context +def zone_update(context, zone_id, values): + zone = session.query(models.Zone).filter_by(id=zone_id).first() + if not zone: + raise exception.NotFound(_("No zone with id %(zone_id)s") % locals()) + zone.update(values) + zone.save() + return zone + + +@require_admin_context +def zone_delete(context, zone_id): + session = get_session() + with session.begin(): + session.execute('delete from zones ' + 'where id=:id', {'id': zone_id}) + + +@require_admin_context +def zone_get(context, zone_id): + session = get_session() + result = session.query(models.Zone).filter_by(id=zone_id).first() + if not result: + raise exception.NotFound(_("No zone with id %(zone_id)s") % locals()) + return result + + +@require_admin_context +def zone_get_all(context): + session = get_session() + return session.query(models.Zone).all() diff --git a/nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py b/nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py new file mode 100644 index 000000000..ade981687 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py @@ -0,0 +1,61 @@ +# Copyright 2010 OpenStack LLC. +# 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. + +from sqlalchemy import * +from migrate import * + +from nova import log as logging + + +meta = MetaData() + + +# +# New Tables +# +zones = Table('zones', meta, + Column('created_at', DateTime(timezone=False)), + Column('updated_at', DateTime(timezone=False)), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean(create_constraint=True, name=None)), + Column('id', Integer(), primary_key=True, nullable=False), + Column('api_url', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('username', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + Column('password', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False)), + ) + + +# +# Tables to alter +# + +# (none currently) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + for table in (zones, ): + try: + table.create() + except Exception: + logging.info(repr(table)) diff --git a/nova/db/sqlalchemy/migration.py b/nova/db/sqlalchemy/migration.py index d7d9777e0..d9e303599 100644 --- a/nova/db/sqlalchemy/migration.py +++ b/nova/db/sqlalchemy/migration.py @@ -55,8 +55,8 @@ def db_version(): engine = sqlalchemy.create_engine(FLAGS.sql_connection, echo=False) meta.reflect(bind=engine) try: - for table in ('auth_tokens', 'export_devices', 'fixed_ips', - 'floating_ips', 'instances', + for table in ('auth_tokens', 'zones', 'export_devices', + 'fixed_ips', 'floating_ips', 'instances', 'key_pairs', 'networks', 'projects', 'quotas', 'security_group_instance_association', 'security_group_rules', 'security_groups', diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index b05f134b7..3f613fd17 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -548,6 +548,15 @@ class Console(BASE, NovaBase): pool = relationship(ConsolePool, backref=backref('consoles')) +class Zone(BASE, NovaBase): + """Represents a child zone of this zone.""" + __tablename__ = 'zones' + id = Column(Integer, primary_key=True) + api_url = Column(String(255)) + username = Column(String(255)) + password = Column(String(255)) + + def register_models(): """Register Models and create metadata. @@ -560,8 +569,7 @@ def register_models(): Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp, Network, SecurityGroup, SecurityGroupIngressRule, SecurityGroupInstanceAssociation, AuthToken, User, - Project, Certificate, ConsolePool, Console, - Migration) # , Image, Host + Project, Certificate, ConsolePool, Console, Zone, Migration) engine = create_engine(FLAGS.sql_connection, echo=False) for model in models: model.metadata.create_all(engine) diff --git a/nova/log.py b/nova/log.py index 87a6dd51b..6b201ffcc 100644 --- a/nova/log.py +++ b/nova/log.py @@ -94,7 +94,7 @@ critical = logging.critical log = logging.log # handlers StreamHandler = logging.StreamHandler -RotatingFileHandler = logging.handlers.RotatingFileHandler +WatchedFileHandler = logging.handlers.WatchedFileHandler # logging.SysLogHandler is nicer than logging.logging.handler.SysLogHandler. SysLogHandler = logging.handlers.SysLogHandler @@ -139,7 +139,7 @@ def basicConfig(): logging.root.addHandler(syslog) logpath = get_log_file_path() if logpath: - logfile = RotatingFileHandler(logpath) + logfile = WatchedFileHandler(logpath) logfile.setFormatter(_formatter) logging.root.addHandler(logfile) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index c1cbff7d8..535ce87bc 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -44,7 +44,7 @@ flags.DEFINE_string('dhcp_domain', flags.DEFINE_string('networks_path', '$state_path/networks', 'Location to keep network config files') -flags.DEFINE_string('public_interface', 'vlan1', +flags.DEFINE_string('public_interface', 'eth0', 'Interface for public IP addresses') flags.DEFINE_string('vlan_interface', 'eth0', 'network device for vlans') diff --git a/nova/network/manager.py b/nova/network/manager.py index b906a83ed..c6eba225e 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -110,6 +110,7 @@ class NetworkManager(manager.Manager): This class must be subclassed to support specific topologies. """ + timeout_fixed_ips = True def __init__(self, network_driver=None, *args, **kwargs): if not network_driver: @@ -138,6 +139,19 @@ class NetworkManager(manager.Manager): self.driver.ensure_floating_forward(floating_ip['address'], fixed_address) + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval.""" + super(NetworkManager, self).periodic_tasks(context) + if self.timeout_fixed_ips: + now = utils.utcnow() + timeout = FLAGS.fixed_ip_disassociate_timeout + time = now - datetime.timedelta(seconds=timeout) + num = self.db.fixed_ip_disassociate_all_by_timeout(context, + self.host, + time) + if num: + LOG.debug(_("Dissassociated %s stale fixed ip(s)"), num) + def set_network_host(self, context, network_id): """Safely sets the host of the network.""" LOG.debug(_("setting network host"), context=context) @@ -306,6 +320,7 @@ class FlatManager(NetworkManager): not do any setup in this mode, it must be done manually. Requests to 169.254.169.254 port 80 will need to be forwarded to the api server. """ + timeout_fixed_ips = False def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): """Gets a fixed ip from the pool.""" @@ -457,18 +472,6 @@ class VlanManager(NetworkManager): instances in its subnet. """ - def periodic_tasks(self, context=None): - """Tasks to be run at a periodic interval.""" - super(VlanManager, self).periodic_tasks(context) - now = datetime.datetime.utcnow() - timeout = FLAGS.fixed_ip_disassociate_timeout - time = now - datetime.timedelta(seconds=timeout) - num = self.db.fixed_ip_disassociate_all_by_timeout(context, - self.host, - time) - if num: - LOG.debug(_("Dissassociated %s stale fixed ip(s)"), num) - def init_host(self): """Do any initialization that needs to be run if this is a standalone service. @@ -509,7 +512,7 @@ class VlanManager(NetworkManager): network_ref['bridge']) def create_networks(self, context, cidr, num_networks, network_size, - cidr_v6, vlan_start, vpn_start): + cidr_v6, vlan_start, vpn_start, **kwargs): """Create networks based on parameters.""" # Check that num_networks + vlan_start is not > 4094, fixes lp708025 if num_networks + vlan_start > 4094: diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py new file mode 100644 index 000000000..5542a1cf3 --- /dev/null +++ b/nova/tests/api/openstack/test_zones.py @@ -0,0 +1,141 @@ +# Copyright 2010 OpenStack LLC. +# 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. + +import unittest + +import stubout +import webob +import json + +import nova.db +from nova import context +from nova import flags +from nova.api.openstack import zones +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS +FLAGS.verbose = True + + +def zone_get(context, zone_id): + return dict(id=1, api_url='http://foo.com', username='bob', + password='xxx') + + +def zone_create(context, values): + zone = dict(id=1) + zone.update(values) + return zone + + +def zone_update(context, zone_id, values): + zone = dict(id=zone_id, api_url='http://foo.com', username='bob', + password='xxx') + zone.update(values) + return zone + + +def zone_delete(context, zone_id): + pass + + +def zone_get_all(context): + return [ + dict(id=1, api_url='http://foo.com', username='bob', + password='xxx'), + dict(id=2, api_url='http://blah.com', username='alice', + password='qwerty') + ] + + +class ZonesTest(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + + self.allow_admin = FLAGS.allow_admin_api + FLAGS.allow_admin_api = True + + self.stubs.Set(nova.db, 'zone_get', zone_get) + self.stubs.Set(nova.db, 'zone_get_all', zone_get_all) + self.stubs.Set(nova.db, 'zone_update', zone_update) + self.stubs.Set(nova.db, 'zone_create', zone_create) + self.stubs.Set(nova.db, 'zone_delete', zone_delete) + + def tearDown(self): + self.stubs.UnsetAll() + FLAGS.allow_admin_api = self.allow_admin + + def test_get_zone_list(self): + req = webob.Request.blank('/v1.0/zones') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(len(res_dict['zones']), 2) + + def test_get_zone_by_id(self): + req = webob.Request.blank('/v1.0/zones/1') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res_dict['zone']['id'], 1) + self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com') + self.assertFalse('password' in res_dict['zone']) + self.assertEqual(res.status_int, 200) + + def test_zone_delete(self): + req = webob.Request.blank('/v1.0/zones/1') + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) + + def test_zone_create(self): + body = dict(zone=dict(api_url='http://blah.zoo', username='fred', + password='fubar')) + req = webob.Request.blank('/v1.0/zones') + req.method = 'POST' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_dict['zone']['id'], 1) + self.assertEqual(res_dict['zone']['api_url'], 'http://blah.zoo') + self.assertFalse('username' in res_dict['zone']) + + def test_zone_update(self): + body = dict(zone=dict(username='zeb', password='sneaky')) + req = webob.Request.blank('/v1.0/zones/1') + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_dict['zone']['id'], 1) + self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com') + self.assertFalse('username' in res_dict['zone']) + + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 5fd1ddaec..abd4cee5f 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -203,6 +203,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 9106ebf03..b7eedc43a 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -170,6 +170,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 d457f2e3f..01ad036b7 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -102,6 +102,7 @@ class VMOps(object): vdi_uuid = VMHelper.fetch_image(self._session, instance.id, instance.image_id, user, project, disk_image_type) vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid) + else: vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', disk) @@ -169,6 +170,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 @@ -182,6 +198,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'), @@ -190,6 +208,7 @@ class VMOps(object): instance['id'], power_state.SHUTDOWN) timer.stop() + return False timer.f = _wait_for_boot @@ -402,6 +421,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, method='hard'): """Shutdown an instance """ state = self.get_info(instance['name'])['state'] @@ -620,6 +665,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 e1c5dcc7c..e39a6226b 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -172,6 +172,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) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index f99ea4082..94eaabe73 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -93,9 +93,8 @@ def password(self, arg_dict): @jsonify def resetnetwork(self, arg_dict): - """ - Writes a resquest to xenstore that tells the agent to reset networking. - + """Writes a resquest to xenstore that tells the agent + to reset networking. """ arg_dict['value'] = json.dumps({'name': 'resetnetwork', 'value': ''}) request_id = arg_dict['id'] diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py index 695bf3448..a35ccd6ab 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py @@ -36,7 +36,15 @@ pluginlib.configure_logging("xenstore") def jsonify(fnc): def wrapper(*args, **kwargs): - return json.dumps(fnc(*args, **kwargs)) + ret = fnc(*args, **kwargs) + try: + json.loads(ret) + except ValueError: + # Value should already be JSON-encoded, but some operations + # may write raw sting values; this will catch those and + # properly encode them. + ret = json.dumps(ret) + return ret return wrapper |