Merge lp:~soren/nova/ec2-security-groups into lp:~hudson-openstack/nova/trunk

Proposed by Soren Hansen
Status: Merged
Approved by: Soren Hansen
Approved revision: 321
Merged at revision: 344
Proposed branch: lp:~soren/nova/ec2-security-groups
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 1687 lines (+1134/-43)
20 files modified
nova/api/ec2/__init__.py (+2/-0)
nova/api/ec2/cloud.py (+225/-11)
nova/auth/manager.py (+2/-0)
nova/compute/manager.py (+5/-0)
nova/db/api.py (+70/-0)
nova/db/sqlalchemy/api.py (+186/-2)
nova/db/sqlalchemy/models.py (+60/-4)
nova/db/sqlalchemy/session.py (+5/-4)
nova/exception.py (+3/-0)
nova/network/manager.py (+0/-1)
nova/process.py (+1/-1)
nova/test.py (+2/-0)
nova/tests/api_unittest.py (+185/-3)
nova/tests/objectstore_unittest.py (+1/-1)
nova/tests/virt_unittest.py (+172/-12)
nova/virt/interfaces.template (+0/-1)
nova/virt/libvirt.qemu.xml.template (+4/-0)
nova/virt/libvirt.uml.xml.template (+4/-0)
nova/virt/libvirt_conn.py (+206/-3)
run_tests.py (+1/-0)
To merge this branch: bzr merge lp:~soren/nova/ec2-security-groups
Reviewer Review Type Date Requested Status
Michael Gundlach (community) Approve
Jay Pipes (community) Approve
Review via email: mp+36119@code.launchpad.net

Description of the change

This patch adds support for EC2 security groups using libvirt's nwfilter mechanism, which in turn uses iptables and ebtables on the individual compute nodes.
This has a number of benefits:
 * Inter-VM network traffic can take the fastest route through the network without our having to worry about getting it through a central firewall.
 * Not relying on a central firewall also removes a potential SPOF.
 * The filtering load is distributed, offering great scalability.

Caveats:
 * It only works with libvirt and only with libvirt drivers that support nwfilter (qemu (and thus kvm) and uml, at the moment)

To post a comment you must log in.
Revision history for this message
Jay Pipes (jaypipes) wrote :

Heya, needs a quick merge with trunk...there's conflicts.

review: Needs Fixing
Revision history for this message
Vish Ishaya (vishvananda) wrote :
Download full text (53.8 KiB)

Soren,

I merged your branch with trunk and fixed a few errors that popped up. Unfortunately libvirt is complaining heartily when i try and launch an instance and not giving very useful error messages. I'm using libvirt from the nova-core ppa
virsh fails too
error: Failed to create domain from libvirt.xml
error: Unknown failure

On Sep 21, 2010, at 3:09 AM, Soren Hansen wrote:

> Soren Hansen has proposed merging lp:~soren/nova/ec2-security-groups into lp:nova.
>
> Requested reviews:
> Nova Core (nova-core)
>
>
> This patch adds support for EC2 security groups using libvirt's nwfilter mechanism, which in turn uses iptables and ebtables on the individual compute nodes.
> This has a number of benefits:
> * Inter-VM network traffic can take the fastest route through the network without our having to worry about getting it through a central firewall.
> * Not relying on a central firewall also removes a potential SPOF.
> * The filtering load is distributed, offering great scalability.
>
> Caveats:
> * It only works with libvirt and only with libvirt drivers that support nwfilter (qemu (and thus kvm) and uml, at the moment)
> --
> https://code.launchpad.net/~soren/nova/ec2-security-groups/+merge/36119
> Your team Nova Core is requested to review the proposed merge of lp:~soren/nova/ec2-security-groups into lp:nova.
> === modified file 'nova/auth/manager.py'
> --- nova/auth/manager.py 2010-09-21 05:08:31 +0000
> +++ nova/auth/manager.py 2010-09-21 10:09:06 +0000
> @@ -490,6 +490,12 @@
> except:
> drv.delete_project(project.id)
> raise
> +
> + values = { 'name' : 'default',
> + 'description' : 'default',
> + 'user_id' : User.safe_id(manager_user),
> + 'project_id' : project.id }
> + db.security_group_create({}, values)
> return project
>
> def modify_project(self, project, manager_user=None, description=None):
> @@ -565,6 +571,16 @@
> except:
> logging.exception('Could not destroy network for %s',
> project)
> + try:
> + project_id = Project.safe_id(project)
> + groups = db.security_group_get_by_project(context={},
> + project_id=project_id)
> + for group in groups:
> + db.security_group_destroy({}, group['id'])
> + except:
> + logging.exception('Could not destroy security groups for %s',
> + project)
> +
> with self.driver() as drv:
> drv.delete_project(Project.safe_id(project))
>
>
> === modified file 'nova/compute/manager.py'
> --- nova/compute/manager.py 2010-09-13 09:15:02 +0000
> +++ nova/compute/manager.py 2010-09-21 10:09:06 +0000
> @@ -64,6 +64,11 @@
>
> @defer.inlineCallbacks
> @exception.wrap_exception
> + def refresh_security_group(self, context, security_group_id, **_kwargs):
> + yield self.driver.refresh_security_group(security_group_id)
> +
> + @defer.inlineCallbacks
> + @exception.wrap...

Revision history for this message
Soren Hansen (soren) wrote :

Erk... that's a lot of fallout. I've got it mostly done, but I have a few unit tests that are acting up. I'm also not feeling very well today, so I may not get it done today after all. :(

Revision history for this message
Soren Hansen (soren) wrote :

Phew. There we go. All cleaned up, and happy test cases again :)

Revision history for this message
Vish Ishaya (vishvananda) wrote :
Download full text (54.9 KiB)

I still can't get libvirt to actually run the instance with this patch in. Is there some configuration i need to do that is left out? I'm using libvirt from nova-core and it chokes badly trying to launch with the filter in the url.

Also, a couple typos in your latest version:

=== modified file 'nova/api/ec2/cloud.py'
--- nova/api/ec2/cloud.py 2010-09-22 20:16:12 +0000
+++ nova/api/ec2/cloud.py 2010-09-22 23:40:30 +0000
@@ -725,9 +725,9 @@

         security_groups = []
         for security_group_name in security_group_arg:
- group = db.security_group_get_by_project(context,
- context.project.id,
- security_group_name)
+ group = db.security_group_get_by_name(context,
+ context.project.id,
+ security_group_name)
             security_groups.append(group['id'])

         reservation_id = utils.generate_uid('r')
@@ -744,6 +744,7 @@
         base_options['user_data'] = kwargs.get('user_data', '')

         type_data = INSTANCE_TYPES[instance_type]
+ base_options['instance_type'] = instance_type
         base_options['memory_mb'] = type_data['memory_mb']
         base_options['vcpus'] = type_data['vcpus']
         base_options['local_gb'] = type_data['local_gb']

On Sep 21, 2010, at 3:09 AM, Soren Hansen wrote:

> Soren Hansen has proposed merging lp:~soren/nova/ec2-security-groups into lp:nova.
>
> Requested reviews:
> Nova Core (nova-core)
>
>
> This patch adds support for EC2 security groups using libvirt's nwfilter mechanism, which in turn uses iptables and ebtables on the individual compute nodes.
> This has a number of benefits:
> * Inter-VM network traffic can take the fastest route through the network without our having to worry about getting it through a central firewall.
> * Not relying on a central firewall also removes a potential SPOF.
> * The filtering load is distributed, offering great scalability.
>
> Caveats:
> * It only works with libvirt and only with libvirt drivers that support nwfilter (qemu (and thus kvm) and uml, at the moment)
> --
> https://code.launchpad.net/~soren/nova/ec2-security-groups/+merge/36119
> Your team Nova Core is requested to review the proposed merge of lp:~soren/nova/ec2-security-groups into lp:nova.
> === modified file 'nova/auth/manager.py'
> --- nova/auth/manager.py 2010-09-21 05:08:31 +0000
> +++ nova/auth/manager.py 2010-09-21 10:09:06 +0000
> @@ -490,6 +490,12 @@
> except:
> drv.delete_project(project.id)
> raise
> +
> + values = { 'name' : 'default',
> + 'description' : 'default',
> + 'user_id' : User.safe_id(manager_user),
> + 'project_id' : project.id }
> + db.security_group_create({}, values)
> return project
>
> def modify_project(self, project, manager_user=None, description=None):
> @@ -565,6 +571,16 @@
> except:
> logging.exce...

Revision history for this message
Vish Ishaya (vishvananda) wrote :
Download full text (53.5 KiB)

s/in the url/in the xml

On Sep 21, 2010, at 3:09 AM, Soren Hansen wrote:

> Soren Hansen has proposed merging lp:~soren/nova/ec2-security-groups into lp:nova.
>
> Requested reviews:
> Nova Core (nova-core)
>
>
> This patch adds support for EC2 security groups using libvirt's nwfilter mechanism, which in turn uses iptables and ebtables on the individual compute nodes.
> This has a number of benefits:
> * Inter-VM network traffic can take the fastest route through the network without our having to worry about getting it through a central firewall.
> * Not relying on a central firewall also removes a potential SPOF.
> * The filtering load is distributed, offering great scalability.
>
> Caveats:
> * It only works with libvirt and only with libvirt drivers that support nwfilter (qemu (and thus kvm) and uml, at the moment)
> --
> https://code.launchpad.net/~soren/nova/ec2-security-groups/+merge/36119
> Your team Nova Core is requested to review the proposed merge of lp:~soren/nova/ec2-security-groups into lp:nova.
> === modified file 'nova/auth/manager.py'
> --- nova/auth/manager.py 2010-09-21 05:08:31 +0000
> +++ nova/auth/manager.py 2010-09-21 10:09:06 +0000
> @@ -490,6 +490,12 @@
> except:
> drv.delete_project(project.id)
> raise
> +
> + values = { 'name' : 'default',
> + 'description' : 'default',
> + 'user_id' : User.safe_id(manager_user),
> + 'project_id' : project.id }
> + db.security_group_create({}, values)
> return project
>
> def modify_project(self, project, manager_user=None, description=None):
> @@ -565,6 +571,16 @@
> except:
> logging.exception('Could not destroy network for %s',
> project)
> + try:
> + project_id = Project.safe_id(project)
> + groups = db.security_group_get_by_project(context={},
> + project_id=project_id)
> + for group in groups:
> + db.security_group_destroy({}, group['id'])
> + except:
> + logging.exception('Could not destroy security groups for %s',
> + project)
> +
> with self.driver() as drv:
> drv.delete_project(Project.safe_id(project))
>
>
> === modified file 'nova/compute/manager.py'
> --- nova/compute/manager.py 2010-09-13 09:15:02 +0000
> +++ nova/compute/manager.py 2010-09-21 10:09:06 +0000
> @@ -64,6 +64,11 @@
>
> @defer.inlineCallbacks
> @exception.wrap_exception
> + def refresh_security_group(self, context, security_group_id, **_kwargs):
> + yield self.driver.refresh_security_group(security_group_id)
> +
> + @defer.inlineCallbacks
> + @exception.wrap_exception
> def run_instance(self, context, instance_id, **_kwargs):
> """Launch a new instance with specified options."""
> instance_ref = self.db.instance_get(context, instance_id)
>
> === modified file 'nova/db/api.py'
> --- nova/db/api.py 2010-09-21 02:17:36 +0000
> +++ nova/db/api....

Revision history for this message
Soren Hansen (soren) wrote :

As for the libvirt problem, it turns out to be apparmour that gets in the way.

Run this on the box, and you should be fine:

sudo aa-complain /usr/sbin/libvirtd

Have fun.

Revision history for this message
Vish Ishaya (vishvananda) wrote :
Download full text (56.6 KiB)

Still not quite there on the security groups. Using your aa-complain fix i still get the following error in lucid:
libvir: Network Filtererror : internal error Could not get access to ACL tech driver 'ebiptables'
ERROR:root:instance i-1: Failed to spawn
Traceback (most recent call last):
  File "/srv/cloud/nova/nova/compute/manager.py", line 91, in run_instance
    yield self.driver.spawn(instance_ref)
  File "/usr/local/lib/python2.6/dist-packages/twisted/internet/defer.py", line 823, in _inlineCallbacks
    result = g.send(result)
  File "/srv/cloud/nova/nova/virt/libvirt_conn.py", line 220, in spawn
    yield self._conn.createXML(xml, 0)
  File "/usr/lib/python2.6/dist-packages/libvirt.py", line 1289, in createXML
    if ret is None:raise libvirtError('virDomainCreateXML() failed', conn=self)
libvirtError: internal error Could not get access to ACL tech driver 'ebiptables'

I tried downloading and installing the maverick dpkg from the nova-repo
I grabbed libnl, libvirt0 and libvirt-bin and they install and run fine. Now I can actually run machines again, but every time it tries to update the rules on the compute host, I get the following:
libvir: Network Filtererror : internal error cannot create rule since iptables tool is missing.
2010-09-25 09:11:49+0000 [-] Unhandled error in Deferred:
2010-09-25 09:11:49+0000 [-] Unhandled Error
        Traceback (most recent call last):
          File "/usr/lib/python2.6/threading.py", line 504, in __bootstrap
            self.__bootstrap_inner()
          File "/usr/lib/python2.6/threading.py", line 532, in __bootstrap_inner
            self.run()
          File "/usr/lib/python2.6/threading.py", line 484, in run
            self.__target(*self.__args, **self.__kwargs)
        --- <exception caught here> ---
          File "/usr/local/lib/python2.6/dist-packages/twisted/python/threadpool.py", line 210, in _worker
            result = context.call(ctx, function, *args, **kwargs)
          File "/usr/local/lib/python2.6/dist-packages/twisted/python/context.py", line 59, in callWithContext
            return self.currentContext().callWithContext(ctx, func, *args, **kw)
          File "/usr/local/lib/python2.6/dist-packages/twisted/python/context.py", line 37, in callWithContext
            return func(*args,**kw)
          File "/usr/lib/python2.6/dist-packages/libvirt.py", line 1646, in nwfilterDefineXML
            if ret is None:raise libvirtError('virNWFilterDefineXML() failed', conn=self)
        libvirt.libvirtError: internal error cannot create rule since iptables tool is missing.

So apparently, I'm missing the 'iptables tool'. Do i need to upgrade another package to a maverick version that wasn't listed as a depend properly in the dpkg?

Or is there some way that I can get a working lucid version?

Also DHCPackets are not coming through at all. I'm thinking that it is due to not being able to set up the rules properly, but I'm a bit surprised the dhcpdiscover isn't getting out, since it is udp.

Any suggestions are most welcome, since we really need to get the security groups tested ASAP.

Thanks,
Vish

On Sep 21, 2010, at 3:09 AM, Soren Hansen wrote:

> Soren Hansen has proposed merging lp...

Revision history for this message
Soren Hansen (soren) wrote :
Download full text (3.6 KiB)

On 25-09-2010 11:34, vishvananda wrote:
> Still not quite there on the security groups. Using your aa-complain fix i still get the following error in lucid:
> libvir: Network Filtererror : internal error Could not get access to ACL tech driver 'ebiptables'
> ERROR:root:instance i-1: Failed to spawn
> Traceback (most recent call last):
> File "/srv/cloud/nova/nova/compute/manager.py", line 91, in run_instance
> yield self.driver.spawn(instance_ref)
> File "/usr/local/lib/python2.6/dist-packages/twisted/internet/defer.py", line 823, in _inlineCallbacks
> result = g.send(result)
> File "/srv/cloud/nova/nova/virt/libvirt_conn.py", line 220, in spawn
> yield self._conn.createXML(xml, 0)
> File "/usr/lib/python2.6/dist-packages/libvirt.py", line 1289, in createXML
> if ret is None:raise libvirtError('virDomainCreateXML() failed', conn=self)
> libvirtError: internal error Could not get access to ACL tech driver 'ebiptables'

Do you have ebtables installed? This corner of libvirt has horrendous
error reporting, unfortunately. You need gawk, ebtables, and iptables
installed. Well, and bash and grep, but I can't imagine you'd be missing
those.

> I tried downloading and installing the maverick dpkg from the nova-repo
> I grabbed libnl, libvirt0 and libvirt-bin and they install and run fine. Now I can actually run machines again, but every time it tries to update the rules on the compute host, I get the following:
> libvir: Network Filtererror : internal error cannot create rule since iptables tool is missing.

Glancing at libvirt's code, that error code means it either failed to
locate iptables in your $PATH, or running "iptables -L FORWARD" failed.
Do you have iptables installed and in the $PATH of the libvirtd process?
(if you're using debian or ubuntu packages, you should be fine). Same

> So apparently, I'm missing the 'iptables tool'. Do i need to upgrade another package to a maverick version that wasn't listed as a depend properly in the dpkg?

Any reasonably recent version of iptables should be fine. Lucid is
definitely reasonably recent.

> Also DHCPackets are not coming through at all.

Yeah. That was the embarassing thing I was talking about on IRC
yesterday :) When I was attempting to get the networking stuff working
and couldn't get DHCP to work I was actually using this branch, and it
turns out it filters dhcp by default. To get DHCP to work, we can either
add a reference to the allow-dhcp or (preferably) allow-dhcp-server
filter for the instance. If the latter, we need to know the IP of the
DHCP server. If we have that, we can just add
<filterref filter="allow-dhcp-server">
  <parameter name="DHCPSERVER" value="<ip of DHCP server>" />
</filterref>

> I'm thinking that it is due to not being able to set up the rules properly, but I'm a bit surprised the dhcpdiscover isn't getting out, since it is udp.

That's an artefact of the no-ip-spoofing filter (part of the
clean-traffic which is included by nova-base-filter), I think. It
refuses traffic from an IP other than the one designated in the libvirt
XML (including DHCP requests that have a NULL IP, IIRC). Adding the
above mentioned allow-dhcp(-server) filter should ...

Read more...

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

Test comment to see if it will subscribe me

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

problem running multiple tests, it may not be instance i-1. This patch fixes.

=== modified file 'nova/tests/virt_unittest.py'
--- nova/tests/virt_unittest.py 2010-09-28 08:26:29 +0000
+++ nova/tests/virt_unittest.py 2010-09-28 17:47:54 +0000
@@ -178,8 +178,14 @@
             self.defined_filters.append(name)
             return True

+ self.fake_libvirt_connection.nwfilterDefineXML = _filterDefineXMLMock
+
+ instance_ref = db.instance_create({}, {'user_id': 'fake',
+ 'project_id': 'fake'})
+ inst_id = instance_ref['id']
+
         def _ensure_all_called(_):
- instance_filter = 'nova-instance-i-1'
+ instance_filter = 'nova-instance-%s' % instance_ref['str_id']
             secgroup_filter = 'nova-secgroup-%s' % self.security_group['id']
             for required in [secgroup_filter, 'allow-dhcp-server',
                              'no-arp-spoofing', 'no-ip-spoofing',
@@ -187,11 +193,6 @@
                 self.assertTrue(required in self.recursive_depends[instance_filter],
                             "Instance's filter does not include %s" % required)

- self.fake_libvirt_connection.nwfilterDefineXML = _filterDefineXMLMock
-
- inst_id = db.instance_create({}, {'user_id': 'fake',
- 'project_id': 'fake'})['id']
-
         self.security_group = self.setup_and_return_security_group()

         db.instance_add_security_group({}, inst_id, self.security_group.id)

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

After that fix, it is an approve from me. But we really need to make sure the lucid package is easily accessible from the ppa and works out of the box. I suggest we make sure that the needed app-armor rules are in, it is the most current libvirt, and that we add dependencies on gawk, iptables, and ebtables.

Revision history for this message
Soren Hansen (soren) wrote :

I've pushed a new libvirt to the nova-core Lucid ppa. It's identical to the one in Ubuntu Maverick, except macvtap has been disabled (Lucid's kernel does not support it). This fixes the AppArmour problems.

I've added iptables, ebtables, and gawk as dependencies of nova-compute.

Revision history for this message
Jay Pipes (jaypipes) wrote :

Approved. This is excellent work. Especially on the test cases.

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

Approve.

FYI db/sqlalchemy/api.py looks like it could be using dict.update instead of a tight for loop in a few places, in case you missed that.

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/db/sqlalchemy/api.py

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

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

text conflict in nova/tests/virt_unittest.py

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

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

text conflict in nova/tests/virt_unittest.py

Revision history for this message
Soren Hansen (soren) wrote :

Oh, come on!

lp:~soren/nova/ec2-security-groups updated
320. By Soren Hansen

Merge trunk.

321. By Soren Hansen

Merge trunk (that's 10 times now, count 'em\!)

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

Woo hoo!
On Oct 12, 2010 1:28 PM, <email address hidden> wrote:
> The proposal to merge lp:~soren/nova/ec2-security-groups into lp:nova has
been updated.
>
> Status: Approved => Merged
> --
> https://code.launchpad.net/~soren/nova/ec2-security-groups/+merge/36119
> You are subscribed to branch lp:~soren/nova/ec2-security-groups.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/ec2/__init__.py'
2--- nova/api/ec2/__init__.py 2010-09-30 09:47:05 +0000
3+++ nova/api/ec2/__init__.py 2010-10-12 20:19:40 +0000
4@@ -142,6 +142,8 @@
5 'CreateKeyPair': ['all'],
6 'DeleteKeyPair': ['all'],
7 'DescribeSecurityGroups': ['all'],
8+ 'AuthorizeSecurityGroupIngress': ['netadmin'],
9+ 'RevokeSecurityGroupIngress': ['netadmin'],
10 'CreateSecurityGroup': ['netadmin'],
11 'DeleteSecurityGroup': ['netadmin'],
12 'GetConsoleOutput': ['projectmanager', 'sysadmin'],
13
14=== modified file 'nova/api/ec2/cloud.py'
15--- nova/api/ec2/cloud.py 2010-10-11 16:01:15 +0000
16+++ nova/api/ec2/cloud.py 2010-10-12 20:19:40 +0000
17@@ -28,6 +28,8 @@
18 import os
19 import time
20
21+import IPy
22+
23 from nova import crypto
24 from nova import db
25 from nova import exception
26@@ -43,6 +45,7 @@
27 FLAGS = flags.FLAGS
28 flags.DECLARE('storage_availability_zone', 'nova.volume.manager')
29
30+InvalidInputException = exception.InvalidInputException
31
32 class QuotaError(exception.ApiError):
33 """Quota Exceeeded"""
34@@ -127,6 +130,15 @@
35 result[key] = [line]
36 return result
37
38+ def _trigger_refresh_security_group(self, security_group):
39+ nodes = set([instance['host'] for instance in security_group.instances
40+ if instance['host'] is not None])
41+ for node in nodes:
42+ rpc.call('%s.%s' % (FLAGS.compute_topic, node),
43+ { "method": "refresh_security_group",
44+ "args": { "context": None,
45+ "security_group_id": security_group.id}})
46+
47 def get_metadata(self, address):
48 instance_ref = db.fixed_ip_get_instance(None, address)
49 if instance_ref is None:
50@@ -246,18 +258,195 @@
51 pass
52 return True
53
54- def describe_security_groups(self, context, group_names, **kwargs):
55- groups = {'securityGroupSet': []}
56-
57- # Stubbed for now to unblock other things.
58- return groups
59-
60- def create_security_group(self, context, group_name, **kwargs):
61+ def describe_security_groups(self, context, group_name=None, **kwargs):
62+ self._ensure_default_security_group(context)
63+ if context.user.is_admin():
64+ groups = db.security_group_get_all(context)
65+ else:
66+ groups = db.security_group_get_by_project(context,
67+ context.project.id)
68+ groups = [self._format_security_group(context, g) for g in groups]
69+ if not group_name is None:
70+ groups = [g for g in groups if g.name in group_name]
71+
72+ return {'securityGroupInfo': groups }
73+
74+ def _format_security_group(self, context, group):
75+ g = {}
76+ g['groupDescription'] = group.description
77+ g['groupName'] = group.name
78+ g['ownerId'] = group.project_id
79+ g['ipPermissions'] = []
80+ for rule in group.rules:
81+ r = {}
82+ r['ipProtocol'] = rule.protocol
83+ r['fromPort'] = rule.from_port
84+ r['toPort'] = rule.to_port
85+ r['groups'] = []
86+ r['ipRanges'] = []
87+ if rule.group_id:
88+ source_group = db.security_group_get(context, rule.group_id)
89+ r['groups'] += [{'groupName': source_group.name,
90+ 'userId': source_group.project_id}]
91+ else:
92+ r['ipRanges'] += [{'cidrIp': rule.cidr}]
93+ g['ipPermissions'] += [r]
94+ return g
95+
96+
97+ def _authorize_revoke_rule_args_to_dict(self, context,
98+ to_port=None, from_port=None,
99+ ip_protocol=None, cidr_ip=None,
100+ user_id=None,
101+ source_security_group_name=None,
102+ source_security_group_owner_id=None):
103+
104+ values = {}
105+
106+ if source_security_group_name:
107+ source_project_id = self._get_source_project_id(context,
108+ source_security_group_owner_id)
109+
110+ source_security_group = \
111+ db.security_group_get_by_name(context,
112+ source_project_id,
113+ source_security_group_name)
114+ values['group_id'] = source_security_group['id']
115+ elif cidr_ip:
116+ # If this fails, it throws an exception. This is what we want.
117+ IPy.IP(cidr_ip)
118+ values['cidr'] = cidr_ip
119+ else:
120+ values['cidr'] = '0.0.0.0/0'
121+
122+ if ip_protocol and from_port and to_port:
123+ from_port = int(from_port)
124+ to_port = int(to_port)
125+ ip_protocol = str(ip_protocol)
126+
127+ if ip_protocol.upper() not in ['TCP','UDP','ICMP']:
128+ raise InvalidInputException('%s is not a valid ipProtocol' %
129+ (ip_protocol,))
130+ if ((min(from_port, to_port) < -1) or
131+ (max(from_port, to_port) > 65535)):
132+ raise InvalidInputException('Invalid port range')
133+
134+ values['protocol'] = ip_protocol
135+ values['from_port'] = from_port
136+ values['to_port'] = to_port
137+ else:
138+ # If cidr based filtering, protocol and ports are mandatory
139+ if 'cidr' in values:
140+ return None
141+
142+ return values
143+
144+
145+ def _security_group_rule_exists(self, security_group, values):
146+ """Indicates whether the specified rule values are already
147+ defined in the given security group.
148+ """
149+ for rule in security_group.rules:
150+ if 'group_id' in values:
151+ if rule['group_id'] == values['group_id']:
152+ return True
153+ else:
154+ is_duplicate = True
155+ for key in ('cidr', 'from_port', 'to_port', 'protocol'):
156+ if rule[key] != values[key]:
157+ is_duplicate = False
158+ break
159+ if is_duplicate:
160+ return True
161+ return False
162+
163+
164+ def revoke_security_group_ingress(self, context, group_name, **kwargs):
165+ self._ensure_default_security_group(context)
166+ security_group = db.security_group_get_by_name(context,
167+ context.project.id,
168+ group_name)
169+
170+ criteria = self._authorize_revoke_rule_args_to_dict(context, **kwargs)
171+ if criteria == None:
172+ raise exception.ApiError("No rule for the specified parameters.")
173+
174+ for rule in security_group.rules:
175+ match = True
176+ for (k,v) in criteria.iteritems():
177+ if getattr(rule, k, False) != v:
178+ match = False
179+ if match:
180+ db.security_group_rule_destroy(context, rule['id'])
181+ self._trigger_refresh_security_group(security_group)
182+ return True
183+ raise exception.ApiError("No rule for the specified parameters.")
184+
185+ # TODO(soren): This has only been tested with Boto as the client.
186+ # Unfortunately, it seems Boto is using an old API
187+ # for these operations, so support for newer API versions
188+ # is sketchy.
189+ def authorize_security_group_ingress(self, context, group_name, **kwargs):
190+ self._ensure_default_security_group(context)
191+ security_group = db.security_group_get_by_name(context,
192+ context.project.id,
193+ group_name)
194+
195+ values = self._authorize_revoke_rule_args_to_dict(context, **kwargs)
196+ values['parent_group_id'] = security_group.id
197+
198+ if self._security_group_rule_exists(security_group, values):
199+ raise exception.ApiError('This rule already exists in group %s' %
200+ group_name)
201+
202+ security_group_rule = db.security_group_rule_create(context, values)
203+
204+ self._trigger_refresh_security_group(security_group)
205+
206 return True
207
208+
209+ def _get_source_project_id(self, context, source_security_group_owner_id):
210+ if source_security_group_owner_id:
211+ # Parse user:project for source group.
212+ source_parts = source_security_group_owner_id.split(':')
213+
214+ # If no project name specified, assume it's same as user name.
215+ # Since we're looking up by project name, the user name is not
216+ # used here. It's only read for EC2 API compatibility.
217+ if len(source_parts) == 2:
218+ source_project_id = source_parts[1]
219+ else:
220+ source_project_id = source_parts[0]
221+ else:
222+ source_project_id = context.project.id
223+
224+ return source_project_id
225+
226+
227+ def create_security_group(self, context, group_name, group_description):
228+ self._ensure_default_security_group(context)
229+ if db.security_group_exists(context, context.project.id, group_name):
230+ raise exception.ApiError('group %s already exists' % group_name)
231+
232+ group = {'user_id' : context.user.id,
233+ 'project_id': context.project.id,
234+ 'name': group_name,
235+ 'description': group_description}
236+ group_ref = db.security_group_create(context, group)
237+
238+ return {'securityGroupSet': [self._format_security_group(context,
239+ group_ref)]}
240+
241+
242 def delete_security_group(self, context, group_name, **kwargs):
243+ security_group = db.security_group_get_by_name(context,
244+ context.project.id,
245+ group_name)
246+ db.security_group_destroy(context, security_group.id)
247 return True
248
249+
250 def get_console_output(self, context, instance_id, **kwargs):
251 # instance_id is passed in as a list of instances
252 ec2_id = instance_id[0]
253@@ -554,6 +743,18 @@
254 "project_id": context.project.id}})
255 return db.queue_get_for(context, FLAGS.network_topic, host)
256
257+ def _ensure_default_security_group(self, context):
258+ try:
259+ db.security_group_get_by_name(context,
260+ context.project.id,
261+ 'default')
262+ except exception.NotFound:
263+ values = { 'name' : 'default',
264+ 'description' : 'default',
265+ 'user_id' : context.user.id,
266+ 'project_id' : context.project.id }
267+ group = db.security_group_create(context, values)
268+
269 def run_instances(self, context, **kwargs):
270 instance_type = kwargs.get('instance_type', 'm1.small')
271 if instance_type not in INSTANCE_TYPES:
272@@ -601,8 +802,17 @@
273 kwargs['key_name'])
274 key_data = key_pair_ref['public_key']
275
276- # TODO: Get the real security group of launch in here
277- security_group = "default"
278+ security_group_arg = kwargs.get('security_group', ["default"])
279+ if not type(security_group_arg) is list:
280+ security_group_arg = [security_group_arg]
281+
282+ security_groups = []
283+ self._ensure_default_security_group(context)
284+ for security_group_name in security_group_arg:
285+ group = db.security_group_get_by_name(context,
286+ context.project.id,
287+ security_group_name)
288+ security_groups.append(group['id'])
289
290 reservation_id = utils.generate_uid('r')
291 base_options = {}
292@@ -616,12 +826,12 @@
293 base_options['user_id'] = context.user.id
294 base_options['project_id'] = context.project.id
295 base_options['user_data'] = kwargs.get('user_data', '')
296- base_options['security_group'] = security_group
297- base_options['instance_type'] = instance_type
298+
299 base_options['display_name'] = kwargs.get('display_name')
300 base_options['display_description'] = kwargs.get('display_description')
301
302 type_data = INSTANCE_TYPES[instance_type]
303+ base_options['instance_type'] = instance_type
304 base_options['memory_mb'] = type_data['memory_mb']
305 base_options['vcpus'] = type_data['vcpus']
306 base_options['local_gb'] = type_data['local_gb']
307@@ -630,6 +840,10 @@
308 instance_ref = db.instance_create(context, base_options)
309 inst_id = instance_ref['id']
310
311+ for security_group_id in security_groups:
312+ db.instance_add_security_group(context, inst_id,
313+ security_group_id)
314+
315 inst = {}
316 inst['mac_address'] = utils.generate_mac()
317 inst['launch_index'] = num
318
319=== modified file 'nova/auth/manager.py'
320--- nova/auth/manager.py 2010-10-12 14:23:24 +0000
321+++ nova/auth/manager.py 2010-10-12 20:19:40 +0000
322@@ -490,6 +490,7 @@
323 except:
324 drv.delete_project(project.id)
325 raise
326+
327 return project
328
329 def modify_project(self, project, manager_user=None, description=None):
330@@ -565,6 +566,7 @@
331 except:
332 logging.exception('Could not destroy network for %s',
333 project)
334+
335 with self.driver() as drv:
336 drv.delete_project(Project.safe_id(project))
337
338
339=== modified file 'nova/compute/manager.py'
340--- nova/compute/manager.py 2010-10-12 17:42:43 +0000
341+++ nova/compute/manager.py 2010-10-12 20:19:40 +0000
342@@ -64,6 +64,11 @@
343
344 @defer.inlineCallbacks
345 @exception.wrap_exception
346+ def refresh_security_group(self, context, security_group_id, **_kwargs):
347+ yield self.driver.refresh_security_group(security_group_id)
348+
349+ @defer.inlineCallbacks
350+ @exception.wrap_exception
351 def run_instance(self, context, instance_id, **_kwargs):
352 """Launch a new instance with specified options."""
353 instance_ref = self.db.instance_get(context, instance_id)
354
355=== modified file 'nova/db/api.py'
356--- nova/db/api.py 2010-10-12 14:23:24 +0000
357+++ nova/db/api.py 2010-10-12 20:19:40 +0000
358@@ -304,6 +304,11 @@
359 return IMPL.instance_update(context, instance_id, values)
360
361
362+def instance_add_security_group(context, instance_id, security_group_id):
363+ """Associate the given security group with the given instance"""
364+ return IMPL.instance_add_security_group(context, instance_id, security_group_id)
365+
366+
367 ###################
368
369
370@@ -571,6 +576,71 @@
371 return IMPL.volume_update(context, volume_id, values)
372
373
374+####################
375+
376+
377+def security_group_get_all(context):
378+ """Get all security groups"""
379+ return IMPL.security_group_get_all(context)
380+
381+
382+def security_group_get(context, security_group_id):
383+ """Get security group by its internal id"""
384+ return IMPL.security_group_get(context, security_group_id)
385+
386+
387+def security_group_get_by_name(context, project_id, group_name):
388+ """Returns a security group with the specified name from a project"""
389+ return IMPL.security_group_get_by_name(context, project_id, group_name)
390+
391+
392+def security_group_get_by_project(context, project_id):
393+ """Get all security groups belonging to a project"""
394+ return IMPL.security_group_get_by_project(context, project_id)
395+
396+
397+def security_group_get_by_instance(context, instance_id):
398+ """Get security groups to which the instance is assigned"""
399+ return IMPL.security_group_get_by_instance(context, instance_id)
400+
401+
402+def security_group_exists(context, project_id, group_name):
403+ """Indicates if a group name exists in a project"""
404+ return IMPL.security_group_exists(context, project_id, group_name)
405+
406+
407+def security_group_create(context, values):
408+ """Create a new security group"""
409+ return IMPL.security_group_create(context, values)
410+
411+
412+def security_group_destroy(context, security_group_id):
413+ """Deletes a security group"""
414+ return IMPL.security_group_destroy(context, security_group_id)
415+
416+
417+def security_group_destroy_all(context):
418+ """Deletes a security group"""
419+ return IMPL.security_group_destroy_all(context)
420+
421+
422+####################
423+
424+
425+def security_group_rule_create(context, values):
426+ """Create a new security group"""
427+ return IMPL.security_group_rule_create(context, values)
428+
429+
430+def security_group_rule_get_by_security_group(context, security_group_id):
431+ """Get all rules for a a given security group"""
432+ return IMPL.security_group_rule_get_by_security_group(context, security_group_id)
433+
434+def security_group_rule_destroy(context, security_group_rule_id):
435+ """Deletes a security group rule"""
436+ return IMPL.security_group_rule_destroy(context, security_group_rule_id)
437+
438+
439 ###################
440
441
442
443=== modified file 'nova/db/sqlalchemy/api.py'
444--- nova/db/sqlalchemy/api.py 2010-10-05 19:52:12 +0000
445+++ nova/db/sqlalchemy/api.py 2010-10-12 20:19:40 +0000
446@@ -29,8 +29,11 @@
447 from nova.db.sqlalchemy.session import get_session
448 from sqlalchemy import or_
449 from sqlalchemy.exc import IntegrityError
450-from sqlalchemy.orm import joinedload, joinedload_all
451-from sqlalchemy.sql import exists, func
452+from sqlalchemy.orm import joinedload
453+from sqlalchemy.orm import joinedload_all
454+from sqlalchemy.sql import exists
455+from sqlalchemy.sql import func
456+from sqlalchemy.orm.exc import NoResultFound
457
458 FLAGS = flags.FLAGS
459
460@@ -571,11 +574,13 @@
461
462 if is_admin_context(context):
463 result = session.query(models.Instance
464+ ).options(joinedload('security_groups')
465 ).filter_by(id=instance_id
466 ).filter_by(deleted=can_read_deleted(context)
467 ).first()
468 elif is_user_context(context):
469 result = session.query(models.Instance
470+ ).options(joinedload('security_groups')
471 ).filter_by(project_id=context.project.id
472 ).filter_by(id=instance_id
473 ).filter_by(deleted=False
474@@ -591,6 +596,7 @@
475 session = get_session()
476 return session.query(models.Instance
477 ).options(joinedload_all('fixed_ip.floating_ips')
478+ ).options(joinedload('security_groups')
479 ).filter_by(deleted=can_read_deleted(context)
480 ).all()
481
482@@ -600,6 +606,7 @@
483 session = get_session()
484 return session.query(models.Instance
485 ).options(joinedload_all('fixed_ip.floating_ips')
486+ ).options(joinedload('security_groups')
487 ).filter_by(deleted=can_read_deleted(context)
488 ).filter_by(user_id=user_id
489 ).all()
490@@ -612,6 +619,7 @@
491 session = get_session()
492 return session.query(models.Instance
493 ).options(joinedload_all('fixed_ip.floating_ips')
494+ ).options(joinedload('security_groups')
495 ).filter_by(project_id=project_id
496 ).filter_by(deleted=can_read_deleted(context)
497 ).all()
498@@ -624,12 +632,14 @@
499 if is_admin_context(context):
500 return session.query(models.Instance
501 ).options(joinedload_all('fixed_ip.floating_ips')
502+ ).options(joinedload('security_groups')
503 ).filter_by(reservation_id=reservation_id
504 ).filter_by(deleted=can_read_deleted(context)
505 ).all()
506 elif is_user_context(context):
507 return session.query(models.Instance
508 ).options(joinedload_all('fixed_ip.floating_ips')
509+ ).options(joinedload('security_groups')
510 ).filter_by(project_id=context.project.id
511 ).filter_by(reservation_id=reservation_id
512 ).filter_by(deleted=False
513@@ -642,11 +652,13 @@
514
515 if is_admin_context(context):
516 result = session.query(models.Instance
517+ ).options(joinedload('security_groups')
518 ).filter_by(internal_id=internal_id
519 ).filter_by(deleted=can_read_deleted(context)
520 ).first()
521 elif is_user_context(context):
522 result = session.query(models.Instance
523+ ).options(joinedload('security_groups')
524 ).filter_by(project_id=context.project.id
525 ).filter_by(internal_id=internal_id
526 ).filter_by(deleted=False
527@@ -718,6 +730,18 @@
528 instance_ref.save(session=session)
529
530
531+def instance_add_security_group(context, instance_id, security_group_id):
532+ """Associate the given security group with the given instance"""
533+ session = get_session()
534+ with session.begin():
535+ instance_ref = instance_get(context, instance_id, session=session)
536+ security_group_ref = security_group_get(context,
537+ security_group_id,
538+ session=session)
539+ instance_ref.security_groups += [security_group_ref]
540+ instance_ref.save(session=session)
541+
542+
543 ###################
544
545
546@@ -1192,6 +1216,7 @@
547
548 @require_admin_context
549 def volume_get_all(context):
550+ session = get_session()
551 return session.query(models.Volume
552 ).filter_by(deleted=can_read_deleted(context)
553 ).all()
554@@ -1282,6 +1307,163 @@
555 ###################
556
557
558+@require_context
559+def security_group_get_all(context):
560+ session = get_session()
561+ return session.query(models.SecurityGroup
562+ ).filter_by(deleted=can_read_deleted(context)
563+ ).options(joinedload_all('rules')
564+ ).all()
565+
566+
567+@require_context
568+def security_group_get(context, security_group_id, session=None):
569+ if not session:
570+ session = get_session()
571+ if is_admin_context(context):
572+ result = session.query(models.SecurityGroup
573+ ).filter_by(deleted=can_read_deleted(context),
574+ ).filter_by(id=security_group_id
575+ ).options(joinedload_all('rules')
576+ ).first()
577+ else:
578+ result = session.query(models.SecurityGroup
579+ ).filter_by(deleted=False
580+ ).filter_by(id=security_group_id
581+ ).filter_by(project_id=context.project_id
582+ ).options(joinedload_all('rules')
583+ ).first()
584+ if not result:
585+ raise exception.NotFound("No secuity group with id %s" %
586+ security_group_id)
587+ return result
588+
589+
590+@require_context
591+def security_group_get_by_name(context, project_id, group_name):
592+ session = get_session()
593+ result = session.query(models.SecurityGroup
594+ ).filter_by(project_id=project_id
595+ ).filter_by(name=group_name
596+ ).filter_by(deleted=False
597+ ).options(joinedload_all('rules')
598+ ).options(joinedload_all('instances')
599+ ).first()
600+ if not result:
601+ raise exception.NotFound(
602+ 'No security group named %s for project: %s' \
603+ % (group_name, project_id))
604+ return result
605+
606+
607+@require_context
608+def security_group_get_by_project(context, project_id):
609+ session = get_session()
610+ return session.query(models.SecurityGroup
611+ ).filter_by(project_id=project_id
612+ ).filter_by(deleted=False
613+ ).options(joinedload_all('rules')
614+ ).all()
615+
616+
617+@require_context
618+def security_group_get_by_instance(context, instance_id):
619+ session = get_session()
620+ return session.query(models.SecurityGroup
621+ ).filter_by(deleted=False
622+ ).options(joinedload_all('rules')
623+ ).join(models.SecurityGroup.instances
624+ ).filter_by(id=instance_id
625+ ).filter_by(deleted=False
626+ ).all()
627+
628+
629+@require_context
630+def security_group_exists(context, project_id, group_name):
631+ try:
632+ group = security_group_get_by_name(context, project_id, group_name)
633+ return group != None
634+ except exception.NotFound:
635+ return False
636+
637+
638+@require_context
639+def security_group_create(context, values):
640+ security_group_ref = models.SecurityGroup()
641+ # FIXME(devcamcar): Unless I do this, rules fails with lazy load exception
642+ # once save() is called. This will get cleaned up in next orm pass.
643+ security_group_ref.rules
644+ for (key, value) in values.iteritems():
645+ security_group_ref[key] = value
646+ security_group_ref.save()
647+ return security_group_ref
648+
649+
650+@require_context
651+def security_group_destroy(context, security_group_id):
652+ session = get_session()
653+ with session.begin():
654+ # TODO(vish): do we have to use sql here?
655+ session.execute('update security_groups set deleted=1 where id=:id',
656+ {'id': security_group_id})
657+ session.execute('update security_group_rules set deleted=1 '
658+ 'where group_id=:id',
659+ {'id': security_group_id})
660+
661+@require_context
662+def security_group_destroy_all(context, session=None):
663+ if not session:
664+ session = get_session()
665+ with session.begin():
666+ # TODO(vish): do we have to use sql here?
667+ session.execute('update security_groups set deleted=1')
668+ session.execute('update security_group_rules set deleted=1')
669+
670+
671+###################
672+
673+
674+@require_context
675+def security_group_rule_get(context, security_group_rule_id, session=None):
676+ if not session:
677+ session = get_session()
678+ if is_admin_context(context):
679+ result = session.query(models.SecurityGroupIngressRule
680+ ).filter_by(deleted=can_read_deleted(context)
681+ ).filter_by(id=security_group_rule_id
682+ ).first()
683+ else:
684+ # TODO(vish): Join to group and check for project_id
685+ result = session.query(models.SecurityGroupIngressRule
686+ ).filter_by(deleted=False
687+ ).filter_by(id=security_group_rule_id
688+ ).first()
689+ if not result:
690+ raise exception.NotFound("No secuity group rule with id %s" %
691+ security_group_rule_id)
692+ return result
693+
694+
695+@require_context
696+def security_group_rule_create(context, values):
697+ security_group_rule_ref = models.SecurityGroupIngressRule()
698+ for (key, value) in values.iteritems():
699+ security_group_rule_ref[key] = value
700+ security_group_rule_ref.save()
701+ return security_group_rule_ref
702+
703+@require_context
704+def security_group_rule_destroy(context, security_group_rule_id):
705+ session = get_session()
706+ with session.begin():
707+ security_group_rule = security_group_rule_get(context,
708+ security_group_rule_id,
709+ session=session)
710+ security_group_rule.delete(session=session)
711+
712+
713+###################
714+
715 @require_admin_context
716 def user_get(context, id, session=None):
717 if not session:
718@@ -1491,6 +1673,8 @@
719 ###################
720
721
722+
723+@require_admin_context
724 def host_get_networks(context, host):
725 session = get_session()
726 with session.begin():
727
728=== modified file 'nova/db/sqlalchemy/models.py'
729--- nova/db/sqlalchemy/models.py 2010-10-12 19:02:24 +0000
730+++ nova/db/sqlalchemy/models.py 2010-10-12 20:19:40 +0000
731@@ -187,7 +187,6 @@
732 launch_index = Column(Integer)
733 key_name = Column(String(255))
734 key_data = Column(Text)
735- security_group = Column(String(255))
736
737 state = Column(Integer)
738 state_description = Column(String(255))
739@@ -289,10 +288,66 @@
740 'ExportDevice.deleted==False)')
741
742
743+class SecurityGroupInstanceAssociation(BASE, NovaBase):
744+ __tablename__ = 'security_group_instance_association'
745+ id = Column(Integer, primary_key=True)
746+ security_group_id = Column(Integer, ForeignKey('security_groups.id'))
747+ instance_id = Column(Integer, ForeignKey('instances.id'))
748+
749+
750+class SecurityGroup(BASE, NovaBase):
751+ """Represents a security group"""
752+ __tablename__ = 'security_groups'
753+ id = Column(Integer, primary_key=True)
754+
755+ name = Column(String(255))
756+ description = Column(String(255))
757+ user_id = Column(String(255))
758+ project_id = Column(String(255))
759+
760+ instances = relationship(Instance,
761+ secondary="security_group_instance_association",
762+ primaryjoin="and_(SecurityGroup.id == SecurityGroupInstanceAssociation.security_group_id,"
763+ "SecurityGroup.deleted == False)",
764+ secondaryjoin="and_(SecurityGroupInstanceAssociation.instance_id == Instance.id,"
765+ "Instance.deleted == False)",
766+ backref='security_groups')
767+
768+ @property
769+ def user(self):
770+ return auth.manager.AuthManager().get_user(self.user_id)
771+
772+ @property
773+ def project(self):
774+ return auth.manager.AuthManager().get_project(self.project_id)
775+
776+
777+class SecurityGroupIngressRule(BASE, NovaBase):
778+ """Represents a rule in a security group"""
779+ __tablename__ = 'security_group_rules'
780+ id = Column(Integer, primary_key=True)
781+
782+ parent_group_id = Column(Integer, ForeignKey('security_groups.id'))
783+ parent_group = relationship("SecurityGroup", backref="rules",
784+ foreign_keys=parent_group_id,
785+ primaryjoin="and_(SecurityGroupIngressRule.parent_group_id == SecurityGroup.id,"
786+ "SecurityGroupIngressRule.deleted == False)")
787+
788+ protocol = Column(String(5)) # "tcp", "udp", or "icmp"
789+ from_port = Column(Integer)
790+ to_port = Column(Integer)
791+ cidr = Column(String(255))
792+
793+ # Note: This is not the parent SecurityGroup. It's SecurityGroup we're
794+ # granting access for.
795+ group_id = Column(Integer, ForeignKey('security_groups.id'))
796+
797+
798 class KeyPair(BASE, NovaBase):
799 """Represents a public key pair for ssh"""
800 __tablename__ = 'key_pairs'
801 id = Column(Integer, primary_key=True)
802+
803 name = Column(String(255))
804
805 user_id = Column(String(255))
806@@ -461,9 +516,10 @@
807 def register_models():
808 """Register Models and create metadata"""
809 from sqlalchemy import create_engine
810- models = (Service, Instance, Volume, ExportDevice,
811- FixedIp, FloatingIp, Network, NetworkIndex,
812- AuthToken, UserProjectAssociation, User, Project) # , Image, Host)
813+ models = (Service, Instance, Volume, ExportDevice, FixedIp,
814+ FloatingIp, Network, NetworkIndex, SecurityGroup,
815+ SecurityGroupIngressRule, SecurityGroupInstanceAssociation,
816+ AuthToken, User, Project) # , Image, Host
817 engine = create_engine(FLAGS.sql_connection, echo=False)
818 for model in models:
819 model.metadata.create_all(engine)
820
821=== modified file 'nova/db/sqlalchemy/session.py'
822--- nova/db/sqlalchemy/session.py 2010-09-08 02:48:12 +0000
823+++ nova/db/sqlalchemy/session.py 2010-10-12 20:19:40 +0000
824@@ -36,7 +36,8 @@
825 if not _MAKER:
826 if not _ENGINE:
827 _ENGINE = create_engine(FLAGS.sql_connection, echo=False)
828- _MAKER = sessionmaker(bind=_ENGINE,
829- autocommit=autocommit,
830- expire_on_commit=expire_on_commit)
831- return _MAKER()
832+ _MAKER = (sessionmaker(bind=_ENGINE,
833+ autocommit=autocommit,
834+ expire_on_commit=expire_on_commit))
835+ session = _MAKER()
836+ return session
837
838=== modified file 'nova/exception.py'
839--- nova/exception.py 2010-09-08 04:53:40 +0000
840+++ nova/exception.py 2010-10-12 20:19:40 +0000
841@@ -69,6 +69,9 @@
842 class Invalid(Error):
843 pass
844
845+class InvalidInputException(Error):
846+ pass
847+
848
849 def wrap_exception(f):
850 def _wrap(*args, **kw):
851
852=== modified file 'nova/network/manager.py'
853--- nova/network/manager.py 2010-10-04 07:38:16 +0000
854+++ nova/network/manager.py 2010-10-12 20:19:40 +0000
855@@ -211,7 +211,6 @@
856 # in the datastore?
857 net = {}
858 net['injected'] = True
859- net['network_str'] = FLAGS.flat_network_network
860 net['netmask'] = FLAGS.flat_network_netmask
861 net['bridge'] = FLAGS.flat_network_bridge
862 net['gateway'] = FLAGS.flat_network_gateway
863
864=== modified file 'nova/process.py'
865--- nova/process.py 2010-09-11 15:16:16 +0000
866+++ nova/process.py 2010-10-12 20:19:40 +0000
867@@ -113,7 +113,7 @@
868 if self.started_deferred:
869 self.started_deferred.callback(self)
870 if self.process_input:
871- self.transport.write(self.process_input)
872+ self.transport.write(str(self.process_input))
873 self.transport.closeStdin()
874
875 def get_process_output(executable, args=None, env=None, path=None,
876
877=== modified file 'nova/test.py'
878--- nova/test.py 2010-09-25 02:25:12 +0000
879+++ nova/test.py 2010-10-12 20:19:40 +0000
880@@ -31,6 +31,7 @@
881 from twisted.internet import defer
882 from twisted.trial import unittest
883
884+from nova import db
885 from nova import fakerabbit
886 from nova import flags
887 from nova import rpc
888@@ -83,6 +84,7 @@
889
890 if FLAGS.fake_rabbit:
891 fakerabbit.reset_all()
892+ db.security_group_destroy_all(None)
893
894 super(TrialTestCase, self).tearDown()
895
896
897=== modified file 'nova/tests/api_unittest.py'
898--- nova/tests/api_unittest.py 2010-09-23 14:45:21 +0000
899+++ nova/tests/api_unittest.py 2010-10-12 20:19:40 +0000
900@@ -91,6 +91,9 @@
901 self.host = '127.0.0.1'
902
903 self.app = api.API()
904+
905+ def expect_http(self, host=None, is_secure=False):
906+ """Returns a new EC2 connection"""
907 self.ec2 = boto.connect_ec2(
908 aws_access_key_id='fake',
909 aws_secret_access_key='fake',
910@@ -100,9 +103,6 @@
911 path='/services/Cloud')
912
913 self.mox.StubOutWithMock(self.ec2, 'new_http_connection')
914-
915- def expect_http(self, host=None, is_secure=False):
916- """Returns a new EC2 connection"""
917 http = FakeHttplibConnection(
918 self.app, '%s:8773' % (self.host), False)
919 # pylint: disable-msg=E1103
920@@ -138,3 +138,185 @@
921 self.assertEquals(len(results), 1)
922 self.manager.delete_project(project)
923 self.manager.delete_user(user)
924+
925+ def test_get_all_security_groups(self):
926+ """Test that we can retrieve security groups"""
927+ self.expect_http()
928+ self.mox.ReplayAll()
929+ user = self.manager.create_user('fake', 'fake', 'fake', admin=True)
930+ project = self.manager.create_project('fake', 'fake', 'fake')
931+
932+ rv = self.ec2.get_all_security_groups()
933+
934+ self.assertEquals(len(rv), 1)
935+ self.assertEquals(rv[0].name, 'default')
936+
937+ self.manager.delete_project(project)
938+ self.manager.delete_user(user)
939+
940+ def test_create_delete_security_group(self):
941+ """Test that we can create a security group"""
942+ self.expect_http()
943+ self.mox.ReplayAll()
944+ user = self.manager.create_user('fake', 'fake', 'fake', admin=True)
945+ project = self.manager.create_project('fake', 'fake', 'fake')
946+
947+ # At the moment, you need both of these to actually be netadmin
948+ self.manager.add_role('fake', 'netadmin')
949+ project.add_role('fake', 'netadmin')
950+
951+ security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd") \
952+ for x in range(random.randint(4, 8)))
953+
954+ self.ec2.create_security_group(security_group_name, 'test group')
955+
956+ self.expect_http()
957+ self.mox.ReplayAll()
958+
959+ rv = self.ec2.get_all_security_groups()
960+ self.assertEquals(len(rv), 2)
961+ self.assertTrue(security_group_name in [group.name for group in rv])
962+
963+ self.expect_http()
964+ self.mox.ReplayAll()
965+
966+ self.ec2.delete_security_group(security_group_name)
967+
968+ self.manager.delete_project(project)
969+ self.manager.delete_user(user)
970+
971+ def test_authorize_revoke_security_group_cidr(self):
972+ """
973+ Test that we can add and remove CIDR based rules
974+ to a security group
975+ """
976+ self.expect_http()
977+ self.mox.ReplayAll()
978+ user = self.manager.create_user('fake', 'fake', 'fake')
979+ project = self.manager.create_project('fake', 'fake', 'fake')
980+
981+ # At the moment, you need both of these to actually be netadmin
982+ self.manager.add_role('fake', 'netadmin')
983+ project.add_role('fake', 'netadmin')
984+
985+ security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd") \
986+ for x in range(random.randint(4, 8)))
987+
988+ group = self.ec2.create_security_group(security_group_name, 'test group')
989+
990+ self.expect_http()
991+ self.mox.ReplayAll()
992+ group.connection = self.ec2
993+
994+ group.authorize('tcp', 80, 81, '0.0.0.0/0')
995+
996+ self.expect_http()
997+ self.mox.ReplayAll()
998+
999+ rv = self.ec2.get_all_security_groups()
1000+ # I don't bother checkng that we actually find it here,
1001+ # because the create/delete unit test further up should
1002+ # be good enough for that.
1003+ for group in rv:
1004+ if group.name == security_group_name:
1005+ self.assertEquals(len(group.rules), 1)
1006+ self.assertEquals(int(group.rules[0].from_port), 80)
1007+ self.assertEquals(int(group.rules[0].to_port), 81)
1008+ self.assertEquals(len(group.rules[0].grants), 1)
1009+ self.assertEquals(str(group.rules[0].grants[0]), '0.0.0.0/0')
1010+
1011+ self.expect_http()
1012+ self.mox.ReplayAll()
1013+ group.connection = self.ec2
1014+
1015+ group.revoke('tcp', 80, 81, '0.0.0.0/0')
1016+
1017+ self.expect_http()
1018+ self.mox.ReplayAll()
1019+
1020+ self.ec2.delete_security_group(security_group_name)
1021+
1022+ self.expect_http()
1023+ self.mox.ReplayAll()
1024+ group.connection = self.ec2
1025+
1026+ rv = self.ec2.get_all_security_groups()
1027+
1028+ self.assertEqual(len(rv), 1)
1029+ self.assertEqual(rv[0].name, 'default')
1030+
1031+ self.manager.delete_project(project)
1032+ self.manager.delete_user(user)
1033+
1034+ return
1035+
1036+ def test_authorize_revoke_security_group_foreign_group(self):
1037+ """
1038+ Test that we can grant and revoke another security group access
1039+ to a security group
1040+ """
1041+ self.expect_http()
1042+ self.mox.ReplayAll()
1043+ user = self.manager.create_user('fake', 'fake', 'fake', admin=True)
1044+ project = self.manager.create_project('fake', 'fake', 'fake')
1045+
1046+ # At the moment, you need both of these to actually be netadmin
1047+ self.manager.add_role('fake', 'netadmin')
1048+ project.add_role('fake', 'netadmin')
1049+
1050+ security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd") \
1051+ for x in range(random.randint(4, 8)))
1052+ other_security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd") \
1053+ for x in range(random.randint(4, 8)))
1054+
1055+ group = self.ec2.create_security_group(security_group_name, 'test group')
1056+
1057+ self.expect_http()
1058+ self.mox.ReplayAll()
1059+
1060+ other_group = self.ec2.create_security_group(other_security_group_name,
1061+ 'some other group')
1062+
1063+ self.expect_http()
1064+ self.mox.ReplayAll()
1065+ group.connection = self.ec2
1066+
1067+ group.authorize(src_group=other_group)
1068+
1069+ self.expect_http()
1070+ self.mox.ReplayAll()
1071+
1072+ rv = self.ec2.get_all_security_groups()
1073+
1074+ # I don't bother checkng that we actually find it here,
1075+ # because the create/delete unit test further up should
1076+ # be good enough for that.
1077+ for group in rv:
1078+ if group.name == security_group_name:
1079+ self.assertEquals(len(group.rules), 1)
1080+ self.assertEquals(len(group.rules[0].grants), 1)
1081+ self.assertEquals(str(group.rules[0].grants[0]),
1082+ '%s-%s' % (other_security_group_name, 'fake'))
1083+
1084+
1085+ self.expect_http()
1086+ self.mox.ReplayAll()
1087+
1088+ rv = self.ec2.get_all_security_groups()
1089+
1090+ for group in rv:
1091+ if group.name == security_group_name:
1092+ self.expect_http()
1093+ self.mox.ReplayAll()
1094+ group.connection = self.ec2
1095+ group.revoke(src_group=other_group)
1096+
1097+ self.expect_http()
1098+ self.mox.ReplayAll()
1099+
1100+ self.ec2.delete_security_group(security_group_name)
1101+
1102+ self.manager.delete_project(project)
1103+ self.manager.delete_user(user)
1104+
1105+ return
1106
1107=== modified file 'nova/tests/objectstore_unittest.py'
1108--- nova/tests/objectstore_unittest.py 2010-10-11 12:09:24 +0000
1109+++ nova/tests/objectstore_unittest.py 2010-10-12 20:19:40 +0000
1110@@ -210,7 +210,7 @@
1111 """Setup users, projects, and start a test server."""
1112 super(S3APITestCase, self).setUp()
1113
1114- FLAGS.auth_driver = 'nova.auth.ldapdriver.FakeLdapDriver',
1115+ FLAGS.auth_driver = 'nova.auth.ldapdriver.FakeLdapDriver'
1116 FLAGS.buckets_path = os.path.join(OSS_TEMPDIR, 'buckets')
1117
1118 self.auth_manager = manager.AuthManager()
1119
1120=== modified file 'nova/tests/virt_unittest.py'
1121--- nova/tests/virt_unittest.py 2010-10-05 08:07:37 +0000
1122+++ nova/tests/virt_unittest.py 2010-10-12 20:19:40 +0000
1123@@ -14,11 +14,16 @@
1124 # License for the specific language governing permissions and limitations
1125 # under the License.
1126
1127-from xml.etree.ElementTree import fromstring as parseXml
1128+from xml.etree.ElementTree import fromstring as xml_to_tree
1129+from xml.dom.minidom import parseString as xml_to_dom
1130
1131+from nova import db
1132 from nova import flags
1133 from nova import test
1134+from nova.api import context
1135+from nova.api.ec2 import cloud
1136 from nova.auth import manager
1137+
1138 # Needed to get FLAGS.instances_path defined:
1139 from nova.compute import manager as compute_manager
1140 from nova.virt import libvirt_conn
1141@@ -33,34 +38,49 @@
1142 FLAGS.instances_path = ''
1143
1144 def test_get_uri_and_template(self):
1145- instance = { 'name' : 'i-cafebabe',
1146- 'id' : 'i-cafebabe',
1147+ ip = '10.11.12.13'
1148+
1149+ instance = { 'internal_id' : 1,
1150 'memory_kb' : '1024000',
1151 'basepath' : '/some/path',
1152 'bridge_name' : 'br100',
1153 'mac_address' : '02:12:34:46:56:67',
1154 'vcpus' : 2,
1155 'project_id' : 'fake',
1156- 'ip_address' : '10.11.12.13',
1157 'bridge' : 'br101',
1158 'instance_type' : 'm1.small'}
1159
1160+ instance_ref = db.instance_create(None, instance)
1161+ network_ref = db.project_get_network(None, self.project.id)
1162+
1163+ fixed_ip = { 'address' : ip,
1164+ 'network_id' : network_ref['id'] }
1165+
1166+ fixed_ip_ref = db.fixed_ip_create(None, fixed_ip)
1167+ db.fixed_ip_update(None, ip, { 'allocated' : True,
1168+ 'instance_id' : instance_ref['id'] })
1169+
1170 type_uri_map = { 'qemu' : ('qemu:///system',
1171- [(lambda t: t.find('.').tag, 'domain'),
1172- (lambda t: t.find('.').get('type'), 'qemu'),
1173+ [(lambda t: t.find('.').get('type'), 'qemu'),
1174 (lambda t: t.find('./os/type').text, 'hvm'),
1175 (lambda t: t.find('./devices/emulator'), None)]),
1176 'kvm' : ('qemu:///system',
1177- [(lambda t: t.find('.').tag, 'domain'),
1178- (lambda t: t.find('.').get('type'), 'kvm'),
1179+ [(lambda t: t.find('.').get('type'), 'kvm'),
1180 (lambda t: t.find('./os/type').text, 'hvm'),
1181 (lambda t: t.find('./devices/emulator'), None)]),
1182 'uml' : ('uml:///system',
1183- [(lambda t: t.find('.').tag, 'domain'),
1184- (lambda t: t.find('.').get('type'), 'uml'),
1185+ [(lambda t: t.find('.').get('type'), 'uml'),
1186 (lambda t: t.find('./os/type').text, 'uml')]),
1187 }
1188
1189+ common_checks = [(lambda t: t.find('.').tag, 'domain'),
1190+ (lambda t: \
1191+ t.find('./devices/interface/filterref/parameter') \
1192+ .get('name'), 'IP'),
1193+ (lambda t: \
1194+ t.find('./devices/interface/filterref/parameter') \
1195+ .get('value'), '10.11.12.13')]
1196+
1197 for (libvirt_type,(expected_uri, checks)) in type_uri_map.iteritems():
1198 FLAGS.libvirt_type = libvirt_type
1199 conn = libvirt_conn.LibvirtConnection(True)
1200@@ -68,13 +88,18 @@
1201 uri, template = conn.get_uri_and_template()
1202 self.assertEquals(uri, expected_uri)
1203
1204- xml = conn.to_xml(instance)
1205- tree = parseXml(xml)
1206+ xml = conn.to_xml(instance_ref)
1207+ tree = xml_to_tree(xml)
1208 for i, (check, expected_result) in enumerate(checks):
1209 self.assertEqual(check(tree),
1210 expected_result,
1211 '%s failed check %d' % (xml, i))
1212
1213+ for i, (check, expected_result) in enumerate(common_checks):
1214+ self.assertEqual(check(tree),
1215+ expected_result,
1216+ '%s failed common check %d' % (xml, i))
1217+
1218 # Deliberately not just assigning this string to FLAGS.libvirt_uri and
1219 # checking against that later on. This way we make sure the
1220 # implementation doesn't fiddle around with the FLAGS.
1221@@ -90,3 +115,138 @@
1222 def tearDown(self):
1223 self.manager.delete_project(self.project)
1224 self.manager.delete_user(self.user)
1225+
1226+class NWFilterTestCase(test.TrialTestCase):
1227+ def setUp(self):
1228+ super(NWFilterTestCase, self).setUp()
1229+
1230+ class Mock(object):
1231+ pass
1232+
1233+ self.manager = manager.AuthManager()
1234+ self.user = self.manager.create_user('fake', 'fake', 'fake', admin=True)
1235+ self.project = self.manager.create_project('fake', 'fake', 'fake')
1236+ self.context = context.APIRequestContext(self.user, self.project)
1237+
1238+ self.fake_libvirt_connection = Mock()
1239+
1240+ self.fw = libvirt_conn.NWFilterFirewall(self.fake_libvirt_connection)
1241+
1242+ def tearDown(self):
1243+ self.manager.delete_project(self.project)
1244+ self.manager.delete_user(self.user)
1245+
1246+
1247+ def test_cidr_rule_nwfilter_xml(self):
1248+ cloud_controller = cloud.CloudController()
1249+ cloud_controller.create_security_group(self.context,
1250+ 'testgroup',
1251+ 'test group description')
1252+ cloud_controller.authorize_security_group_ingress(self.context,
1253+ 'testgroup',
1254+ from_port='80',
1255+ to_port='81',
1256+ ip_protocol='tcp',
1257+ cidr_ip='0.0.0.0/0')
1258+
1259+
1260+ security_group = db.security_group_get_by_name(self.context,
1261+ 'fake',
1262+ 'testgroup')
1263+
1264+ xml = self.fw.security_group_to_nwfilter_xml(security_group.id)
1265+
1266+ dom = xml_to_dom(xml)
1267+ self.assertEqual(dom.firstChild.tagName, 'filter')
1268+
1269+ rules = dom.getElementsByTagName('rule')
1270+ self.assertEqual(len(rules), 1)
1271+
1272+ # It's supposed to allow inbound traffic.
1273+ self.assertEqual(rules[0].getAttribute('action'), 'accept')
1274+ self.assertEqual(rules[0].getAttribute('direction'), 'in')
1275+
1276+ # Must be lower priority than the base filter (which blocks everything)
1277+ self.assertTrue(int(rules[0].getAttribute('priority')) < 1000)
1278+
1279+ ip_conditions = rules[0].getElementsByTagName('tcp')
1280+ self.assertEqual(len(ip_conditions), 1)
1281+ self.assertEqual(ip_conditions[0].getAttribute('srcipaddr'), '0.0.0.0')
1282+ self.assertEqual(ip_conditions[0].getAttribute('srcipmask'), '0.0.0.0')
1283+ self.assertEqual(ip_conditions[0].getAttribute('dstportstart'), '80')
1284+ self.assertEqual(ip_conditions[0].getAttribute('dstportend'), '81')
1285+
1286+
1287+ self.teardown_security_group()
1288+
1289+ def teardown_security_group(self):
1290+ cloud_controller = cloud.CloudController()
1291+ cloud_controller.delete_security_group(self.context, 'testgroup')
1292+
1293+
1294+ def setup_and_return_security_group(self):
1295+ cloud_controller = cloud.CloudController()
1296+ cloud_controller.create_security_group(self.context,
1297+ 'testgroup',
1298+ 'test group description')
1299+ cloud_controller.authorize_security_group_ingress(self.context,
1300+ 'testgroup',
1301+ from_port='80',
1302+ to_port='81',
1303+ ip_protocol='tcp',
1304+ cidr_ip='0.0.0.0/0')
1305+
1306+ return db.security_group_get_by_name(self.context, 'fake', 'testgroup')
1307+
1308+ def test_creates_base_rule_first(self):
1309+ # These come pre-defined by libvirt
1310+ self.defined_filters = ['no-mac-spoofing',
1311+ 'no-ip-spoofing',
1312+ 'no-arp-spoofing',
1313+ 'allow-dhcp-server']
1314+
1315+ self.recursive_depends = {}
1316+ for f in self.defined_filters:
1317+ self.recursive_depends[f] = []
1318+
1319+ def _filterDefineXMLMock(xml):
1320+ dom = xml_to_dom(xml)
1321+ name = dom.firstChild.getAttribute('name')
1322+ self.recursive_depends[name] = []
1323+ for f in dom.getElementsByTagName('filterref'):
1324+ ref = f.getAttribute('filter')
1325+ self.assertTrue(ref in self.defined_filters,
1326+ ('%s referenced filter that does ' +
1327+ 'not yet exist: %s') % (name, ref))
1328+ dependencies = [ref] + self.recursive_depends[ref]
1329+ self.recursive_depends[name] += dependencies
1330+
1331+ self.defined_filters.append(name)
1332+ return True
1333+
1334+ self.fake_libvirt_connection.nwfilterDefineXML = _filterDefineXMLMock
1335+
1336+ instance_ref = db.instance_create(self.context,
1337+ {'user_id': 'fake',
1338+ 'project_id': 'fake'})
1339+ inst_id = instance_ref['id']
1340+
1341+ def _ensure_all_called(_):
1342+ instance_filter = 'nova-instance-%s' % instance_ref['name']
1343+ secgroup_filter = 'nova-secgroup-%s' % self.security_group['id']
1344+ for required in [secgroup_filter, 'allow-dhcp-server',
1345+ 'no-arp-spoofing', 'no-ip-spoofing',
1346+ 'no-mac-spoofing']:
1347+ self.assertTrue(required in self.recursive_depends[instance_filter],
1348+ "Instance's filter does not include %s" % required)
1349+
1350+ self.security_group = self.setup_and_return_security_group()
1351+
1352+ db.instance_add_security_group(self.context, inst_id, self.security_group.id)
1353+ instance = db.instance_get(self.context, inst_id)
1354+
1355+ d = self.fw.setup_nwfilters_for_instance(instance)
1356+ d.addCallback(_ensure_all_called)
1357+ d.addCallback(lambda _:self.teardown_security_group())
1358+
1359+ return d
1360
1361=== modified file 'nova/virt/interfaces.template'
1362--- nova/virt/interfaces.template 2010-08-13 21:45:26 +0000
1363+++ nova/virt/interfaces.template 2010-10-12 20:19:40 +0000
1364@@ -10,7 +10,6 @@
1365 iface eth0 inet static
1366 address %(address)s
1367 netmask %(netmask)s
1368- network %(network)s
1369 broadcast %(broadcast)s
1370 gateway %(gateway)s
1371 dns-nameservers %(dns)s
1372
1373=== modified file 'nova/virt/libvirt.qemu.xml.template'
1374--- nova/virt/libvirt.qemu.xml.template 2010-09-07 12:34:37 +0000
1375+++ nova/virt/libvirt.qemu.xml.template 2010-10-12 20:19:40 +0000
1376@@ -20,6 +20,10 @@
1377 <source bridge='%(bridge_name)s'/>
1378 <mac address='%(mac_address)s'/>
1379 <!-- <model type='virtio'/> CANT RUN virtio network right now -->
1380+ <filterref filter="nova-instance-%(name)s">
1381+ <parameter name="IP" value="%(ip_address)s" />
1382+ <parameter name="DHCPSERVER" value="%(dhcp_server)s" />
1383+ </filterref>
1384 </interface>
1385 <serial type="file">
1386 <source path='%(basepath)s/console.log'/>
1387
1388=== modified file 'nova/virt/libvirt.uml.xml.template'
1389--- nova/virt/libvirt.uml.xml.template 2010-09-07 12:34:37 +0000
1390+++ nova/virt/libvirt.uml.xml.template 2010-10-12 20:19:40 +0000
1391@@ -14,6 +14,10 @@
1392 <interface type='bridge'>
1393 <source bridge='%(bridge_name)s'/>
1394 <mac address='%(mac_address)s'/>
1395+ <filterref filter="nova-instance-%(name)s">
1396+ <parameter name="IP" value="%(ip_address)s" />
1397+ <parameter name="DHCPSERVER" value="%(dhcp_server)s" />
1398+ </filterref>
1399 </interface>
1400 <console type="file">
1401 <source path='%(basepath)s/console.log'/>
1402
1403=== modified file 'nova/virt/libvirt_conn.py'
1404--- nova/virt/libvirt_conn.py 2010-09-09 15:55:09 +0000
1405+++ nova/virt/libvirt_conn.py 2010-10-12 20:19:40 +0000
1406@@ -25,14 +25,17 @@
1407 import os
1408 import shutil
1409
1410+import IPy
1411 from twisted.internet import defer
1412 from twisted.internet import task
1413+from twisted.internet import threads
1414
1415 from nova import db
1416 from nova import exception
1417 from nova import flags
1418 from nova import process
1419 from nova import utils
1420+#from nova.api import context
1421 from nova.auth import manager
1422 from nova.compute import disk
1423 from nova.compute import instance_types
1424@@ -60,6 +63,9 @@
1425 '',
1426 'Override the default libvirt URI (which is dependent'
1427 ' on libvirt_type)')
1428+flags.DEFINE_bool('allow_project_net_traffic',
1429+ True,
1430+ 'Whether to allow in project network traffic')
1431
1432
1433 def get_connection(read_only):
1434@@ -134,7 +140,7 @@
1435 d.addCallback(lambda _: self._cleanup(instance))
1436 # FIXME: What does this comment mean?
1437 # TODO(termie): short-circuit me for tests
1438- # WE'LL save this for when we do shutdown,
1439+ # WE'LL save this for when we do shutdown,
1440 # instead of destroy - but destroy returns immediately
1441 timer = task.LoopingCall(f=None)
1442 def _wait_for_shutdown():
1443@@ -214,6 +220,7 @@
1444 instance['id'],
1445 power_state.NOSTATE,
1446 'launching')
1447+ yield NWFilterFirewall(self._conn).setup_nwfilters_for_instance(instance)
1448 yield self._create_image(instance, xml)
1449 yield self._conn.createXML(xml, 0)
1450 # TODO(termie): this should actually register
1451@@ -285,7 +292,6 @@
1452 address = db.instance_get_fixed_address(None, inst['id'])
1453 with open(FLAGS.injected_network_template) as f:
1454 net = f.read() % {'address': address,
1455- 'network': network_ref['network'],
1456 'netmask': network_ref['netmask'],
1457 'gateway': network_ref['gateway'],
1458 'broadcast': network_ref['broadcast'],
1459@@ -317,6 +323,9 @@
1460 network = db.project_get_network(None, instance['project_id'])
1461 # FIXME(vish): stick this in db
1462 instance_type = instance_types.INSTANCE_TYPES[instance['instance_type']]
1463+ ip_address = db.instance_get_fixed_address({}, instance['id'])
1464+ # Assume that the gateway also acts as the dhcp server.
1465+ dhcp_server = network['gateway']
1466 xml_info = {'type': FLAGS.libvirt_type,
1467 'name': instance['name'],
1468 'basepath': os.path.join(FLAGS.instances_path,
1469@@ -324,7 +333,9 @@
1470 'memory_kb': instance_type['memory_mb'] * 1024,
1471 'vcpus': instance_type['vcpus'],
1472 'bridge_name': network['bridge'],
1473- 'mac_address': instance['mac_address']}
1474+ 'mac_address': instance['mac_address'],
1475+ 'ip_address': ip_address,
1476+ 'dhcp_server': dhcp_server }
1477 libvirt_xml = self.libvirt_xml % xml_info
1478 logging.debug('instance %s: finished toXML method', instance['name'])
1479
1480@@ -438,3 +449,195 @@
1481 """
1482 domain = self._conn.lookupByName(instance_name)
1483 return domain.interfaceStats(interface)
1484+
1485+
1486+ def refresh_security_group(self, security_group_id):
1487+ fw = NWFilterFirewall(self._conn)
1488+ fw.ensure_security_group_filter(security_group_id)
1489+
1490+
1491+class NWFilterFirewall(object):
1492+ """
1493+ This class implements a network filtering mechanism versatile
1494+ enough for EC2 style Security Group filtering by leveraging
1495+ libvirt's nwfilter.
1496+
1497+ First, all instances get a filter ("nova-base-filter") applied.
1498+ This filter drops all incoming ipv4 and ipv6 connections.
1499+ Outgoing connections are never blocked.
1500+
1501+ Second, every security group maps to a nwfilter filter(*).
1502+ NWFilters can be updated at runtime and changes are applied
1503+ immediately, so changes to security groups can be applied at
1504+ runtime (as mandated by the spec).
1505+
1506+ Security group rules are named "nova-secgroup-<id>" where <id>
1507+ is the internal id of the security group. They're applied only on
1508+ hosts that have instances in the security group in question.
1509+
1510+ Updates to security groups are done by updating the data model
1511+ (in response to API calls) followed by a request sent to all
1512+ the nodes with instances in the security group to refresh the
1513+ security group.
1514+
1515+ Each instance has its own NWFilter, which references the above
1516+ mentioned security group NWFilters. This was done because
1517+ interfaces can only reference one filter while filters can
1518+ reference multiple other filters. This has the added benefit of
1519+ actually being able to add and remove security groups from an
1520+ instance at run time. This functionality is not exposed anywhere,
1521+ though.
1522+
1523+ Outstanding questions:
1524+
1525+ The name is unique, so would there be any good reason to sync
1526+ the uuid across the nodes (by assigning it from the datamodel)?
1527+
1528+
1529+ (*) This sentence brought to you by the redundancy department of
1530+ redundancy.
1531+ """
1532+
1533+ def __init__(self, get_connection):
1534+ self._conn = get_connection
1535+
1536+
1537+ nova_base_filter = '''<filter name='nova-base' chain='root'>
1538+ <uuid>26717364-50cf-42d1-8185-29bf893ab110</uuid>
1539+ <filterref filter='no-mac-spoofing'/>
1540+ <filterref filter='no-ip-spoofing'/>
1541+ <filterref filter='no-arp-spoofing'/>
1542+ <filterref filter='allow-dhcp-server'/>
1543+ <filterref filter='nova-allow-dhcp-server'/>
1544+ <filterref filter='nova-base-ipv4'/>
1545+ <filterref filter='nova-base-ipv6'/>
1546+ </filter>'''
1547+
1548+ nova_dhcp_filter = '''<filter name='nova-allow-dhcp-server' chain='ipv4'>
1549+ <uuid>891e4787-e5c0-d59b-cbd6-41bc3c6b36fc</uuid>
1550+ <rule action='accept' direction='out'
1551+ priority='100'>
1552+ <udp srcipaddr='0.0.0.0'
1553+ dstipaddr='255.255.255.255'
1554+ srcportstart='68'
1555+ dstportstart='67'/>
1556+ </rule>
1557+ <rule action='accept' direction='in' priority='100'>
1558+ <udp srcipaddr='$DHCPSERVER'
1559+ srcportstart='67'
1560+ dstportstart='68'/>
1561+ </rule>
1562+ </filter>'''
1563+
1564+ def nova_base_ipv4_filter(self):
1565+ retval = "<filter name='nova-base-ipv4' chain='ipv4'>"
1566+ for protocol in ['tcp', 'udp', 'icmp']:
1567+ for direction,action,priority in [('out','accept', 399),
1568+ ('inout','drop', 400)]:
1569+ retval += """<rule action='%s' direction='%s' priority='%d'>
1570+ <%s />
1571+ </rule>""" % (action, direction,
1572+ priority, protocol)
1573+ retval += '</filter>'
1574+ return retval
1575+
1576+
1577+ def nova_base_ipv6_filter(self):
1578+ retval = "<filter name='nova-base-ipv6' chain='ipv6'>"
1579+ for protocol in ['tcp', 'udp', 'icmp']:
1580+ for direction,action,priority in [('out','accept',399),
1581+ ('inout','drop',400)]:
1582+ retval += """<rule action='%s' direction='%s' priority='%d'>
1583+ <%s-ipv6 />
1584+ </rule>""" % (action, direction,
1585+ priority, protocol)
1586+ retval += '</filter>'
1587+ return retval
1588+
1589+
1590+ def nova_project_filter(self, project, net, mask):
1591+ retval = "<filter name='nova-project-%s' chain='ipv4'>" % project
1592+ for protocol in ['tcp', 'udp', 'icmp']:
1593+ retval += """<rule action='accept' direction='in' priority='200'>
1594+ <%s srcipaddr='%s' srcipmask='%s' />
1595+ </rule>""" % (protocol, net, mask)
1596+ retval += '</filter>'
1597+ return retval
1598+
1599+
1600+ def _define_filter(self, xml):
1601+ if callable(xml):
1602+ xml = xml()
1603+ d = threads.deferToThread(self._conn.nwfilterDefineXML, xml)
1604+ return d
1605+
1606+
1607+ @staticmethod
1608+ def _get_net_and_mask(cidr):
1609+ net = IPy.IP(cidr)
1610+ return str(net.net()), str(net.netmask())
1611+
1612+ @defer.inlineCallbacks
1613+ def setup_nwfilters_for_instance(self, instance):
1614+ """
1615+ Creates an NWFilter for the given instance. In the process,
1616+ it makes sure the filters for the security groups as well as
1617+ the base filter are all in place.
1618+ """
1619+
1620+ yield self._define_filter(self.nova_base_ipv4_filter)
1621+ yield self._define_filter(self.nova_base_ipv6_filter)
1622+ yield self._define_filter(self.nova_dhcp_filter)
1623+ yield self._define_filter(self.nova_base_filter)
1624+
1625+ nwfilter_xml = ("<filter name='nova-instance-%s' chain='root'>\n" +
1626+ " <filterref filter='nova-base' />\n"
1627+ ) % instance['name']
1628+
1629+ if FLAGS.allow_project_net_traffic:
1630+ network_ref = db.project_get_network({}, instance['project_id'])
1631+ net, mask = self._get_net_and_mask(network_ref['cidr'])
1632+ project_filter = self.nova_project_filter(instance['project_id'],
1633+ net, mask)
1634+ yield self._define_filter(project_filter)
1635+
1636+ nwfilter_xml += (" <filterref filter='nova-project-%s' />\n"
1637+ ) % instance['project_id']
1638+
1639+ for security_group in instance.security_groups:
1640+ yield self.ensure_security_group_filter(security_group['id'])
1641+
1642+ nwfilter_xml += (" <filterref filter='nova-secgroup-%d' />\n"
1643+ ) % security_group['id']
1644+ nwfilter_xml += "</filter>"
1645+
1646+ yield self._define_filter(nwfilter_xml)
1647+ return
1648+
1649+ def ensure_security_group_filter(self, security_group_id):
1650+ return self._define_filter(
1651+ self.security_group_to_nwfilter_xml(security_group_id))
1652+
1653+
1654+ def security_group_to_nwfilter_xml(self, security_group_id):
1655+ security_group = db.security_group_get({}, security_group_id)
1656+ rule_xml = ""
1657+ for rule in security_group.rules:
1658+ rule_xml += "<rule action='accept' direction='in' priority='300'>"
1659+ if rule.cidr:
1660+ net, mask = self._get_net_and_mask(rule.cidr)
1661+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % (rule.protocol, net, mask)
1662+ if rule.protocol in ['tcp', 'udp']:
1663+ rule_xml += "dstportstart='%s' dstportend='%s' " % \
1664+ (rule.from_port, rule.to_port)
1665+ elif rule.protocol == 'icmp':
1666+ logging.info('rule.protocol: %r, rule.from_port: %r, rule.to_port: %r' % (rule.protocol, rule.from_port, rule.to_port))
1667+ if rule.from_port != -1:
1668+ rule_xml += "type='%s' " % rule.from_port
1669+ if rule.to_port != -1:
1670+ rule_xml += "code='%s' " % rule.to_port
1671+
1672+ rule_xml += '/>\n'
1673+ rule_xml += "</rule>\n"
1674+ xml = '''<filter name='nova-secgroup-%s' chain='ipv4'>%s</filter>''' % (security_group_id, rule_xml,)
1675+ return xml
1676
1677=== modified file 'run_tests.py'
1678--- run_tests.py 2010-10-05 08:06:54 +0000
1679+++ run_tests.py 2010-10-12 20:19:40 +0000
1680@@ -65,6 +65,7 @@
1681 from nova.tests.validator_unittest import *
1682 from nova.tests.virt_unittest import *
1683 from nova.tests.volume_unittest import *
1684+from nova.tests.virt_unittest import *
1685
1686
1687 FLAGS = flags.FLAGS