summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohannes Erdfelt <johannes.erdfelt@rackspace.com>2011-06-21 16:43:10 +0000
committerTarmac <>2011-06-21 16:43:10 +0000
commit29e2d55e9d47dd4abffdb871ee88af9083d022d1 (patch)
tree1c25bb5b5437a979e79a16b9e9e2bd84f59fe2aa
parenta62e0f3e10cae4938ca2fec047268064cab3bff2 (diff)
parentf94041278e22acc557dc878bbf3f1b1f70351446 (diff)
downloadnova-29e2d55e9d47dd4abffdb871ee88af9083d022d1.tar.gz
nova-29e2d55e9d47dd4abffdb871ee88af9083d022d1.tar.xz
nova-29e2d55e9d47dd4abffdb871ee88af9083d022d1.zip
This branch adds support to the xenapi driver for updating the guest agent on creation of a new instance. This ensures that the guest agent is running the latest code before nova starts configuring networking, setting root password or injecting files.
-rwxr-xr-xbin/nova-manage65
-rw-r--r--nova/compute/api.py4
-rw-r--r--nova/compute/manager.py18
-rw-r--r--nova/db/api.py29
-rw-r--r--nova/db/sqlalchemy/api.py51
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/026_add_agent_table.py73
-rw-r--r--nova/db/sqlalchemy/models.py15
-rw-r--r--nova/tests/test_compute.py8
-rw-r--r--nova/tests/test_xenapi.py56
-rw-r--r--nova/virt/driver.py4
-rw-r--r--nova/virt/fake.py15
-rw-r--r--nova/virt/xenapi/vmops.py72
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/agent36
13 files changed, 430 insertions, 16 deletions
diff --git a/bin/nova-manage b/bin/nova-manage
index dbdb798a7..48d576931 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -1071,6 +1071,70 @@ class ImageCommands(object):
self._convert_images(machine_images)
+class AgentBuildCommands(object):
+ """Class for managing agent builds."""
+
+ def create(self, os, architecture, version, url, md5hash,
+ hypervisor='xen'):
+ """Creates a new agent build.
+ arguments: os architecture version url md5hash [hypervisor='xen']"""
+ ctxt = context.get_admin_context()
+ agent_build = db.agent_build_create(ctxt,
+ {'hypervisor': hypervisor,
+ 'os': os,
+ 'architecture': architecture,
+ 'version': version,
+ 'url': url,
+ 'md5hash': md5hash})
+
+ def delete(self, os, architecture, hypervisor='xen'):
+ """Deletes an existing agent build.
+ arguments: os architecture [hypervisor='xen']"""
+ ctxt = context.get_admin_context()
+ agent_build_ref = db.agent_build_get_by_triple(ctxt,
+ hypervisor, os, architecture)
+ db.agent_build_destroy(ctxt, agent_build_ref['id'])
+
+ def list(self, hypervisor=None):
+ """Lists all agent builds.
+ arguments: <none>"""
+ fmt = "%-10s %-8s %12s %s"
+ ctxt = context.get_admin_context()
+ by_hypervisor = {}
+ for agent_build in db.agent_build_get_all(ctxt):
+ buildlist = by_hypervisor.get(agent_build.hypervisor)
+ if not buildlist:
+ buildlist = by_hypervisor[agent_build.hypervisor] = []
+
+ buildlist.append(agent_build)
+
+ for key, buildlist in by_hypervisor.iteritems():
+ if hypervisor and key != hypervisor:
+ continue
+
+ print "Hypervisor: %s" % key
+ print fmt % ('-' * 10, '-' * 8, '-' * 12, '-' * 32)
+ for agent_build in buildlist:
+ print fmt % (agent_build.os, agent_build.architecture,
+ agent_build.version, agent_build.md5hash)
+ print ' %s' % agent_build.url
+
+ print
+
+ def modify(self, os, architecture, version, url, md5hash,
+ hypervisor='xen'):
+ """Update an existing agent build.
+ arguments: os architecture version url md5hash [hypervisor='xen']
+ """
+ ctxt = context.get_admin_context()
+ agent_build_ref = db.agent_build_get_by_triple(ctxt,
+ hypervisor, os, architecture)
+ db.agent_build_update(ctxt, agent_build_ref['id'],
+ {'version': version,
+ 'url': url,
+ 'md5hash': md5hash})
+
+
class ConfigCommands(object):
"""Class for exposing the flags defined by flag_file(s)."""
@@ -1083,6 +1147,7 @@ class ConfigCommands(object):
CATEGORIES = [
('account', AccountCommands),
+ ('agent', AgentBuildCommands),
('config', ConfigCommands),
('db', DbCommands),
('fixed', FixedIpCommands),
diff --git a/nova/compute/api.py b/nova/compute/api.py
index e6cffb6b3..a7ea88d51 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -178,6 +178,9 @@ class API(base.Base):
os_type = None
if 'properties' in image and 'os_type' in image['properties']:
os_type = image['properties']['os_type']
+ architecture = None
+ if 'properties' in image and 'arch' in image['properties']:
+ architecture = image['properties']['arch']
vm_mode = None
if 'properties' in image and 'vm_mode' in image['properties']:
vm_mode = image['properties']['vm_mode']
@@ -243,6 +246,7 @@ class API(base.Base):
'metadata': metadata,
'availability_zone': availability_zone,
'os_type': os_type,
+ 'architecture': architecture,
'vm_mode': vm_mode}
return (num_instances, base_options, security_groups)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index a7ec021b8..4e006e677 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -562,6 +562,24 @@ class ComputeManager(manager.SchedulerDependentManager):
@exception.wrap_exception
@checks_instance_lock
+ def agent_update(self, context, instance_id, url, md5hash):
+ """Update agent running on an instance on this host."""
+ 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 update agent on a non-running '
+ 'instance: %(instance_id)s (state: %(instance_state)s '
+ 'expected: %(expected_state)s)') % locals())
+ nm = instance_ref['name']
+ msg = _('instance %(nm)s: updating agent to %(url)s') % locals()
+ LOG.audit(msg)
+ self.driver.agent_update(instance_ref, url, md5hash)
+
+ @exception.wrap_exception
+ @checks_instance_lock
def rescue_instance(self, context, instance_id):
"""Rescue an instance on this host."""
context = context.elevated()
diff --git a/nova/db/api.py b/nova/db/api.py
index 5fd081ca8..8f8e856b8 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -1287,3 +1287,32 @@ def instance_metadata_delete(context, instance_id, key):
def instance_metadata_update_or_create(context, instance_id, metadata):
"""Create or update instance metadata."""
IMPL.instance_metadata_update_or_create(context, instance_id, metadata)
+
+
+####################
+
+
+def agent_build_create(context, values):
+ """Create a new agent build entry."""
+ return IMPL.agent_build_create(context, values)
+
+
+def agent_build_get_by_triple(context, hypervisor, os, architecture):
+ """Get agent build by hypervisor/OS/architecture triple."""
+ return IMPL.agent_build_get_by_triple(context, hypervisor, os,
+ architecture)
+
+
+def agent_build_get_all(context):
+ """Get all agent builds."""
+ return IMPL.agent_build_get_all(context)
+
+
+def agent_build_destroy(context, agent_update_id):
+ """Destroy agent build entry."""
+ IMPL.agent_build_destroy(context, agent_update_id)
+
+
+def agent_build_update(context, agent_build_id, values):
+ """Update agent build entry."""
+ IMPL.agent_build_update(context, agent_build_id, values)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index a7e5125d5..a2500a38d 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -2792,3 +2792,54 @@ def instance_metadata_update_or_create(context, instance_id, metadata):
meta_ref.save(session=session)
return metadata
+
+
+@require_admin_context
+def agent_build_create(context, values):
+ agent_build_ref = models.AgentBuild()
+ agent_build_ref.update(values)
+ agent_build_ref.save()
+ return agent_build_ref
+
+
+@require_admin_context
+def agent_build_get_by_triple(context, hypervisor, os, architecture,
+ session=None):
+ if not session:
+ session = get_session()
+ return session.query(models.AgentBuild).\
+ filter_by(hypervisor=hypervisor).\
+ filter_by(os=os).\
+ filter_by(architecture=architecture).\
+ filter_by(deleted=False).\
+ first()
+
+
+@require_admin_context
+def agent_build_get_all(context):
+ session = get_session()
+ return session.query(models.AgentBuild).\
+ filter_by(deleted=False).\
+ all()
+
+
+@require_admin_context
+def agent_build_destroy(context, agent_build_id):
+ session = get_session()
+ with session.begin():
+ session.query(models.AgentBuild).\
+ filter_by(id=agent_build_id).\
+ update({'deleted': 1,
+ 'deleted_at': datetime.datetime.utcnow(),
+ 'updated_at': literal_column('updated_at')})
+
+
+@require_admin_context
+def agent_build_update(context, agent_build_id, values):
+ session = get_session()
+ with session.begin():
+ agent_build_ref = session.query(models.AgentBuild).\
+ filter_by(id=agent_build_id). \
+ first()
+ agent_build_ref.update(values)
+ agent_build_ref.save(session=session)
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/026_add_agent_table.py b/nova/db/sqlalchemy/migrate_repo/versions/026_add_agent_table.py
new file mode 100644
index 000000000..640e96138
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/026_add_agent_table.py
@@ -0,0 +1,73 @@
+# Copyright 2011 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 Boolean, Column, DateTime, Integer
+from sqlalchemy import MetaData, String, Table
+from nova import log as logging
+
+meta = MetaData()
+
+#
+# New Tables
+#
+builds = Table('agent_builds', 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('hypervisor',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('os',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('architecture',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('version',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('url',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('md5hash',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ )
+
+
+#
+# New Column
+#
+
+architecture = Column('architecture', String(length=255))
+
+
+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 (builds, ):
+ try:
+ table.create()
+ except Exception:
+ logging.info(repr(table))
+
+ instances = Table('instances', meta, autoload=True,
+ autoload_with=migrate_engine)
+
+ # Add columns to existing tables
+ instances.create_column(architecture)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 81711545a..f9375960d 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -232,6 +232,7 @@ class Instance(BASE, NovaBase):
locked = Column(Boolean)
os_type = Column(String(255))
+ architecture = Column(String(255))
vm_mode = Column(String(255))
uuid = Column(String(36))
@@ -713,6 +714,18 @@ class Zone(BASE, NovaBase):
password = Column(String(255))
+class AgentBuild(BASE, NovaBase):
+ """Represents an agent build."""
+ __tablename__ = 'agent_builds'
+ id = Column(Integer, primary_key=True)
+ hypervisor = Column(String(255))
+ os = Column(String(255))
+ architecture = Column(String(255))
+ version = Column(String(255))
+ url = Column(String(255))
+ md5hash = Column(String(255))
+
+
def register_models():
"""Register Models and create metadata.
@@ -726,7 +739,7 @@ def register_models():
Network, SecurityGroup, SecurityGroupIngressRule,
SecurityGroupInstanceAssociation, AuthToken, User,
Project, Certificate, ConsolePool, Console, Zone,
- InstanceMetadata, Migration)
+ AgentBuild, InstanceMetadata, Migration)
engine = create_engine(FLAGS.sql_connection, echo=False)
for model in models:
model.metadata.create_all(engine)
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 2fa4c7278..439508b27 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -281,6 +281,14 @@ class ComputeTestCase(test.TestCase):
"File Contents")
self.compute.terminate_instance(self.context, instance_id)
+ def test_agent_update(self):
+ """Ensure instance can have its agent updated"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.agent_update(self.context, instance_id,
+ 'http://127.0.0.1/agent', '00112233445566778899aabbccddeeff')
+ 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/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index c0213250a..d9a514745 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -37,9 +37,8 @@ from nova import exception
from nova.virt import xenapi_conn
from nova.virt.xenapi import fake as xenapi_fake
from nova.virt.xenapi import volume_utils
+from nova.virt.xenapi import vmops
from nova.virt.xenapi import vm_utils
-from nova.virt.xenapi.vmops import SimpleDH
-from nova.virt.xenapi.vmops import VMOps
from nova.tests.db import fakes as db_fakes
from nova.tests.xenapi import stubs
from nova.tests.glance import stubs as glance_stubs
@@ -85,7 +84,8 @@ class XenAPIVolumeTestCase(test.TestCase):
'ramdisk_id': 3,
'instance_type_id': '3', # m1.large
'mac_address': 'aa:bb:cc:dd:ee:ff',
- 'os_type': 'linux'}
+ 'os_type': 'linux',
+ 'architecture': 'x86-64'}
def _create_volume(self, size='0'):
"""Create a volume object."""
@@ -192,7 +192,7 @@ class XenAPIVMTestCase(test.TestCase):
stubs.stubout_get_this_vm_uuid(self.stubs)
stubs.stubout_stream_disk(self.stubs)
stubs.stubout_is_vdi_pv(self.stubs)
- self.stubs.Set(VMOps, 'reset_network', reset_network)
+ self.stubs.Set(vmops.VMOps, 'reset_network', reset_network)
stubs.stub_out_vm_methods(self.stubs)
glance_stubs.stubout_glance_client(self.stubs)
fake_utils.stub_out_utils_execute(self.stubs)
@@ -212,7 +212,8 @@ class XenAPIVMTestCase(test.TestCase):
'ramdisk_id': 3,
'instance_type_id': '3', # m1.large
'mac_address': 'aa:bb:cc:dd:ee:ff',
- 'os_type': 'linux'}
+ 'os_type': 'linux',
+ 'architecture': 'x86-64'}
instance = db.instance_create(self.context, values)
self.conn.spawn(instance)
@@ -370,7 +371,8 @@ class XenAPIVMTestCase(test.TestCase):
def _test_spawn(self, image_ref, kernel_id, ramdisk_id,
instance_type_id="3", os_type="linux",
- instance_id=1, check_injection=False):
+ architecture="x86-64", instance_id=1,
+ check_injection=False):
stubs.stubout_loopingcall_start(self.stubs)
values = {'id': instance_id,
'project_id': self.project.id,
@@ -380,11 +382,14 @@ class XenAPIVMTestCase(test.TestCase):
'ramdisk_id': ramdisk_id,
'instance_type_id': instance_type_id,
'mac_address': 'aa:bb:cc:dd:ee:ff',
- 'os_type': os_type}
+ 'os_type': os_type,
+ 'architecture': architecture}
instance = db.instance_create(self.context, values)
self.conn.spawn(instance)
self.create_vm_record(self.conn, os_type, instance_id)
self.check_vm_record(self.conn, check_injection)
+ self.assertTrue(instance.os_type)
+ self.assertTrue(instance.architecture)
def test_spawn_not_enough_memory(self):
FLAGS.xenapi_image_service = 'glance'
@@ -409,7 +414,7 @@ class XenAPIVMTestCase(test.TestCase):
def test_spawn_vhd_glance_linux(self):
FLAGS.xenapi_image_service = 'glance'
self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
- os_type="linux")
+ os_type="linux", architecture="x86-64")
self.check_vm_params_for_linux()
def test_spawn_vhd_glance_swapdisk(self):
@@ -438,7 +443,7 @@ class XenAPIVMTestCase(test.TestCase):
def test_spawn_vhd_glance_windows(self):
FLAGS.xenapi_image_service = 'glance'
self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
- os_type="windows")
+ os_type="windows", architecture="i386")
self.check_vm_params_for_windows()
def test_spawn_glance(self):
@@ -589,7 +594,8 @@ class XenAPIVMTestCase(test.TestCase):
'ramdisk_id': 3,
'instance_type_id': '3', # m1.large
'mac_address': 'aa:bb:cc:dd:ee:ff',
- 'os_type': 'linux'}
+ 'os_type': 'linux',
+ 'architecture': 'x86-64'}
instance = db.instance_create(self.context, values)
self.conn.spawn(instance)
return instance
@@ -599,8 +605,8 @@ class XenAPIDiffieHellmanTestCase(test.TestCase):
"""Unit tests for Diffie-Hellman code."""
def setUp(self):
super(XenAPIDiffieHellmanTestCase, self).setUp()
- self.alice = SimpleDH()
- self.bob = SimpleDH()
+ self.alice = vmops.SimpleDH()
+ self.bob = vmops.SimpleDH()
def test_shared(self):
alice_pub = self.alice.get_public()
@@ -664,7 +670,8 @@ class XenAPIMigrateInstance(test.TestCase):
'local_gb': 5,
'instance_type_id': '3', # m1.large
'mac_address': 'aa:bb:cc:dd:ee:ff',
- 'os_type': 'linux'}
+ 'os_type': 'linux',
+ 'architecture': 'x86-64'}
fake_utils.stub_out_utils_execute(self.stubs)
stubs.stub_out_migration_methods(self.stubs)
@@ -703,6 +710,7 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase):
self.fake_instance = FakeInstance()
self.fake_instance.id = 42
self.fake_instance.os_type = 'linux'
+ self.fake_instance.architecture = 'x86-64'
def assert_disk_type(self, disk_type):
dt = vm_utils.VMHelper.determine_disk_image_type(
@@ -747,6 +755,28 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase):
self.assert_disk_type(vm_utils.ImageType.DISK_VHD)
+class CompareVersionTestCase(test.TestCase):
+ def test_less_than(self):
+ """Test that cmp_version compares a as less than b"""
+ self.assertTrue(vmops.cmp_version('1.2.3.4', '1.2.3.5') < 0)
+
+ def test_greater_than(self):
+ """Test that cmp_version compares a as greater than b"""
+ self.assertTrue(vmops.cmp_version('1.2.3.5', '1.2.3.4') > 0)
+
+ def test_equal(self):
+ """Test that cmp_version compares a as equal to b"""
+ self.assertTrue(vmops.cmp_version('1.2.3.4', '1.2.3.4') == 0)
+
+ def test_non_lexical(self):
+ """Test that cmp_version compares non-lexically"""
+ self.assertTrue(vmops.cmp_version('1.2.3.10', '1.2.3.4') > 0)
+
+ def test_length(self):
+ """Test that cmp_version compares by length as last resort"""
+ self.assertTrue(vmops.cmp_version('1.2.3', '1.2.3.4') < 0)
+
+
class FakeXenApi(object):
"""Fake XenApi for testing HostState."""
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index 6341e81d2..8dd7057a3 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -234,6 +234,10 @@ class ComputeDriver(object):
"""
raise NotImplementedError()
+ def agent_update(self, instance, url, md5hash):
+ """Update agent on the VM instance."""
+ raise NotImplementedError()
+
def inject_network_info(self, instance):
"""inject network info for specified instance"""
raise NotImplementedError()
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 3a65fec8b..94aecec24 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -225,6 +225,21 @@ class FakeConnection(driver.ComputeDriver):
"""
pass
+ def agent_update(self, instance, url, md5hash):
+ """
+ Update agent 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 URL of the agent to be fetched and updated on the
+ instance; the third is the md5 hash of the file for verification
+ purposes.
+
+ 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 d105cf300..2f4286184 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -47,6 +47,21 @@ LOG = logging.getLogger("nova.virt.xenapi.vmops")
FLAGS = flags.FLAGS
+def cmp_version(a, b):
+ """Compare two version strings (eg 0.0.1.10 > 0.0.1.9)"""
+ a = a.split('.')
+ b = b.split('.')
+
+ # Compare each individual portion of both version strings
+ for va, vb in zip(a, b):
+ ret = int(va) - int(vb)
+ if ret:
+ return ret
+
+ # Fallback to comparing length last
+ return len(a) - len(b)
+
+
class VMOps(object):
"""
Management class for VM-related tasks
@@ -218,6 +233,34 @@ class VMOps(object):
LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.')
% locals())
+ ctx = context.get_admin_context()
+ agent_build = db.agent_build_get_by_triple(ctx, 'xen',
+ instance.os_type, instance.architecture)
+ if agent_build:
+ LOG.info(_('Latest agent build for %(hypervisor)s/%(os)s' + \
+ '/%(architecture)s is %(version)s') % agent_build)
+ else:
+ LOG.info(_('No agent build found for %(hypervisor)s/%(os)s' + \
+ '/%(architecture)s') % {
+ 'hypervisor': 'xen',
+ 'os': instance.os_type,
+ 'architecture': instance.architecture})
+
+ def _check_agent_version():
+ version = self.get_agent_version(instance)
+ if not version:
+ LOG.info(_('No agent version returned by instance'))
+ return
+
+ LOG.info(_('Instance agent version: %s') % version)
+ if not agent_build:
+ return
+
+ if cmp_version(version, agent_build['version']) < 0:
+ LOG.info(_('Updating Agent to %s') % agent_build['version'])
+ self.agent_update(instance, agent_build['url'],
+ agent_build['md5hash'])
+
def _inject_files():
injected_files = instance.injected_files
if injected_files:
@@ -252,6 +295,7 @@ class VMOps(object):
if state == power_state.RUNNING:
LOG.debug(_('Instance %s: booted'), instance_name)
timer.stop()
+ _check_agent_version()
_inject_files()
_set_admin_password()
return True
@@ -458,6 +502,34 @@ class VMOps(object):
task = self._session.call_xenapi('Async.VM.clean_reboot', vm_ref)
self._session.wait_for_task(task, instance.id)
+ def get_agent_version(self, instance):
+ """Get the version of the agent running on the VM instance."""
+
+ # Send the encrypted password
+ transaction_id = str(uuid.uuid4())
+ args = {'id': transaction_id}
+ resp = self._make_agent_call('version', instance, '', args)
+ if resp is None:
+ # No response from the agent
+ return
+ resp_dict = json.loads(resp)
+ return resp_dict['message']
+
+ def agent_update(self, instance, url, md5sum):
+ """Update agent on the VM instance."""
+
+ # Send the encrypted password
+ transaction_id = str(uuid.uuid4())
+ args = {'id': transaction_id, 'url': url, 'md5sum': md5sum}
+ resp = self._make_agent_call('agentupdate', instance, '', args)
+ if resp is None:
+ # No response from the agent
+ return
+ resp_dict = json.loads(resp)
+ if resp_dict['returncode'] != '0':
+ raise RuntimeError(resp_dict['message'])
+ return resp_dict['message']
+
def set_admin_password(self, instance, new_pass):
"""Set the root/admin password on the VM instance.
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
index 9e761f264..b8a1b936a 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
@@ -53,6 +53,19 @@ class TimeoutError(StandardError):
pass
+def version(self, arg_dict):
+ """Get version of agent."""
+ arg_dict["value"] = json.dumps({"name": "version", "value": ""})
+ request_id = arg_dict["id"]
+ arg_dict["path"] = "data/host/%s" % request_id
+ xenstore.write_record(self, arg_dict)
+ try:
+ resp = _wait_for_agent(self, request_id, arg_dict)
+ except TimeoutError, e:
+ raise PluginError(e)
+ return resp
+
+
def key_init(self, arg_dict):
"""Handles the Diffie-Hellman key exchange with the agent to
establish the shared secret key used to encrypt/decrypt sensitive
@@ -144,6 +157,23 @@ def inject_file(self, arg_dict):
return resp
+def agent_update(self, arg_dict):
+ """Expects an URL and md5sum of the contents, then directs the agent to
+ update itself."""
+ request_id = arg_dict["id"]
+ url = arg_dict["url"]
+ md5sum = arg_dict["md5sum"]
+ arg_dict["value"] = json.dumps({"name": "agentupdate",
+ "value": {"url": url, "md5sum": md5sum}})
+ arg_dict["path"] = "data/host/%s" % request_id
+ xenstore.write_record(self, arg_dict)
+ try:
+ resp = _wait_for_agent(self, request_id, arg_dict)
+ except TimeoutError, e:
+ raise PluginError(e)
+ return resp
+
+
def _agent_has_method(self, method):
"""Check that the agent has a particular method by checking its
features. Cache the features so we don't have to query the agent
@@ -201,7 +231,9 @@ def _wait_for_agent(self, request_id, arg_dict):
if __name__ == "__main__":
XenAPIPlugin.dispatch(
- {"key_init": key_init,
+ {"version": version,
+ "key_init": key_init,
"password": password,
"resetnetwork": resetnetwork,
- "inject_file": inject_file})
+ "inject_file": inject_file,
+ "agentupdate": agent_update})