Merge lp:~anso/nova/rename into lp:~hudson-openstack/nova/trunk

Proposed by Todd Willey
Status: Merged
Approved by: Vish Ishaya
Approved revision: 287
Merged at revision: 306
Proposed branch: lp:~anso/nova/rename
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 351 lines (+163/-10)
8 files modified
nova/api/ec2/__init__.py (+2/-0)
nova/api/ec2/cloud.py (+35/-0)
nova/api/ec2/images.py (+8/-0)
nova/db/sqlalchemy/models.py (+8/-1)
nova/objectstore/handler.py (+19/-8)
nova/objectstore/image.py (+10/-0)
nova/tests/cloud_unittest.py (+75/-1)
nova/tests/objectstore_unittest.py (+6/-0)
To merge this branch: bzr merge lp:~anso/nova/rename
Reviewer Review Type Date Requested Status
Vish Ishaya (community) Approve
Devin Carlen (community) Approve
Michael Gundlach Pending
Review via email: mp+36382@code.launchpad.net

Commit message

Add user-editable name & notes/description to volumes, instances, and images.

Description of the change

Add user-editable name & notes/description to volumes, instances, and images.

To post a comment you must log in.
Revision history for this message
Jesse Andrews (anotherjesse) wrote :

merge conflict.

plus we should figure out how to add the support to the RS API as well? Maybe ask Gundlach?

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

fixed conflicts, invited ~gundlach to review so we can discuss here

Revision history for this message
Michael Gundlach (gundlach) wrote :

images are readonly in RS API till post-Austin.

i don't see why RS API won't be able to use your update_instance() as it stands.

Revision history for this message
Michael Gundlach (gundlach) wrote :

(and RS API has no concept of volumes, which i think is the third and last thing you're touching in this branch.)

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

lgtm, RS API issues not withstanding

review: Approve
Revision history for this message
Vish Ishaya (vishvananda) wrote :

Fixed one test. Looks good.

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :

Attempt to merge lp:~anso/nova/rename into lp:nova failed due to merge conflicts:

text conflict in nova/tests/cloud_unittest.py

lp:~anso/nova/rename updated
287. By Todd Willey

Merge trunk and fix test.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/ec2/__init__.py'
2--- nova/api/ec2/__init__.py 2010-09-23 21:06:23 +0000
3+++ nova/api/ec2/__init__.py 2010-09-29 03:43:40 +0000
4@@ -158,12 +158,14 @@
5 'RunInstances': ['projectmanager', 'sysadmin'],
6 'TerminateInstances': ['projectmanager', 'sysadmin'],
7 'RebootInstances': ['projectmanager', 'sysadmin'],
8+ 'UpdateInstance': ['projectmanager', 'sysadmin'],
9 'DeleteVolume': ['projectmanager', 'sysadmin'],
10 'DescribeImages': ['all'],
11 'DeregisterImage': ['projectmanager', 'sysadmin'],
12 'RegisterImage': ['projectmanager', 'sysadmin'],
13 'DescribeImageAttribute': ['all'],
14 'ModifyImageAttribute': ['projectmanager', 'sysadmin'],
15+ 'UpdateImage': ['projectmanager', 'sysadmin'],
16 },
17 'AdminController': {
18 # All actions have the same permission: ['none'] (the default)
19
20=== modified file 'nova/api/ec2/cloud.py'
21--- nova/api/ec2/cloud.py 2010-09-28 23:54:57 +0000
22+++ nova/api/ec2/cloud.py 2010-09-29 03:43:40 +0000
23@@ -285,6 +285,9 @@
24 'volume_id': volume['ec2_id']}]
25 else:
26 v['attachmentSet'] = [{}]
27+
28+ v['display_name'] = volume['display_name']
29+ v['display_description'] = volume['display_description']
30 return v
31
32 def create_volume(self, context, size, **kwargs):
33@@ -302,6 +305,8 @@
34 vol['availability_zone'] = FLAGS.storage_availability_zone
35 vol['status'] = "creating"
36 vol['attach_status'] = "detached"
37+ vol['display_name'] = kwargs.get('display_name')
38+ vol['display_description'] = kwargs.get('display_description')
39 volume_ref = db.volume_create(context, vol)
40
41 rpc.cast(FLAGS.scheduler_topic,
42@@ -368,6 +373,16 @@
43 lst = [lst]
44 return [{label: x} for x in lst]
45
46+ def update_volume(self, context, volume_id, **kwargs):
47+ updatable_fields = ['display_name', 'display_description']
48+ changes = {}
49+ for field in updatable_fields:
50+ if field in kwargs:
51+ changes[field] = kwargs[field]
52+ if changes:
53+ db.volume_update(context, volume_id, kwargs)
54+ return True
55+
56 def describe_instances(self, context, **kwargs):
57 return self._format_describe_instances(context)
58
59@@ -420,6 +435,8 @@
60 i['instanceType'] = instance['instance_type']
61 i['launchTime'] = instance['created_at']
62 i['amiLaunchIndex'] = instance['launch_index']
63+ i['displayName'] = instance['display_name']
64+ i['displayDescription'] = instance['display_description']
65 if not reservations.has_key(instance['reservation_id']):
66 r = {}
67 r['reservationId'] = instance['reservation_id']
68@@ -577,6 +594,8 @@
69 base_options['user_data'] = kwargs.get('user_data', '')
70 base_options['security_group'] = security_group
71 base_options['instance_type'] = instance_type
72+ base_options['display_name'] = kwargs.get('display_name')
73+ base_options['display_description'] = kwargs.get('display_description')
74
75 type_data = INSTANCE_TYPES[instance_type]
76 base_options['memory_mb'] = type_data['memory_mb']
77@@ -673,6 +692,18 @@
78 "instance_id": instance_ref['id']}})
79 return True
80
81+ def update_instance(self, context, instance_id, **kwargs):
82+ updatable_fields = ['display_name', 'display_description']
83+ changes = {}
84+ for field in updatable_fields:
85+ if field in kwargs:
86+ changes[field] = kwargs[field]
87+ if changes:
88+ db_context = {}
89+ inst = db.instance_get_by_ec2_id(db_context, instance_id)
90+ db.instance_update(db_context, inst['id'], kwargs)
91+ return True
92+
93 def delete_volume(self, context, volume_id, **kwargs):
94 # TODO: return error if not authorized
95 volume_ref = db.volume_get_by_ec2_id(context, volume_id)
96@@ -728,3 +759,7 @@
97 if not operation_type in ['add', 'remove']:
98 raise exception.ApiError('operation_type must be add or remove')
99 return images.modify(context, image_id, operation_type)
100+
101+ def update_image(self, context, image_id, **kwargs):
102+ result = images.update(context, image_id, dict(kwargs))
103+ return result
104
105=== modified file 'nova/api/ec2/images.py'
106--- nova/api/ec2/images.py 2010-09-16 15:33:42 +0000
107+++ nova/api/ec2/images.py 2010-09-29 03:43:40 +0000
108@@ -43,6 +43,14 @@
109
110 return True
111
112+def update(context, image_id, attributes):
113+ """update an image's attributes / info.json"""
114+ attributes.update({"image_id": image_id})
115+ conn(context).make_request(
116+ method='POST',
117+ bucket='_images',
118+ query_args=qs(attributes))
119+ return True
120
121 def register(context, image_location):
122 """ rpc call to register a new image based from a manifest """
123
124=== modified file 'nova/db/sqlalchemy/models.py'
125--- nova/db/sqlalchemy/models.py 2010-09-29 02:38:15 +0000
126+++ nova/db/sqlalchemy/models.py 2010-09-29 03:43:40 +0000
127@@ -239,7 +239,6 @@
128 vcpus = Column(Integer)
129 local_gb = Column(Integer)
130
131-
132 hostname = Column(String(255))
133 host = Column(String(255)) # , ForeignKey('hosts.id'))
134
135@@ -253,6 +252,10 @@
136 scheduled_at = Column(DateTime)
137 launched_at = Column(DateTime)
138 terminated_at = Column(DateTime)
139+
140+ display_name = Column(String(255))
141+ display_description = Column(String(255))
142+
143 # TODO(vish): see Ewan's email about state improvements, probably
144 # should be in a driver base class or some such
145 # vmstate_state = running, halted, suspended, paused
146@@ -289,6 +292,10 @@
147 launched_at = Column(DateTime)
148 terminated_at = Column(DateTime)
149
150+ display_name = Column(String(255))
151+ display_description = Column(String(255))
152+
153+
154 class Quota(BASE, NovaBase):
155 """Represents quota overrides for a project"""
156 __tablename__ = 'quotas'
157
158=== modified file 'nova/objectstore/handler.py'
159--- nova/objectstore/handler.py 2010-09-07 20:00:01 +0000
160+++ nova/objectstore/handler.py 2010-09-29 03:43:40 +0000
161@@ -352,6 +352,8 @@
162 m[u'imageType'] = m['type']
163 elif 'imageType' in m:
164 m[u'type'] = m['imageType']
165+ if 'displayName' not in m:
166+ m[u'displayName'] = u''
167 return m
168
169 request.write(json.dumps([decorate(i.metadata) for i in images]))
170@@ -382,16 +384,25 @@
171 def render_POST(self, request): # pylint: disable-msg=R0201
172 """Update image attributes: public/private"""
173
174+ # image_id required for all requests
175 image_id = get_argument(request, 'image_id', u'')
176+ image_object = image.Image(image_id)
177+ if not image_object.is_authorized(request.context):
178+ logging.debug("not authorized for render_POST in images")
179+ raise exception.NotAuthorized
180+
181 operation = get_argument(request, 'operation', u'')
182-
183- image_object = image.Image(image_id)
184-
185- if not image_object.is_authorized(request.context):
186- raise exception.NotAuthorized
187-
188- image_object.set_public(operation=='add')
189-
190+ if operation:
191+ # operation implies publicity toggle
192+ logging.debug("handling publicity toggle")
193+ image_object.set_public(operation=='add')
194+ else:
195+ # other attributes imply update
196+ logging.debug("update user fields")
197+ clean_args = {}
198+ for arg in request.args.keys():
199+ clean_args[arg] = request.args[arg][0]
200+ image_object.update_user_editable_fields(clean_args)
201 return ''
202
203 def render_DELETE(self, request): # pylint: disable-msg=R0201
204
205=== modified file 'nova/objectstore/image.py'
206--- nova/objectstore/image.py 2010-08-18 21:14:24 +0000
207+++ nova/objectstore/image.py 2010-09-29 03:43:40 +0000
208@@ -82,6 +82,16 @@
209 with open(os.path.join(self.path, 'info.json'), 'w') as f:
210 json.dump(md, f)
211
212+ def update_user_editable_fields(self, args):
213+ """args is from the request parameters, so requires extra cleaning"""
214+ fields = {'display_name': 'displayName', 'description': 'description'}
215+ info = self.metadata
216+ for field in fields.keys():
217+ if field in args:
218+ info[fields[field]] = args[field]
219+ with open(os.path.join(self.path, 'info.json'), 'w') as f:
220+ json.dump(info, f)
221+
222 @staticmethod
223 def all():
224 images = []
225
226=== modified file 'nova/tests/cloud_unittest.py'
227--- nova/tests/cloud_unittest.py 2010-09-25 02:25:12 +0000
228+++ nova/tests/cloud_unittest.py 2010-09-29 03:43:40 +0000
229@@ -16,10 +16,13 @@
230 # License for the specific language governing permissions and limitations
231 # under the License.
232
233+import json
234 import logging
235 from M2Crypto import BIO
236 from M2Crypto import RSA
237+import os
238 import StringIO
239+import tempfile
240 import time
241
242 from twisted.internet import defer
243@@ -36,15 +39,22 @@
244 from nova.compute import power_state
245 from nova.api.ec2 import context
246 from nova.api.ec2 import cloud
247+from nova.objectstore import image
248
249
250 FLAGS = flags.FLAGS
251
252
253+# Temp dirs for working with image attributes through the cloud controller
254+# (stole this from objectstore_unittest.py)
255+OSS_TEMPDIR = tempfile.mkdtemp(prefix='test_oss-')
256+IMAGES_PATH = os.path.join(OSS_TEMPDIR, 'images')
257+os.makedirs(IMAGES_PATH)
258+
259 class CloudTestCase(test.TrialTestCase):
260 def setUp(self):
261 super(CloudTestCase, self).setUp()
262- self.flags(connection_type='fake')
263+ self.flags(connection_type='fake', images_path=IMAGES_PATH)
264
265 self.conn = rpc.Connection.instance()
266 logging.getLogger().setLevel(logging.DEBUG)
267@@ -191,3 +201,67 @@
268 #for i in xrange(4):
269 # data = self.cloud.get_metadata(instance(i)['private_dns_name'])
270 # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i)
271+
272+ @staticmethod
273+ def _fake_set_image_description(ctxt, image_id, description):
274+ from nova.objectstore import handler
275+ class req:
276+ pass
277+ request = req()
278+ request.context = ctxt
279+ request.args = {'image_id': [image_id],
280+ 'description': [description]}
281+
282+ resource = handler.ImagesResource()
283+ resource.render_POST(request)
284+
285+ def test_user_editable_image_endpoint(self):
286+ pathdir = os.path.join(FLAGS.images_path, 'ami-testing')
287+ os.mkdir(pathdir)
288+ info = {'isPublic': False}
289+ with open(os.path.join(pathdir, 'info.json'), 'w') as f:
290+ json.dump(info, f)
291+ img = image.Image('ami-testing')
292+ # self.cloud.set_image_description(self.context, 'ami-testing',
293+ # 'Foo Img')
294+ # NOTE(vish): Above won't work unless we start objectstore or create
295+ # a fake version of api/ec2/images.py conn that can
296+ # call methods directly instead of going through boto.
297+ # for now, just cheat and call the method directly
298+ self._fake_set_image_description(self.context, 'ami-testing',
299+ 'Foo Img')
300+ self.assertEqual('Foo Img', img.metadata['description'])
301+ self._fake_set_image_description(self.context, 'ami-testing', '')
302+ self.assertEqual('', img.metadata['description'])
303+
304+ def test_update_of_instance_display_fields(self):
305+ inst = db.instance_create({}, {})
306+ self.cloud.update_instance(self.context, inst['ec2_id'],
307+ display_name='c00l 1m4g3')
308+ inst = db.instance_get({}, inst['id'])
309+ self.assertEqual('c00l 1m4g3', inst['display_name'])
310+ db.instance_destroy({}, inst['id'])
311+
312+ def test_update_of_instance_wont_update_private_fields(self):
313+ inst = db.instance_create({}, {})
314+ self.cloud.update_instance(self.context, inst['id'],
315+ mac_address='DE:AD:BE:EF')
316+ inst = db.instance_get({}, inst['id'])
317+ self.assertEqual(None, inst['mac_address'])
318+ db.instance_destroy({}, inst['id'])
319+
320+ def test_update_of_volume_display_fields(self):
321+ vol = db.volume_create({}, {})
322+ self.cloud.update_volume(self.context, vol['id'],
323+ display_name='c00l v0lum3')
324+ vol = db.volume_get({}, vol['id'])
325+ self.assertEqual('c00l v0lum3', vol['display_name'])
326+ db.volume_destroy({}, vol['id'])
327+
328+ def test_update_of_volume_wont_update_private_fields(self):
329+ vol = db.volume_create({}, {})
330+ self.cloud.update_volume(self.context, vol['id'],
331+ mountpoint='/not/here')
332+ vol = db.volume_get({}, vol['id'])
333+ self.assertEqual(None, vol['mountpoint'])
334+ db.volume_destroy({}, vol['id'])
335
336=== modified file 'nova/tests/objectstore_unittest.py'
337--- nova/tests/objectstore_unittest.py 2010-09-08 17:57:29 +0000
338+++ nova/tests/objectstore_unittest.py 2010-09-29 03:43:40 +0000
339@@ -164,6 +164,12 @@
340 self.context.project = self.auth_manager.get_project('proj2')
341 self.assertFalse(my_img.is_authorized(self.context))
342
343+ # change user-editable fields
344+ my_img.update_user_editable_fields({'display_name': 'my cool image'})
345+ self.assertEqual('my cool image', my_img.metadata['displayName'])
346+ my_img.update_user_editable_fields({'display_name': ''})
347+ self.assert_(not my_img.metadata['displayName'])
348+
349
350 class TestHTTPChannel(http.HTTPChannel):
351 """Dummy site required for twisted.web"""