Merge lp:~tpatil/nova/add-options-network-create-os-apis into lp:~hudson-openstack/nova/trunk

Proposed by Tushar Patil
Status: Merged
Approved by: Vish Ishaya
Approved revision: 1282
Merged at revision: 1476
Proposed branch: lp:~tpatil/nova/add-options-network-create-os-apis
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 1617 lines (+1069/-68)
17 files modified
bin/nova-manage (+19/-16)
nova/api/openstack/contrib/createserverext.py (+66/-0)
nova/api/openstack/create_instance_helper.py (+72/-1)
nova/compute/api.py (+34/-17)
nova/compute/manager.py (+4/-1)
nova/db/api.py (+9/-3)
nova/db/sqlalchemy/api.py (+55/-8)
nova/db/sqlalchemy/migrate_repo/versions/040_add_uuid_to_networks.py (+43/-0)
nova/db/sqlalchemy/models.py (+1/-0)
nova/exception.py (+22/-0)
nova/network/api.py (+9/-0)
nova/network/manager.py (+100/-21)
nova/tests/api/openstack/contrib/test_createserverext.py (+306/-0)
nova/tests/api/openstack/test_extensions.py (+1/-0)
nova/tests/api/openstack/test_servers.py (+183/-0)
nova/tests/test_network.py (+129/-1)
nova/utils.py (+16/-0)
To merge this branch: bzr merge lp:~tpatil/nova/add-options-network-create-os-apis
Reviewer Review Type Date Requested Status
Vish Ishaya (community) Approve
Tushar Patil (community) Approve
Brian Waldon (community) Approve
Review via email: mp+68292@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Hey Tushar,

This seems like a very useful feature, but the implementation in the apis will have to change a little:
1) The v1.0 api cannot be changed. You will need to limit your changes to the v1.1 api.
2) This is not in the v1.1 spec, so it will have to be documented as an extension. I think it is okay for the v1.1 code to remain unchanged, but you will need to add an extension descriptor to document the change.

I will gladly take a closer look at this once those things are addressed :)

review: Needs Fixing
Revision history for this message
Tushar Patil (tpatil) wrote :

Thank you for your time to review my code.

> 1) The v1.0 api cannot be changed. You will need to limit your changes to the
> v1.1 api.
Ok, understood.
> 2) This is not in the v1.1 spec, so it will have to be documented as an
> extension. I think it is okay for the v1.1 code to remain unchanged, but you
> will need to add an extension descriptor to document the change.
You mean I should add this as an extension under api/openstack/contrib directory of nova. Please confirm.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> Thank you for your time to review my code.
>
> > 1) The v1.0 api cannot be changed. You will need to limit your changes to
> the
> > v1.1 api.
> Ok, understood.
> > 2) This is not in the v1.1 spec, so it will have to be documented as an
> > extension. I think it is okay for the v1.1 code to remain unchanged, but you
> > will need to add an extension descriptor to document the change.
> You mean I should add this as an extension under api/openstack/contrib
> directory of nova. Please confirm.

You should add a module in api.openstack.contrib, but the only code you need in it is an implementation of nova.api.openstack.extensions.ExtensionDescriptor. See api.openstack.contrib.volumes for an example of the descriptor. Basically, you need to describe the difference in our (Nova's) implementation of the v1.1 spec.

Revision history for this message
Tushar Patil (tpatil) wrote :

I have merged trunk changes to my code which includes ha-net changes. Request you all to please review my code.

Revision history for this message
Tushar Patil (tpatil) wrote :

> You should add a module in api.openstack.contrib, but the only code you need
> in it is an implementation of
> nova.api.openstack.extensions.ExtensionDescriptor. See
> api.openstack.contrib.volumes for an example of the descriptor. Basically, you
> need to describe the difference in our (Nova's) implementation of the v1.1
> spec.

Fixed. I have implemented a new extension for the Create server API with an additional network information parameter.

Revision history for this message
Tushar Patil (tpatil) wrote :

Set the status to Work in progress as I am merging the latest changes in the trunk after network refactoring code is in. Some of the my unit test cases are failing so fixing them right now.

Revision history for this message
Tushar Patil (tpatil) wrote :

Fixed all broken unit test cases after merging trunk changes. Please review.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

I think it makes more sense for you to put whatever changes you need to make in the main create_instance_helper.py module. The duplication in nova/api/openstack/contrib/createserverext.py is going to be difficult to keep up with. Like I said earlier, your code can live in the main codebase, you just need to have an ExtensionDescriptor in contrib/ (which you do).

Revision history for this message
Tushar Patil (tpatil) wrote :

> I think it makes more sense for you to put whatever changes you need to make
> in the main create_instance_helper.py module. The duplication in
> nova/api/openstack/contrib/createserverext.py is going to be difficult to keep
> up with. Like I said earlier, your code can live in the main codebase, you
> just need to have an ExtensionDescriptor in contrib/ (which you do).
Now, I have understood your comments correctly. I will revert my changes accordingly. I will also have to write my unit test cases in the test_server.py instead of test/api/openstack/contrip/test_createserverext.py.

In one of your previous comments, you mention I have to limit these new changes only in the V1.1 without disturbing V1.0. To do this, I will need to add a new method _network_ref_from_req_data to both ControllerV11 and ControllerV10. ControllerV10 will simply return None whereas ControllerV11 will return the actual network reference data.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> > I think it makes more sense for you to put whatever changes you need to make
> > in the main create_instance_helper.py module. The duplication in
> > nova/api/openstack/contrib/createserverext.py is going to be difficult to
> keep
> > up with. Like I said earlier, your code can live in the main codebase, you
> > just need to have an ExtensionDescriptor in contrib/ (which you do).
> Now, I have understood your comments correctly. I will revert my changes
> accordingly. I will also have to write my unit test cases in the
> test_server.py instead of test/api/openstack/contrip/test_createserverext.py.
>
> In one of your previous comments, you mention I have to limit these new
> changes only in the V1.1 without disturbing V1.0. To do this, I will need to
> add a new method _network_ref_from_req_data to both ControllerV11 and
> ControllerV10. ControllerV10 will simply return None whereas ControllerV11
> will return the actual network reference data.

Great. Don't worry about only exposing this in V1.1. It isn't changing the interface, it is just adding to it. We will cover this in the future once we have schema validation.

Revision history for this message
Tushar Patil (tpatil) wrote :

> Great. Don't worry about only exposing this in V1.1. It isn't changing the
> interface, it is just adding to it. We will cover this in the future once we
> have schema validation.

I need one more clarification to avoid rework. If I add everything in the create_instance_helper.py, my changes will be accessible by both OS V1.0 and V1.1 since CreateInstanceHelper class is used by both of the controllers. After schema validation will be implemented each controller will do the schema validation based on its own schema and take an action.

But till the time schema validation is not implemented, it is possible to create server with the required network information using below URIs. Is this acceptable?
http://10.2.3.150:8774/v1.1/os-create-server-ext
http://10.2.3.150:8774/v1.0/servers
http://10.2.3.150:8774/v1.1/servers

XML request
-------------
<?xml version="1.0" encoding="UTF-8"?>
<server name="new-server-test" flavorRef="1"
        imageRef="46">
  <metadata>
    <meta key="My Server Name">Apache1</meta>
  </metadata>
<personality/>
<networks>
    <network id="1" fixed_ip="10.0.1.15" />
</networks>
</server>

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> > Great. Don't worry about only exposing this in V1.1. It isn't changing the
> > interface, it is just adding to it. We will cover this in the future once we
> > have schema validation.
>
> I need one more clarification to avoid rework. If I add everything in the
> create_instance_helper.py, my changes will be accessible by both OS V1.0 and
> V1.1 since CreateInstanceHelper class is used by both of the controllers.
> After schema validation will be implemented each controller will do the schema
> validation based on its own schema and take an action.
>
> But till the time schema validation is not implemented, it is possible to
> create server with the required network information using below URIs. Is this
> acceptable?

Yep, this is fine.

Revision history for this message
Tushar Patil (tpatil) wrote :

Couple of new extensions are added into trunk because of which the test_extensions unit testcases are broken.

review: Needs Fixing
Revision history for this message
Tushar Patil (tpatil) wrote :

Fixed all broken unit testcases after merging my branch with the trunk changes. Please review.

Revision history for this message
dan wendlandt (danwent) wrote :

Hi Tushar,

Thanks for this work, it will indeed be very helpful. In fact, we need something quite similar for our QuantumManager work in nova. I've implemented this in another branch, but I don't see a point in trying to merge two sets of code that do basically the same thing. I think with one small tweak this code could work for us as well.

The change would be to no longer assume that the network-id must be an integer, but instead allow it to be a UUID (which is what quantum uses to identify networks).

This would apply in create_instance_helper.py, where the code calls:

network_id = int(network_id)

and then catches any resulting exceptions. It seems like that validation could actually be performed as part of the call to validate networks:

self.network_api.validate_networks(context, requested_networks)

This way, a NetworkManager way the typing could be specific to the network manager.

Thoughts?

Revision history for this message
Tushar Patil (tpatil) wrote :

> Hi Tushar,
>
> Thanks for this work, it will indeed be very helpful. In fact, we need
> something quite similar for our QuantumManager work in nova. I've implemented
> this in another branch, but I don't see a point in trying to merge two sets of
> code that do basically the same thing. I think with one small tweak this code
> could work for us as well.
>
> The change would be to no longer assume that the network-id must be an
> integer, but instead allow it to be a UUID (which is what quantum uses to
> identify networks).
>
> This would apply in create_instance_helper.py, where the code calls:
>
> network_id = int(network_id)
>
> and then catches any resulting exceptions. It seems like that validation
> could actually be performed as part of the call to validate networks:
>
> self.network_api.validate_networks(context, requested_networks)
>
> This way, a NetworkManager way the typing could be specific to the network
> manager.
>
> Thoughts?
It make sense to use uuid in order to comply with quantum.

I am thinking of generating this uuid for network in the db/api.py network_create_safe in the same manner as the uuid is generated for instances in the instance_create method. Next expose this uuid in the nova-manage network list command.

Revision history for this message
dan wendlandt (danwent) wrote :

On Mon, Aug 15, 2011 at 3:11 PM, Tushar Patil <email address hidden> wrote:

>
> >
> > This way, a NetworkManager way the typing could be specific to the
> network
> > manager.
> >
> > Thoughts?
> It make sense to use uuid in order to comply with quantum.
>

Great. The QuantumManager doesn't actually require Nova to store a UUID in
the networks table, as the QuantumManager simply uses the UUID in a call to
Quantum. So all I really need (I believe) is to remove the check that
forces the value to be an integer.

>
> I am thinking of generating this uuid for network in the db/api.py
> network_create_safe in the same manner as the uuid is generated for
> instances in the instance_create method. Next expose this uuid in the
> nova-manage network list command.
> --
>
> https://code.launchpad.net/~tpatil/nova/add-options-network-create-os-apis/+merge/68292
> You are subscribed to branch
> lp:~tpatil/nova/add-options-network-create-os-apis.
>

--
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Dan Wendlandt
Nicira Networks, Inc.
www.nicira.com | www.openvswitch.org
Sr. Product Manager
cell: 650-906-2650
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Revision history for this message
Tushar Patil (tpatil) wrote :

> Great. The QuantumManager doesn't actually require Nova to store a UUID in
> the networks table, as the QuantumManager simply uses the UUID in a call to
> Quantum. So all I really need (I believe) is to remove the check that
> forces the value to be an integer.
>
I really don't want to change the API format later if I decide to use quantum as network service.
Hence I wanted to make these changes in the diablo timeframe.

Revision history for this message
dan wendlandt (danwent) wrote :

On Tue, Aug 16, 2011 at 10:56 AM, Tushar Patil <email address hidden> wrote:

> > Great. The QuantumManager doesn't actually require Nova to store a UUID
> in
> > the networks table, as the QuantumManager simply uses the UUID in a call
> to
> > Quantum. So all I really need (I believe) is to remove the check that
> > forces the value to be an integer.
> >
> I really don't want to change the API format later if I decide to use
> quantum as network service.
> Hence I wanted to make these changes in the diablo timeframe.
>

great!

> --
>
> https://code.launchpad.net/~tpatil/nova/add-options-network-create-os-apis/+merge/68292
> You are subscribed to branch
> lp:~tpatil/nova/add-options-network-create-os-apis.
>

--
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Dan Wendlandt
Nicira Networks, Inc.
www.nicira.com | www.openvswitch.org
Sr. Product Manager
cell: 650-906-2650
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Revision history for this message
Tushar Patil (tpatil) wrote :

Quantum uses uuid for referencing networks instead of integer id. I have modified API format (networks section) to accept network as uuid instead of id so that this extension could be used with both new quantum and the existing network service.

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

> 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.

For this to be useful, it seems like we need to recycle ip addresses more quickly. I'm wondering about switching to expiring dnsmasq whenever an ip is released. This requires building the dhcp_release script that is in dnsmasq contrib and using it to manually release ips when they are disassociated.

Revision history for this message
Tushar Patil (tpatil) wrote :

>
> > 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.
>
> For this to be useful, it seems like we need to recycle ip addresses more
> quickly. I'm wondering about switching to expiring dnsmasq whenever an ip is
> released. This requires building the dhcp_release script that is in dnsmasq
> contrib and using it to manually release ips when they are disassociated.
It's a good idea. I think this can be worked upon later after this code is merged into trunk.

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

On Aug 17, 2011, at 11:31 AM, Tushar Patil wrote:

> It's a good idea. I think this can be worked upon later after this code is merged into trunk.

Agreed. I have monty putting the needed scripts for dnsmasq in our ppa. I made a bug to track the issue here:

https://bugs.launchpad.net/nova/+bug/828267

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Sorry this has sat so long, Tushar! I like the code quite a bit, bug being such a big merge prop, I did find a handful of things:

267: I'm curious why you chose to break this out into a separate function. Can you please explain why?

454: Can this function simply be 'fixed_ip_associate'? I think the 'by_address' part is implied. Please correct me if I'm wrong here.

477: This name could be made a bit clearer by avoiding repetition of 'network'. 'network_get_all_by_uuids' might be a better option

492: I think this function can be accomplished with a keyword argument (maybe 'project'?) to the network_get_networks_by_uuids function.

509: I'm a bit confused by this change. I could use some help understanding what is going on.

520: I think this behavior could lead to some unexpected consequences. What is the motive behind supporting this? If I understand what is happening correctly, I would explicitly call fixed_ip_associate_pool from any client code I wrote.

628: This migration number needs to increase.

695: I don't see this exception class as adding much value at this point. How would you feel about just removing it?

747: This could slow down the code quite a bit. Is it necessary to do this over rpc, or can the network api just take care of the validation?

871/956: These functions are extremely similar. I think you could write it in such a way that you didn't need to copy it into the VlanManager.

Feel free to contact me in IRC to discuss any of these changes :)

Revision history for this message
Tushar Patil (tpatil) wrote :

> Sorry this has sat so long, Tushar! I like the code quite a bit, bug being
> such a big merge prop, I did find a handful of things:
>
> 267: I'm curious why you chose to break this out into a separate function. Can
> you please explain why?

Reverted back to its original state. Fixed

> 454: Can this function simply be 'fixed_ip_associate'? I think the
> 'by_address' part is implied. Please correct me if I'm wrong here.

Fixed as per our discussion IRC, I have combined the functionality of existing fixed_ip_associate and newly added method fixed_ip_associate_by_address into one in the fixed_ip_associate method.

> 477: This name could be made a bit clearer by avoiding repetition of
> 'network'. 'network_get_all_by_uuids' might be a better option

Fixed

> 492: I think this function can be accomplished with a keyword argument (maybe
> 'project'?) to the network_get_networks_by_uuids function.

Fixed

> 509: I'm a bit confused by this change. I could use some help understanding
> what is going on.
I really don't know how this change happen from my side. I have reverted it back to its original state.Fixed

> 520: I think this behavior could lead to some unexpected consequences. What is
> the motive behind supporting this? If I understand what is happening
> correctly, I would explicitly call fixed_ip_associate_pool from any client
> code I wrote.

Fixed

> 628: This migration number needs to increase.

Fixed

> 695: I don't see this exception class as adding much value at this point. How
> would you feel about just removing it?

Removed this exception class.Fixed

> 747: This could slow down the code quite a bit. Is it necessary to do this
> over rpc, or can the network api just take care of the validation?

Validation is different for different network models and I don't think it make sense to do this validation based on the network_manager flag in the network/api.py, so rpc call is necessary.
Do you have any suggestion here?

> 871/956: These functions are extremely similar. I think you could write it in
> such a way that you didn't need to copy it into the VlanManager.

Fixed

Please review

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Looks great, Tushar!

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

looks good.

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

Attempt to merge into lp:nova failed due to conflicts:

text conflict in nova/api/openstack/create_instance_helper.py
text conflict in nova/compute/api.py
text conflict in nova/tests/api/openstack/test_servers.py

Revision history for this message
Tushar Patil (tpatil) wrote :

Resolved all conflicts and fixed broken unit tests in the test_createservertext.py due to the changes in the extension which now includes ProjectMapper.

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

thx, trying again.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/nova-manage'
2--- bin/nova-manage 2011-08-15 20:33:37 +0000
3+++ bin/nova-manage 2011-08-22 23:37:26 +0000
4@@ -763,23 +763,26 @@
5
6 def list(self):
7 """List all created networks"""
8- print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (
9- _('IPv4'),
10- _('IPv6'),
11- _('start address'),
12- _('DNS1'),
13- _('DNS2'),
14- _('VlanID'),
15- 'project')
16+ _fmt = "%-5s\t%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s"
17+ print _fmt % (_('id'),
18+ _('IPv4'),
19+ _('IPv6'),
20+ _('start address'),
21+ _('DNS1'),
22+ _('DNS2'),
23+ _('VlanID'),
24+ _('project'),
25+ _("uuid"))
26 for network in db.network_get_all(context.get_admin_context()):
27- print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (
28- network.cidr,
29- network.cidr_v6,
30- network.dhcp_start,
31- network.dns1,
32- network.dns2,
33- network.vlan,
34- network.project_id)
35+ print _fmt % (network.id,
36+ network.cidr,
37+ network.cidr_v6,
38+ network.dhcp_start,
39+ network.dns1,
40+ network.dns2,
41+ network.vlan,
42+ network.project_id,
43+ network.uuid)
44
45 @args('--network', dest="fixed_range", metavar='<x.x.x.x/yy>',
46 help='Network to delete')
47
48=== added file 'nova/api/openstack/contrib/createserverext.py'
49--- nova/api/openstack/contrib/createserverext.py 1970-01-01 00:00:00 +0000
50+++ nova/api/openstack/contrib/createserverext.py 2011-08-22 23:37:26 +0000
51@@ -0,0 +1,66 @@
52+# vim: tabstop=4 shiftwidth=4 softtabstop=4
53+
54+# Copyright 2011 OpenStack LLC.
55+#
56+# Licensed under the Apache License, Version 2.0 (the "License"); you may
57+# not use this file except in compliance with the License. You may obtain
58+# a copy of the License at
59+#
60+# http://www.apache.org/licenses/LICENSE-2.0
61+#
62+# Unless required by applicable law or agreed to in writing, software
63+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
64+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
65+# License for the specific language governing permissions and limitations
66+# under the License
67+
68+from nova.api.openstack import create_instance_helper as helper
69+from nova.api.openstack import extensions
70+from nova.api.openstack import servers
71+from nova.api.openstack import wsgi
72+
73+
74+class Createserverext(extensions.ExtensionDescriptor):
75+ """The servers create ext
76+
77+ Exposes addFixedIp and removeFixedIp actions on servers.
78+
79+ """
80+ def get_name(self):
81+ return "Createserverext"
82+
83+ def get_alias(self):
84+ return "os-create-server-ext"
85+
86+ def get_description(self):
87+ return "Extended support to the Create Server v1.1 API"
88+
89+ def get_namespace(self):
90+ return "http://docs.openstack.org/ext/createserverext/api/v1.1"
91+
92+ def get_updated(self):
93+ return "2011-07-19T00:00:00+00:00"
94+
95+ def get_resources(self):
96+ resources = []
97+
98+ headers_serializer = servers.HeadersSerializer()
99+ body_serializers = {
100+ 'application/xml': servers.ServerXMLSerializer(),
101+ }
102+
103+ body_deserializers = {
104+ 'application/xml': helper.ServerXMLDeserializerV11(),
105+ }
106+
107+ serializer = wsgi.ResponseSerializer(body_serializers,
108+ headers_serializer)
109+ deserializer = wsgi.RequestDeserializer(body_deserializers)
110+
111+ res = extensions.ResourceExtension('os-create-server-ext',
112+ controller=servers.ControllerV11(),
113+ deserializer=deserializer,
114+ serializer=serializer)
115+ resources.append(res)
116+
117+ return resources
118
119=== modified file 'nova/api/openstack/create_instance_helper.py'
120--- nova/api/openstack/create_instance_helper.py 2011-08-22 03:17:04 +0000
121+++ nova/api/openstack/create_instance_helper.py 2011-08-22 23:37:26 +0000
122@@ -29,7 +29,7 @@
123 from nova.compute import instance_types
124 from nova.api.openstack import common
125 from nova.api.openstack import wsgi
126-
127+from nova.rpc.common import RemoteError
128
129 LOG = logging.getLogger('nova.api.openstack.create_instance_helper')
130 FLAGS = flags.FLAGS
131@@ -120,6 +120,11 @@
132
133 sg_names = list(set(sg_names))
134
135+ requested_networks = server_dict.get('networks')
136+ if requested_networks is not None:
137+ requested_networks = self._get_requested_networks(
138+ requested_networks)
139+
140 try:
141 flavor_id = self.controller._flavor_id_from_req_data(body)
142 except ValueError as error:
143@@ -175,6 +180,7 @@
144 reservation_id=reservation_id,
145 min_count=min_count,
146 max_count=max_count,
147+ requested_networks=requested_networks,
148 security_group=sg_names,
149 user_data=user_data,
150 availability_zone=availability_zone))
151@@ -188,6 +194,10 @@
152 raise exc.HTTPBadRequest(explanation=msg)
153 except exception.SecurityGroupNotFound as error:
154 raise exc.HTTPBadRequest(explanation=unicode(error))
155+ except RemoteError as err:
156+ msg = "%(err_type)s: %(err_msg)s" % \
157+ {'err_type': err.exc_type, 'err_msg': err.value}
158+ raise exc.HTTPBadRequest(explanation=msg)
159 # Let the caller deal with unhandled exceptions.
160
161 def _handle_quota_error(self, error):
162@@ -316,6 +326,46 @@
163 raise exc.HTTPBadRequest(explanation=msg)
164 return password
165
166+ def _get_requested_networks(self, requested_networks):
167+ """
168+ Create a list of requested networks from the networks attribute
169+ """
170+ networks = []
171+ for network in requested_networks:
172+ try:
173+ network_uuid = network['uuid']
174+
175+ if not utils.is_uuid_like(network_uuid):
176+ msg = _("Bad networks format: network uuid is not in"
177+ " proper format (%s)") % network_uuid
178+ raise exc.HTTPBadRequest(explanation=msg)
179+
180+ #fixed IP address is optional
181+ #if the fixed IP address is not provided then
182+ #it will use one of the available IP address from the network
183+ address = network.get('fixed_ip', None)
184+ if address is not None and not utils.is_valid_ipv4(address):
185+ msg = _("Invalid fixed IP address (%s)") % address
186+ raise exc.HTTPBadRequest(explanation=msg)
187+ # check if the network id is already present in the list,
188+ # we don't want duplicate networks to be passed
189+ # at the boot time
190+ for id, ip in networks:
191+ if id == network_uuid:
192+ expl = _("Duplicate networks (%s) are not allowed")\
193+ % network_uuid
194+ raise exc.HTTPBadRequest(explanation=expl)
195+
196+ networks.append((network_uuid, address))
197+ except KeyError as key:
198+ expl = _('Bad network format: missing %s') % key
199+ raise exc.HTTPBadRequest(explanation=expl)
200+ except TypeError:
201+ expl = _('Bad networks format')
202+ raise exc.HTTPBadRequest(explanation=expl)
203+
204+ return networks
205+
206
207 class ServerXMLDeserializer(wsgi.XMLDeserializer):
208 """
209@@ -480,6 +530,10 @@
210 if personality is not None:
211 server["personality"] = personality
212
213+ networks = self._extract_networks(server_node)
214+ if networks is not None:
215+ server["networks"] = networks
216+
217 security_groups = self._extract_security_groups(server_node)
218 if security_groups is not None:
219 server["security_groups"] = security_groups
220@@ -501,6 +555,23 @@
221 else:
222 return None
223
224+ def _extract_networks(self, server_node):
225+ """Marshal the networks attribute of a parsed request"""
226+ node = self.find_first_child_named(server_node, "networks")
227+ if node is not None:
228+ networks = []
229+ for network_node in self.find_children_named(node,
230+ "network"):
231+ item = {}
232+ if network_node.hasAttribute("uuid"):
233+ item["uuid"] = network_node.getAttribute("uuid")
234+ if network_node.hasAttribute("fixed_ip"):
235+ item["fixed_ip"] = network_node.getAttribute("fixed_ip")
236+ networks.append(item)
237+ return networks
238+ else:
239+ return None
240+
241 def _extract_security_groups(self, server_node):
242 """Marshal the security_groups attribute of a parsed request"""
243 node = self.find_first_child_named(server_node, "security_groups")
244
245=== modified file 'nova/compute/api.py'
246--- nova/compute/api.py 2011-08-22 03:17:04 +0000
247+++ nova/compute/api.py 2011-08-22 23:37:26 +0000
248@@ -146,6 +146,16 @@
249 LOG.warn(msg)
250 raise quota.QuotaError(msg, "MetadataLimitExceeded")
251
252+ def _check_requested_networks(self, context, requested_networks):
253+ """ Check if the networks requested belongs to the project
254+ and the fixed IP address for each network provided is within
255+ same the network block
256+ """
257+ if requested_networks is None:
258+ return
259+
260+ self.network_api.validate_networks(context, requested_networks)
261+
262 def _check_create_parameters(self, context, instance_type,
263 image_href, kernel_id=None, ramdisk_id=None,
264 min_count=None, max_count=None,
265@@ -153,7 +163,8 @@
266 key_name=None, key_data=None, security_group='default',
267 availability_zone=None, user_data=None, metadata=None,
268 injected_files=None, admin_password=None, zone_blob=None,
269- reservation_id=None, access_ip_v4=None, access_ip_v6=None):
270+ reservation_id=None, access_ip_v4=None, access_ip_v6=None,
271+ requested_networks=None):
272 """Verify all the input parameters regardless of the provisioning
273 strategy being performed."""
274
275@@ -182,6 +193,7 @@
276
277 self._check_metadata_properties_quota(context, metadata)
278 self._check_injected_file_quota(context, injected_files)
279+ self._check_requested_networks(context, requested_networks)
280
281 (image_service, image_id) = nova.image.get_image_service(image_href)
282 image = image_service.show(context, image_id)
283@@ -400,9 +412,9 @@
284 def _ask_scheduler_to_create_instance(self, context, base_options,
285 instance_type, zone_blob,
286 availability_zone, injected_files,
287- admin_password,
288- image,
289- instance_id=None, num_instances=1):
290+ admin_password, image,
291+ instance_id=None, num_instances=1,
292+ requested_networks=None):
293 """Send the run_instance request to the schedulers for processing."""
294 pid = context.project_id
295 uid = context.user_id
296@@ -430,7 +442,8 @@
297 "request_spec": request_spec,
298 "availability_zone": availability_zone,
299 "admin_password": admin_password,
300- "injected_files": injected_files}})
301+ "injected_files": injected_files,
302+ "requested_networks": requested_networks}})
303
304 def create_all_at_once(self, context, instance_type,
305 image_href, kernel_id=None, ramdisk_id=None,
306@@ -440,7 +453,8 @@
307 availability_zone=None, user_data=None, metadata=None,
308 injected_files=None, admin_password=None, zone_blob=None,
309 reservation_id=None, block_device_mapping=None,
310- access_ip_v4=None, access_ip_v6=None):
311+ access_ip_v4=None, access_ip_v6=None,
312+ requested_networks=None):
313 """Provision the instances by passing the whole request to
314 the Scheduler for execution. Returns a Reservation ID
315 related to the creation of all of these instances."""
316@@ -456,14 +470,15 @@
317 key_name, key_data, security_group,
318 availability_zone, user_data, metadata,
319 injected_files, admin_password, zone_blob,
320- reservation_id, access_ip_v4, access_ip_v6)
321+ reservation_id, access_ip_v4, access_ip_v6,
322+ requested_networks)
323
324 self._ask_scheduler_to_create_instance(context, base_options,
325 instance_type, zone_blob,
326 availability_zone, injected_files,
327- admin_password,
328- image,
329- num_instances=num_instances)
330+ admin_password, image,
331+ num_instances=num_instances,
332+ requested_networks=requested_networks)
333
334 return base_options['reservation_id']
335
336@@ -475,7 +490,8 @@
337 availability_zone=None, user_data=None, metadata=None,
338 injected_files=None, admin_password=None, zone_blob=None,
339 reservation_id=None, block_device_mapping=None,
340- access_ip_v4=None, access_ip_v6=None):
341+ access_ip_v4=None, access_ip_v6=None,
342+ requested_networks=None):
343 """
344 Provision the instances by sending off a series of single
345 instance requests to the Schedulers. This is fine for trival
346@@ -499,7 +515,8 @@
347 key_name, key_data, security_group,
348 availability_zone, user_data, metadata,
349 injected_files, admin_password, zone_blob,
350- reservation_id, access_ip_v4, access_ip_v6)
351+ reservation_id, access_ip_v4, access_ip_v6,
352+ requested_networks)
353
354 block_device_mapping = block_device_mapping or []
355 instances = []
356@@ -513,11 +530,11 @@
357 instance_id = instance['id']
358
359 self._ask_scheduler_to_create_instance(context, base_options,
360- instance_type, zone_blob,
361- availability_zone, injected_files,
362- admin_password,
363- image,
364- instance_id=instance_id)
365+ instance_type, zone_blob,
366+ availability_zone, injected_files,
367+ admin_password, image,
368+ instance_id=instance_id,
369+ requested_networks=requested_networks)
370
371 return [dict(x.iteritems()) for x in instances]
372
373
374=== modified file 'nova/compute/manager.py'
375--- nova/compute/manager.py 2011-08-18 20:44:30 +0000
376+++ nova/compute/manager.py 2011-08-22 23:37:26 +0000
377@@ -382,6 +382,8 @@
378 context = context.elevated()
379 instance = self.db.instance_get(context, instance_id)
380
381+ requested_networks = kwargs.get('requested_networks', None)
382+
383 if instance['name'] in self.driver.list_instances():
384 raise exception.Error(_("Instance has already been created"))
385
386@@ -411,7 +413,8 @@
387 # will eventually also need to save the address here.
388 if not FLAGS.stub_network:
389 network_info = self.network_api.allocate_for_instance(context,
390- instance, vpn=is_vpn)
391+ instance, vpn=is_vpn,
392+ requested_networks=requested_networks)
393 LOG.debug(_("instance network_info: |%s|"), network_info)
394 else:
395 # TODO(tr3buchet) not really sure how this should be handled.
396
397=== modified file 'nova/db/api.py'
398--- nova/db/api.py 2011-08-18 00:17:47 +0000
399+++ nova/db/api.py 2011-08-22 23:37:26 +0000
400@@ -323,13 +323,13 @@
401 ####################
402
403
404-def fixed_ip_associate(context, address, instance_id):
405+def fixed_ip_associate(context, address, instance_id, network_id=None):
406 """Associate fixed ip to instance.
407
408 Raises if fixed ip is not available.
409
410 """
411- return IMPL.fixed_ip_associate(context, address, instance_id)
412+ return IMPL.fixed_ip_associate(context, address, instance_id, network_id)
413
414
415 def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None):
416@@ -396,7 +396,6 @@
417 """Create a fixed ip from the values dictionary."""
418 return IMPL.fixed_ip_update(context, address, values)
419
420-
421 ####################
422
423
424@@ -686,7 +685,14 @@
425 return IMPL.network_get_all(context)
426
427
428+def network_get_all_by_uuids(context, network_uuids, project_id=None):
429+ """Return networks by ids."""
430+ return IMPL.network_get_all_by_uuids(context, network_uuids, project_id)
431+
432+
433 # pylint: disable=C0103
434+
435+
436 def network_get_associated_fixed_ips(context, network_id):
437 """Get all network's ips that have been associated."""
438 return IMPL.network_get_associated_fixed_ips(context, network_id)
439
440=== modified file 'nova/db/sqlalchemy/api.py'
441--- nova/db/sqlalchemy/api.py 2011-08-20 20:15:05 +0000
442+++ nova/db/sqlalchemy/api.py 2011-08-22 23:37:26 +0000
443@@ -652,23 +652,36 @@
444 ###################
445
446
447-@require_context
448-def fixed_ip_associate(context, address, instance_id):
449+@require_admin_context
450+def fixed_ip_associate(context, address, instance_id, network_id=None):
451 session = get_session()
452 with session.begin():
453- instance = instance_get(context, instance_id, session=session)
454+ network_or_none = or_(models.FixedIp.network_id == network_id,
455+ models.FixedIp.network_id == None)
456 fixed_ip_ref = session.query(models.FixedIp).\
457+ filter(network_or_none).\
458+ filter_by(reserved=False).\
459+ filter_by(deleted=False).\
460 filter_by(address=address).\
461- filter_by(deleted=False).\
462- filter_by(instance=None).\
463 with_lockmode('update').\
464 first()
465 # NOTE(vish): if with_lockmode isn't supported, as in sqlite,
466 # then this has concurrency issues
467- if not fixed_ip_ref:
468- raise exception.NoMoreFixedIps()
469- fixed_ip_ref.instance = instance
470+ if fixed_ip_ref is None:
471+ raise exception.FixedIpNotFoundForNetwork(address=address,
472+ network_id=network_id)
473+ if fixed_ip_ref.instance is not None:
474+ raise exception.FixedIpAlreadyInUse(address=address)
475+
476+ if not fixed_ip_ref.network:
477+ fixed_ip_ref.network = network_get(context,
478+ network_id,
479+ session=session)
480+ fixed_ip_ref.instance = instance_get(context,
481+ instance_id,
482+ session=session)
483 session.add(fixed_ip_ref)
484+ return fixed_ip_ref['address']
485
486
487 @require_admin_context
488@@ -1755,6 +1768,40 @@
489 return result
490
491
492+@require_admin_context
493+def network_get_all_by_uuids(context, network_uuids, project_id=None):
494+ session = get_session()
495+ project_or_none = or_(models.Network.project_id == project_id,
496+ models.Network.project_id == None)
497+ result = session.query(models.Network).\
498+ filter(models.Network.uuid.in_(network_uuids)).\
499+ filter(project_or_none).\
500+ filter_by(deleted=False).all()
501+ if not result:
502+ raise exception.NoNetworksFound()
503+
504+ #check if host is set to all of the networks
505+ # returned in the result
506+ for network in result:
507+ if network['host'] is None:
508+ raise exception.NetworkHostNotSet(network_id=network['id'])
509+
510+ #check if the result contains all the networks
511+ #we are looking for
512+ for network_uuid in network_uuids:
513+ found = False
514+ for network in result:
515+ if network['uuid'] == network_uuid:
516+ found = True
517+ break
518+ if not found:
519+ if project_id:
520+ raise exception.NetworkNotFoundForProject(network_uuid=uuid,
521+ project_id=context.project_id)
522+ raise exception.NetworkNotFound(network_id=network_uuid)
523+
524+ return result
525+
526 # NOTE(vish): pylint complains because of the long method name, but
527 # it fits with the names of the rest of the methods
528 # pylint: disable=C0103
529
530=== added file 'nova/db/sqlalchemy/migrate_repo/versions/040_add_uuid_to_networks.py'
531--- nova/db/sqlalchemy/migrate_repo/versions/040_add_uuid_to_networks.py 1970-01-01 00:00:00 +0000
532+++ nova/db/sqlalchemy/migrate_repo/versions/040_add_uuid_to_networks.py 2011-08-22 23:37:26 +0000
533@@ -0,0 +1,43 @@
534+# vim: tabstop=4 shiftwidth=4 softtabstop=4
535+
536+# Copyright 2011 OpenStack LLC.
537+#
538+# Licensed under the Apache License, Version 2.0 (the "License"); you may
539+# not use this file except in compliance with the License. You may obtain
540+# a copy of the License at
541+#
542+# http://www.apache.org/licenses/LICENSE-2.0
543+#
544+# Unless required by applicable law or agreed to in writing, software
545+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
546+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
547+# License for the specific language governing permissions and limitations
548+# under the License.
549+
550+from sqlalchemy import Column, Integer, MetaData, String, Table
551+
552+from nova import utils
553+
554+
555+meta = MetaData()
556+
557+networks = Table("networks", meta,
558+ Column("id", Integer(), primary_key=True, nullable=False))
559+uuid_column = Column("uuid", String(36))
560+
561+
562+def upgrade(migrate_engine):
563+ meta.bind = migrate_engine
564+ networks.create_column(uuid_column)
565+
566+ rows = migrate_engine.execute(networks.select())
567+ for row in rows:
568+ networks_uuid = str(utils.gen_uuid())
569+ migrate_engine.execute(networks.update()\
570+ .where(networks.c.id == row[0])\
571+ .values(uuid=networks_uuid))
572+
573+
574+def downgrade(migrate_engine):
575+ meta.bind = migrate_engine
576+ networks.drop_column(uuid_column)
577
578=== modified file 'nova/db/sqlalchemy/models.py'
579--- nova/db/sqlalchemy/models.py 2011-08-19 15:26:32 +0000
580+++ nova/db/sqlalchemy/models.py 2011-08-22 23:37:26 +0000
581@@ -561,6 +561,7 @@
582
583 project_id = Column(String(255))
584 host = Column(String(255)) # , ForeignKey('hosts.id'))
585+ uuid = Column(String(36))
586
587
588 class VirtualInterface(BASE, NovaBase):
589
590=== modified file 'nova/exception.py'
591--- nova/exception.py 2011-08-20 22:38:13 +0000
592+++ nova/exception.py 2011-08-22 23:37:26 +0000
593@@ -423,6 +423,15 @@
594 message = _("No networks defined.")
595
596
597+class NetworkNotFoundForProject(NotFound):
598+ message = _("Either Network uuid %(network_uuid)s is not present or "
599+ "is not assigned to the project %(project_id)s.")
600+
601+
602+class NetworkHostNotSet(NovaException):
603+ message = _("Host is not set to the network (%(network_id)s).")
604+
605+
606 class DatastoreNotFound(NotFound):
607 message = _("Could not find the datastore reference(s) which the VM uses.")
608
609@@ -456,6 +465,19 @@
610 message = _("Host %(host)s has zero fixed ips.")
611
612
613+class FixedIpNotFoundForNetwork(FixedIpNotFound):
614+ message = _("Fixed IP address (%(address)s) does not exist in "
615+ "network (%(network_uuid)s).")
616+
617+
618+class FixedIpAlreadyInUse(NovaException):
619+ message = _("Fixed IP address %(address)s is already in use.")
620+
621+
622+class FixedIpInvalid(Invalid):
623+ message = _("Fixed IP address %(address)s is invalid.")
624+
625+
626 class NoMoreFixedIps(Error):
627 message = _("Zero fixed ips available.")
628
629
630=== modified file 'nova/network/api.py'
631--- nova/network/api.py 2011-07-27 21:39:27 +0000
632+++ nova/network/api.py 2011-08-22 23:37:26 +0000
633@@ -195,3 +195,12 @@
634 return rpc.call(context, FLAGS.network_topic,
635 {'method': 'get_instance_nw_info',
636 'args': args})
637+
638+ def validate_networks(self, context, requested_networks):
639+ """validate the networks passed at the time of creating
640+ the server
641+ """
642+ args = {'networks': requested_networks}
643+ return rpc.call(context, FLAGS.network_topic,
644+ {'method': 'validate_networks',
645+ 'args': args})
646
647=== modified file 'nova/network/manager.py'
648--- nova/network/manager.py 2011-08-18 06:50:50 +0000
649+++ nova/network/manager.py 2011-08-22 23:37:26 +0000
650@@ -131,7 +131,15 @@
651 green_pool = greenpool.GreenPool()
652
653 vpn = kwargs.pop('vpn')
654+ requested_networks = kwargs.pop('requested_networks')
655+
656 for network in networks:
657+ address = None
658+ if requested_networks is not None:
659+ for address in (fixed_ip for (uuid, fixed_ip) in \
660+ requested_networks if network['uuid'] == uuid):
661+ break
662+
663 # NOTE(vish): if we are not multi_host pass to the network host
664 if not network['multi_host']:
665 host = network['host']
666@@ -148,6 +156,7 @@
667 args = {}
668 args['instance_id'] = instance_id
669 args['network_id'] = network['id']
670+ args['address'] = address
671 args['vpn'] = vpn
672
673 green_pool.spawn_n(rpc.call, context, topic,
674@@ -155,7 +164,8 @@
675 'args': args})
676 else:
677 # i am the correct host, run here
678- self.allocate_fixed_ip(context, instance_id, network, vpn=vpn)
679+ self.allocate_fixed_ip(context, instance_id, network,
680+ vpn=vpn, address=address)
681
682 # wait for all of the allocates (if any) to finish
683 green_pool.waitall()
684@@ -199,6 +209,7 @@
685 """
686 instance_id = kwargs.get('instance_id')
687 project_id = kwargs.get('project_id')
688+ requested_networks = kwargs.get('requested_networks')
689 LOG.debug(_("floating IP allocation for instance |%s|"), instance_id,
690 context=context)
691 # call the next inherited class's allocate_for_instance()
692@@ -380,16 +391,21 @@
693 self.compute_api.trigger_security_group_members_refresh(admin_context,
694 group_ids)
695
696- def _get_networks_for_instance(self, context, instance_id, project_id):
697+ def _get_networks_for_instance(self, context, instance_id, project_id,
698+ requested_networks=None):
699 """Determine & return which networks an instance should connect to."""
700 # TODO(tr3buchet) maybe this needs to be updated in the future if
701 # there is a better way to determine which networks
702 # a non-vlan instance should connect to
703- try:
704- networks = self.db.network_get_all(context)
705- except exception.NoNetworksFound:
706- return []
707-
708+ if requested_networks is not None and len(requested_networks) != 0:
709+ network_uuids = [uuid for (uuid, fixed_ip) in requested_networks]
710+ networks = self.db.network_get_all_by_uuids(context,
711+ network_uuids)
712+ else:
713+ try:
714+ networks = self.db.network_get_all(context)
715+ except exception.NoNetworksFound:
716+ return []
717 # return only networks which are not vlan networks
718 return [network for network in networks if
719 not network['vlan']]
720@@ -403,16 +419,18 @@
721 host = kwargs.pop('host')
722 project_id = kwargs.pop('project_id')
723 type_id = kwargs.pop('instance_type_id')
724+ requested_networks = kwargs.get('requested_networks')
725 vpn = kwargs.pop('vpn')
726 admin_context = context.elevated()
727 LOG.debug(_("network allocations for instance %s"), instance_id,
728 context=context)
729- networks = self._get_networks_for_instance(admin_context, instance_id,
730- project_id)
731- LOG.warn(networks)
732+ networks = self._get_networks_for_instance(admin_context,
733+ instance_id, project_id,
734+ requested_networks=requested_networks)
735 self._allocate_mac_addresses(context, instance_id, networks)
736- self._allocate_fixed_ips(admin_context, instance_id, host, networks,
737- vpn=vpn)
738+ self._allocate_fixed_ips(admin_context, instance_id,
739+ host, networks, vpn=vpn,
740+ requested_networks=requested_networks)
741 return self.get_instance_nw_info(context, instance_id, type_id, host)
742
743 def deallocate_for_instance(self, context, **kwargs):
744@@ -570,9 +588,15 @@
745 # network_get_by_compute_host
746 address = None
747 if network['cidr']:
748- address = self.db.fixed_ip_associate_pool(context.elevated(),
749- network['id'],
750- instance_id)
751+ address = kwargs.get('address', None)
752+ if address:
753+ address = self.db.fixed_ip_associate(context,
754+ address, instance_id,
755+ network['id'])
756+ else:
757+ address = self.db.fixed_ip_associate_pool(context.elevated(),
758+ network['id'],
759+ instance_id)
760 self._do_trigger_security_group_members_refresh_for_instance(
761 instance_id)
762 get_vif = self.db.virtual_interface_get_by_instance_and_network
763@@ -798,6 +822,35 @@
764 """Sets up network on this host."""
765 raise NotImplementedError()
766
767+ def validate_networks(self, context, networks):
768+ """check if the networks exists and host
769+ is set to each network.
770+ """
771+ if networks is None or len(networks) == 0:
772+ return
773+
774+ network_uuids = [uuid for (uuid, fixed_ip) in networks]
775+
776+ self._get_networks_by_uuids(context, network_uuids)
777+
778+ for network_uuid, address in networks:
779+ # check if the fixed IP address is valid and
780+ # it actually belongs to the network
781+ if address is not None:
782+ if not utils.is_valid_ipv4(address):
783+ raise exception.FixedIpInvalid(address=address)
784+
785+ fixed_ip_ref = self.db.fixed_ip_get_by_address(context,
786+ address)
787+ if fixed_ip_ref['network']['uuid'] != network_uuid:
788+ raise exception.FixedIpNotFoundForNetwork(address=address,
789+ network_uuid=network_uuid)
790+ if fixed_ip_ref['instance'] is not None:
791+ raise exception.FixedIpAlreadyInUse(address=address)
792+
793+ def _get_networks_by_uuids(self, context, network_uuids):
794+ return self.db.network_get_all_by_uuids(context, network_uuids)
795+
796
797 class FlatManager(NetworkManager):
798 """Basic network where no vlans are used.
799@@ -832,8 +885,16 @@
800 def _allocate_fixed_ips(self, context, instance_id, host, networks,
801 **kwargs):
802 """Calls allocate_fixed_ip once for each network."""
803+ requested_networks = kwargs.pop('requested_networks')
804 for network in networks:
805- self.allocate_fixed_ip(context, instance_id, network)
806+ address = None
807+ if requested_networks is not None:
808+ for address in (fixed_ip for (uuid, fixed_ip) in \
809+ requested_networks if network['uuid'] == uuid):
810+ break
811+
812+ self.allocate_fixed_ip(context, instance_id,
813+ network, address=address)
814
815 def deallocate_fixed_ip(self, context, address, **kwargs):
816 """Returns a fixed ip to the pool."""
817@@ -927,9 +988,15 @@
818 address,
819 instance_id)
820 else:
821- address = self.db.fixed_ip_associate_pool(context,
822- network['id'],
823- instance_id)
824+ address = kwargs.get('address', None)
825+ if address:
826+ address = self.db.fixed_ip_associate(context, address,
827+ instance_id,
828+ network['id'])
829+ else:
830+ address = self.db.fixed_ip_associate_pool(context,
831+ network['id'],
832+ instance_id)
833 self._do_trigger_security_group_members_refresh_for_instance(
834 instance_id)
835 vif = self.db.virtual_interface_get_by_instance_and_network(context,
836@@ -945,10 +1012,18 @@
837 """Force adds another network to a project."""
838 self.db.network_associate(context, project_id, force=True)
839
840- def _get_networks_for_instance(self, context, instance_id, project_id):
841+ def _get_networks_for_instance(self, context, instance_id, project_id,
842+ requested_networks=None):
843 """Determine which networks an instance should connect to."""
844 # get networks associated with project
845- return self.db.project_get_networks(context, project_id)
846+ if requested_networks is not None and len(requested_networks) != 0:
847+ network_uuids = [uuid for (uuid, fixed_ip) in requested_networks]
848+ networks = self.db.network_get_all_by_uuids(context,
849+ network_uuids,
850+ project_id)
851+ else:
852+ networks = self.db.project_get_networks(context, project_id)
853+ return networks
854
855 def create_networks(self, context, **kwargs):
856 """Create networks based on parameters."""
857@@ -997,6 +1072,10 @@
858 self.db.network_update(context, network_ref['id'],
859 {'gateway_v6': gateway})
860
861+ def _get_networks_by_uuids(self, context, network_uuids):
862+ return self.db.network_get_all_by_uuids(context, network_uuids,
863+ context.project_id)
864+
865 @property
866 def _bottom_reserved_ips(self):
867 """Number of reserved ips at the bottom of the range."""
868
869=== added file 'nova/tests/api/openstack/contrib/test_createserverext.py'
870--- nova/tests/api/openstack/contrib/test_createserverext.py 1970-01-01 00:00:00 +0000
871+++ nova/tests/api/openstack/contrib/test_createserverext.py 2011-08-22 23:37:26 +0000
872@@ -0,0 +1,306 @@
873+# vim: tabstop=4 shiftwidth=4 softtabstop=4
874+
875+# Copyright 2010-2011 OpenStack LLC.
876+# All Rights Reserved.
877+#
878+# Licensed under the Apache License, Version 2.0 (the "License"); you may
879+# not use this file except in compliance with the License. You may obtain
880+# a copy of the License at
881+#
882+# http://www.apache.org/licenses/LICENSE-2.0
883+#
884+# Unless required by applicable law or agreed to in writing, software
885+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
886+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
887+# License for the specific language governing permissions and limitations
888+# under the License.
889+
890+import base64
891+import json
892+import unittest
893+from xml.dom import minidom
894+
895+import stubout
896+import webob
897+
898+from nova import exception
899+from nova import flags
900+from nova import test
901+from nova import utils
902+import nova.api.openstack
903+from nova.api.openstack import servers
904+from nova.api.openstack.contrib import createserverext
905+import nova.compute.api
906+
907+import nova.scheduler.api
908+import nova.image.fake
909+import nova.rpc
910+from nova.tests.api.openstack import fakes
911+
912+
913+FLAGS = flags.FLAGS
914+FLAGS.verbose = True
915+
916+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
917+
918+FAKE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'),
919+ ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '10.0.2.12')]
920+
921+DUPLICATE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'),
922+ ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12')]
923+
924+INVALID_NETWORKS = [('invalid', 'invalid-ip-address')]
925+
926+
927+class CreateserverextTest(test.TestCase):
928+
929+ def setUp(self):
930+ super(CreateserverextTest, self).setUp()
931+ self.stubs = stubout.StubOutForTesting()
932+ fakes.FakeAuthManager.auth_data = {}
933+ fakes.FakeAuthDatabase.data = {}
934+ fakes.stub_out_auth(self.stubs)
935+ fakes.stub_out_image_service(self.stubs)
936+ fakes.stub_out_key_pair_funcs(self.stubs)
937+ self.allow_admin = FLAGS.allow_admin_api
938+
939+ def tearDown(self):
940+ self.stubs.UnsetAll()
941+ FLAGS.allow_admin_api = self.allow_admin
942+ super(CreateserverextTest, self).tearDown()
943+
944+ def _setup_mock_compute_api(self):
945+
946+ class MockComputeAPI(nova.compute.API):
947+
948+ def __init__(self):
949+ self.injected_files = None
950+ self.networks = None
951+
952+ def create(self, *args, **kwargs):
953+ if 'injected_files' in kwargs:
954+ self.injected_files = kwargs['injected_files']
955+ else:
956+ self.injected_files = None
957+
958+ if 'requested_networks' in kwargs:
959+ self.networks = kwargs['requested_networks']
960+ else:
961+ self.networks = None
962+ return [{'id': '1234', 'display_name': 'fakeinstance',
963+ 'uuid': FAKE_UUID,
964+ 'created_at': "",
965+ 'updated_at': ""}]
966+
967+ def set_admin_password(self, *args, **kwargs):
968+ pass
969+
970+ def make_stub_method(canned_return):
971+ def stub_method(*args, **kwargs):
972+ return canned_return
973+ return stub_method
974+
975+ compute_api = MockComputeAPI()
976+ self.stubs.Set(nova.compute, 'API', make_stub_method(compute_api))
977+ self.stubs.Set(
978+ nova.api.openstack.create_instance_helper.CreateInstanceHelper,
979+ '_get_kernel_ramdisk_from_image', make_stub_method((1, 1)))
980+ return compute_api
981+
982+ def _create_networks_request_dict(self, networks):
983+ server = {}
984+ server['name'] = 'new-server-test'
985+ server['imageRef'] = 1
986+ server['flavorRef'] = 1
987+ if networks is not None:
988+ network_list = []
989+ for uuid, fixed_ip in networks:
990+ network_list.append({'uuid': uuid, 'fixed_ip': fixed_ip})
991+ server['networks'] = network_list
992+ return {'server': server}
993+
994+ def _get_create_request_json(self, body_dict):
995+ req = webob.Request.blank('/v1.1/123/os-create-server-ext')
996+ req.headers['Content-Type'] = 'application/json'
997+ req.method = 'POST'
998+ req.body = json.dumps(body_dict)
999+ return req
1000+
1001+ def _run_create_instance_with_mock_compute_api(self, request):
1002+ compute_api = self._setup_mock_compute_api()
1003+ response = request.get_response(fakes.wsgi_app())
1004+ return compute_api, response
1005+
1006+ def _format_xml_request_body(self, body_dict):
1007+ server = body_dict['server']
1008+ body_parts = []
1009+ body_parts.extend([
1010+ '<?xml version="1.0" encoding="UTF-8"?>',
1011+ '<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.1"',
1012+ ' name="%s" imageRef="%s" flavorRef="%s">' % (
1013+ server['name'], server['imageRef'], server['flavorRef'])])
1014+ if 'metadata' in server:
1015+ metadata = server['metadata']
1016+ body_parts.append('<metadata>')
1017+ for item in metadata.iteritems():
1018+ body_parts.append('<meta key="%s">%s</meta>' % item)
1019+ body_parts.append('</metadata>')
1020+ if 'personality' in server:
1021+ personalities = server['personality']
1022+ body_parts.append('<personality>')
1023+ for file in personalities:
1024+ item = (file['path'], file['contents'])
1025+ body_parts.append('<file path="%s">%s</file>' % item)
1026+ body_parts.append('</personality>')
1027+ if 'networks' in server:
1028+ networks = server['networks']
1029+ body_parts.append('<networks>')
1030+ for network in networks:
1031+ item = (network['uuid'], network['fixed_ip'])
1032+ body_parts.append('<network uuid="%s" fixed_ip="%s"></network>'
1033+ % item)
1034+ body_parts.append('</networks>')
1035+ body_parts.append('</server>')
1036+ return ''.join(body_parts)
1037+
1038+ def _get_create_request_xml(self, body_dict):
1039+ req = webob.Request.blank('/v1.1/123/os-create-server-ext')
1040+ req.content_type = 'application/xml'
1041+ req.accept = 'application/xml'
1042+ req.method = 'POST'
1043+ req.body = self._format_xml_request_body(body_dict)
1044+ return req
1045+
1046+ def _create_instance_with_networks_json(self, networks):
1047+ body_dict = self._create_networks_request_dict(networks)
1048+ request = self._get_create_request_json(body_dict)
1049+ compute_api, response = \
1050+ self._run_create_instance_with_mock_compute_api(request)
1051+ return request, response, compute_api.networks
1052+
1053+ def _create_instance_with_networks_xml(self, networks):
1054+ body_dict = self._create_networks_request_dict(networks)
1055+ request = self._get_create_request_xml(body_dict)
1056+ compute_api, response = \
1057+ self._run_create_instance_with_mock_compute_api(request)
1058+ return request, response, compute_api.networks
1059+
1060+ def test_create_instance_with_no_networks(self):
1061+ request, response, networks = \
1062+ self._create_instance_with_networks_json(networks=None)
1063+ self.assertEquals(response.status_int, 202)
1064+ self.assertEquals(networks, None)
1065+
1066+ def test_create_instance_with_no_networks_xml(self):
1067+ request, response, networks = \
1068+ self._create_instance_with_networks_xml(networks=None)
1069+ self.assertEquals(response.status_int, 202)
1070+ self.assertEquals(networks, None)
1071+
1072+ def test_create_instance_with_one_network(self):
1073+ request, response, networks = \
1074+ self._create_instance_with_networks_json([FAKE_NETWORKS[0]])
1075+ self.assertEquals(response.status_int, 202)
1076+ self.assertEquals(networks, [FAKE_NETWORKS[0]])
1077+
1078+ def test_create_instance_with_one_network_xml(self):
1079+ request, response, networks = \
1080+ self._create_instance_with_networks_xml([FAKE_NETWORKS[0]])
1081+ self.assertEquals(response.status_int, 202)
1082+ self.assertEquals(networks, [FAKE_NETWORKS[0]])
1083+
1084+ def test_create_instance_with_two_networks(self):
1085+ request, response, networks = \
1086+ self._create_instance_with_networks_json(FAKE_NETWORKS)
1087+ self.assertEquals(response.status_int, 202)
1088+ self.assertEquals(networks, FAKE_NETWORKS)
1089+
1090+ def test_create_instance_with_two_networks_xml(self):
1091+ request, response, networks = \
1092+ self._create_instance_with_networks_xml(FAKE_NETWORKS)
1093+ self.assertEquals(response.status_int, 202)
1094+ self.assertEquals(networks, FAKE_NETWORKS)
1095+
1096+ def test_create_instance_with_duplicate_networks(self):
1097+ request, response, networks = \
1098+ self._create_instance_with_networks_json(DUPLICATE_NETWORKS)
1099+ self.assertEquals(response.status_int, 400)
1100+ self.assertEquals(networks, None)
1101+
1102+ def test_create_instance_with_duplicate_networks_xml(self):
1103+ request, response, networks = \
1104+ self._create_instance_with_networks_xml(DUPLICATE_NETWORKS)
1105+ self.assertEquals(response.status_int, 400)
1106+ self.assertEquals(networks, None)
1107+
1108+ def test_create_instance_with_network_no_id(self):
1109+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
1110+ del body_dict['server']['networks'][0]['uuid']
1111+ request = self._get_create_request_json(body_dict)
1112+ compute_api, response = \
1113+ self._run_create_instance_with_mock_compute_api(request)
1114+ self.assertEquals(response.status_int, 400)
1115+ self.assertEquals(compute_api.networks, None)
1116+
1117+ def test_create_instance_with_network_no_id_xml(self):
1118+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
1119+ request = self._get_create_request_xml(body_dict)
1120+ uuid = ' uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"'
1121+ request.body = request.body.replace(uuid, '')
1122+ compute_api, response = \
1123+ self._run_create_instance_with_mock_compute_api(request)
1124+ self.assertEquals(response.status_int, 400)
1125+ self.assertEquals(compute_api.networks, None)
1126+
1127+ def test_create_instance_with_network_invalid_id(self):
1128+ request, response, networks = \
1129+ self._create_instance_with_networks_json(INVALID_NETWORKS)
1130+ self.assertEquals(response.status_int, 400)
1131+ self.assertEquals(networks, None)
1132+
1133+ def test_create_instance_with_network_invalid_id_xml(self):
1134+ request, response, networks = \
1135+ self._create_instance_with_networks_xml(INVALID_NETWORKS)
1136+ self.assertEquals(response.status_int, 400)
1137+ self.assertEquals(networks, None)
1138+
1139+ def test_create_instance_with_network_empty_fixed_ip(self):
1140+ networks = [('1', '')]
1141+ request, response, networks = \
1142+ self._create_instance_with_networks_json(networks)
1143+ self.assertEquals(response.status_int, 400)
1144+ self.assertEquals(networks, None)
1145+
1146+ def test_create_instance_with_network_non_string_fixed_ip(self):
1147+ networks = [('1', 12345)]
1148+ request, response, networks = \
1149+ self._create_instance_with_networks_json(networks)
1150+ self.assertEquals(response.status_int, 400)
1151+ self.assertEquals(networks, None)
1152+
1153+ def test_create_instance_with_network_empty_fixed_ip_xml(self):
1154+ networks = [('1', '')]
1155+ request, response, networks = \
1156+ self._create_instance_with_networks_xml(networks)
1157+ self.assertEquals(response.status_int, 400)
1158+ self.assertEquals(networks, None)
1159+
1160+ def test_create_instance_with_network_no_fixed_ip(self):
1161+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
1162+ del body_dict['server']['networks'][0]['fixed_ip']
1163+ request = self._get_create_request_json(body_dict)
1164+ compute_api, response = \
1165+ self._run_create_instance_with_mock_compute_api(request)
1166+ self.assertEquals(response.status_int, 202)
1167+ self.assertEquals(compute_api.networks,
1168+ [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)])
1169+
1170+ def test_create_instance_with_network_no_fixed_ip_xml(self):
1171+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
1172+ request = self._get_create_request_xml(body_dict)
1173+ request.body = request.body.replace(' fixed_ip="10.0.1.12"', '')
1174+ compute_api, response = \
1175+ self._run_create_instance_with_mock_compute_api(request)
1176+ self.assertEquals(response.status_int, 202)
1177+ self.assertEquals(compute_api.networks,
1178+ [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)])
1179
1180=== modified file 'nova/tests/api/openstack/test_extensions.py'
1181--- nova/tests/api/openstack/test_extensions.py 2011-08-19 13:54:05 +0000
1182+++ nova/tests/api/openstack/test_extensions.py 2011-08-22 23:37:26 +0000
1183@@ -85,6 +85,7 @@
1184 ext_path = os.path.join(os.path.dirname(__file__), "extensions")
1185 self.flags(osapi_extensions_path=ext_path)
1186 self.ext_list = [
1187+ "Createserverext",
1188 "FlavorExtraSpecs",
1189 "Floating_ips",
1190 "Fox In Socks",
1191
1192=== modified file 'nova/tests/api/openstack/test_servers.py'
1193--- nova/tests/api/openstack/test_servers.py 2011-08-22 14:19:01 +0000
1194+++ nova/tests/api/openstack/test_servers.py 2011-08-22 23:37:26 +0000
1195@@ -1890,6 +1890,29 @@
1196 res = req.get_response(fakes.wsgi_app())
1197 self.assertEqual(res.status_int, 400)
1198
1199+ def test_create_instance_whitespace_name(self):
1200+ self._setup_for_create_instance()
1201+
1202+ body = {
1203+ 'server': {
1204+ 'name': ' ',
1205+ 'imageId': 3,
1206+ 'flavorId': 1,
1207+ 'metadata': {
1208+ 'hello': 'world',
1209+ 'open': 'stack',
1210+ },
1211+ 'personality': {},
1212+ },
1213+ }
1214+
1215+ req = webob.Request.blank('/v1.0/servers')
1216+ req.method = 'POST'
1217+ req.body = json.dumps(body)
1218+ req.headers["content-type"] = "application/json"
1219+ res = req.get_response(fakes.wsgi_app())
1220+ self.assertEqual(res.status_int, 400)
1221+
1222 def test_update_server_no_body(self):
1223 req = webob.Request.blank('/v1.0/servers/1')
1224 req.method = 'PUT'
1225@@ -2829,6 +2852,164 @@
1226 }
1227 self.assertEquals(request['body'], expected)
1228
1229+ def test_request_with_empty_networks(self):
1230+ serial_request = """
1231+<server xmlns="http://docs.openstack.org/compute/api/v1.1"
1232+ name="new-server-test" imageRef="1" flavorRef="1">
1233+ <networks/>
1234+</server>"""
1235+ request = self.deserializer.deserialize(serial_request, 'create')
1236+ expected = {"server": {
1237+ "name": "new-server-test",
1238+ "imageRef": "1",
1239+ "flavorRef": "1",
1240+ "networks": []
1241+ }}
1242+ self.assertEquals(request['body'], expected)
1243+
1244+ def test_request_with_one_network(self):
1245+ serial_request = """
1246+<server xmlns="http://docs.openstack.org/compute/api/v1.1"
1247+ name="new-server-test" imageRef="1" flavorRef="1">
1248+ <networks>
1249+ <network uuid="1" fixed_ip="10.0.1.12"/>
1250+ </networks>
1251+</server>"""
1252+ request = self.deserializer.deserialize(serial_request, 'create')
1253+ expected = {"server": {
1254+ "name": "new-server-test",
1255+ "imageRef": "1",
1256+ "flavorRef": "1",
1257+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}],
1258+ }}
1259+ self.assertEquals(request['body'], expected)
1260+
1261+ def test_request_with_two_networks(self):
1262+ serial_request = """
1263+<server xmlns="http://docs.openstack.org/compute/api/v1.1"
1264+ name="new-server-test" imageRef="1" flavorRef="1">
1265+ <networks>
1266+ <network uuid="1" fixed_ip="10.0.1.12"/>
1267+ <network uuid="2" fixed_ip="10.0.2.12"/>
1268+ </networks>
1269+</server>"""
1270+ request = self.deserializer.deserialize(serial_request, 'create')
1271+ expected = {"server": {
1272+ "name": "new-server-test",
1273+ "imageRef": "1",
1274+ "flavorRef": "1",
1275+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"},
1276+ {"uuid": "2", "fixed_ip": "10.0.2.12"}],
1277+ }}
1278+ self.assertEquals(request['body'], expected)
1279+
1280+ def test_request_with_second_network_node_ignored(self):
1281+ serial_request = """
1282+<server xmlns="http://docs.openstack.org/compute/api/v1.1"
1283+ name="new-server-test" imageRef="1" flavorRef="1">
1284+ <networks>
1285+ <network uuid="1" fixed_ip="10.0.1.12"/>
1286+ </networks>
1287+ <networks>
1288+ <network uuid="2" fixed_ip="10.0.2.12"/>
1289+ </networks>
1290+</server>"""
1291+ request = self.deserializer.deserialize(serial_request, 'create')
1292+ expected = {"server": {
1293+ "name": "new-server-test",
1294+ "imageRef": "1",
1295+ "flavorRef": "1",
1296+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}],
1297+ }}
1298+ self.assertEquals(request['body'], expected)
1299+
1300+ def test_request_with_one_network_missing_id(self):
1301+ serial_request = """
1302+<server xmlns="http://docs.openstack.org/compute/api/v1.1"
1303+ name="new-server-test" imageRef="1" flavorRef="1">
1304+ <networks>
1305+ <network fixed_ip="10.0.1.12"/>
1306+ </networks>
1307+</server>"""
1308+ request = self.deserializer.deserialize(serial_request, 'create')
1309+ expected = {"server": {
1310+ "name": "new-server-test",
1311+ "imageRef": "1",
1312+ "flavorRef": "1",
1313+ "networks": [{"fixed_ip": "10.0.1.12"}],
1314+ }}
1315+ self.assertEquals(request['body'], expected)
1316+
1317+ def test_request_with_one_network_missing_fixed_ip(self):
1318+ serial_request = """
1319+<server xmlns="http://docs.openstack.org/compute/api/v1.1"
1320+ name="new-server-test" imageRef="1" flavorRef="1">
1321+ <networks>
1322+ <network uuid="1"/>
1323+ </networks>
1324+</server>"""
1325+ request = self.deserializer.deserialize(serial_request, 'create')
1326+ expected = {"server": {
1327+ "name": "new-server-test",
1328+ "imageRef": "1",
1329+ "flavorRef": "1",
1330+ "networks": [{"uuid": "1"}],
1331+ }}
1332+ self.assertEquals(request['body'], expected)
1333+
1334+ def test_request_with_one_network_empty_id(self):
1335+ serial_request = """
1336+ <server xmlns="http://docs.openstack.org/compute/api/v1.1"
1337+ name="new-server-test" imageRef="1" flavorRef="1">
1338+ <networks>
1339+ <network uuid="" fixed_ip="10.0.1.12"/>
1340+ </networks>
1341+ </server>"""
1342+ request = self.deserializer.deserialize(serial_request, 'create')
1343+ expected = {"server": {
1344+ "name": "new-server-test",
1345+ "imageRef": "1",
1346+ "flavorRef": "1",
1347+ "networks": [{"uuid": "", "fixed_ip": "10.0.1.12"}],
1348+ }}
1349+ self.assertEquals(request['body'], expected)
1350+
1351+ def test_request_with_one_network_empty_fixed_ip(self):
1352+ serial_request = """
1353+ <server xmlns="http://docs.openstack.org/compute/api/v1.1"
1354+ name="new-server-test" imageRef="1" flavorRef="1">
1355+ <networks>
1356+ <network uuid="1" fixed_ip=""/>
1357+ </networks>
1358+ </server>"""
1359+ request = self.deserializer.deserialize(serial_request, 'create')
1360+ expected = {"server": {
1361+ "name": "new-server-test",
1362+ "imageRef": "1",
1363+ "flavorRef": "1",
1364+ "networks": [{"uuid": "1", "fixed_ip": ""}],
1365+ }}
1366+ self.assertEquals(request['body'], expected)
1367+
1368+ def test_request_with_networks_duplicate_ids(self):
1369+ serial_request = """
1370+ <server xmlns="http://docs.openstack.org/compute/api/v1.1"
1371+ name="new-server-test" imageRef="1" flavorRef="1">
1372+ <networks>
1373+ <network uuid="1" fixed_ip="10.0.1.12"/>
1374+ <network uuid="1" fixed_ip="10.0.2.12"/>
1375+ </networks>
1376+ </server>"""
1377+ request = self.deserializer.deserialize(serial_request, 'create')
1378+ expected = {"server": {
1379+ "name": "new-server-test",
1380+ "imageRef": "1",
1381+ "flavorRef": "1",
1382+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"},
1383+ {"uuid": "1", "fixed_ip": "10.0.2.12"}],
1384+ }}
1385+ self.assertEquals(request['body'], expected)
1386+
1387
1388 class TestAddressesXMLSerialization(test.TestCase):
1389
1390@@ -2899,12 +3080,14 @@
1391
1392 def __init__(self):
1393 self.injected_files = None
1394+ self.networks = None
1395
1396 def create(self, *args, **kwargs):
1397 if 'injected_files' in kwargs:
1398 self.injected_files = kwargs['injected_files']
1399 else:
1400 self.injected_files = None
1401+
1402 return [{'id': '1234', 'display_name': 'fakeinstance',
1403 'uuid': FAKE_UUID}]
1404
1405
1406=== modified file 'nova/tests/test_network.py'
1407--- nova/tests/test_network.py 2011-08-18 06:50:50 +0000
1408+++ nova/tests/test_network.py 2011-08-22 23:37:26 +0000
1409@@ -15,6 +15,7 @@
1410 # License for the specific language governing permissions and limitations
1411 # under the License.
1412
1413+from nova import context
1414 from nova import db
1415 from nova import exception
1416 from nova import log as logging
1417@@ -41,6 +42,7 @@
1418
1419
1420 networks = [{'id': 0,
1421+ 'uuid': "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
1422 'label': 'test0',
1423 'injected': False,
1424 'multi_host': False,
1425@@ -60,6 +62,7 @@
1426 'project_id': 'fake_project',
1427 'vpn_public_address': '192.168.0.2'},
1428 {'id': 1,
1429+ 'uuid': "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
1430 'label': 'test1',
1431 'injected': False,
1432 'multi_host': False,
1433@@ -126,6 +129,8 @@
1434 super(FlatNetworkTestCase, self).setUp()
1435 self.network = network_manager.FlatManager(host=HOST)
1436 self.network.db = db
1437+ self.context = context.RequestContext('testuser', 'testproject',
1438+ is_admin=False)
1439
1440 def test_get_instance_nw_info(self):
1441 self.mox.StubOutWithMock(db, 'fixed_ip_get_by_instance')
1442@@ -183,12 +188,73 @@
1443 'netmask': '255.255.255.0'}]
1444 self.assertDictListMatch(nw[1]['ips'], check)
1445
1446+ def test_validate_networks(self):
1447+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1448+ self.mox.StubOutWithMock(db, "fixed_ip_get_by_address")
1449+
1450+ requested_networks = [("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
1451+ "192.168.1.100")]
1452+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1453+ mox.IgnoreArg()).AndReturn(networks)
1454+
1455+ fixed_ips[1]['network'] = FakeModel(**networks[1])
1456+ fixed_ips[1]['instance'] = None
1457+ db.fixed_ip_get_by_address(mox.IgnoreArg(),
1458+ mox.IgnoreArg()).AndReturn(fixed_ips[1])
1459+
1460+ self.mox.ReplayAll()
1461+ self.network.validate_networks(self.context, requested_networks)
1462+
1463+ def test_validate_networks_none_requested_networks(self):
1464+ self.network.validate_networks(self.context, None)
1465+
1466+ def test_validate_networks_empty_requested_networks(self):
1467+ requested_networks = []
1468+ self.mox.ReplayAll()
1469+
1470+ self.network.validate_networks(self.context, requested_networks)
1471+
1472+ def test_validate_networks_invalid_fixed_ip(self):
1473+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1474+ requested_networks = [(1, "192.168.0.100.1")]
1475+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1476+ mox.IgnoreArg()).AndReturn(networks)
1477+ self.mox.ReplayAll()
1478+
1479+ self.assertRaises(exception.FixedIpInvalid,
1480+ self.network.validate_networks, None,
1481+ requested_networks)
1482+
1483+ def test_validate_networks_empty_fixed_ip(self):
1484+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1485+
1486+ requested_networks = [(1, "")]
1487+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1488+ mox.IgnoreArg()).AndReturn(networks)
1489+ self.mox.ReplayAll()
1490+
1491+ self.assertRaises(exception.FixedIpInvalid,
1492+ self.network.validate_networks,
1493+ None, requested_networks)
1494+
1495+ def test_validate_networks_none_fixed_ip(self):
1496+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1497+
1498+ requested_networks = [(1, None)]
1499+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1500+ mox.IgnoreArg()).AndReturn(networks)
1501+ self.mox.ReplayAll()
1502+
1503+ self.network.validate_networks(None, requested_networks)
1504+
1505
1506 class VlanNetworkTestCase(test.TestCase):
1507 def setUp(self):
1508 super(VlanNetworkTestCase, self).setUp()
1509 self.network = network_manager.VlanManager(host=HOST)
1510 self.network.db = db
1511+ self.context = context.RequestContext('testuser', 'testproject',
1512+ is_admin=False)
1513
1514 def test_vpn_allocate_fixed_ip(self):
1515 self.mox.StubOutWithMock(db, 'fixed_ip_associate')
1516@@ -232,7 +298,7 @@
1517
1518 network = dict(networks[0])
1519 network['vpn_private_address'] = '192.168.0.2'
1520- self.network.allocate_fixed_ip(None, 0, network)
1521+ self.network.allocate_fixed_ip(self.context, 0, network)
1522
1523 def test_create_networks_too_big(self):
1524 self.assertRaises(ValueError, self.network.create_networks, None,
1525@@ -243,6 +309,68 @@
1526 num_networks=100, vlan_start=1,
1527 cidr='192.168.0.1/24', network_size=100)
1528
1529+ def test_validate_networks(self):
1530+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1531+ self.mox.StubOutWithMock(db, "fixed_ip_get_by_address")
1532+
1533+ requested_networks = [("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
1534+ "192.168.1.100")]
1535+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1536+ mox.IgnoreArg(),
1537+ mox.IgnoreArg()).AndReturn(networks)
1538+
1539+ fixed_ips[1]['network'] = FakeModel(**networks[1])
1540+ fixed_ips[1]['instance'] = None
1541+ db.fixed_ip_get_by_address(mox.IgnoreArg(),
1542+ mox.IgnoreArg()).AndReturn(fixed_ips[1])
1543+
1544+ self.mox.ReplayAll()
1545+ self.network.validate_networks(self.context, requested_networks)
1546+
1547+ def test_validate_networks_none_requested_networks(self):
1548+ self.network.validate_networks(self.context, None)
1549+
1550+ def test_validate_networks_empty_requested_networks(self):
1551+ requested_networks = []
1552+ self.mox.ReplayAll()
1553+
1554+ self.network.validate_networks(self.context, requested_networks)
1555+
1556+ def test_validate_networks_invalid_fixed_ip(self):
1557+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1558+ requested_networks = [(1, "192.168.0.100.1")]
1559+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1560+ mox.IgnoreArg(),
1561+ mox.IgnoreArg()).AndReturn(networks)
1562+ self.mox.ReplayAll()
1563+
1564+ self.assertRaises(exception.FixedIpInvalid,
1565+ self.network.validate_networks, self.context,
1566+ requested_networks)
1567+
1568+ def test_validate_networks_empty_fixed_ip(self):
1569+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1570+
1571+ requested_networks = [(1, "")]
1572+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1573+ mox.IgnoreArg(),
1574+ mox.IgnoreArg()).AndReturn(networks)
1575+ self.mox.ReplayAll()
1576+
1577+ self.assertRaises(exception.FixedIpInvalid,
1578+ self.network.validate_networks,
1579+ self.context, requested_networks)
1580+
1581+ def test_validate_networks_none_fixed_ip(self):
1582+ self.mox.StubOutWithMock(db, 'network_get_all_by_uuids')
1583+
1584+ requested_networks = [(1, None)]
1585+ db.network_get_all_by_uuids(mox.IgnoreArg(),
1586+ mox.IgnoreArg(),
1587+ mox.IgnoreArg()).AndReturn(networks)
1588+ self.mox.ReplayAll()
1589+ self.network.validate_networks(self.context, requested_networks)
1590+
1591
1592 class CommonNetworkTestCase(test.TestCase):
1593
1594
1595=== modified file 'nova/utils.py'
1596--- nova/utils.py 2011-08-22 12:28:12 +0000
1597+++ nova/utils.py 2011-08-22 23:37:26 +0000
1598@@ -844,3 +844,19 @@
1599 return True if int(val) else False
1600 except ValueError:
1601 return val.lower() == 'true'
1602+
1603+
1604+def is_valid_ipv4(address):
1605+ """valid the address strictly as per format xxx.xxx.xxx.xxx.
1606+ where xxx is a value between 0 and 255.
1607+ """
1608+ parts = address.split(".")
1609+ if len(parts) != 4:
1610+ return False
1611+ for item in parts:
1612+ try:
1613+ if not 0 <= int(item) <= 255:
1614+ return False
1615+ except ValueError:
1616+ return False
1617+ return True