Merge lp:~0x44/nova/config-drive into lp:~hudson-openstack/nova/trunk
- config-drive
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Portable Configuration Drive
(High)
|
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 |
Commit message
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).
Dan Prince (dan-prince) wrote : | # |
Your using FLAGS.default_
Matt Dietz (cerberus) wrote : | # |
184 + instance_
185 + migrate_
186 + .where(
187 + .values(
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?
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.
Matt Dietz (cerberus) wrote : | # |
Thanks for the changes, Chris.
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?
- 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 integrationNext 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
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-
Reference: http://
Christopher MacGown (0x44) wrote : | # |
Eric: The original plan was that it would be an ISO9660 filesystem but the people who documented/
Dan: I'll move the osapi stuff into an extension and look at adding another flag to turn off the publicly exposed feature.
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/
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:/
You are reviewing the proposed m...
- 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.
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.
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/
--- nova/virt/
+++ nova/virt/
@@ -742,7 +742,7 @@
# everything
key, net, metadata = _prepare_
- mount_required = key or net
+ mount_required = key or net or metadata
if not mount_required:
return
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(
+ utils.execute(
- 1472. By William Wolf
-
implemented tenant ids to be included in request uris.
Erica Windisch (ewindisch) wrote : | # |
Metadata wasn't written on XenServer unless using flat_injected networking.
=== modified file 'nova/virt/
--- nova/virt/
+++ nova/virt/
@@ -240,7 +240,8 @@
vdis)
# Alter the image before VM start for, e.g. network injection
- if FLAGS.flat_
+ # also alter if there is metadata
+ if FLAGS.flat_injected or instance[
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.
Christopher MacGown (0x44) wrote : | # |
I'll add your fixes in my next push.
- 1473. By Alex Meade
-
The FixedIpCommands
TestCase 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">
ICAgICAgDQoiQS BjbG91ZCBkb2VzI G5vdCBrbm93IHdo eSBp
</file>
</personality>
<networks>
<network uuid="6622436e-5289-460f- 8479-e4dcc63f16 c5" fixed_ip= "10.0.0. 3">
<network uuid="d97efefc-e071-4174- b6dd-b33af0a377 06" 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
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.
Preview Diff
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 |
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.