summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorJustin Santa Barbara <justin@fathomdb.com>2011-02-23 18:11:35 +0000
committerTarmac <>2011-02-23 18:11:35 +0000
commit70bc1d4280065f6eba368187af126cebcb6b69bb (patch)
tree3d73657fc3fa7b80bb82c51e21ff80818ceb2c77 /nova
parent35991d1e5b1902d5a09776302d8c25761ae248d7 (diff)
parentef0dfb6809f31cfe8ca8056892fc9dcc2f00a0d7 (diff)
downloadnova-70bc1d4280065f6eba368187af126cebcb6b69bb.tar.gz
nova-70bc1d4280065f6eba368187af126cebcb6b69bb.tar.xz
nova-70bc1d4280065f6eba368187af126cebcb6b69bb.zip
Initial support for per-instance metadata, though the OpenStack API. Key/value pairs can be specified at instance creation time and are returned in the details view. Support limits based on quota system.
Diffstat (limited to 'nova')
-rw-r--r--nova/api/openstack/servers.py27
-rw-r--r--nova/compute/api.py27
-rw-r--r--nova/db/sqlalchemy/api.py2
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/005_add_instance_metadata.py78
-rw-r--r--nova/db/sqlalchemy/models.py18
-rw-r--r--nova/quota.py14
-rw-r--r--nova/tests/api/openstack/test_servers.py11
-rw-r--r--nova/tests/test_quota.py25
8 files changed, 192 insertions, 10 deletions
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 0bac4c64d..a8e597d73 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -77,9 +77,14 @@ def _translate_detail_keys(inst):
except KeyError:
LOG.debug(_("Failed to read public ip(s)"))
- inst_dict['metadata'] = {}
inst_dict['hostId'] = ''
+ # Return the metadata as a dictionary
+ metadata = {}
+ for item in inst['metadata']:
+ metadata[item['key']] = item['value']
+ inst_dict['metadata'] = metadata
+
return dict(server=inst_dict)
@@ -161,18 +166,29 @@ class Controller(wsgi.Controller):
if not env:
return faults.Fault(exc.HTTPUnprocessableEntity())
- key_pairs = auth_manager.AuthManager.get_key_pairs(
- req.environ['nova.context'])
+ context = req.environ['nova.context']
+ key_pairs = auth_manager.AuthManager.get_key_pairs(context)
if not key_pairs:
raise exception.NotFound(_("No keypairs defined"))
key_pair = key_pairs[0]
image_id = common.get_image_id_from_image_hash(self._image_service,
- req.environ['nova.context'], env['server']['imageId'])
+ context, env['server']['imageId'])
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
req, image_id)
+
+ # Metadata is a list, not a Dictionary, because we allow duplicate keys
+ # (even though JSON can't encode this)
+ # In future, we may not allow duplicate keys.
+ # However, the CloudServers API is not definitive on this front,
+ # and we want to be compatible.
+ metadata = []
+ if env['server']['metadata']:
+ for k, v in env['server']['metadata'].items():
+ metadata.append({'key': k, 'value': v})
+
instances = self.compute_api.create(
- req.environ['nova.context'],
+ context,
instance_types.get_by_flavor_id(env['server']['flavorId']),
image_id,
kernel_id=kernel_id,
@@ -181,6 +197,7 @@ class Controller(wsgi.Controller):
display_description=env['server']['name'],
key_name=key_pair['name'],
key_data=key_pair['public_key'],
+ metadata=metadata,
onset_files=env.get('onset_files', []))
return _translate_keys(instances[0])
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 0caadc32e..d9431c679 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -85,7 +85,7 @@ 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, metadata=[],
onset_files=None):
"""Create the number of instances requested if quota and
other arguments check out ok.
@@ -100,6 +100,30 @@ class API(base.Base):
"run %s more instances of this type.") %
num_instances, "InstanceLimitExceeded")
+ num_metadata = len(metadata)
+ quota_metadata = quota.allowed_metadata_items(context, num_metadata)
+ if quota_metadata < num_metadata:
+ pid = context.project_id
+ msg = (_("Quota exceeeded for %(pid)s,"
+ " tried to set %(num_metadata)s metadata properties")
+ % locals())
+ LOG.warn(msg)
+ raise quota.QuotaError(msg, "MetadataLimitExceeded")
+
+ # Because metadata is stored in the DB, we hard-code the size limits
+ # In future, we may support more variable length strings, so we act
+ # as if this is quota-controlled for forwards compatibility
+ for metadata_item in metadata:
+ k = metadata_item['key']
+ v = metadata_item['value']
+ if len(k) > 255 or len(v) > 255:
+ pid = context.project_id
+ msg = (_("Quota exceeeded for %(pid)s,"
+ " metadata property key or value too long")
+ % locals())
+ LOG.warn(msg)
+ raise quota.QuotaError(msg, "MetadataLimitExceeded")
+
image = self.image_service.show(context, image_id)
if kernel_id is None:
kernel_id = image.get('kernel_id', None)
@@ -154,6 +178,7 @@ class API(base.Base):
'key_name': key_name,
'key_data': key_data,
'locked': False,
+ 'metadata': metadata,
'availability_zone': availability_zone}
elevated = context.elevated()
instances = []
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 2ab402e1c..be29fe2a0 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -715,6 +715,7 @@ def instance_get(context, instance_id, session=None):
options(joinedload_all('security_groups.rules')).\
options(joinedload('volumes')).\
options(joinedload_all('fixed_ip.network')).\
+ options(joinedload('metadata')).\
filter_by(id=instance_id).\
filter_by(deleted=can_read_deleted(context)).\
first()
@@ -723,6 +724,7 @@ def instance_get(context, instance_id, session=None):
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload_all('security_groups.rules')).\
options(joinedload('volumes')).\
+ options(joinedload('metadata')).\
filter_by(project_id=context.project_id).\
filter_by(id=instance_id).\
filter_by(deleted=False).\
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/005_add_instance_metadata.py b/nova/db/sqlalchemy/migrate_repo/versions/005_add_instance_metadata.py
new file mode 100644
index 000000000..4cb07e0d8
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/005_add_instance_metadata.py
@@ -0,0 +1,78 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# 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()
+
+
+# Just for the ForeignKey and column creation to succeed, these are not the
+# actual definitions of instances or services.
+instances = Table('instances', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+quotas = Table('quotas', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+
+#
+# New Tables
+#
+
+instance_metadata_table = Table('instance_metadata', 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('instance_id',
+ Integer(),
+ ForeignKey('instances.id'),
+ nullable=False),
+ Column('key',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('value',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)))
+
+
+#
+# New columns
+#
+quota_metadata_items = Column('metadata_items', Integer())
+
+
+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 (instance_metadata_table, ):
+ try:
+ table.create()
+ except Exception:
+ logging.info(repr(table))
+ logging.exception('Exception while creating table')
+ raise
+
+ quotas.create_column(quota_metadata_items)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 40a96fc17..a842e4cc4 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -256,6 +256,7 @@ class Quota(BASE, NovaBase):
volumes = Column(Integer)
gigabytes = Column(Integer)
floating_ips = Column(Integer)
+ metadata_items = Column(Integer)
class ExportDevice(BASE, NovaBase):
@@ -536,6 +537,20 @@ class Console(BASE, NovaBase):
pool = relationship(ConsolePool, backref=backref('consoles'))
+class InstanceMetadata(BASE, NovaBase):
+ """Represents a metadata key/value pair for an instance"""
+ __tablename__ = 'instance_metadata'
+ id = Column(Integer, primary_key=True)
+ key = Column(String(255))
+ value = Column(String(255))
+ instance_id = Column(Integer, ForeignKey('instances.id'), nullable=False)
+ instance = relationship(Instance, backref="metadata",
+ foreign_keys=instance_id,
+ primaryjoin='and_('
+ 'InstanceMetadata.instance_id == Instance.id,'
+ 'InstanceMetadata.deleted == False)')
+
+
class Zone(BASE, NovaBase):
"""Represents a child zone of this zone."""
__tablename__ = 'zones'
@@ -557,7 +572,8 @@ def register_models():
Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp,
Network, SecurityGroup, SecurityGroupIngressRule,
SecurityGroupInstanceAssociation, AuthToken, User,
- Project, Certificate, ConsolePool, Console, Zone)
+ Project, Certificate, ConsolePool, Console, Zone,
+ InstanceMetadata)
engine = create_engine(FLAGS.sql_connection, echo=False)
for model in models:
model.metadata.create_all(engine)
diff --git a/nova/quota.py b/nova/quota.py
index 3884eb308..6b52a97fa 100644
--- a/nova/quota.py
+++ b/nova/quota.py
@@ -35,6 +35,8 @@ flags.DEFINE_integer('quota_gigabytes', 1000,
'number of volume gigabytes allowed per project')
flags.DEFINE_integer('quota_floating_ips', 10,
'number of floating ips allowed per project')
+flags.DEFINE_integer('quota_metadata_items', 128,
+ 'number of metadata items allowed per instance')
def get_quota(context, project_id):
@@ -42,7 +44,8 @@ def get_quota(context, project_id):
'cores': FLAGS.quota_cores,
'volumes': FLAGS.quota_volumes,
'gigabytes': FLAGS.quota_gigabytes,
- 'floating_ips': FLAGS.quota_floating_ips}
+ 'floating_ips': FLAGS.quota_floating_ips,
+ 'metadata_items': FLAGS.quota_metadata_items}
try:
quota = db.quota_get(context, project_id)
for key in rval.keys():
@@ -94,6 +97,15 @@ def allowed_floating_ips(context, num_floating_ips):
return min(num_floating_ips, allowed_floating_ips)
+def allowed_metadata_items(context, num_metadata_items):
+ """Check quota; return min(num_metadata_items,allowed_metadata_items)"""
+ project_id = context.project_id
+ context = context.elevated()
+ quota = get_quota(context, project_id)
+ num_allowed_metadata_items = quota['metadata_items']
+ return min(num_metadata_items, num_allowed_metadata_items)
+
+
class QuotaError(exception.ApiError):
"""Quota Exceeeded"""
pass
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 589f3d3eb..054996658 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -28,6 +28,7 @@ import nova.api.openstack
from nova.api.openstack import servers
import nova.db.api
from nova.db.sqlalchemy.models import Instance
+from nova.db.sqlalchemy.models import InstanceMetadata
import nova.rpc
from nova.tests.api.openstack import fakes
@@ -64,6 +65,9 @@ def instance_address(context, instance_id):
def stub_instance(id, user_id=1, private_address=None, public_addresses=None):
+ metadata = []
+ metadata.append(InstanceMetadata(key='seq', value=id))
+
if public_addresses == None:
public_addresses = list()
@@ -95,7 +99,8 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None):
"availability_zone": "",
"display_name": "server%s" % id,
"display_description": "",
- "locked": False}
+ "locked": False,
+ "metadata": metadata}
instance["fixed_ip"] = {
"address": private_address,
@@ -214,7 +219,8 @@ class ServersTest(unittest.TestCase):
"get_image_id_from_image_hash", image_id_from_hash)
body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
+ name='server_test', imageId=2, flavorId=2,
+ metadata={'hello': 'world', 'open': 'stack'},
personality={}))
req = webob.Request.blank('/v1.0/servers')
req.method = 'POST'
@@ -291,6 +297,7 @@ class ServersTest(unittest.TestCase):
self.assertEqual(s['id'], i)
self.assertEqual(s['name'], 'server%d' % i)
self.assertEqual(s['imageId'], 10)
+ self.assertEqual(s['metadata']['seq'], i)
i += 1
def test_server_pause(self):
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index 9548a8c13..1e42fddf3 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -16,6 +16,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from nova import compute
from nova import context
from nova import db
from nova import flags
@@ -87,6 +88,18 @@ class QuotaTestCase(test.TestCase):
num_instances = quota.allowed_instances(self.context, 100,
instance_types.INSTANCE_TYPES['m1.small'])
self.assertEqual(num_instances, 10)
+
+ # metadata_items
+ too_many_items = FLAGS.quota_metadata_items + 1000
+ num_metadata_items = quota.allowed_metadata_items(self.context,
+ too_many_items)
+ self.assertEqual(num_metadata_items, FLAGS.quota_metadata_items)
+ db.quota_update(self.context, self.project.id, {'metadata_items': 5})
+ num_metadata_items = quota.allowed_metadata_items(self.context,
+ too_many_items)
+ self.assertEqual(num_metadata_items, 5)
+
+ # Cleanup
db.quota_destroy(self.context, self.project.id)
def test_too_many_instances(self):
@@ -151,3 +164,15 @@ class QuotaTestCase(test.TestCase):
self.assertRaises(quota.QuotaError, self.cloud.allocate_address,
self.context)
db.floating_ip_destroy(context.get_admin_context(), address)
+
+ def test_too_many_metadata_items(self):
+ metadata = {}
+ for i in range(FLAGS.quota_metadata_items + 1):
+ metadata['key%s' % i] = 'value%s' % i
+ self.assertRaises(quota.QuotaError, compute.API().create,
+ self.context,
+ min_count=1,
+ max_count=1,
+ instance_type='m1.small',
+ image_id='fake',
+ metadata=metadata)