Merge lp:~justin-fathomdb/nova/justinsb-metadata into lp:~hudson-openstack/nova/trunk

Proposed by justinsb
Status: Superseded
Proposed branch: lp:~justin-fathomdb/nova/justinsb-metadata
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 365 lines (+173/-13)
8 files modified
nova/api/openstack/servers.py (+24/-6)
nova/compute/api.py (+27/-2)
nova/db/sqlalchemy/api.py (+2/-0)
nova/db/sqlalchemy/migrate_repo/versions/003_add_instance_metadata.py (+78/-0)
nova/db/sqlalchemy/models.py (+18/-1)
nova/quota.py (+13/-1)
nova/tests/api/openstack/test_servers.py (+8/-2)
run_tests.sh (+3/-1)
To merge this branch: bzr merge lp:~justin-fathomdb/nova/justinsb-metadata
Reviewer Review Type Date Requested Status
Todd Willey (community) Approve
Dan Prince (community) Needs Fixing
Review via email: mp+49490@code.launchpad.net

This proposal has been superseded by a proposal from 2011-02-15.

Commit message

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.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Dan Prince (dan-prince) wrote :

Justin,

Does this branch work with Glance? I'm pretty sure Glance stores kernel ID as image['properties']['kernel_id']. Additionally ramdisk ID is stored as image['properties']['ramdisk_id'].

Perhaps you were developing/testing against S3ImageStore?

Dan

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

To answer Dan's question, no, this patch won't work if FLAGS.image_service == 'glance'. But, that's not necessarily Justin's fault of course, and the OpenStack servers.py controller shouldn't be relying on the Glance image service being set, either...

Devin and Dan have been working on fixing the inconsistencies in the keys returned from the S3ImageService and the GlanceImageService when it comes to the kernel_id (kernelId) and ramdisk_id (ramdiskId) keys. Please see this bug and patch here for a few more details: https://bugs.launchpad.net/nova/+bug/712782

As for the specifics of this patch, Justin, I think it would be clearer if you renamed the Metadata model to InstanceProperty and the table name from metadata to instance_properties, to make it clear what the metadata properties refer to. Since there is already the concept of Metadata in SQLAlchemy, it would make reading the code a tad easier.

Cheers!
jay

Revision history for this message
justinsb (justin-fathomdb) wrote :

I think I'll probably revert the lines that touched the kernel_id & ramdisk_id - I had to put them in there to get this to work, but it probably doesn't really belong in this patch.

I agree that 'metadata', 'key' and 'value' are potentially problematic names. Two issues though: First, there will be metadata for more than just instances - I can either have one table 'item_properties', or I can go with InstanceProperties, VolumeProperties etc and have multiple tables. And second, "properties" may be a bit overloaded already (e.g. the images already has 'properties' in Dan's code snippet!) Although I'd like the special flags (kernel_id, ramdisk_id, zone, image_type, volume_size etc) to just be treated as well-known metadata, I don't see this happening any time soon, so I don't think we can use 'properties'. Maybe 'Tags'? What do you think Jay?

Revision history for this message
Jay Pipes (jaypipes) wrote :

> I think I'll probably revert the lines that touched the kernel_id & ramdisk_id
> - I had to put them in there to get this to work, but it probably doesn't
> really belong in this patch.

k, agreed.

> I agree that 'metadata', 'key' and 'value' are potentially problematic names.
> Two issues though: First, there will be metadata for more than just instances
> - I can either have one table 'item_properties', or I can go with
> InstanceProperties, VolumeProperties etc and have multiple tables. And
> second, "properties" may be a bit overloaded already (e.g. the images already
> has 'properties' in Dan's code snippet!)

Heh, yes it does, because Glance's registry database has a table image_properties, with a model, yep, you guessed it, ImageProperty :)

So, I think that InstanceProperties is perfectly fine in this case :)

> Although I'd like the special flags
> (kernel_id, ramdisk_id, zone, image_type, volume_size etc) to just be treated
> as well-known metadata, I don't see this happening any time soon, so I don't
> think we can use 'properties'. Maybe 'Tags'? What do you think Jay?

This should be a mailing list question, I believe. If it's an attribute that *every* instance has, or possibly that *most* instances have, I think it should be in the Instance model directly, but this is just my opinion. Best to ask the ML I think...

-jay

Revision history for this message
Dan Prince (dan-prince) wrote :

This branch currently doesn't appear to restrict the number of metadata items per instances(server) to 5.

According to the Cloud Servers API docs:

"The maximum size of the metadata key and value is each 255 bytes and the maximum number of key-value pairs that can be supplied per server is 5."

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

<diplomacy-hat>
OpenStack != Cloud Servers, of course, but I would slightly modify what Dan said to:

Could we add a FLAG option that would allow the deployer to set a limit to the number of metadata items per object? ;)
</diplomacy-hat>

Revision history for this message
Devin Carlen (devcamcar) wrote :

Agree, make the number of metadata items configurable

667. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

Revision history for this message
Dan Prince (dan-prince) wrote :

Absolutely. Making this a configuration flag would be great.

I might suggest we not make the FLAG option default to being 'unlimited'. The ability to specify an unlimited number of items seems like kind of a security concern.

10 items maybe?

668. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

669. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

670. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

671. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

672. By SuperStack <email address hidden>

Fixed up note

673. By SuperStack <email address hidden>

Comment to confirm that dictionary use is intentional

Revision history for this message
Todd Willey (xtoddx) wrote :

lgtm

review: Approve

Unmerged revisions

673. By SuperStack <email address hidden>

Comment to confirm that dictionary use is intentional

672. By SuperStack <email address hidden>

Fixed up note

671. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

670. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

669. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

668. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

667. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

666. By SuperStack <email address hidden>

Merge branch 'metadata' of https://github.com/justinsb/nova into justinsb-metadata

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/openstack/servers.py'
2--- nova/api/openstack/servers.py 2011-01-26 16:16:16 +0000
3+++ nova/api/openstack/servers.py 2011-02-15 21:42:14 +0000
4@@ -32,6 +32,7 @@
5 from nova.compute import instance_types
6 from nova.compute import power_state
7 import nova.api.openstack
8+from nova.db.sqlalchemy.models import InstanceProperty
9
10
11 LOG = logging.getLogger('server')
12@@ -64,9 +65,13 @@
13
14 inst_dict['status'] = power_mapping[inst_dict['status']]
15 inst_dict['addresses'] = dict(public=[], private=[])
16- inst_dict['metadata'] = {}
17 inst_dict['hostId'] = ''
18
19+ metadata = {}
20+ for item in inst['properties']:
21+ metadata[item['key']] = item['value']
22+ inst_dict['metadata'] = metadata
23+
24 return dict(server=inst_dict)
25
26
27@@ -148,14 +153,26 @@
28 if not env:
29 return faults.Fault(exc.HTTPUnprocessableEntity())
30
31- key_pair = auth_manager.AuthManager.get_key_pairs(
32- req.environ['nova.context'])[0]
33+ context = req.environ['nova.context']
34+
35+ key_pair = auth_manager.AuthManager.get_key_pairs(context)[0]
36 image_id = common.get_image_id_from_image_hash(self._image_service,
37- req.environ['nova.context'], env['server']['imageId'])
38+ context, env['server']['imageId'])
39 kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
40 req, image_id)
41+
42+ # Metadata is a list, not a Dictionary, because we allow duplicate keys
43+ # (even though JSON can't encode this)
44+ # In future, we may not allow duplicate keys.
45+ # However, the CloudServers API is not definitive on this front,
46+ # and we want to be compatible.
47+ properties = []
48+ if env['server']['metadata']:
49+ for k, v in env['server']['metadata'].items():
50+ properties.append(InstanceProperty(key=k, value=v))
51+
52 instances = self.compute_api.create(
53- req.environ['nova.context'],
54+ context,
55 instance_types.get_by_flavor_id(env['server']['flavorId']),
56 image_id,
57 kernel_id=kernel_id,
58@@ -163,7 +180,8 @@
59 display_name=env['server']['name'],
60 display_description=env['server']['name'],
61 key_name=key_pair['name'],
62- key_data=key_pair['public_key'])
63+ key_data=key_pair['public_key'],
64+ properties=properties)
65 return _translate_keys(instances[0])
66
67 def update(self, req, id):
68
69=== modified file 'nova/compute/api.py'
70--- nova/compute/api.py 2011-02-15 18:19:52 +0000
71+++ nova/compute/api.py 2011-02-15 21:42:14 +0000
72@@ -85,7 +85,7 @@
73 min_count=1, max_count=1,
74 display_name='', display_description='',
75 key_name=None, key_data=None, security_group='default',
76- availability_zone=None, user_data=None):
77+ availability_zone=None, user_data=None, properties=[]):
78 """Create the number of instances requested if quota and
79 other arguments check out ok."""
80
81@@ -99,6 +99,30 @@
82 "run %s more instances of this type.") %
83 num_instances, "InstanceLimitExceeded")
84
85+ num_metadata = len(properties)
86+ quota_metadata = quota.allowed_metadata_items(context, num_metadata)
87+ if quota_metadata < num_metadata:
88+ pid = context.project_id
89+ msg = (_("Quota exceeeded for %(pid)s,"
90+ " tried to set %(num_metadata)s metadata properties")
91+ % locals())
92+ LOG.warn(msg)
93+ raise quota.QuotaError(msg, "MetadataLimitExceeded")
94+
95+ # Because metadata is stored in the DB, we hard-code the size limits
96+ # In future, we may support more variable length strings, so we act
97+ # as if this is quota-controlled for forwards compatibility
98+ for metadata_item in properties:
99+ k = metadata_item.key
100+ v = metadata_item.value
101+ if len(k) > 255 or len(v) > 255:
102+ pid = context.project_id
103+ msg = (_("Quota exceeeded for %(pid)s,"
104+ " metadata property key or value too long")
105+ % locals())
106+ LOG.warn(msg)
107+ raise quota.QuotaError(msg, "MetadataLimitExceeded")
108+
109 is_vpn = image_id == FLAGS.vpn_image_id
110 if not is_vpn:
111 image = self.image_service.show(context, image_id)
112@@ -155,7 +179,8 @@
113 'key_name': key_name,
114 'key_data': key_data,
115 'locked': False,
116- 'availability_zone': availability_zone}
117+ 'availability_zone': availability_zone,
118+ 'properties': properties}
119
120 elevated = context.elevated()
121 instances = []
122
123=== modified file 'nova/db/sqlalchemy/api.py'
124--- nova/db/sqlalchemy/api.py 2011-02-11 19:30:56 +0000
125+++ nova/db/sqlalchemy/api.py 2011-02-15 21:42:14 +0000
126@@ -693,6 +693,7 @@
127 options(joinedload_all('security_groups.rules')).\
128 options(joinedload('volumes')).\
129 options(joinedload_all('fixed_ip.network')).\
130+ options(joinedload('properties')).\
131 filter_by(id=instance_id).\
132 filter_by(deleted=can_read_deleted(context)).\
133 first()
134@@ -701,6 +702,7 @@
135 options(joinedload_all('fixed_ip.floating_ips')).\
136 options(joinedload_all('security_groups.rules')).\
137 options(joinedload('volumes')).\
138+ options(joinedload('properties')).\
139 filter_by(project_id=context.project_id).\
140 filter_by(id=instance_id).\
141 filter_by(deleted=False).\
142
143=== added file 'nova/db/sqlalchemy/migrate_repo/versions/003_add_instance_metadata.py'
144--- nova/db/sqlalchemy/migrate_repo/versions/003_add_instance_metadata.py 1970-01-01 00:00:00 +0000
145+++ nova/db/sqlalchemy/migrate_repo/versions/003_add_instance_metadata.py 2011-02-15 21:42:14 +0000
146@@ -0,0 +1,78 @@
147+# vim: tabstop=4 shiftwidth=4 softtabstop=4
148+
149+# Copyright 2011 Justin Santa Barbara
150+# All Rights Reserved.
151+#
152+# Licensed under the Apache License, Version 2.0 (the "License"); you may
153+# not use this file except in compliance with the License. You may obtain
154+# a copy of the License at
155+#
156+# http://www.apache.org/licenses/LICENSE-2.0
157+#
158+# Unless required by applicable law or agreed to in writing, software
159+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
160+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
161+# License for the specific language governing permissions and limitations
162+# under the License.
163+
164+from sqlalchemy import *
165+from migrate import *
166+
167+from nova import log as logging
168+
169+
170+meta = MetaData()
171+
172+
173+# Just for the ForeignKey and column creation to succeed, these are not the
174+# actual definitions of instances or services.
175+instances = Table('instances', meta,
176+ Column('id', Integer(), primary_key=True, nullable=False),
177+ )
178+
179+quotas = Table('quotas', meta,
180+ Column('id', Integer(), primary_key=True, nullable=False),
181+ )
182+
183+
184+#
185+# New Tables
186+#
187+
188+instance_properties_table = Table('instance_properties', meta,
189+ Column('created_at', DateTime(timezone=False)),
190+ Column('updated_at', DateTime(timezone=False)),
191+ Column('deleted_at', DateTime(timezone=False)),
192+ Column('deleted', Boolean(create_constraint=True, name=None)),
193+ Column('id', Integer(), primary_key=True, nullable=False),
194+ Column('instance_id',
195+ Integer(),
196+ ForeignKey('instances.id'),
197+ nullable=False),
198+ Column('key',
199+ String(length=255, convert_unicode=False, assert_unicode=None,
200+ unicode_error=None, _warn_on_bytestring=False)),
201+ Column('value',
202+ String(length=255, convert_unicode=False, assert_unicode=None,
203+ unicode_error=None, _warn_on_bytestring=False)))
204+
205+
206+#
207+# New columns
208+#
209+quota_metadata_items = Column('metadata_items', Integer())
210+
211+
212+def upgrade(migrate_engine):
213+ # Upgrade operations go here. Don't create your own engine;
214+ # bind migrate_engine to your metadata
215+ meta.bind = migrate_engine
216+ for table in (instance_properties_table, ):
217+ try:
218+ table.create()
219+ except Exception:
220+ logging.info(repr(table))
221+ logging.exception('Exception while creating table')
222+ raise
223+
224+ quotas.create_column(quota_metadata_items)
225
226=== modified file 'nova/db/sqlalchemy/models.py'
227--- nova/db/sqlalchemy/models.py 2011-02-01 16:09:34 +0000
228+++ nova/db/sqlalchemy/models.py 2011-02-15 21:42:14 +0000
229@@ -256,6 +256,7 @@
230 volumes = Column(Integer)
231 gigabytes = Column(Integer)
232 floating_ips = Column(Integer)
233+ metadata_items = Column(Integer)
234
235
236 class ExportDevice(BASE, NovaBase):
237@@ -535,6 +536,20 @@
238 pool = relationship(ConsolePool, backref=backref('consoles'))
239
240
241+class InstanceProperty(BASE, NovaBase):
242+ """Represents a metadata key/value pair for an instance"""
243+ __tablename__ = 'instance_properties'
244+ id = Column(Integer, primary_key=True)
245+ key = Column(String(255))
246+ value = Column(String(255))
247+ instance_id = Column(Integer, ForeignKey('instances.id'), nullable=False)
248+ instance = relationship(Instance, backref="properties",
249+ foreign_keys=instance_id,
250+ primaryjoin='and_('
251+ 'InstanceProperty.instance_id == Instance.id,'
252+ 'InstanceProperty.deleted == False)')
253+
254+
255 def register_models():
256 """Register Models and create metadata.
257
258@@ -547,7 +562,9 @@
259 Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp,
260 Network, SecurityGroup, SecurityGroupIngressRule,
261 SecurityGroupInstanceAssociation, AuthToken, User,
262- Project, Certificate, ConsolePool, Console) # , Image, Host
263+ Project, Certificate, ConsolePool, Console,
264+ InstanceProperty)
265+ # , Image, Host
266 engine = create_engine(FLAGS.sql_connection, echo=False)
267 for model in models:
268 model.metadata.create_all(engine)
269
270=== modified file 'nova/quota.py'
271--- nova/quota.py 2011-01-04 21:56:36 +0000
272+++ nova/quota.py 2011-02-15 21:42:14 +0000
273@@ -35,6 +35,8 @@
274 'number of volume gigabytes allowed per project')
275 flags.DEFINE_integer('quota_floating_ips', 10,
276 'number of floating ips allowed per project')
277+flags.DEFINE_integer('quota_metadata_items', 128,
278+ 'number of metadata items allowed per instance')
279
280
281 def get_quota(context, project_id):
282@@ -42,7 +44,8 @@
283 'cores': FLAGS.quota_cores,
284 'volumes': FLAGS.quota_volumes,
285 'gigabytes': FLAGS.quota_gigabytes,
286- 'floating_ips': FLAGS.quota_floating_ips}
287+ 'floating_ips': FLAGS.quota_floating_ips,
288+ 'metadata_items': FLAGS.quota_metadata_items}
289 try:
290 quota = db.quota_get(context, project_id)
291 for key in rval.keys():
292@@ -94,6 +97,15 @@
293 return min(num_floating_ips, allowed_floating_ips)
294
295
296+def allowed_metadata_items(context, num_metadata_items):
297+ """Check quota; return min(num_metadata_items,allowed_metadata_items)"""
298+ project_id = context.project_id
299+ context = context.elevated()
300+ quota = get_quota(context, project_id)
301+ num_allowed_metadata_items = quota['metadata_items']
302+ return min(num_metadata_items, num_allowed_metadata_items)
303+
304+
305 class QuotaError(exception.ApiError):
306 """Quota Exceeeded"""
307 pass
308
309=== modified file 'nova/tests/api/openstack/test_servers.py'
310--- nova/tests/api/openstack/test_servers.py 2011-01-27 18:48:00 +0000
311+++ nova/tests/api/openstack/test_servers.py 2011-02-15 21:42:14 +0000
312@@ -27,6 +27,7 @@
313 from nova.api.openstack import servers
314 import nova.db.api
315 from nova.db.sqlalchemy.models import Instance
316+from nova.db.sqlalchemy.models import InstanceProperty
317 import nova.rpc
318 from nova.tests.api.openstack import fakes
319
320@@ -56,8 +57,11 @@
321
322
323 def stub_instance(id, user_id=1):
324+ metadata = []
325+ metadata.append(InstanceProperty(key='seq', value=id))
326 return Instance(id=id, state=0, image_id=10, user_id=user_id,
327- display_name='server%s' % id)
328+ display_name='server%s' % id,
329+ metadata=metadata)
330
331
332 def fake_compute_api(cls, req, id):
333@@ -154,7 +158,8 @@
334 "get_image_id_from_image_hash", image_id_from_hash)
335
336 body = dict(server=dict(
337- name='server_test', imageId=2, flavorId=2, metadata={},
338+ name='server_test', imageId=2, flavorId=2,
339+ metadata={'hello': 'world', 'open': 'stack'},
340 personality={}))
341 req = webob.Request.blank('/v1.0/servers')
342 req.method = 'POST'
343@@ -231,6 +236,7 @@
344 self.assertEqual(s['id'], i)
345 self.assertEqual(s['name'], 'server%d' % i)
346 self.assertEqual(s['imageId'], 10)
347+ self.assertEqual(s['metadata']['seq'], i)
348 i += 1
349
350 def test_server_pause(self):
351
352=== modified file 'run_tests.sh'
353--- run_tests.sh 2011-01-31 05:55:32 +0000
354+++ run_tests.sh 2011-02-15 21:42:14 +0000
355@@ -73,7 +73,9 @@
356
357 if [ -z "$noseargs" ];
358 then
359- run_tests && pep8 --repeat --show-pep8 --show-source --exclude=vcsversion.py bin/* nova setup.py || exit 1
360+ srcfiles=`find bin -type f ! -name "nova.conf*"`
361+ srcfiles+=" nova setup.py"
362+ run_tests && pep8 --repeat --show-pep8 --show-source --exclude=vcsversion.py ${srcfiles} || exit 1
363 else
364 run_tests
365 fi