Merge lp:~0x44/nova/config-drive into lp:~hudson-openstack/nova/trunk

Proposed by Christopher MacGown
Status: Merged
Approved by: Vish Ishaya
Approved revision: 1478
Merged at revision: 1477
Proposed branch: lp:~0x44/nova/config-drive
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 868 lines (+319/-41)
14 files modified
Authors (+1/-0)
nova/api/openstack/create_instance_helper.py (+5/-1)
nova/api/openstack/views/servers.py (+5/-0)
nova/compute/api.py (+15/-5)
nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py (+38/-0)
nova/db/sqlalchemy/models.py (+2/-0)
nova/tests/api/openstack/test_servers.py (+137/-3)
nova/tests/test_compute.py (+15/-0)
nova/virt/disk.py (+25/-7)
nova/virt/libvirt.xml.template (+7/-0)
nova/virt/libvirt/connection.py (+56/-16)
nova/virt/xenapi/vm_utils.py (+9/-6)
nova/virt/xenapi/vmops.py (+3/-2)
run_tests.sh (+1/-1)
To merge this branch: bzr merge lp:~0x44/nova/config-drive
Reviewer Review Type Date Requested Status
Vish Ishaya (community) Approve
Erica Windisch (community) Needs Fixing
Dan Prince (community) Needs Fixing
Matt Dietz (community) Approve
Review via email: mp+71429@code.launchpad.net

Description of the change

Implements first-pass of config-drive that adds a vfat format drive to a vm when config_drive is True (or an image id).

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

Hi Chris,

Is there a reason that this feature is exposed via the OSAPI? Why not just enable or disable it via a flag via the config file? If we do need to expose it via the API then perhaps an extension would be better?

If guests are using this to pull in configuration metadata then it seems like you would just always want it on.

---

Also, You have a conflict in nova/api/openstack/create_instance_helper.py.

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

Your using FLAGS.default_local_format but it doesn't appear to be defined anywhere.

review: Needs Fixing
Revision history for this message
Matt Dietz (cerberus) wrote :

184 + instance_config_drive = None # pre-existing instances don't have one.
185 + migrate_engine.execute(instances.update()\
186 + .where(instances.c.id == row[0])\
187 + .values(config_drive=instance_config_drive))

It's likely I don't know something about sqlalchemy, but couldn't you just set the new column default value to be NULL and accomplish the same thing?

review: Needs Fixing
Revision history for this message
Christopher MacGown (0x44) wrote :

Dan,

This is exposed in the OSAPI to support one of the use cases requested in the blueprint discussion on the etherpad. Having the config-drive always be a VFAT formatted drive didn't work for the people at Canonical, so they requested being able to pass in a config-drive image reference on instance create that would populate a local volume with the contents of that image in lieu of the general VFAT formatted drive.

I'm ambivalent on the idea of exposing it with a FLAG, but if you really prefer it to be, it's a trivial change that wouldn't take long to do.

Revision history for this message
Matt Dietz (cerberus) wrote :

Thanks for the changes, Chris.

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

I guess my thought is that 'config_drive' isn't part of the core OSAPI. At the very least it should be documented as an extension in the code base. It seems like some service providers might like to make use of config drives "under the covers" but not allow end users to provide custom images for them.

---

Also, Can you bump the DB migration number so it doesn't conflict with trunk?

review: Needs Fixing
lp:~0x44/nova/config-drive updated
1466. By Ed Leafe

Removes the incorrect hard-coded filter path.

1467. By Vish Ishaya

Next round of prep for keystone integration.

 * adds middleware for authenticating ec2 signature with keystone
 * adds middleware for converting keystone response into request context
 * gives examples of alternative pipelines for keystone integration

Next steps:
 * provide default config with no keystone integration (perhaps setting every context to admin?)
 * write authmanager to keystone conversion code
 * add api extension to create and destroy access/secret keys
 * deprecate authmanager
 * rename project to tenant

1468. By Anthony Young

Assorted fixes to os-floating-ips to make it play nicely with an in-progress novaclient implementation, as well as some changes to make it more consistent with other os rest apis. Changes include:

* switch associate/disassociate to PUTs. Previously, it was doing create calls with one-off parameter resources.
* allow graceful handling when there are no floating ips for a tenant
* allow graceful handling when disassociating an already disassociated address

Revision history for this message
Erica Windisch (ewindisch) wrote :

This config_drive feature looks just like the OVF environment specification.

In this specification, you provide a transport over which an environment file is provided. The specification defines an 'iso' transport which is a virtual CDROM drive with an ISO9660 filesystem, but we could make this a FAT-family filesystem instead.

In the filesystem, for OVF compatibility, we would create an XML file containing details about the execution environment and could pass metadata.

Whatever is implemented here must be considered for interoperability and forward-compatibility. OVF may not be perfect and XML might not be the most popular container, but it has passed through a standardization body (DMTF) and already exists.

Reference: http://www.dmtf.org/sites/default/files/standards/documents/DSP0243_1.1.0.pdf

review: Needs Fixing
Revision history for this message
Christopher MacGown (0x44) wrote :

Eric: The original plan was that it would be an ISO9660 filesystem but the people who documented/requested the blueprint requested a read-write filesystem that you cannot obtain with ISO9660. You can follow the entire discussion in the blueprint linked in the description.

Dan: I'll move the osapi stuff into an extension and look at adding another flag to turn off the publicly exposed feature.

Revision history for this message
Erica Windisch (ewindisch) wrote :

I'm not opposed to FAT, OVF can work with FAT just as well as ISO9660.
Although, I must admit to prefer the simplicity of mkisofs to mkfs.vfat as
with FAT there is a need to mount & manipulate the filesystem. Also, there
is the option of UDF as well and the fact that FAT lacks access controls
that are available with, say, RockRidge.

Despite the filesystem, which I consider a lesser concern, there is the
matter of the format of the file itself. The proposed patch implements JSON
encapulation of metadata. This isn't necessarily bad, but I maintain it is
important to consider if implementing the OVF environment XML format might
not be the better choice here. I'm open to criticisms regarding this format,
if any, and suggestions of alternatives. On the surface, it seems to be the
right choice and a fairly minor change.

On Aug 20, 2011 10:34 AM, "Christopher MacGown" <email address hidden> wrote:
Eric: The original plan was that it would be an ISO9660 filesystem but the
people who documented/requested the blueprint requested a read-write
filesystem that you cannot obtain with ISO9660. You can follow the entire
discussion in the blueprint linked in the description.

Dan: I'll move the osapi stuff into an extension and look at adding another
flag to turn off the publicly exposed feature.

--
https://code.launchpad.net/~0x44/nova/config-drive/+merge/71429
You are reviewing the proposed m...

lp:~0x44/nova/config-drive updated
1469. By Tushar Patil

Added OS APIs to associate/disassociate security groups to/from instances.

I will add views to return list of security groups associated with the servers later after this branch is merged into trunk. The reason I will do this later is because my previous merge proposal (https://code.launchpad.net/~tpatil/nova/add-options-network-create-os-apis/+merge/68292) is dependent on this work. In this merge proposal I have added a new extension which still uses default OS v1.1 controllers and views, but I am going to override views in this extension to send extra information like security groups.

1470. By Sandy Walsh

Fixes utils.to_primitive (again) to handle modules, builtins and whatever other crap might be hiding in an object.

1471. By Alex Meade

Adds accessIPv4 and accessIPv6 to servers requests and responses as per the current spec.

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

I'm ok with adding this using the json format and putting in a bug for supporting the xml ovf format as well.

review: Approve
Revision history for this message
Erica Windisch (ewindisch) wrote :

Vish, while I'd honestly prefer one or the other, not both, it doesn't matter significantly. Overall, I'd prefer to establish a blueprint for something more flexible and revamping the entire injection mechanism.

Performing further code review, I think we also need the following:

=== modified file 'nova/virt/xenapi/vm_utils.py'
--- nova/virt/xenapi/vm_utils.py 2011-08-19 15:50:48 +0000
+++ nova/virt/xenapi/vm_utils.py 2011-08-22 19:43:08 +0000
@@ -742,7 +742,7 @@
         # everything
         mount_required = False
         key, net, metadata = _prepare_injectables(instance, network_info)
- mount_required = key or net
+ mount_required = key or net or metadata
         if not mount_required:
             return

Revision history for this message
Erica Windisch (ewindisch) wrote :

The metadata file should be written to, not appended to. Append makes more sense for SSH keys than it does for JSON.

=== modified file 'nova/virt/disk.py'
--- nova/virt/disk.py 2011-08-12 21:23:10 +0000
+++ nova/virt/disk.py 2011-08-22 19:56:34 +0000
@@ -228,7 +228,7 @@
     metadata_path = os.path.join(fs, "meta.js")
     metadata = dict([(m.key, m.value) for m in metadata])

- utils.execute('sudo', 'tee', '-a', metadata_path,
+ utils.execute('sudo', 'tee', metadata_path,
                   process_input=json.dumps(metadata))

lp:~0x44/nova/config-drive updated
1472. By William Wolf

implemented tenant ids to be included in request uris.

Revision history for this message
Erica Windisch (ewindisch) wrote :

Metadata wasn't written on XenServer unless using flat_injected networking.

=== modified file 'nova/virt/xenapi/vmops.py'
--- nova/virt/xenapi/vmops.py 2011-08-17 03:15:54 +0000
+++ nova/virt/xenapi/vmops.py 2011-08-22 20:13:04 +0000
@@ -240,7 +240,8 @@
             vdis)

         # Alter the image before VM start for, e.g. network injection
- if FLAGS.flat_injected:
+ # also alter if there is metadata
+ if FLAGS.flat_injected or instance['metadata']:
             VMHelper.preconfigure_instance(self._session, instance,
                                            first_vdi_ref, network_info)

Revision history for this message
Christopher MacGown (0x44) wrote :

Eric: There's already a blueprint for a way of facilitating two-way communication between the host and guest that would mostly end up superseding config-drive except as a place to write personality.

Revision history for this message
Christopher MacGown (0x44) wrote :

I'll add your fixes in my next push.

lp:~0x44/nova/config-drive updated
1473. By Alex Meade

The FixedIpCommandsTestCase in test_nova_manage previously accessed the database. This branch stubs out the database for these tests, lowering their run time from 104 secs -> .02 secs total.

I have verified the tested functionality is still being tested.

1474. By Soren Hansen

Move documentation from nova.virt.fake into nova.virt.driver.

1475. By Alex Meade

Fixes bug 831627 where nova-manage does not exit when given a non-existent network address

1476. By Tushar Patil

Our goal is to add optional parameter to the Create server OS 1.0 and 1.1 API to achieve following objectives:-

1) Specify number and order of networks to the create server API.

In the current implementation every instance you launch for a project having 3 networks assigned to it will always have 3 vnics. In this case it is not possible to have one instance with 2 vnics ,another with 1 vnic and so on. This is not flexible enough and the network resources are not used effectively.

2) Specify fixed IP address to the vnic at the boot time. When you launch a server, you can specify the fixed IP address you want to be assigned to the vnic from a particular network. If this fixed IP address is already in use, it will give exception.

Example of Server Create API request XML:
<?xml version="1.0" encoding="UTF-8"?>

<server xmlns="http://docs.nttpflab.com/servers/api/v1.0"
        name="new-server-test" imageId="1" flavorId="1">
  <metadata>
    <meta key="My Server Name">Apache1</meta>
  </metadata>
  <personality>
    <file path="/etc/banner.txt">
        ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
    </file>
  </personality>
  <networks>
      <network uuid="6622436e-5289-460f-8479-e4dcc63f16c5" fixed_ip="10.0.0.3">
      <network uuid="d97efefc-e071-4174-b6dd-b33af0a37706" fixed_ip="10.0.1.3">
  </networks>
</server>

3) Networks is an optional parameter, so if you don't provide any networks to the server Create API, it will behave exactly the same as of today.

This feature is supported to all of the network models.

1477. By Christopher MacGown <email address hidden>

Merged trunk

1478. By Christopher MacGown <email address hidden>

Moved migration and fixed tests from upstream

Revision history for this message
Christopher MacGown (0x44) wrote :

After discussion with RCB and the API discussion on the mailing list, I'm going to document config-drive as a core feature of the 1.1 API as it makes another core feature of the 1.1 API possible on non-XenServer hypervisors. I've added the other fixes suggested above.

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

relgtm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Authors'
2--- Authors 2011-08-17 07:41:17 +0000
3+++ Authors 2011-08-23 05:22:32 +0000
4@@ -18,6 +18,7 @@
5 Chmouel Boudjnah <chmouel@chmouel.com>
6 Chris Behrens <cbehrens@codestud.com>
7 Christian Berendt <berendt@b1-systems.de>
8+Christopher MacGown <chris@pistoncloud.com>
9 Chuck Short <zulcss@ubuntu.com>
10 Cory Wright <corywright@gmail.com>
11 Dan Prince <dan.prince@rackspace.com>
12
13=== modified file 'nova/api/openstack/create_instance_helper.py'
14--- nova/api/openstack/create_instance_helper.py 2011-08-22 23:35:09 +0000
15+++ nova/api/openstack/create_instance_helper.py 2011-08-23 05:22:32 +0000
16@@ -1,4 +1,5 @@
17 # Copyright 2011 OpenStack LLC.
18+# Copyright 2011 Piston Cloud Computing, Inc.
19 # All Rights Reserved.
20 #
21 # Licensed under the Apache License, Version 2.0 (the "License"); you may
22@@ -106,6 +107,7 @@
23 raise exc.HTTPBadRequest(explanation=msg)
24
25 personality = server_dict.get('personality')
26+ config_drive = server_dict.get('config_drive')
27
28 injected_files = []
29 if personality:
30@@ -159,6 +161,7 @@
31 extra_values = {
32 'instance_type': inst_type,
33 'image_ref': image_href,
34+ 'config_drive': config_drive,
35 'password': password}
36
37 return (extra_values,
38@@ -183,7 +186,8 @@
39 requested_networks=requested_networks,
40 security_group=sg_names,
41 user_data=user_data,
42- availability_zone=availability_zone))
43+ availability_zone=availability_zone,
44+ config_drive=config_drive,))
45 except quota.QuotaError as error:
46 self._handle_quota_error(error)
47 except exception.ImageNotFound as error:
48
49=== modified file 'nova/api/openstack/views/servers.py'
50--- nova/api/openstack/views/servers.py 2011-08-22 12:28:12 +0000
51+++ nova/api/openstack/views/servers.py 2011-08-23 05:22:32 +0000
52@@ -1,6 +1,7 @@
53 # vim: tabstop=4 shiftwidth=4 softtabstop=4
54
55 # Copyright 2010-2011 OpenStack LLC.
56+# Copyright 2011 Piston Cloud Computing, Inc.
57 # All Rights Reserved.
58 #
59 # Licensed under the Apache License, Version 2.0 (the "License"); you may
60@@ -187,6 +188,7 @@
61 def _build_extra(self, response, inst):
62 self._build_links(response, inst)
63 response['uuid'] = inst['uuid']
64+ self._build_config_drive(response, inst)
65
66 def _build_links(self, response, inst):
67 href = self.generate_href(inst["id"])
68@@ -205,6 +207,9 @@
69
70 response["links"] = links
71
72+ def _build_config_drive(self, response, inst):
73+ response['config_drive'] = inst.get('config_drive')
74+
75 def generate_href(self, server_id):
76 """Create an url that refers to a specific server id."""
77 return os.path.join(self.base_url, self.project_id,
78
79=== modified file 'nova/compute/api.py'
80--- nova/compute/api.py 2011-08-22 23:35:09 +0000
81+++ nova/compute/api.py 2011-08-23 05:22:32 +0000
82@@ -2,6 +2,7 @@
83
84 # Copyright 2010 United States Government as represented by the
85 # Administrator of the National Aeronautics and Space Administration.
86+# Copyright 2011 Piston Cloud Computing, Inc.
87 # All Rights Reserved.
88 #
89 # Licensed under the Apache License, Version 2.0 (the "License"); you may
90@@ -164,7 +165,7 @@
91 availability_zone=None, user_data=None, metadata=None,
92 injected_files=None, admin_password=None, zone_blob=None,
93 reservation_id=None, access_ip_v4=None, access_ip_v6=None,
94- requested_networks=None):
95+ requested_networks=None, config_drive=None,):
96 """Verify all the input parameters regardless of the provisioning
97 strategy being performed."""
98
99@@ -198,6 +199,11 @@
100 (image_service, image_id) = nova.image.get_image_service(image_href)
101 image = image_service.show(context, image_id)
102
103+ config_drive_id = None
104+ if config_drive and config_drive is not True:
105+ # config_drive is volume id
106+ config_drive, config_drive_id = None, config_drive
107+
108 os_type = None
109 if 'properties' in image and 'os_type' in image['properties']:
110 os_type = image['properties']['os_type']
111@@ -225,6 +231,8 @@
112 image_service.show(context, kernel_id)
113 if ramdisk_id:
114 image_service.show(context, ramdisk_id)
115+ if config_drive_id:
116+ image_service.show(context, config_drive_id)
117
118 self.ensure_default_security_group(context)
119
120@@ -243,6 +251,8 @@
121 'image_ref': image_href,
122 'kernel_id': kernel_id or '',
123 'ramdisk_id': ramdisk_id or '',
124+ 'config_drive_id': config_drive_id or '',
125+ 'config_drive': config_drive or '',
126 'state': 0,
127 'state_description': 'scheduling',
128 'user_id': context.user_id,
129@@ -454,7 +464,7 @@
130 injected_files=None, admin_password=None, zone_blob=None,
131 reservation_id=None, block_device_mapping=None,
132 access_ip_v4=None, access_ip_v6=None,
133- requested_networks=None):
134+ requested_networks=None, config_drive=None):
135 """Provision the instances by passing the whole request to
136 the Scheduler for execution. Returns a Reservation ID
137 related to the creation of all of these instances."""
138@@ -471,7 +481,7 @@
139 availability_zone, user_data, metadata,
140 injected_files, admin_password, zone_blob,
141 reservation_id, access_ip_v4, access_ip_v6,
142- requested_networks)
143+ requested_networks, config_drive)
144
145 self._ask_scheduler_to_create_instance(context, base_options,
146 instance_type, zone_blob,
147@@ -491,7 +501,7 @@
148 injected_files=None, admin_password=None, zone_blob=None,
149 reservation_id=None, block_device_mapping=None,
150 access_ip_v4=None, access_ip_v6=None,
151- requested_networks=None):
152+ requested_networks=None, config_drive=None,):
153 """
154 Provision the instances by sending off a series of single
155 instance requests to the Schedulers. This is fine for trival
156@@ -516,7 +526,7 @@
157 availability_zone, user_data, metadata,
158 injected_files, admin_password, zone_blob,
159 reservation_id, access_ip_v4, access_ip_v6,
160- requested_networks)
161+ requested_networks, config_drive)
162
163 block_device_mapping = block_device_mapping or []
164 instances = []
165
166=== added file 'nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py'
167--- nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py 1970-01-01 00:00:00 +0000
168+++ nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py 2011-08-23 05:22:32 +0000
169@@ -0,0 +1,38 @@
170+# vim: tabstop=4 shiftwidth=4 softtabstop=4
171+#
172+# Copyright 2011 Piston Cloud Computing, Inc.
173+#
174+# Licensed under the Apache License, Version 2.0 (the "License"); you may
175+# not use this file except in compliance with the License. You may obtain
176+# a copy of the License at
177+#
178+# http://www.apache.org/licenses/LICENSE-2.0
179+#
180+# Unless required by applicable law or agreed to in writing, software
181+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
182+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
183+# License for the specific language governing permissions and limitations
184+# under the License.
185+
186+from sqlalchemy import Column, Integer, MetaData, String, Table
187+
188+from nova import utils
189+
190+
191+meta = MetaData()
192+
193+instances = Table("instances", meta,
194+ Column("id", Integer(), primary_key=True, nullable=False))
195+
196+# matches the size of an image_ref
197+config_drive_column = Column("config_drive", String(255), nullable=True)
198+
199+
200+def upgrade(migrate_engine):
201+ meta.bind = migrate_engine
202+ instances.create_column(config_drive_column)
203+
204+
205+def downgrade(migrate_engine):
206+ meta.bind = migrate_engine
207+ instances.drop_column(config_drive_column)
208
209=== modified file 'nova/db/sqlalchemy/models.py'
210--- nova/db/sqlalchemy/models.py 2011-08-22 23:35:09 +0000
211+++ nova/db/sqlalchemy/models.py 2011-08-23 05:22:32 +0000
212@@ -2,6 +2,7 @@
213
214 # Copyright 2010 United States Government as represented by the
215 # Administrator of the National Aeronautics and Space Administration.
216+# Copyright 2011 Piston Cloud Computing, Inc.
217 # All Rights Reserved.
218 #
219 # Licensed under the Apache License, Version 2.0 (the "License"); you may
220@@ -230,6 +231,7 @@
221 uuid = Column(String(36))
222
223 root_device_name = Column(String(255))
224+ config_drive = Column(String(255))
225
226 # User editable field meant to represent what ip should be used
227 # to connect to the instance
228
229=== modified file 'nova/tests/api/openstack/test_servers.py'
230--- nova/tests/api/openstack/test_servers.py 2011-08-22 23:35:09 +0000
231+++ nova/tests/api/openstack/test_servers.py 2011-08-23 05:22:32 +0000
232@@ -1,6 +1,7 @@
233 # vim: tabstop=4 shiftwidth=4 softtabstop=4
234
235 # Copyright 2010-2011 OpenStack LLC.
236+# Copyright 2011 Piston Cloud Computing, Inc.
237 # All Rights Reserved.
238 #
239 # Licensed under the Apache License, Version 2.0 (the "License"); you may
240@@ -233,7 +234,6 @@
241
242
243 class ServersTest(test.TestCase):
244-
245 def setUp(self):
246 self.maxDiff = None
247 super(ServersTest, self).setUp()
248@@ -265,6 +265,7 @@
249 self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api)
250
251 self.webreq = common.webob_factory('/v1.0/servers')
252+ self.config_drive = None
253
254 def test_get_server_by_id(self):
255 req = webob.Request.blank('/v1.0/servers/1')
256@@ -379,6 +380,7 @@
257 "metadata": {
258 "seq": "1",
259 },
260+ "config_drive": None,
261 "links": [
262 {
263 "rel": "self",
264@@ -545,6 +547,7 @@
265 "metadata": {
266 "seq": "1",
267 },
268+ "config_drive": None,
269 "links": [
270 {
271 "rel": "self",
272@@ -638,6 +641,7 @@
273 "metadata": {
274 "seq": "1",
275 },
276+ "config_drive": None,
277 "links": [
278 {
279 "rel": "self",
280@@ -1399,6 +1403,7 @@
281 'image_ref': image_ref,
282 "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
283 "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
284+ "config_drive": self.config_drive,
285 }
286
287 def server_update(context, id, params):
288@@ -1424,8 +1429,7 @@
289 self.stubs.Set(nova.db.api, 'instance_create', instance_create)
290 self.stubs.Set(nova.rpc, 'cast', fake_method)
291 self.stubs.Set(nova.rpc, 'call', fake_method)
292- self.stubs.Set(nova.db.api, 'instance_update',
293- server_update)
294+ self.stubs.Set(nova.db.api, 'instance_update', server_update)
295 self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for)
296 self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip',
297 fake_method)
298@@ -1768,6 +1772,129 @@
299 res = req.get_response(fakes.wsgi_app())
300 self.assertEqual(res.status_int, 400)
301
302+ def test_create_instance_with_config_drive_v1_1(self):
303+ self.config_drive = True
304+ self._setup_for_create_instance()
305+
306+ image_href = 'http://localhost/v1.1/123/images/2'
307+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
308+ body = {
309+ 'server': {
310+ 'name': 'config_drive_test',
311+ 'imageRef': image_href,
312+ 'flavorRef': flavor_ref,
313+ 'metadata': {
314+ 'hello': 'world',
315+ 'open': 'stack',
316+ },
317+ 'personality': {},
318+ 'config_drive': True,
319+ },
320+ }
321+
322+ req = webob.Request.blank('/v1.1/123/servers')
323+ req.method = 'POST'
324+ req.body = json.dumps(body)
325+ req.headers["content-type"] = "application/json"
326+
327+ res = req.get_response(fakes.wsgi_app())
328+ print res
329+ self.assertEqual(res.status_int, 202)
330+ server = json.loads(res.body)['server']
331+ self.assertEqual(1, server['id'])
332+ self.assertTrue(server['config_drive'])
333+
334+ def test_create_instance_with_config_drive_as_id_v1_1(self):
335+ self.config_drive = 2
336+ self._setup_for_create_instance()
337+
338+ image_href = 'http://localhost/v1.1/123/images/2'
339+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
340+ body = {
341+ 'server': {
342+ 'name': 'config_drive_test',
343+ 'imageRef': image_href,
344+ 'flavorRef': flavor_ref,
345+ 'metadata': {
346+ 'hello': 'world',
347+ 'open': 'stack',
348+ },
349+ 'personality': {},
350+ 'config_drive': 2,
351+ },
352+ }
353+
354+ req = webob.Request.blank('/v1.1/123/servers')
355+ req.method = 'POST'
356+ req.body = json.dumps(body)
357+ req.headers["content-type"] = "application/json"
358+
359+ res = req.get_response(fakes.wsgi_app())
360+
361+ self.assertEqual(res.status_int, 202)
362+ server = json.loads(res.body)['server']
363+ self.assertEqual(1, server['id'])
364+ self.assertTrue(server['config_drive'])
365+ self.assertEqual(2, server['config_drive'])
366+
367+ def test_create_instance_with_bad_config_drive_v1_1(self):
368+ self.config_drive = "asdf"
369+ self._setup_for_create_instance()
370+
371+ image_href = 'http://localhost/v1.1/123/images/2'
372+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
373+ body = {
374+ 'server': {
375+ 'name': 'config_drive_test',
376+ 'imageRef': image_href,
377+ 'flavorRef': flavor_ref,
378+ 'metadata': {
379+ 'hello': 'world',
380+ 'open': 'stack',
381+ },
382+ 'personality': {},
383+ 'config_drive': 'asdf',
384+ },
385+ }
386+
387+ req = webob.Request.blank('/v1.1/123/servers')
388+ req.method = 'POST'
389+ req.body = json.dumps(body)
390+ req.headers["content-type"] = "application/json"
391+
392+ res = req.get_response(fakes.wsgi_app())
393+ self.assertEqual(res.status_int, 400)
394+
395+ def test_create_instance_without_config_drive_v1_1(self):
396+ self._setup_for_create_instance()
397+
398+ image_href = 'http://localhost/v1.1/123/images/2'
399+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
400+ body = {
401+ 'server': {
402+ 'name': 'config_drive_test',
403+ 'imageRef': image_href,
404+ 'flavorRef': flavor_ref,
405+ 'metadata': {
406+ 'hello': 'world',
407+ 'open': 'stack',
408+ },
409+ 'personality': {},
410+ 'config_drive': True,
411+ },
412+ }
413+
414+ req = webob.Request.blank('/v1.1/123/servers')
415+ req.method = 'POST'
416+ req.body = json.dumps(body)
417+ req.headers["content-type"] = "application/json"
418+
419+ res = req.get_response(fakes.wsgi_app())
420+ self.assertEqual(res.status_int, 202)
421+ server = json.loads(res.body)['server']
422+ self.assertEqual(1, server['id'])
423+ self.assertFalse(server['config_drive'])
424+
425 def test_create_instance_v1_1_bad_href(self):
426 self._setup_for_create_instance()
427
428@@ -3449,6 +3576,7 @@
429 "href": "http://localhost/servers/1",
430 },
431 ],
432+ "config_drive": None,
433 }
434 }
435
436@@ -3461,6 +3589,7 @@
437 "id": 1,
438 "uuid": self.instance['uuid'],
439 "name": "test_server",
440+ "config_drive": None,
441 "links": [
442 {
443 "rel": "self",
444@@ -3513,6 +3642,7 @@
445 },
446 "addresses": {},
447 "metadata": {},
448+ "config_drive": None,
449 "links": [
450 {
451 "rel": "self",
452@@ -3566,6 +3696,7 @@
453 },
454 "addresses": {},
455 "metadata": {},
456+ "config_drive": None,
457 "links": [
458 {
459 "rel": "self",
460@@ -3618,6 +3749,7 @@
461 },
462 "addresses": {},
463 "metadata": {},
464+ "config_drive": None,
465 "accessIPv4": "1.2.3.4",
466 "accessIPv6": "",
467 "links": [
468@@ -3672,6 +3804,7 @@
469 },
470 "addresses": {},
471 "metadata": {},
472+ "config_drive": None,
473 "accessIPv4": "",
474 "accessIPv6": "fead::1234",
475 "links": [
476@@ -3734,6 +3867,7 @@
477 "Open": "Stack",
478 "Number": "1",
479 },
480+ "config_drive": None,
481 "links": [
482 {
483 "rel": "self",
484
485=== modified file 'nova/tests/test_compute.py'
486--- nova/tests/test_compute.py 2011-08-16 19:12:42 +0000
487+++ nova/tests/test_compute.py 2011-08-23 05:22:32 +0000
488@@ -2,6 +2,7 @@
489
490 # Copyright 2010 United States Government as represented by the
491 # Administrator of the National Aeronautics and Space Administration.
492+# Copyright 2011 Piston Cloud Computing, Inc.
493 # All Rights Reserved.
494 #
495 # Licensed under the Apache License, Version 2.0 (the "License"); you may
496@@ -159,6 +160,20 @@
497 db.security_group_destroy(self.context, group['id'])
498 db.instance_destroy(self.context, ref[0]['id'])
499
500+ def test_create_instance_associates_config_drive(self):
501+ """Make sure create associates a config drive."""
502+
503+ instance_id = self._create_instance(params={'config_drive': True, })
504+
505+ try:
506+ self.compute.run_instance(self.context, instance_id)
507+ instances = db.instance_get_all(context.get_admin_context())
508+ instance = instances[0]
509+
510+ self.assertTrue(instance.config_drive)
511+ finally:
512+ db.instance_destroy(self.context, instance_id)
513+
514 def test_default_hostname_generator(self):
515 cases = [(None, 'server_1'), ('Hello, Server!', 'hello_server'),
516 ('<}\x1fh\x10e\x08l\x02l\x05o\x12!{>', 'hello')]
517
518=== modified file 'nova/virt/disk.py'
519--- nova/virt/disk.py 2011-08-05 14:23:48 +0000
520+++ nova/virt/disk.py 2011-08-23 05:22:32 +0000
521@@ -2,6 +2,9 @@
522
523 # Copyright 2010 United States Government as represented by the
524 # Administrator of the National Aeronautics and Space Administration.
525+#
526+# Copyright 2011, Piston Cloud Computing, Inc.
527+#
528 # All Rights Reserved.
529 #
530 # Licensed under the Apache License, Version 2.0 (the "License"); you may
531@@ -22,6 +25,7 @@
532
533 """
534
535+import json
536 import os
537 import tempfile
538 import time
539@@ -60,7 +64,8 @@
540 utils.execute('resize2fs', image, check_exit_code=False)
541
542
543-def inject_data(image, key=None, net=None, partition=None, nbd=False):
544+def inject_data(image, key=None, net=None, metadata=None,
545+ partition=None, nbd=False, tune2fs=True):
546 """Injects a ssh key and optionally net data into a disk image.
547
548 it will mount the image as a fully partitioned disk and attempt to inject
549@@ -89,10 +94,10 @@
550 ' only inject raw disk images): %s' %
551 mapped_device)
552
553- # Configure ext2fs so that it doesn't auto-check every N boots
554- out, err = utils.execute('tune2fs', '-c', 0, '-i', 0,
555- mapped_device, run_as_root=True)
556-
557+ if tune2fs:
558+ # Configure ext2fs so that it doesn't auto-check every N boots
559+ out, err = utils.execute('tune2fs', '-c', 0, '-i', 0,
560+ mapped_device, run_as_root=True)
561 tmpdir = tempfile.mkdtemp()
562 try:
563 # mount loopback to dir
564@@ -103,7 +108,8 @@
565 % err)
566
567 try:
568- inject_data_into_fs(tmpdir, key, net, utils.execute)
569+ inject_data_into_fs(tmpdir, key, net, metadata,
570+ utils.execute)
571 finally:
572 # unmount device
573 utils.execute('umount', mapped_device, run_as_root=True)
574@@ -155,6 +161,7 @@
575
576 def _link_device(image, nbd):
577 """Link image to device using loopback or nbd"""
578+
579 if nbd:
580 device = _allocate_device()
581 utils.execute('qemu-nbd', '-c', device, image, run_as_root=True)
582@@ -190,6 +197,7 @@
583 # NOTE(vish): This assumes no other processes are allocating nbd devices.
584 # It may race cause a race condition if multiple
585 # workers are running on a given machine.
586+
587 while True:
588 if not _DEVICES:
589 raise exception.Error(_('No free nbd devices'))
590@@ -203,7 +211,7 @@
591 _DEVICES.append(device)
592
593
594-def inject_data_into_fs(fs, key, net, execute):
595+def inject_data_into_fs(fs, key, net, metadata, execute):
596 """Injects data into a filesystem already mounted by the caller.
597 Virt connections can call this directly if they mount their fs
598 in a different way to inject_data
599@@ -212,6 +220,16 @@
600 _inject_key_into_fs(key, fs, execute=execute)
601 if net:
602 _inject_net_into_fs(net, fs, execute=execute)
603+ if metadata:
604+ _inject_metadata_into_fs(metadata, fs, execute=execute)
605+
606+
607+def _inject_metadata_into_fs(metadata, fs, execute=None):
608+ metadata_path = os.path.join(fs, "meta.js")
609+ metadata = dict([(m.key, m.value) for m in metadata])
610+
611+ utils.execute('sudo', 'tee', metadata_path,
612+ process_input=json.dumps(metadata))
613
614
615 def _inject_key_into_fs(key, fs, execute=None):
616
617=== modified file 'nova/virt/libvirt.xml.template'
618--- nova/virt/libvirt.xml.template 2011-08-02 02:36:58 +0000
619+++ nova/virt/libvirt.xml.template 2011-08-23 05:22:32 +0000
620@@ -106,6 +106,13 @@
621 </disk>
622 #end for
623 #end if
624+ #if $getVar('config_drive', False)
625+ <disk type='file'>
626+ <driver type='raw' />
627+ <source file='${basepath}/disk.config' />
628+ <target dev='${disk_prefix}z' bus='${disk_bus}' />
629+ </disk>
630+ #end if
631 #end if
632
633 #for $nic in $nics
634
635=== modified file 'nova/virt/libvirt/connection.py'
636--- nova/virt/libvirt/connection.py 2011-08-18 12:57:34 +0000
637+++ nova/virt/libvirt/connection.py 2011-08-23 05:22:32 +0000
638@@ -4,6 +4,7 @@
639 # Administrator of the National Aeronautics and Space Administration.
640 # All Rights Reserved.
641 # Copyright (c) 2010 Citrix Systems, Inc.
642+# Copyright (c) 2011 Piston Cloud Computing, Inc
643 #
644 # Licensed under the Apache License, Version 2.0 (the "License"); you may
645 # not use this file except in compliance with the License. You may obtain
646@@ -130,6 +131,10 @@
647 flags.DEFINE_string('libvirt_vif_driver',
648 'nova.virt.libvirt.vif.LibvirtBridgeDriver',
649 'The libvirt VIF driver to configure the VIFs.')
650+flags.DEFINE_string('default_local_format',
651+ None,
652+ 'The default format a local_volume will be formatted with '
653+ 'on creation.')
654
655
656 def get_connection(read_only):
657@@ -586,6 +591,7 @@
658 self.firewall_driver.prepare_instance_filter(instance, network_info)
659 self._create_image(context, instance, xml, network_info=network_info,
660 block_device_info=block_device_info)
661+
662 domain = self._create_new_domain(xml)
663 LOG.debug(_("instance %s: is running"), instance['name'])
664 self.firewall_driver.apply_instance_filter(instance, network_info)
665@@ -759,10 +765,15 @@
666 if size:
667 disk.extend(target, size)
668
669- def _create_local(self, target, local_gb):
670+ def _create_local(self, target, local_size, prefix='G', fs_format=None):
671 """Create a blank image of specified size"""
672- utils.execute('truncate', target, '-s', "%dG" % local_gb)
673- # TODO(vish): should we format disk by default?
674+
675+ if not fs_format:
676+ fs_format = FLAGS.default_local_format
677+
678+ utils.execute('truncate', target, '-s', "%d%c" % (local_size, prefix))
679+ if fs_format:
680+ utils.execute('mkfs', '-t', fs_format, target)
681
682 def _create_swap(self, target, swap_gb):
683 """Create a swap file of specified size"""
684@@ -849,14 +860,14 @@
685 target=basepath('disk.local'),
686 fname="local_%s" % local_gb,
687 cow=FLAGS.use_cow_images,
688- local_gb=local_gb)
689+ local_size=local_gb)
690
691 for eph in driver.block_device_info_get_ephemerals(block_device_info):
692 self._cache_image(fn=self._create_local,
693 target=basepath(_get_eph_disk(eph)),
694 fname="local_%s" % eph['size'],
695 cow=FLAGS.use_cow_images,
696- local_gb=eph['size'])
697+ local_size=eph['size'])
698
699 swap_gb = 0
700
701@@ -882,9 +893,24 @@
702 if not inst['kernel_id']:
703 target_partition = "1"
704
705- if FLAGS.libvirt_type == 'lxc':
706+ config_drive_id = inst.get('config_drive_id')
707+ config_drive = inst.get('config_drive')
708+
709+ if any((FLAGS.libvirt_type == 'lxc', config_drive, config_drive_id)):
710 target_partition = None
711
712+ if config_drive_id:
713+ fname = '%08x' % int(config_drive_id)
714+ self._cache_image(fn=self._fetch_image,
715+ target=basepath('disk.config'),
716+ fname=fname,
717+ image_id=config_drive_id,
718+ user=user,
719+ project=project)
720+ elif config_drive:
721+ self._create_local(basepath('disk.config'), 64, prefix="M",
722+ fs_format='msdos') # 64MB
723+
724 if inst['key_data']:
725 key = str(inst['key_data'])
726 else:
727@@ -928,19 +954,29 @@
728 searchList=[{'interfaces': nets,
729 'use_ipv6': FLAGS.use_ipv6}]))
730
731- if key or net:
732+ metadata = inst.get('metadata')
733+ if any((key, net, metadata)):
734 inst_name = inst['name']
735- img_id = inst.image_ref
736- if key:
737- LOG.info(_('instance %(inst_name)s: injecting key into'
738- ' image %(img_id)s') % locals())
739- if net:
740- LOG.info(_('instance %(inst_name)s: injecting net into'
741- ' image %(img_id)s') % locals())
742+
743+ if config_drive: # Should be True or None by now.
744+ injection_path = basepath('disk.config')
745+ img_id = 'config-drive'
746+ tune2fs = False
747+ else:
748+ injection_path = basepath('disk')
749+ img_id = inst.image_ref
750+ tune2fs = True
751+
752+ for injection in ('metadata', 'key', 'net'):
753+ if locals()[injection]:
754+ LOG.info(_('instance %(inst_name)s: injecting '
755+ '%(injection)s into image %(img_id)s'
756+ % locals()))
757 try:
758- disk.inject_data(basepath('disk'), key, net,
759+ disk.inject_data(injection_path, key, net, metadata,
760 partition=target_partition,
761- nbd=FLAGS.use_cow_images)
762+ nbd=FLAGS.use_cow_images,
763+ tune2fs=tune2fs)
764
765 if FLAGS.libvirt_type == 'lxc':
766 disk.setup_container(basepath('disk'),
767@@ -1070,6 +1106,10 @@
768 block_device_info)):
769 xml_info['swap_device'] = self.default_swap_device
770
771+ config_drive = False
772+ if instance.get('config_drive') or instance.get('config_drive_id'):
773+ xml_info['config_drive'] = xml_info['basepath'] + "/disk.config"
774+
775 if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'):
776 xml_info['vncserver_host'] = FLAGS.vncserver_host
777 xml_info['vnc_keymap'] = FLAGS.vnc_keymap
778
779=== modified file 'nova/virt/xenapi/vm_utils.py'
780--- nova/virt/xenapi/vm_utils.py 2011-08-15 18:35:53 +0000
781+++ nova/virt/xenapi/vm_utils.py 2011-08-23 05:22:32 +0000
782@@ -1,6 +1,7 @@
783 # vim: tabstop=4 shiftwidth=4 softtabstop=4
784
785 # Copyright (c) 2010 Citrix Systems, Inc.
786+# Copyright 2011 Piston Cloud Computing, Inc.
787 #
788 # Licensed under the Apache License, Version 2.0 (the "License"); you may
789 # not use this file except in compliance with the License. You may obtain
790@@ -740,13 +741,14 @@
791 # if at all, so determine whether it's required first, and then do
792 # everything
793 mount_required = False
794- key, net = _prepare_injectables(instance, network_info)
795- mount_required = key or net
796+ key, net, metadata = _prepare_injectables(instance, network_info)
797+ mount_required = key or net or metadata
798 if not mount_required:
799 return
800
801 with_vdi_attached_here(session, vdi_ref, False,
802- lambda dev: _mounted_processing(dev, key, net))
803+ lambda dev: _mounted_processing(dev, key, net,
804+ metadata))
805
806 @classmethod
807 def lookup_kernel_ramdisk(cls, session, vm):
808@@ -1198,7 +1200,7 @@
809 return False
810
811
812-def _mounted_processing(device, key, net):
813+def _mounted_processing(device, key, net, metadata):
814 """Callback which runs with the image VDI attached"""
815
816 dev_path = '/dev/' + device + '1' # NB: Partition 1 hardcoded
817@@ -1212,7 +1214,7 @@
818 if not _find_guest_agent(tmpdir, FLAGS.xenapi_agent_path):
819 LOG.info(_('Manipulating interface files '
820 'directly'))
821- disk.inject_data_into_fs(tmpdir, key, net,
822+ disk.inject_data_into_fs(tmpdir, key, net, metadata,
823 utils.execute)
824 finally:
825 utils.execute('umount', dev_path, run_as_root=True)
826@@ -1235,6 +1237,7 @@
827 template = t.Template
828 template_data = open(FLAGS.injected_network_template).read()
829
830+ metadata = inst['metadata']
831 key = str(inst['key_data'])
832 net = None
833 if networks_info:
834@@ -1272,4 +1275,4 @@
835 net = str(template(template_data,
836 searchList=[{'interfaces': interfaces_info,
837 'use_ipv6': FLAGS.use_ipv6}]))
838- return key, net
839+ return key, net, metadata
840
841=== modified file 'nova/virt/xenapi/vmops.py'
842--- nova/virt/xenapi/vmops.py 2011-08-17 03:15:54 +0000
843+++ nova/virt/xenapi/vmops.py 2011-08-23 05:22:32 +0000
844@@ -239,8 +239,9 @@
845 self._attach_disks(instance, disk_image_type, vm_ref, first_vdi_ref,
846 vdis)
847
848- # Alter the image before VM start for, e.g. network injection
849- if FLAGS.flat_injected:
850+ # Alter the image before VM start for, e.g. network injection also
851+ # alter the image if there's metadata.
852+ if FLAGS.flat_injected or instance['metadata']:
853 VMHelper.preconfigure_instance(self._session, instance,
854 first_vdi_ref, network_info)
855
856
857=== modified file 'run_tests.sh'
858--- run_tests.sh 2011-07-29 14:54:20 +0000
859+++ run_tests.sh 2011-08-23 05:22:32 +0000
860@@ -67,7 +67,7 @@
861 ERRSIZE=`wc -l run_tests.log | awk '{print \$1}'`
862 if [ "$ERRSIZE" -lt "40" ];
863 then
864- cat run_tests.log
865+ cat run_tests.log
866 fi
867 fi
868 return $RESULT