Merge lp:~ntt-pf-lab/nova/ipv6-support into lp:~hudson-openstack/nova/trunk

Proposed by Hisaharu Ishii
Status: Superseded
Proposed branch: lp:~ntt-pf-lab/nova/ipv6-support
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 1528 lines (+798/-53)
24 files modified
Authors (+4/-0)
bin/nova-manage (+7/-3)
contrib/boto_v6/__init__.py (+37/-0)
contrib/boto_v6/ec2/connection.py (+41/-0)
contrib/boto_v6/ec2/instance.py (+37/-0)
contrib/nova.sh (+9/-0)
nova/api/ec2/cloud.py (+13/-0)
nova/db/api.py (+12/-0)
nova/db/sqlalchemy/api.py (+27/-0)
nova/db/sqlalchemy/models.py (+4/-0)
nova/network/linux_net.py (+83/-0)
nova/network/manager.py (+29/-3)
nova/test.py (+2/-1)
nova/tests/test_api.py (+66/-0)
nova/tests/test_network.py (+22/-0)
nova/utils.py (+36/-0)
nova/virt/libvirt.xml.template (+1/-0)
nova/virt/libvirt_conn.py (+137/-34)
smoketests/admin_smoketests.py (+1/-2)
smoketests/base.py (+15/-2)
smoketests/flags.py (+2/-1)
smoketests/public_network_smoketests.py (+180/-0)
smoketests/user_smoketests.py (+32/-7)
tools/pip-requires (+1/-0)
To merge this branch: bzr merge lp:~ntt-pf-lab/nova/ipv6-support
Reviewer Review Type Date Requested Status
Vish Ishaya (community) Approve
Devin Carlen Pending
Soren Hansen Pending
Review via email: mp+46103@code.launchpad.net

This proposal supersedes a proposal from 2011-01-13.

This proposal has been superseded by a proposal from 2011-01-13.

Description of the change

OpenStack Compute (Nova) IPv4/IPv6 dual stack support
http://wiki.openstack.org/BexarIpv6supportReadme

Tested with
 unit test
 smoke test

No conflict with current branch r 557.
Fixed comment by Soren

To post a comment you must log in.
Revision history for this message
Soren Hansen (soren) wrote : Posted in a previous version of this proposal
Download full text (9.6 KiB)

Please find my comments inline.

Please note that ipv6 is still a bit of a mystery to me, so I'll review
structure and style moreso than technical correctness.

2011/1/5 Nachi Ueno <email address hidden>:
> Nachi Ueno has proposed merging lp:~ntt-pf-lab/nova/ipv6-support into lp:nova.
>
> Requested reviews:
>  Nova Core (nova-core)
>
> For more details, see:
> https://code.launchpad.net/~ntt-pf-lab/nova/ipv6-support/+merge/45228
>
> OpenStack Compute (Nova) IPv4/IPv6 dual stack support
> http://wiki.openstack.org/BexarIpv6supportReadme
>
> Tested with
>  unit test
>  smoke test
>
> No conflict with current branch r 515.
> --
> https://code.launchpad.net/~ntt-pf-lab/nova/ipv6-support/+merge/45228
> You are subscribed to branch lp:nova.
>
> === modified file 'bin/nova-manage'
> --- bin/nova-manage     2010-12-28 20:11:41 +0000
> +++ bin/nova-manage     2011-01-05 11:56:16 +0000
> @@ -89,7 +89,7 @@
>  flags.DECLARE('network_size', 'nova.network.manager')
>  flags.DECLARE('vlan_start', 'nova.network.manager')
>  flags.DECLARE('vpn_start', 'nova.network.manager')
> -
> +flags.DECLARE('fixed_range_v6', 'nova.network.manager')
>
>  class VpnCommands(object):
>     """Class for managing VPNs."""
> @@ -432,11 +432,11 @@
>     """Class for managing networks."""
>
>     def create(self, fixed_range=None, num_networks=None,
> -               network_size=None, vlan_start=None, vpn_start=None):
> +               network_size=None, vlan_start=None, vpn_start=None,fixed_range_v6=None):

This line is too long. Nova is pep8 clean. You can install the pep8
utility and check for yourself.

>         """Creates fixed ips for host by range
>         arguments: [fixed_range=FLAG], [num_networks=FLAG],
>                    [network_size=FLAG], [vlan_start=FLAG],
> -                   [vpn_start=FLAG]"""
> +                   [vpn_start=FLAG],[fixed_range_v6=FLAG]"""

Missing a space (for consistency).

>         if not fixed_range:
>             fixed_range = FLAGS.fixed_range
>         if not num_networks:
> @@ -447,11 +447,15 @@
>             vlan_start = FLAGS.vlan_start
>         if not vpn_start:
>             vpn_start = FLAGS.vpn_start
> +        if not fixed_range_v6:
> +            fixed_range_v6 = FLAGS.fixed_range_v6
>         net_manager = utils.import_object(FLAGS.network_manager)
>         net_manager.create_networks(context.get_admin_context(),
>                                     fixed_range, int(num_networks),
>                                     int(network_size), int(vlan_start),
> -                                    int(vpn_start))
> +                                    int(vpn_start),fixed_range_v6)
> +
> +

Same here. Please add a space (pep8 will tell you this, too).

> === modified file 'nova/api/ec2/cloud.py'
> --- nova/api/ec2/cloud.py       2011-01-03 19:29:39 +0000
> +++ nova/api/ec2/cloud.py       2011-01-05 11:56:16 +0000
> @@ -30,7 +30,7 @@
>
>  from nova import context
>  import IPy
> -
> +import urllib
>  from nova import crypto
>  from nova import db
>  from nova import exception
> @@ -347,6 +347,7 @@
>             values['group_id'] = source_security_group['id']
>         elif cidr_ip:
>             # If this fails, it throws an...

Read more...

Revision history for this message
Soren Hansen (soren) : Posted in a previous version of this proposal
review: Needs Fixing
Revision history for this message
Devin Carlen (devcamcar) wrote : Posted in a previous version of this proposal

It looks like we are essentially providing our own IPV6 version of boto with this patch, which is a bit scary. If we aren't able to use the official libraries (boto, etc.) for IPV6, then maybe we shouldn't be trying to do this with the EC2 API? Forgive me if this has already been discussed by the community, but I think it's a fair question.

review: Needs Information
Revision history for this message
Nachi Ueno (nati-ueno) wrote : Posted in a previous version of this proposal

>Devin Carlen
Thank you for your comment.
We extended DescribeInstance. However we will revert DescribeInstance,and We will add DescribeInstanceV6 today.
Users,who don't need to see v6 address in a DescribeInstance response,can use standard boto libraries with no side effect in current version.
(Users can use EC2 API and DescribeInstanceV6 API)

We continue to support IPv6 version of boto.
As you can see, we created boto_v6 by extending boto classes.
So the code is really simple :).

Revision history for this message
Soren Hansen (soren) wrote : Posted in a previous version of this proposal
Download full text (5.8 KiB)

I'm getting a number of failures when I run the test suite.

======================================================================
ERROR: test_associate_disassociate_address (nova.tests.test_cloud.CloudTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/soren/src/openstack/nova/nova/nova/tests/test_cloud.py", line 112, in test_associate_disassociate_address
    public_ip=address)
  File "/home/soren/src/openstack/nova/nova/nova/api/ec2/cloud.py", line 758, in associate_address
    self.compute_api.associate_floating_ip(context, instance_id, public_ip)
  File "/home/soren/src/openstack/nova/nova/nova/compute/api.py", line 501, in associate_floating_ip
    instance['fixed_ip'])
  File "/home/soren/src/openstack/nova/nova/nova/network/api.py", line 70, in associate_floating_ip
    host = fixed_ip['network']['host']
  File "/home/soren/src/openstack/nova/nova/nova/db/sqlalchemy/models.py", line 74, in __getitem__
    return getattr(self, key)
  File "/usr/lib/python2.6/dist-packages/sqlalchemy/orm/attributes.py", line 163, in __get__
    instance_dict(instance))
  File "/usr/lib/python2.6/dist-packages/sqlalchemy/orm/attributes.py", line 382, in get
    value = callable_(passive=passive)
  File "/usr/lib/python2.6/dist-packages/sqlalchemy/orm/strategies.py", line 578, in __call__
    (mapperutil.state_str(state), self.key)
DetachedInstanceError: Parent instance <FixedIp at 0x5a13e90> is not bound to a Session; lazy load operation of attribute 'network' cannot proceed

======================================================================
ERROR: test_private_ipv6 (nova.tests.test_network.NetworkTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/soren/src/openstack/nova/nova/nova/tests/test_network.py", line 108, in test_private_ipv6
    instance_ref['id'])
  File "/home/soren/src/openstack/nova/nova/nova/db/api.py", line 361, in instance_get_fixed_address_v6
    return IMPL.instance_get_fixed_address_v6(context, instance_id)
  File "/home/soren/src/openstack/nova/nova/nova/db/sqlalchemy/api.py", line 107, in wrapper
    return f(*args, **kwargs)
  File "/home/soren/src/openstack/nova/nova/nova/db/sqlalchemy/api.py", line 810, in instance_get_fixed_address_v6
    network_ref = network_get_by_instance(context, instance_id)
  File "/home/soren/src/openstack/nova/nova/nova/db/sqlalchemy/api.py", line 95, in wrapper
    raise exception.NotAuthorized()
NotAuthorized: None

======================================================================
ERROR: test_static_filters (nova.tests.test_virt.IptablesFirewallTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/soren/src/openstack/nova/nova/nova/tests/test_virt.py", line 306, in test_static_filters
    out_rules = self.fw.modify_rules(self.in_rules)
TypeError: modify_rules() takes exactly 3 arguments (2 given)

======================================================================
FAIL: test_describe_instances (nova.tests.test_cloud.CloudTestCase)
---------------------------...

Read more...

review: Needs Fixing
Revision history for this message
Nachi Ueno (nati-ueno) wrote : Posted in a previous version of this proposal

Thank you.
We fixed some bug.

export TEST=1
nova/contrib/nova.sh run

~~~~
----------------------------------------------------------------------
Ran 277 tests in 501.898s

OK

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

Tests all pass for me. It looks like we need a dependency on python-netaddr and radvd for packaging. It should probably also be added to the pip requires:

=== modified file 'tools/pip-requires'
--- tools/pip-requires 2010-12-28 00:10:26 +0000
+++ tools/pip-requires 2011-01-13 20:39:01 +0000
@@ -25,3 +25,4 @@
 Twisted>=10.1.0
 PasteDeploy
 paste
+netaddr

It appears we will need a new/updated version of Iptables Security Groups for this, but I think that can be put in as a bug and fixed later.

I don't think the following code will work:

383 + if(FLAGS.use_ipv6):
384 + _execute('sudo bash -c ' +
385 + '"echo 1 > /proc/sys/net/ipv6/conf/all/forwarding"')
386 + _execute('sudo bash -c ' +
387 + '"echo 0 > /proc/sys/net/ipv6/conf/all/accept_ra"')

we don't want to give add bash to sudo commands for nova user, so I think we're going to have to depend on admins to set this up in advance or do it in packaging. Perhaps move it to nova.sh for now?

Aside from that the branch looks very clean and well put together. I'm happy to approve after the above changes.

review: Needs Fixing
lp:~ntt-pf-lab/nova/ipv6-support updated
492. By Nachi Ueno

Added netaddr for pip-requires

493. By Nachi Ueno

Moved commands which needs sudo to nova.sh

494. By Nachi Ueno

Merged with r561

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

lgtm

review: Approve
lp:~ntt-pf-lab/nova/ipv6-support updated
495. By Hisaharu Ishii

Merged with r562

496. By Hisaharu Ishii

sort Authors

497. By Koji Iida

Merged to rev.563

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Authors'
2--- Authors 2011-01-12 19:39:25 +0000
3+++ Authors 2011-01-13 21:53:11 +0000
4@@ -45,3 +45,7 @@
5 Vishvananda Ishaya <vishvananda@gmail.com>
6 Youcef Laribi <Youcef.Laribi@eu.citrix.com>
7 Zhixue Wu <Zhixue.Wu@citrix.com>
8+Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
9+Koji Iida <iida.koji@lab.ntt.co.jp>
10+Nachi Ueno <ueno.nachi@lab.ntt.co.jp> <openstack@lab.ntt.co.jp> <nati.ueno@gmail.com> <nova@u4>
11+
12
13=== modified file 'bin/nova-manage'
14--- bin/nova-manage 2011-01-12 20:12:08 +0000
15+++ bin/nova-manage 2011-01-13 21:53:11 +0000
16@@ -91,6 +91,7 @@
17 flags.DECLARE('network_size', 'nova.network.manager')
18 flags.DECLARE('vlan_start', 'nova.network.manager')
19 flags.DECLARE('vpn_start', 'nova.network.manager')
20+flags.DECLARE('fixed_range_v6', 'nova.network.manager')
21
22
23 class VpnCommands(object):
24@@ -439,11 +440,12 @@
25 """Class for managing networks."""
26
27 def create(self, fixed_range=None, num_networks=None,
28- network_size=None, vlan_start=None, vpn_start=None):
29+ network_size=None, vlan_start=None, vpn_start=None,
30+ fixed_range_v6=None):
31 """Creates fixed ips for host by range
32 arguments: [fixed_range=FLAG], [num_networks=FLAG],
33 [network_size=FLAG], [vlan_start=FLAG],
34- [vpn_start=FLAG]"""
35+ [vpn_start=FLAG], [fixed_range_v6=FLAG]"""
36 if not fixed_range:
37 fixed_range = FLAGS.fixed_range
38 if not num_networks:
39@@ -454,11 +456,13 @@
40 vlan_start = FLAGS.vlan_start
41 if not vpn_start:
42 vpn_start = FLAGS.vpn_start
43+ if not fixed_range_v6:
44+ fixed_range_v6 = FLAGS.fixed_range_v6
45 net_manager = utils.import_object(FLAGS.network_manager)
46 net_manager.create_networks(context.get_admin_context(),
47 fixed_range, int(num_networks),
48 int(network_size), int(vlan_start),
49- int(vpn_start))
50+ int(vpn_start), fixed_range_v6)
51
52
53 class ServiceCommands(object):
54
55=== added directory 'contrib/boto_v6'
56=== added file 'contrib/boto_v6/__init__.py'
57--- contrib/boto_v6/__init__.py 1970-01-01 00:00:00 +0000
58+++ contrib/boto_v6/__init__.py 2011-01-13 21:53:11 +0000
59@@ -0,0 +1,37 @@
60+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
61+# Copyright (c) 2010, Eucalyptus Systems, Inc.
62+# All rights reserved.
63+#
64+# Permission is hereby granted, free of charge, to any person obtaining a
65+# copy of this software and associated documentation files (the
66+# "Software"), to deal in the Software without restriction, including
67+# without limitation the rights to use, copy, modify, merge, publish, dis-
68+# tribute, sublicense, and/or sell copies of the Software, and to permit
69+# persons to whom the Software is furnished to do so, subject to the fol-
70+# lowing conditions:
71+#
72+# The above copyright notice and this permission notice shall be included
73+# in all copies or substantial portions of the Software.
74+#
75+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
76+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
77+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
78+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
79+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
80+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
81+# IN THE SOFTWARE.
82+
83+
84+def connect_ec2(aws_access_key_id=None, aws_secret_access_key=None, **kwargs):
85+ """
86+ :type aws_access_key_id: string
87+ :param aws_access_key_id: Your AWS Access Key ID
88+
89+ :type aws_secret_access_key: string
90+ :param aws_secret_access_key: Your AWS Secret Access Key
91+
92+ :rtype: :class:`boto.ec2.connection.EC2Connection`
93+ :return: A connection to Amazon's EC2
94+ """
95+ from boto_v6.ec2.connection import EC2ConnectionV6
96+ return EC2ConnectionV6(aws_access_key_id, aws_secret_access_key, **kwargs)
97
98=== added directory 'contrib/boto_v6/ec2'
99=== added file 'contrib/boto_v6/ec2/__init__.py'
100=== added file 'contrib/boto_v6/ec2/connection.py'
101--- contrib/boto_v6/ec2/connection.py 1970-01-01 00:00:00 +0000
102+++ contrib/boto_v6/ec2/connection.py 2011-01-13 21:53:11 +0000
103@@ -0,0 +1,41 @@
104+'''
105+Created on 2010/12/20
106+
107+@author: Nachi Ueno <ueno.nachi@lab.ntt.co.jp>
108+'''
109+import boto
110+import boto.ec2
111+from boto_v6.ec2.instance import ReservationV6
112+
113+
114+class EC2ConnectionV6(boto.ec2.EC2Connection):
115+ '''
116+ EC2Connection for OpenStack IPV6 mode
117+ '''
118+ def get_all_instances(self, instance_ids=None, filters=None):
119+ """
120+ Retrieve all the instances associated with your account.
121+
122+ :type instance_ids: list
123+ :param instance_ids: A list of strings of instance IDs
124+
125+ :type filters: dict
126+ :param filters: Optional filters that can be used to limit
127+ the results returned. Filters are provided
128+ in the form of a dictionary consisting of
129+ filter names as the key and filter values
130+ as the value. The set of allowable filter
131+ names/values is dependent on the request
132+ being performed. Check the EC2 API guide
133+ for details.
134+
135+ :rtype: list
136+ :return: A list of :class:`boto.ec2.instance.Reservation`
137+ """
138+ params = {}
139+ if instance_ids:
140+ self.build_list_params(params, instance_ids, 'InstanceId')
141+ if filters:
142+ self.build_filter_params(params, filters)
143+ return self.get_list('DescribeInstancesV6', params,
144+ [('item', ReservationV6)])
145
146=== added file 'contrib/boto_v6/ec2/instance.py'
147--- contrib/boto_v6/ec2/instance.py 1970-01-01 00:00:00 +0000
148+++ contrib/boto_v6/ec2/instance.py 2011-01-13 21:53:11 +0000
149@@ -0,0 +1,37 @@
150+'''
151+Created on 2010/12/20
152+
153+@author: Nachi Ueno <ueno.nachi@lab.ntt.co.jp>
154+'''
155+import boto
156+from boto.resultset import ResultSet
157+from boto.ec2.instance import Reservation
158+from boto.ec2.instance import Group
159+from boto.ec2.instance import Instance
160+
161+
162+class ReservationV6(Reservation):
163+ def startElement(self, name, attrs, connection):
164+ if name == 'instancesSet':
165+ self.instances = ResultSet([('item', InstanceV6)])
166+ return self.instances
167+ elif name == 'groupSet':
168+ self.groups = ResultSet([('item', Group)])
169+ return self.groups
170+ else:
171+ return None
172+
173+
174+class InstanceV6(Instance):
175+ def __init__(self, connection=None):
176+ Instance.__init__(self, connection)
177+ self.dns_name_v6 = None
178+
179+ def endElement(self, name, value, connection):
180+ Instance.endElement(self, name, value, connection)
181+ if name == 'dnsNameV6':
182+ self.dns_name_v6 = value
183+
184+ def _update(self, updated):
185+ self.__dict__.update(updated.__dict__)
186+ self.dns_name_v6 = updated.dns_name_v6
187
188=== modified file 'contrib/nova.sh'
189--- contrib/nova.sh 2010-12-30 00:07:41 +0000
190+++ contrib/nova.sh 2011-01-13 21:53:11 +0000
191@@ -86,6 +86,13 @@
192 sudo apt-get install -y python-twisted python-sqlalchemy python-mox python-greenlet python-carrot
193 sudo apt-get install -y python-daemon python-eventlet python-gflags python-tornado python-ipy
194 sudo apt-get install -y python-libvirt python-libxml2 python-routes
195+#For IPV6
196+ sudo apt-get install -y python-netaddr
197+ sudo apt-get install -y radvd
198+#(Nati) Note that this configuration is only needed for nova-network node.
199+ sudo bash -c "echo 1 > /proc/sys/net/ipv6/conf/all/forwarding"
200+ sudo bash -c "echo 0 > /proc/sys/net/ipv6/conf/all/accept_ra"
201+
202 if [ "$USE_MYSQL" == 1 ]; then
203 cat <<MYSQL_PRESEED | debconf-set-selections
204 mysql-server-5.1 mysql-server/root_password password $MYSQL_PASS
205@@ -107,6 +114,8 @@
206
207 if [ "$CMD" == "run" ]; then
208 killall dnsmasq
209+ #For IPv6
210+ killall radvd
211 screen -d -m -S nova -t nova
212 sleep 1
213 if [ "$USE_MYSQL" == 1 ]; then
214
215=== modified file 'nova/api/ec2/cloud.py'
216--- nova/api/ec2/cloud.py 2011-01-13 18:24:06 +0000
217+++ nova/api/ec2/cloud.py 2011-01-13 21:53:11 +0000
218@@ -26,9 +26,11 @@
219 import datetime
220 import IPy
221 import os
222+import urllib
223
224 from nova import compute
225 from nova import context
226+
227 from nova import crypto
228 from nova import db
229 from nova import exception
230@@ -374,6 +376,7 @@
231 values['group_id'] = source_security_group['id']
232 elif cidr_ip:
233 # If this fails, it throws an exception. This is what we want.
234+ cidr_ip = urllib.unquote(cidr_ip).decode()
235 IPy.IP(cidr_ip)
236 values['cidr'] = cidr_ip
237 else:
238@@ -643,6 +646,10 @@
239 def describe_instances(self, context, **kwargs):
240 return self._format_describe_instances(context, **kwargs)
241
242+ def describe_instances_v6(self, context, **kwargs):
243+ kwargs['use_v6'] = True
244+ return self._format_describe_instances(context, **kwargs)
245+
246 def _format_describe_instances(self, context, **kwargs):
247 return {'reservationSet': self._format_instances(context, **kwargs)}
248
249@@ -678,10 +685,16 @@
250 if instance['fixed_ip']['floating_ips']:
251 fixed = instance['fixed_ip']
252 floating_addr = fixed['floating_ips'][0]['address']
253+ if instance['fixed_ip']['network'] and 'use_v6' in kwargs:
254+ i['dnsNameV6'] = utils.to_global_ipv6(
255+ instance['fixed_ip']['network']['cidr_v6'],
256+ instance['mac_address'])
257+
258 i['privateDnsName'] = fixed_addr
259 i['publicDnsName'] = floating_addr
260 i['dnsName'] = i['publicDnsName'] or i['privateDnsName']
261 i['keyName'] = instance['key_name']
262+
263 if context.user.is_admin():
264 i['keyName'] = '%s (%s, %s)' % (i['keyName'],
265 instance['project_id'],
266
267=== modified file 'nova/db/api.py'
268--- nova/db/api.py 2011-01-12 23:03:08 +0000
269+++ nova/db/api.py 2011-01-13 21:53:11 +0000
270@@ -299,6 +299,10 @@
271 return IMPL.fixed_ip_get_instance(context, address)
272
273
274+def fixed_ip_get_instance_v6(context, address):
275+ return IMPL.fixed_ip_get_instance_v6(context, address)
276+
277+
278 def fixed_ip_get_network(context, address):
279 """Get a network for a fixed ip by address."""
280 return IMPL.fixed_ip_get_network(context, address)
281@@ -357,6 +361,10 @@
282 return IMPL.instance_get_fixed_address(context, instance_id)
283
284
285+def instance_get_fixed_address_v6(context, instance_id):
286+ return IMPL.instance_get_fixed_address_v6(context, instance_id)
287+
288+
289 def instance_get_floating_address(context, instance_id):
290 """Get the first floating ip address of an instance."""
291 return IMPL.instance_get_floating_address(context, instance_id)
292@@ -552,6 +560,10 @@
293 return IMPL.project_get_network(context, project_id)
294
295
296+def project_get_network_v6(context, project_id):
297+ return IMPL.project_get_network_v6(context, project_id)
298+
299+
300 ###################
301
302
303
304=== modified file 'nova/db/sqlalchemy/api.py'
305--- nova/db/sqlalchemy/api.py 2011-01-12 22:24:16 +0000
306+++ nova/db/sqlalchemy/api.py 2011-01-13 21:53:11 +0000
307@@ -606,6 +606,17 @@
308 return fixed_ip_ref.instance
309
310
311+@require_context
312+def fixed_ip_get_instance_v6(context, address):
313+ session = get_session()
314+ mac = utils.to_mac(address)
315+
316+ result = session.query(models.Instance
317+ ).filter_by(mac_address=mac
318+ ).first()
319+ return result
320+
321+
322 @require_admin_context
323 def fixed_ip_get_network(context, address):
324 fixed_ip_ref = fixed_ip_get_by_address(context, address)
325@@ -794,6 +805,17 @@
326
327
328 @require_context
329+def instance_get_fixed_address_v6(context, instance_id):
330+ session = get_session()
331+ with session.begin():
332+ instance_ref = instance_get(context, instance_id, session=session)
333+ network_ref = network_get_by_instance(context, instance_id)
334+ prefix = network_ref.cidr_v6
335+ mac = instance_ref.mac_address
336+ return utils.to_global_ipv6(prefix, mac)
337+
338+
339+@require_context
340 def instance_get_floating_address(context, instance_id):
341 session = get_session()
342 with session.begin():
343@@ -1130,6 +1152,11 @@
344 return result
345
346
347+@require_context
348+def project_get_network_v6(context, project_id):
349+ return project_get_network(context, project_id)
350+
351+
352 ###################
353
354
355
356=== modified file 'nova/db/sqlalchemy/models.py'
357--- nova/db/sqlalchemy/models.py 2011-01-12 23:03:08 +0000
358+++ nova/db/sqlalchemy/models.py 2011-01-13 21:53:11 +0000
359@@ -411,6 +411,10 @@
360
361 injected = Column(Boolean, default=False)
362 cidr = Column(String(255), unique=True)
363+ cidr_v6 = Column(String(255), unique=True)
364+
365+ ra_server = Column(String(255))
366+
367 netmask = Column(String(255))
368 bridge = Column(String(255))
369 gateway = Column(String(255))
370
371=== modified file 'nova/network/linux_net.py'
372--- nova/network/linux_net.py 2011-01-11 05:49:46 +0000
373+++ nova/network/linux_net.py 2011-01-13 21:53:11 +0000
374@@ -50,6 +50,7 @@
375 'Public IP of network host')
376 flags.DEFINE_bool('use_nova_chains', False,
377 'use the nova_ routing chains instead of default')
378+
379 flags.DEFINE_string('dns_server', None,
380 'if set, uses specific dns server for dnsmasq')
381 flags.DEFINE_string('dmz_cidr', '10.128.0.0/24',
382@@ -196,6 +197,10 @@
383 net_attrs['gateway'],
384 net_attrs['broadcast'],
385 net_attrs['netmask']))
386+ if(FLAGS.use_ipv6):
387+ _execute("sudo ifconfig %s add %s up" % \
388+ (bridge,
389+ net_attrs['cidr_v6']))
390 else:
391 _execute("sudo ifconfig %s up" % bridge)
392 if FLAGS.use_nova_chains:
393@@ -262,6 +267,50 @@
394 _execute(command, addl_env=env)
395
396
397+def update_ra(context, network_id):
398+ network_ref = db.network_get(context, network_id)
399+
400+ conffile = _ra_file(network_ref['bridge'], 'conf')
401+ with open(conffile, 'w') as f:
402+ conf_str = """
403+interface %s
404+{
405+ AdvSendAdvert on;
406+ MinRtrAdvInterval 3;
407+ MaxRtrAdvInterval 10;
408+ prefix %s
409+ {
410+ AdvOnLink on;
411+ AdvAutonomous on;
412+ };
413+};
414+""" % (network_ref['bridge'], network_ref['cidr_v6'])
415+ f.write(conf_str)
416+
417+ # Make sure radvd can actually read it (it setuid()s to "nobody")
418+ os.chmod(conffile, 0644)
419+
420+ pid = _ra_pid_for(network_ref['bridge'])
421+
422+ # if radvd is already running, then tell it to reload
423+ if pid:
424+ out, _err = _execute('cat /proc/%d/cmdline'
425+ % pid, check_exit_code=False)
426+ if conffile in out:
427+ try:
428+ _execute('sudo kill -HUP %d' % pid)
429+ return
430+ except Exception as exc: # pylint: disable-msg=W0703
431+ LOG.debug(_("Hupping radvd threw %s"), exc)
432+ else:
433+ LOG.debug(_("Pid %d is stale, relaunching radvd"), pid)
434+ command = _ra_cmd(network_ref)
435+ _execute(command)
436+ db.network_update(context, network_id,
437+ {"ra_server":
438+ utils.get_my_linklocal(network_ref['bridge'])})
439+
440+
441 def _host_dhcp(fixed_ip_ref):
442 """Return a host string for an address"""
443 instance_ref = fixed_ip_ref['instance']
444@@ -323,6 +372,15 @@
445 return ''.join(cmd)
446
447
448+def _ra_cmd(net):
449+ """Builds radvd command"""
450+ cmd = ['sudo -E radvd',
451+# ' -u nobody',
452+ ' -C %s' % _ra_file(net['bridge'], 'conf'),
453+ ' -p %s' % _ra_file(net['bridge'], 'pid')]
454+ return ''.join(cmd)
455+
456+
457 def _stop_dnsmasq(network):
458 """Stops the dnsmasq instance for a given network"""
459 pid = _dnsmasq_pid_for(network)
460@@ -344,6 +402,16 @@
461 kind))
462
463
464+def _ra_file(bridge, kind):
465+ """Return path to a pid or conf file for a bridge"""
466+
467+ if not os.path.exists(FLAGS.networks_path):
468+ os.makedirs(FLAGS.networks_path)
469+ return os.path.abspath("%s/nova-ra-%s.%s" % (FLAGS.networks_path,
470+ bridge,
471+ kind))
472+
473+
474 def _dnsmasq_pid_for(bridge):
475 """Returns the pid for prior dnsmasq instance for a bridge
476
477@@ -357,3 +425,18 @@
478 if os.path.exists(pid_file):
479 with open(pid_file, 'r') as f:
480 return int(f.read())
481+
482+
483+def _ra_pid_for(bridge):
484+ """Returns the pid for prior radvd instance for a bridge
485+
486+ Returns None if no pid file exists
487+
488+ If machine has rebooted pid might be incorrect (caller should check)
489+ """
490+
491+ pid_file = _ra_file(bridge, 'pid')
492+
493+ if os.path.exists(pid_file):
494+ with open(pid_file, 'r') as f:
495+ return int(f.read())
496
497=== modified file 'nova/network/manager.py'
498--- nova/network/manager.py 2011-01-10 02:08:54 +0000
499+++ nova/network/manager.py 2011-01-13 21:53:11 +0000
500@@ -82,6 +82,7 @@
501 flags.DEFINE_string('floating_range', '4.4.4.0/24',
502 'Floating IP address block')
503 flags.DEFINE_string('fixed_range', '10.0.0.0/8', 'Fixed IP address block')
504+flags.DEFINE_string('fixed_range_v6', 'fd00::/48', 'Fixed IPv6 address block')
505 flags.DEFINE_integer('cnt_vpn_clients', 5,
506 'Number of addresses reserved for vpn clients')
507 flags.DEFINE_string('network_driver', 'nova.network.linux_net',
508@@ -90,6 +91,9 @@
509 'Whether to update dhcp when fixed_ip is disassociated')
510 flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600,
511 'Seconds after which a deallocated ip is disassociated')
512+
513+flags.DEFINE_bool('use_ipv6', True,
514+ 'use the ipv6')
515 flags.DEFINE_string('network_host', socket.gethostname(),
516 'Network host to use for ip allocation in flat modes')
517 flags.DEFINE_bool('fake_call', False,
518@@ -235,7 +239,7 @@
519 """Get the network host for the current context."""
520 raise NotImplementedError()
521
522- def create_networks(self, context, num_networks, network_size,
523+ def create_networks(self, context, num_networks, network_size, cidr_v6,
524 *args, **kwargs):
525 """Create networks based on parameters."""
526 raise NotImplementedError()
527@@ -321,9 +325,11 @@
528 pass
529
530 def create_networks(self, context, cidr, num_networks, network_size,
531- *args, **kwargs):
532+ cidr_v6, *args, **kwargs):
533 """Create networks based on parameters."""
534 fixed_net = IPy.IP(cidr)
535+ fixed_net_v6 = IPy.IP(cidr_v6)
536+ significant_bits_v6 = 64
537 for index in range(num_networks):
538 start = index * network_size
539 significant_bits = 32 - int(math.log(network_size, 2))
540@@ -336,7 +342,13 @@
541 net['gateway'] = str(project_net[1])
542 net['broadcast'] = str(project_net.broadcast())
543 net['dhcp_start'] = str(project_net[2])
544+
545+ if(FLAGS.use_ipv6):
546+ cidr_v6 = "%s/%s" % (fixed_net_v6[0], significant_bits_v6)
547+ net['cidr_v6'] = cidr_v6
548+
549 network_ref = self.db.network_create_safe(context, net)
550+
551 if network_ref:
552 self._create_fixed_ips(context, network_ref['id'])
553
554@@ -482,12 +494,16 @@
555 network_ref['bridge'])
556
557 def create_networks(self, context, cidr, num_networks, network_size,
558- vlan_start, vpn_start):
559+ vlan_start, vpn_start, cidr_v6):
560 """Create networks based on parameters."""
561 fixed_net = IPy.IP(cidr)
562+ fixed_net_v6 = IPy.IP(cidr_v6)
563+ network_size_v6 = 1 << 64
564+ significant_bits_v6 = 64
565 for index in range(num_networks):
566 vlan = vlan_start + index
567 start = index * network_size
568+ start_v6 = index * network_size_v6
569 significant_bits = 32 - int(math.log(network_size, 2))
570 cidr = "%s/%s" % (fixed_net[start], significant_bits)
571 project_net = IPy.IP(cidr)
572@@ -500,6 +516,13 @@
573 net['dhcp_start'] = str(project_net[3])
574 net['vlan'] = vlan
575 net['bridge'] = 'br%s' % vlan
576+ if(FLAGS.use_ipv6):
577+ cidr_v6 = "%s/%s" % (
578+ fixed_net_v6[start_v6],
579+ significant_bits_v6
580+ )
581+ net['cidr_v6'] = cidr_v6
582+
583 # NOTE(vish): This makes ports unique accross the cloud, a more
584 # robust solution would be to make them unique per ip
585 net['vpn_public_port'] = vpn_start + index
586@@ -538,6 +561,7 @@
587 self.driver.ensure_vlan_bridge(network_ref['vlan'],
588 network_ref['bridge'],
589 network_ref)
590+
591 # NOTE(vish): only ensure this forward if the address hasn't been set
592 # manually.
593 if address == FLAGS.vpn_ip:
594@@ -546,6 +570,8 @@
595 network_ref['vpn_private_address'])
596 if not FLAGS.fake_network:
597 self.driver.update_dhcp(context, network_id)
598+ if(FLAGS.use_ipv6):
599+ self.driver.update_ra(context, network_id)
600
601 @property
602 def _bottom_reserved_ips(self):
603
604=== modified file 'nova/test.py'
605--- nova/test.py 2010-12-17 01:05:54 +0000
606+++ nova/test.py 2011-01-13 21:53:11 +0000
607@@ -156,7 +156,8 @@
608 FLAGS.fixed_range,
609 5, 16,
610 FLAGS.vlan_start,
611- FLAGS.vpn_start)
612+ FLAGS.vpn_start,
613+ FLAGS.fixed_range_v6)
614
615 # emulate some of the mox stuff, we can't use the metaclass
616 # because it screws with our generators
617
618=== modified file 'nova/tests/test_api.py'
619--- nova/tests/test_api.py 2010-12-17 01:05:54 +0000
620+++ nova/tests/test_api.py 2011-01-13 21:53:11 +0000
621@@ -265,6 +265,72 @@
622
623 return
624
625+ def test_authorize_revoke_security_group_cidr_v6(self):
626+ """
627+ Test that we can add and remove CIDR based rules
628+ to a security group for IPv6
629+ """
630+ self.expect_http()
631+ self.mox.ReplayAll()
632+ user = self.manager.create_user('fake', 'fake', 'fake')
633+ project = self.manager.create_project('fake', 'fake', 'fake')
634+
635+ # At the moment, you need both of these to actually be netadmin
636+ self.manager.add_role('fake', 'netadmin')
637+ project.add_role('fake', 'netadmin')
638+
639+ security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd")
640+ for x in range(random.randint(4, 8)))
641+
642+ group = self.ec2.create_security_group(security_group_name,
643+ 'test group')
644+
645+ self.expect_http()
646+ self.mox.ReplayAll()
647+ group.connection = self.ec2
648+
649+ group.authorize('tcp', 80, 81, '::/0')
650+
651+ self.expect_http()
652+ self.mox.ReplayAll()
653+
654+ rv = self.ec2.get_all_security_groups()
655+ # I don't bother checkng that we actually find it here,
656+ # because the create/delete unit test further up should
657+ # be good enough for that.
658+ for group in rv:
659+ if group.name == security_group_name:
660+ self.assertEquals(len(group.rules), 1)
661+ self.assertEquals(int(group.rules[0].from_port), 80)
662+ self.assertEquals(int(group.rules[0].to_port), 81)
663+ self.assertEquals(len(group.rules[0].grants), 1)
664+ self.assertEquals(str(group.rules[0].grants[0]), '::/0')
665+
666+ self.expect_http()
667+ self.mox.ReplayAll()
668+ group.connection = self.ec2
669+
670+ group.revoke('tcp', 80, 81, '::/0')
671+
672+ self.expect_http()
673+ self.mox.ReplayAll()
674+
675+ self.ec2.delete_security_group(security_group_name)
676+
677+ self.expect_http()
678+ self.mox.ReplayAll()
679+ group.connection = self.ec2
680+
681+ rv = self.ec2.get_all_security_groups()
682+
683+ self.assertEqual(len(rv), 1)
684+ self.assertEqual(rv[0].name, 'default')
685+
686+ self.manager.delete_project(project)
687+ self.manager.delete_user(user)
688+
689+ return
690+
691 def test_authorize_revoke_security_group_foreign_group(self):
692 """
693 Test that we can grant and revoke another security group access
694
695=== modified file 'nova/tests/test_network.py'
696--- nova/tests/test_network.py 2011-01-04 05:23:35 +0000
697+++ nova/tests/test_network.py 2011-01-13 21:53:11 +0000
698@@ -96,6 +96,28 @@
699 self.context.project_id = self.projects[project_num].id
700 self.network.deallocate_fixed_ip(self.context, address)
701
702+ def test_private_ipv6(self):
703+ """Make sure ipv6 is OK"""
704+ if FLAGS.use_ipv6:
705+ instance_ref = self._create_instance(0)
706+ address = self._create_address(0, instance_ref['id'])
707+ network_ref = db.project_get_network(
708+ context.get_admin_context(),
709+ self.context.project_id)
710+ address_v6 = db.instance_get_fixed_address_v6(
711+ context.get_admin_context(),
712+ instance_ref['id'])
713+ self.assertEqual(instance_ref['mac_address'],
714+ utils.to_mac(address_v6))
715+ instance_ref2 = db.fixed_ip_get_instance_v6(
716+ context.get_admin_context(),
717+ address_v6)
718+ self.assertEqual(instance_ref['id'], instance_ref2['id'])
719+ self.assertEqual(address_v6,
720+ utils.to_global_ipv6(
721+ network_ref['cidr_v6'],
722+ instance_ref['mac_address']))
723+
724 def test_public_network_association(self):
725 """Makes sure that we can allocaate a public ip"""
726 # TODO(vish): better way of adding floating ips
727
728=== modified file 'nova/utils.py'
729--- nova/utils.py 2011-01-12 01:32:12 +0000
730+++ nova/utils.py 2011-01-13 21:53:11 +0000
731@@ -30,6 +30,8 @@
732 import sys
733 import time
734 from xml.sax import saxutils
735+import re
736+import netaddr
737
738 from eventlet import event
739 from eventlet import greenthread
740@@ -200,6 +202,40 @@
741 return int(address.split(".")[-1])
742
743
744+def get_my_linklocal(interface):
745+ try:
746+ if_str = execute("ip -f inet6 -o addr show %s" % interface)
747+ condition = "\s+inet6\s+([0-9a-f:]+/\d+)\s+scope\s+link"
748+ links = [re.search(condition, x) for x in if_str[0].split('\n')]
749+ address = [w.group(1) for w in links if w is not None]
750+ if address[0] is not None:
751+ return address[0]
752+ else:
753+ return 'fe00::'
754+ except IndexError as ex:
755+ LOG.warn(_("Couldn't get Link Local IP of %s :%s"), interface, ex)
756+ except ProcessExecutionError as ex:
757+ LOG.warn(_("Couldn't get Link Local IP of %s :%s"), interface, ex)
758+ except:
759+ return 'fe00::'
760+
761+
762+def to_global_ipv6(prefix, mac):
763+ mac64 = netaddr.EUI(mac).eui64().words
764+ int_addr = int(''.join(['%02x' % i for i in mac64]), 16)
765+ mac64_addr = netaddr.IPAddress(int_addr)
766+ maskIP = netaddr.IPNetwork(prefix).ip
767+ return (mac64_addr ^ netaddr.IPAddress('::0200:0:0:0') | maskIP).format()
768+
769+
770+def to_mac(ipv6_address):
771+ address = netaddr.IPAddress(ipv6_address)
772+ mask1 = netaddr.IPAddress("::ffff:ffff:ffff:ffff")
773+ mask2 = netaddr.IPAddress("::0200:0:0:0")
774+ mac64 = netaddr.EUI(int(address & mask1 ^ mask2)).words
775+ return ":".join(["%02x" % i for i in mac64[0:3] + mac64[5:8]])
776+
777+
778 def utcnow():
779 """Overridable version of datetime.datetime.utcnow."""
780 if utcnow.override_time:
781
782=== modified file 'nova/virt/libvirt.xml.template'
783--- nova/virt/libvirt.xml.template 2011-01-05 00:22:47 +0000
784+++ nova/virt/libvirt.xml.template 2011-01-13 21:53:11 +0000
785@@ -66,6 +66,7 @@
786 <filterref filter="nova-instance-${name}">
787 <parameter name="IP" value="${ip_address}" />
788 <parameter name="DHCPSERVER" value="${dhcp_server}" />
789+ <parameter name="RASERVER" value="${ra_server}" />
790 #if $getVar('extra_params', False)
791 ${extra_params}
792 #end if
793
794=== modified file 'nova/virt/libvirt_conn.py'
795--- nova/virt/libvirt_conn.py 2011-01-13 17:08:53 +0000
796+++ nova/virt/libvirt_conn.py 2011-01-13 21:53:11 +0000
797@@ -127,6 +127,16 @@
798 return str(net.net()), str(net.netmask())
799
800
801+def _get_net_and_prefixlen(cidr):
802+ net = IPy.IP(cidr)
803+ return str(net.net()), str(net.prefixlen())
804+
805+
806+def _get_ip_version(cidr):
807+ net = IPy.IP(cidr)
808+ return int(net.version())
809+
810+
811 class LibvirtConnection(object):
812
813 def __init__(self, read_only):
814@@ -372,7 +382,6 @@
815 instance['id'],
816 power_state.NOSTATE,
817 'launching')
818-
819 self.nwfilter.setup_basic_filtering(instance)
820 self.firewall_driver.prepare_instance_filter(instance)
821 self._create_image(instance, xml)
822@@ -541,12 +550,16 @@
823 if network_ref['injected']:
824 admin_context = context.get_admin_context()
825 address = db.instance_get_fixed_address(admin_context, inst['id'])
826+ ra_server = network_ref['ra_server']
827+ if not ra_server:
828+ ra_server = "fd00::"
829 with open(FLAGS.injected_network_template) as f:
830 net = f.read() % {'address': address,
831 'netmask': network_ref['netmask'],
832 'gateway': network_ref['gateway'],
833 'broadcast': network_ref['broadcast'],
834- 'dns': network_ref['dns']}
835+ 'dns': network_ref['dns'],
836+ 'ra_server': ra_server}
837 if key or net:
838 if key:
839 LOG.info(_('instance %s: injecting key into image %s'),
840@@ -601,13 +614,30 @@
841 instance['id'])
842 # Assume that the gateway also acts as the dhcp server.
843 dhcp_server = network['gateway']
844-
845+ ra_server = network['ra_server']
846+ if not ra_server:
847+ ra_server = 'fd00::'
848 if FLAGS.allow_project_net_traffic:
849- net, mask = _get_net_and_mask(network['cidr'])
850- extra_params = ("<parameter name=\"PROJNET\" "
851- "value=\"%s\" />\n"
852- "<parameter name=\"PROJMASK\" "
853- "value=\"%s\" />\n") % (net, mask)
854+ if FLAGS.use_ipv6:
855+ net, mask = _get_net_and_mask(network['cidr'])
856+ net_v6, prefixlen_v6 = _get_net_and_prefixlen(
857+ network['cidr_v6'])
858+ extra_params = ("<parameter name=\"PROJNET\" "
859+ "value=\"%s\" />\n"
860+ "<parameter name=\"PROJMASK\" "
861+ "value=\"%s\" />\n"
862+ "<parameter name=\"PROJNETV6\" "
863+ "value=\"%s\" />\n"
864+ "<parameter name=\"PROJMASKV6\" "
865+ "value=\"%s\" />\n") % \
866+ (net, mask, net_v6, prefixlen_v6)
867+ else:
868+ net, mask = _get_net_and_mask(network['cidr'])
869+ extra_params = ("<parameter name=\"PROJNET\" "
870+ "value=\"%s\" />\n"
871+ "<parameter name=\"PROJMASK\" "
872+ "value=\"%s\" />\n") % \
873+ (net, mask)
874 else:
875 extra_params = "\n"
876
877@@ -621,6 +651,7 @@
878 'mac_address': instance['mac_address'],
879 'ip_address': ip_address,
880 'dhcp_server': dhcp_server,
881+ 'ra_server': ra_server,
882 'extra_params': extra_params,
883 'rescue': rescue}
884 if not rescue:
885@@ -882,6 +913,15 @@
886 </rule>
887 </filter>'''
888
889+ def nova_ra_filter(self):
890+ return '''<filter name='nova-allow-ra-server' chain='root'>
891+ <uuid>d707fa71-4fb5-4b27-9ab7-ba5ca19c8804</uuid>
892+ <rule action='accept' direction='inout'
893+ priority='100'>
894+ <icmpv6 srcipaddr='$RASERVER'/>
895+ </rule>
896+ </filter>'''
897+
898 def setup_basic_filtering(self, instance):
899 """Set up basic filtering (MAC, IP, and ARP spoofing protection)"""
900 logging.info('called setup_basic_filtering in nwfilter')
901@@ -906,13 +946,17 @@
902 ['no-mac-spoofing',
903 'no-ip-spoofing',
904 'no-arp-spoofing',
905- 'allow-dhcp-server']))
906+ 'allow-dhcp-server'
907+ ]))
908 self._define_filter(self.nova_base_ipv4_filter)
909 self._define_filter(self.nova_base_ipv6_filter)
910 self._define_filter(self.nova_dhcp_filter)
911+ self._define_filter(self.nova_ra_filter)
912 self._define_filter(self.nova_vpn_filter)
913 if FLAGS.allow_project_net_traffic:
914 self._define_filter(self.nova_project_filter)
915+ if FLAGS.use_ipv6:
916+ self._define_filter(self.nova_project_filter_v6)
917
918 self.static_filters_configured = True
919
920@@ -944,13 +988,13 @@
921
922 def nova_base_ipv6_filter(self):
923 retval = "<filter name='nova-base-ipv6' chain='ipv6'>"
924- for protocol in ['tcp', 'udp', 'icmp']:
925+ for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
926 for direction, action, priority in [('out', 'accept', 399),
927 ('in', 'drop', 400)]:
928 retval += """<rule action='%s' direction='%s' priority='%d'>
929- <%s-ipv6 />
930+ <%s />
931 </rule>""" % (action, direction,
932- priority, protocol)
933+ priority, protocol)
934 retval += '</filter>'
935 return retval
936
937@@ -963,10 +1007,20 @@
938 retval += '</filter>'
939 return retval
940
941+ def nova_project_filter_v6(self):
942+ retval = "<filter name='nova-project-v6' chain='ipv6'>"
943+ for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
944+ retval += """<rule action='accept' direction='inout'
945+ priority='200'>
946+ <%s srcipaddr='$PROJNETV6'
947+ srcipmask='$PROJMASKV6' />
948+ </rule>""" % (protocol)
949+ retval += '</filter>'
950+ return retval
951+
952 def _define_filter(self, xml):
953 if callable(xml):
954 xml = xml()
955-
956 # execute in a native thread and block current greenthread until done
957 tpool.execute(self._conn.nwfilterDefineXML, xml)
958
959@@ -980,7 +1034,6 @@
960 it makes sure the filters for the security groups as well as
961 the base filter are all in place.
962 """
963-
964 if instance['image_id'] == FLAGS.vpn_image_id:
965 base_filter = 'nova-vpn'
966 else:
967@@ -992,11 +1045,15 @@
968 instance_secgroup_filter_children = ['nova-base-ipv4',
969 'nova-base-ipv6',
970 'nova-allow-dhcp-server']
971+ if FLAGS.use_ipv6:
972+ instance_secgroup_filter_children += ['nova-allow-ra-server']
973
974 ctxt = context.get_admin_context()
975
976 if FLAGS.allow_project_net_traffic:
977 instance_filter_children += ['nova-project']
978+ if FLAGS.use_ipv6:
979+ instance_filter_children += ['nova-project-v6']
980
981 for security_group in db.security_group_get_by_instance(ctxt,
982 instance['id']):
983@@ -1024,12 +1081,19 @@
984 security_group = db.security_group_get(context.get_admin_context(),
985 security_group_id)
986 rule_xml = ""
987+ v6protocol = {'tcp': 'tcp-ipv6', 'udp': 'udp-ipv6', 'icmp': 'icmpv6'}
988 for rule in security_group.rules:
989 rule_xml += "<rule action='accept' direction='in' priority='300'>"
990 if rule.cidr:
991- net, mask = _get_net_and_mask(rule.cidr)
992- rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
993- (rule.protocol, net, mask)
994+ version = _get_ip_version(rule.cidr)
995+ if(FLAGS.use_ipv6 and version == 6):
996+ net, prefixlen = _get_net_and_prefixlen(rule.cidr)
997+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
998+ (v6protocol[rule.protocol], net, prefixlen)
999+ else:
1000+ net, mask = _get_net_and_mask(rule.cidr)
1001+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
1002+ (rule.protocol, net, mask)
1003 if rule.protocol in ['tcp', 'udp']:
1004 rule_xml += "dstportstart='%s' dstportend='%s' " % \
1005 (rule.from_port, rule.to_port)
1006@@ -1044,8 +1108,11 @@
1007
1008 rule_xml += '/>\n'
1009 rule_xml += "</rule>\n"
1010- xml = "<filter name='nova-secgroup-%s' chain='ipv4'>%s</filter>" % \
1011- (security_group_id, rule_xml,)
1012+ xml = "<filter name='nova-secgroup-%s' " % security_group_id
1013+ if(FLAGS.use_ipv6):
1014+ xml += "chain='root'>%s</filter>" % rule_xml
1015+ else:
1016+ xml += "chain='ipv4'>%s</filter>" % rule_xml
1017 return xml
1018
1019 def _instance_filter_name(self, instance):
1020@@ -1082,11 +1149,17 @@
1021 def apply_ruleset(self):
1022 current_filter, _ = self.execute('sudo iptables-save -t filter')
1023 current_lines = current_filter.split('\n')
1024- new_filter = self.modify_rules(current_lines)
1025+ new_filter = self.modify_rules(current_lines, 4)
1026 self.execute('sudo iptables-restore',
1027 process_input='\n'.join(new_filter))
1028+ if(FLAGS.use_ipv6):
1029+ current_filter, _ = self.execute('sudo ip6tables-save -t filter')
1030+ current_lines = current_filter.split('\n')
1031+ new_filter = self.modify_rules(current_lines, 6)
1032+ self.execute('sudo ip6tables-restore',
1033+ process_input='\n'.join(new_filter))
1034
1035- def modify_rules(self, current_lines):
1036+ def modify_rules(self, current_lines, ip_version=4):
1037 ctxt = context.get_admin_context()
1038 # Remove any trace of nova rules.
1039 new_filter = filter(lambda l: 'nova-' not in l, current_lines)
1040@@ -1100,8 +1173,8 @@
1041 if not new_filter[rules_index].startswith(':'):
1042 break
1043
1044- our_chains = [':nova-ipv4-fallback - [0:0]']
1045- our_rules = ['-A nova-ipv4-fallback -j DROP']
1046+ our_chains = [':nova-fallback - [0:0]']
1047+ our_rules = ['-A nova-fallback -j DROP']
1048
1049 our_chains += [':nova-local - [0:0]']
1050 our_rules += ['-A FORWARD -j nova-local']
1051@@ -1112,7 +1185,10 @@
1052 for instance_id in self.instances:
1053 instance = self.instances[instance_id]
1054 chain_name = self._instance_chain_name(instance)
1055- ip_address = self._ip_for_instance(instance)
1056+ if(ip_version == 4):
1057+ ip_address = self._ip_for_instance(instance)
1058+ elif(ip_version == 6):
1059+ ip_address = self._ip_for_instance_v6(instance)
1060
1061 our_chains += [':%s - [0:0]' % chain_name]
1062
1063@@ -1139,13 +1215,19 @@
1064
1065 our_rules += ['-A %s -j %s' % (chain_name, sg_chain_name)]
1066
1067- # Allow DHCP responses
1068- dhcp_server = self._dhcp_server_for_instance(instance)
1069- our_rules += ['-A %s -s %s -p udp --sport 67 --dport 68' %
1070- (chain_name, dhcp_server)]
1071+ if(ip_version == 4):
1072+ # Allow DHCP responses
1073+ dhcp_server = self._dhcp_server_for_instance(instance)
1074+ our_rules += ['-A %s -s %s -p udp --sport 67 --dport 68' %
1075+ (chain_name, dhcp_server)]
1076+ elif(ip_version == 6):
1077+ # Allow RA responses
1078+ ra_server = self._ra_server_for_instance(instance)
1079+ our_rules += ['-A %s -s %s -p icmpv6' %
1080+ (chain_name, ra_server)]
1081
1082 # If nothing matches, jump to the fallback chain
1083- our_rules += ['-A %s -j nova-ipv4-fallback' % (chain_name,)]
1084+ our_rules += ['-A %s -j nova-fallback' % (chain_name,)]
1085
1086 # then, security group chains and rules
1087 for security_group_id in security_groups:
1088@@ -1158,15 +1240,22 @@
1089
1090 for rule in rules:
1091 logging.info('%r', rule)
1092- args = ['-A', chain_name, '-p', rule.protocol]
1093
1094- if rule.cidr:
1095- args += ['-s', rule.cidr]
1096- else:
1097+ if not rule.cidr:
1098 # Eventually, a mechanism to grant access for security
1099 # groups will turn up here. It'll use ipsets.
1100 continue
1101
1102+ version = _get_ip_version(rule.cidr)
1103+ if version != ip_version:
1104+ continue
1105+
1106+ protocol = rule.protocol
1107+ if version == 6 and rule.protocol == 'icmp':
1108+ protocol = 'icmpv6'
1109+
1110+ args = ['-A', chain_name, '-p', protocol, '-s', rule.cidr]
1111+
1112 if rule.protocol in ['udp', 'tcp']:
1113 if rule.from_port == rule.to_port:
1114 args += ['--dport', '%s' % (rule.from_port,)]
1115@@ -1186,7 +1275,12 @@
1116 icmp_type_arg += '/%s' % icmp_code
1117
1118 if icmp_type_arg:
1119- args += ['-m', 'icmp', '--icmp-type', icmp_type_arg]
1120+ if(ip_version == 4):
1121+ args += ['-m', 'icmp', '--icmp-type',
1122+ icmp_type_arg]
1123+ elif(ip_version == 6):
1124+ args += ['-m', 'icmp6', '--icmpv6-type',
1125+ icmp_type_arg]
1126
1127 args += ['-j ACCEPT']
1128 our_rules += [' '.join(args)]
1129@@ -1212,7 +1306,16 @@
1130 return db.instance_get_fixed_address(context.get_admin_context(),
1131 instance['id'])
1132
1133+ def _ip_for_instance_v6(self, instance):
1134+ return db.instance_get_fixed_address_v6(context.get_admin_context(),
1135+ instance['id'])
1136+
1137 def _dhcp_server_for_instance(self, instance):
1138 network = db.project_get_network(context.get_admin_context(),
1139 instance['project_id'])
1140 return network['gateway']
1141+
1142+ def _ra_server_for_instance(self, instance):
1143+ network = db.project_get_network(context.get_admin_context(),
1144+ instance['project_id'])
1145+ return network['ra_server']
1146
1147=== modified file 'smoketests/admin_smoketests.py'
1148--- smoketests/admin_smoketests.py 2011-01-06 01:51:05 +0000
1149+++ smoketests/admin_smoketests.py 2011-01-13 21:53:11 +0000
1150@@ -43,7 +43,7 @@
1151 # TODO(devamcar): Use random tempfile
1152 ZIP_FILENAME = '/tmp/nova-me-x509.zip'
1153
1154-TEST_PREFIX = 'test%s' % int(random.random()*1000000)
1155+TEST_PREFIX = 'test%s' % int(random.random() * 1000000)
1156 TEST_USERNAME = '%suser' % TEST_PREFIX
1157 TEST_PROJECTNAME = '%sproject' % TEST_PREFIX
1158
1159@@ -96,4 +96,3 @@
1160 if __name__ == "__main__":
1161 suites = {'user': unittest.makeSuite(UserTests)}
1162 sys.exit(base.run_tests(suites))
1163-
1164
1165=== modified file 'smoketests/base.py'
1166--- smoketests/base.py 2010-11-04 22:50:23 +0000
1167+++ smoketests/base.py 2011-01-13 21:53:11 +0000
1168@@ -17,6 +17,7 @@
1169 # under the License.
1170
1171 import boto
1172+import boto_v6
1173 import commands
1174 import httplib
1175 import os
1176@@ -69,6 +70,17 @@
1177 'test.')
1178
1179 parts = self.split_clc_url(clc_url)
1180+ if FLAGS.use_ipv6:
1181+ return boto_v6.connect_ec2(aws_access_key_id=access_key,
1182+ aws_secret_access_key=secret_key,
1183+ is_secure=parts['is_secure'],
1184+ region=RegionInfo(None,
1185+ 'nova',
1186+ parts['ip']),
1187+ port=parts['port'],
1188+ path='/services/Cloud',
1189+ **kwargs)
1190+
1191 return boto.connect_ec2(aws_access_key_id=access_key,
1192 aws_secret_access_key=secret_key,
1193 is_secure=parts['is_secure'],
1194@@ -115,7 +127,8 @@
1195 return True
1196
1197 def upload_image(self, bucket_name, image):
1198- cmd = 'euca-upload-bundle -b %s -m /tmp/%s.manifest.xml' % (bucket_name, image)
1199+ cmd = 'euca-upload-bundle -b '
1200+ cmd += '%s -m /tmp/%s.manifest.xml' % (bucket_name, image)
1201 status, output = commands.getstatusoutput(cmd)
1202 if status != 0:
1203 print '%s -> \n %s' % (cmd, output)
1204@@ -130,6 +143,7 @@
1205 raise Exception(output)
1206 return True
1207
1208+
1209 def run_tests(suites):
1210 argv = FLAGS(sys.argv)
1211
1212@@ -151,4 +165,3 @@
1213 else:
1214 for suite in suites.itervalues():
1215 unittest.TextTestRunner(verbosity=2).run(suite)
1216-
1217
1218=== modified file 'smoketests/flags.py'
1219--- smoketests/flags.py 2010-11-04 22:50:23 +0000
1220+++ smoketests/flags.py 2011-01-13 21:53:11 +0000
1221@@ -33,6 +33,7 @@
1222 # __GLOBAL FLAGS ONLY__
1223 # Define any app-specific flags in their own files, docs at:
1224 # http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39
1225+
1226 DEFINE_string('region', 'nova', 'Region to use')
1227 DEFINE_string('test_image', 'ami-tiny', 'Image to use for launch tests')
1228-
1229+DEFINE_string('use_ipv6', True, 'use the ipv6 or not')
1230
1231=== added file 'smoketests/public_network_smoketests.py'
1232--- smoketests/public_network_smoketests.py 1970-01-01 00:00:00 +0000
1233+++ smoketests/public_network_smoketests.py 2011-01-13 21:53:11 +0000
1234@@ -0,0 +1,180 @@
1235+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1236+
1237+# Copyright 2010 United States Government as represented by the
1238+# Administrator of the National Aeronautics and Space Administration.
1239+# All Rights Reserved.
1240+#
1241+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1242+# not use this file except in compliance with the License. You may obtain
1243+# a copy of the License at
1244+#
1245+# http://www.apache.org/licenses/LICENSE-2.0
1246+#
1247+# Unless required by applicable law or agreed to in writing, software
1248+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1249+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1250+# License for the specific language governing permissions and limitations
1251+# under the License.
1252+
1253+import commands
1254+import os
1255+import random
1256+import socket
1257+import sys
1258+import time
1259+import unittest
1260+
1261+from smoketests import flags
1262+from smoketests import base
1263+from smoketests import user_smoketests
1264+
1265+#Note that this test should run from
1266+#public network (outside of private network segments)
1267+#Please set EC2_URL correctly
1268+#You should use admin account in this test
1269+
1270+FLAGS = flags.FLAGS
1271+
1272+TEST_PREFIX = 'test%s' % int(random.random() * 1000000)
1273+TEST_BUCKET = '%s_bucket' % TEST_PREFIX
1274+TEST_KEY = '%s_key' % TEST_PREFIX
1275+TEST_KEY2 = '%s_key2' % TEST_PREFIX
1276+TEST_DATA = {}
1277+
1278+
1279+class InstanceTestsFromPublic(user_smoketests.UserSmokeTestCase):
1280+ def test_001_can_create_keypair(self):
1281+ key = self.create_key_pair(self.conn, TEST_KEY)
1282+ self.assertEqual(key.name, TEST_KEY)
1283+
1284+ def test_002_security_group(self):
1285+ security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd")
1286+ for x in range(random.randint(4, 8)))
1287+ group = self.conn.create_security_group(security_group_name,
1288+ 'test group')
1289+ group.connection = self.conn
1290+ group.authorize('tcp', 22, 22, '0.0.0.0/0')
1291+ if FLAGS.use_ipv6:
1292+ group.authorize('tcp', 22, 22, '::/0')
1293+
1294+ reservation = self.conn.run_instances(FLAGS.test_image,
1295+ key_name=TEST_KEY,
1296+ security_groups=[security_group_name],
1297+ instance_type='m1.tiny')
1298+ self.data['security_group_name'] = security_group_name
1299+ self.data['group'] = group
1300+ self.data['instance_id'] = reservation.instances[0].id
1301+
1302+ def test_003_instance_with_group_runs_within_60_seconds(self):
1303+ reservations = self.conn.get_all_instances([self.data['instance_id']])
1304+ instance = reservations[0].instances[0]
1305+ # allow 60 seconds to exit pending with IP
1306+ for x in xrange(60):
1307+ instance.update()
1308+ if instance.state == u'running':
1309+ break
1310+ time.sleep(1)
1311+ else:
1312+ self.fail('instance failed to start')
1313+ ip = reservations[0].instances[0].private_dns_name
1314+ self.failIf(ip == '0.0.0.0')
1315+ self.data['private_ip'] = ip
1316+ if FLAGS.use_ipv6:
1317+ ipv6 = reservations[0].instances[0].dns_name_v6
1318+ self.failIf(ipv6 is None)
1319+ self.data['ip_v6'] = ipv6
1320+
1321+ def test_004_can_ssh_to_ipv6(self):
1322+ if FLAGS.use_ipv6:
1323+ for x in xrange(20):
1324+ try:
1325+ conn = self.connect_ssh(
1326+ self.data['ip_v6'], TEST_KEY)
1327+ conn.close()
1328+ except Exception as ex:
1329+ print ex
1330+ time.sleep(1)
1331+ else:
1332+ break
1333+ else:
1334+ self.fail('could not ssh to instance')
1335+
1336+ def test_012_can_create_instance_with_keypair(self):
1337+ if 'instance_id' in self.data:
1338+ self.conn.terminate_instances([self.data['instance_id']])
1339+ reservation = self.conn.run_instances(FLAGS.test_image,
1340+ key_name=TEST_KEY,
1341+ instance_type='m1.tiny')
1342+ self.assertEqual(len(reservation.instances), 1)
1343+ self.data['instance_id'] = reservation.instances[0].id
1344+
1345+ def test_013_instance_runs_within_60_seconds(self):
1346+ reservations = self.conn.get_all_instances([self.data['instance_id']])
1347+ instance = reservations[0].instances[0]
1348+ # allow 60 seconds to exit pending with IP
1349+ for x in xrange(60):
1350+ instance.update()
1351+ if instance.state == u'running':
1352+ break
1353+ time.sleep(1)
1354+ else:
1355+ self.fail('instance failed to start')
1356+ ip = reservations[0].instances[0].private_dns_name
1357+ self.failIf(ip == '0.0.0.0')
1358+ self.data['private_ip'] = ip
1359+ if FLAGS.use_ipv6:
1360+ ipv6 = reservations[0].instances[0].dns_name_v6
1361+ self.failIf(ipv6 is None)
1362+ self.data['ip_v6'] = ipv6
1363+
1364+ def test_014_can_not_ping_private_ip(self):
1365+ for x in xrange(4):
1366+ # ping waits for 1 second
1367+ status, output = commands.getstatusoutput(
1368+ 'ping -c1 %s' % self.data['private_ip'])
1369+ if status == 0:
1370+ self.fail('can ping private ip from public network')
1371+ if FLAGS.use_ipv6:
1372+ status, output = commands.getstatusoutput(
1373+ 'ping6 -c1 %s' % self.data['ip_v6'])
1374+ if status == 0:
1375+ self.fail('can ping ipv6 from public network')
1376+ else:
1377+ pass
1378+
1379+ def test_015_can_not_ssh_to_private_ip(self):
1380+ for x in xrange(1):
1381+ try:
1382+ conn = self.connect_ssh(self.data['private_ip'], TEST_KEY)
1383+ conn.close()
1384+ except Exception:
1385+ time.sleep(1)
1386+ else:
1387+ self.fail('can ssh for ipv4 address from public network')
1388+
1389+ if FLAGS.use_ipv6:
1390+ for x in xrange(1):
1391+ try:
1392+ conn = self.connect_ssh(
1393+ self.data['ip_v6'], TEST_KEY)
1394+ conn.close()
1395+ except Exception:
1396+ time.sleep(1)
1397+ else:
1398+ self.fail('can ssh for ipv6 address from public network')
1399+
1400+ def test_999_tearDown(self):
1401+ self.delete_key_pair(self.conn, TEST_KEY)
1402+ security_group_name = self.data['security_group_name']
1403+ group = self.data['group']
1404+ if group:
1405+ group.revoke('tcp', 22, 22, '0.0.0.0/0')
1406+ if FLAGS.use_ipv6:
1407+ group.revoke('tcp', 22, 22, '::/0')
1408+ self.conn.delete_security_group(security_group_name)
1409+ if 'instance_id' in self.data:
1410+ self.conn.terminate_instances([self.data['instance_id']])
1411+
1412+if __name__ == "__main__":
1413+ suites = {'instance': unittest.makeSuite(InstanceTestsFromPublic)}
1414+ sys.exit(base.run_tests(suites))
1415
1416=== modified file 'smoketests/user_smoketests.py'
1417--- smoketests/user_smoketests.py 2011-01-06 01:51:05 +0000
1418+++ smoketests/user_smoketests.py 2011-01-13 21:53:11 +0000
1419@@ -45,7 +45,7 @@
1420 flags.DEFINE_string('bundle_image', 'openwrt-x86-ext2.image',
1421 'Local image file to use for bundling tests')
1422
1423-TEST_PREFIX = 'test%s' % int (random.random()*1000000)
1424+TEST_PREFIX = 'test%s' % int(random.random() * 1000000)
1425 TEST_BUCKET = '%s_bucket' % TEST_PREFIX
1426 TEST_KEY = '%s_key' % TEST_PREFIX
1427 TEST_GROUP = '%s_group' % TEST_PREFIX
1428@@ -80,7 +80,7 @@
1429
1430 def test_006_can_register_kernel(self):
1431 kernel_id = self.conn.register_image('%s/%s.manifest.xml' %
1432- (TEST_BUCKET, FLAGS.bundle_kernel))
1433+ (TEST_BUCKET, FLAGS.bundle_kernel))
1434 self.assert_(kernel_id is not None)
1435 self.data['kernel_id'] = kernel_id
1436
1437@@ -92,7 +92,7 @@
1438 time.sleep(1)
1439 else:
1440 print image.state
1441- self.assert_(False) # wasn't available within 10 seconds
1442+ self.assert_(False) # wasn't available within 10 seconds
1443 self.assert_(image.type == 'machine')
1444
1445 for i in xrange(10):
1446@@ -101,7 +101,7 @@
1447 break
1448 time.sleep(1)
1449 else:
1450- self.assert_(False) # wasn't available within 10 seconds
1451+ self.assert_(False) # wasn't available within 10 seconds
1452 self.assert_(kernel.type == 'kernel')
1453
1454 def test_008_can_describe_image_attribute(self):
1455@@ -152,14 +152,17 @@
1456 for x in xrange(60):
1457 instance.update()
1458 if instance.state == u'running':
1459- break
1460+ break
1461 time.sleep(1)
1462 else:
1463 self.fail('instance failed to start')
1464 ip = reservations[0].instances[0].private_dns_name
1465 self.failIf(ip == '0.0.0.0')
1466 self.data['private_ip'] = ip
1467- print self.data['private_ip']
1468+ if FLAGS.use_ipv6:
1469+ ipv6 = reservations[0].instances[0].dns_name_v6
1470+ self.failIf(ipv6 is None)
1471+ self.data['ip_v6'] = ipv6
1472
1473 def test_004_can_ping_private_ip(self):
1474 for x in xrange(120):
1475@@ -171,6 +174,16 @@
1476 else:
1477 self.fail('could not ping instance')
1478
1479+ if FLAGS.use_ipv6:
1480+ for x in xrange(120):
1481+ # ping waits for 1 second
1482+ status, output = commands.getstatusoutput(
1483+ 'ping6 -c1 %s' % self.data['ip_v6'])
1484+ if status == 0:
1485+ break
1486+ else:
1487+ self.fail('could not ping instance')
1488+
1489 def test_005_can_ssh_to_private_ip(self):
1490 for x in xrange(30):
1491 try:
1492@@ -183,6 +196,19 @@
1493 else:
1494 self.fail('could not ssh to instance')
1495
1496+ if FLAGS.use_ipv6:
1497+ for x in xrange(30):
1498+ try:
1499+ conn = self.connect_ssh(
1500+ self.data['ip_v6'], TEST_KEY)
1501+ conn.close()
1502+ except Exception:
1503+ time.sleep(1)
1504+ else:
1505+ break
1506+ else:
1507+ self.fail('could not ssh to instance v6')
1508+
1509 def test_006_can_allocate_elastic_ip(self):
1510 result = self.conn.allocate_address()
1511 self.assertTrue(hasattr(result, 'public_ip'))
1512@@ -388,7 +414,6 @@
1513 raise Exception("Timeout")
1514 time.sleep(1)
1515
1516-
1517 def test_999_tearDown(self):
1518 self.conn.delete_key_pair(TEST_KEY)
1519 self.conn.delete_security_group(TEST_GROUP)
1520
1521=== modified file 'tools/pip-requires'
1522--- tools/pip-requires 2010-12-28 00:10:26 +0000
1523+++ tools/pip-requires 2011-01-13 21:53:11 +0000
1524@@ -25,3 +25,4 @@
1525 Twisted>=10.1.0
1526 PasteDeploy
1527 paste
1528+netaddr