Merge lp:~xtoddx/nova/provider-fw-rules into lp:~hudson-openstack/nova/trunk

Proposed by Todd Willey
Status: Merged
Approved by: Christopher MacGown
Approved revision: 631
Merged at revision: 1209
Proposed branch: lp:~xtoddx/nova/provider-fw-rules
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 847 lines (+556/-15)
15 files modified
nova/api/ec2/admin.py (+45/-0)
nova/compute/api.py (+10/-0)
nova/compute/manager.py (+5/-0)
nova/db/api.py (+13/-0)
nova/db/sqlalchemy/api.py (+19/-0)
nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py (+75/-0)
nova/db/sqlalchemy/models.py (+11/-0)
nova/network/linux_net.py (+7/-0)
nova/tests/test_adminapi.py (+89/-0)
nova/tests/test_libvirt.py (+60/-2)
nova/tests/test_network.py (+30/-0)
nova/virt/driver.py (+4/-0)
nova/virt/fake.py (+16/-0)
nova/virt/libvirt/connection.py (+3/-0)
nova/virt/libvirt/firewall.py (+169/-13)
To merge this branch: bzr merge lp:~xtoddx/nova/provider-fw-rules
Reviewer Review Type Date Requested Status
Devin Carlen (community) Approve
Vish Ishaya (community) Approve
Brian Waldon (community) Needs Fixing
Brian Lamar (community) Needs Information
Soren Hansen (community) Needs Fixing
Dan Prince (community) Needs Fixing
Ed Leafe (community) Approve
Review via email: mp+62517@code.launchpad.net

Description of the change

This adds a way to create global firewall blocks that apply to all instances in your nova installation.

The mechanism for managing these rules is very similar to how security group rules are managed except there is only ever one instance of the provider rule table, as opposed to multiple security group tables. Each instance will simply jump into the provider firewall table as one of its first actions (before security groups, so these rules cannot be overridden on a per-user basis).

Most of the changes are straightforward if you understand how security groups work. There are a few small logging and variable name changes as well.

Right now this only exposes the creation of provider firewall rules. If we agree this is the best path forward I will quickly be adding a list and destroy method and updating nova-adminclient.

To post a comment you must log in.
Revision history for this message
Ed Leafe (ed-leafe) wrote :

The code for "_provider_fw_rule_exists" could be greatly simplified:

for old_rule in db.provider_fw_rule_get_all(context):
    return all([rule[k] == old_rule[k]
            for k in ('cidr', 'from_port', 'to_port', 'protocol')])

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

line 66: should be simply: if not rules_added:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Other than that, lgtm!

Revision history for this message
Todd Willey (xtoddx) wrote :

Thanks Ed! New version addresses both of those suggestions.

Revision history for this message
Ed Leafe (ed-leafe) wrote :

Ah, glad you caught my incorrect 'return' on the all() code. Didn't think of the whole method.

lgtm now.

review: Approve
Revision history for this message
justinsb (justin-fathomdb) wrote :

Broadly looks good to me. Looks like this is libvirt only, but I think that firewall rules are currently libvirt only? What's the strategy around firewall rules going forward? Is this all going to be the responsibility of the networking service?

Revision history for this message
Todd Willey (xtoddx) wrote :

All iptables and nwfilter firewall controls are libvirt only, including this. I'll add this to virt/driver.py#ComputeDriver to raise NotImplemented for other drivers.

I'm not sure what changes are going to be coming in with network as a service. Whatever change needs to be made will have to be made to the entire libvirt/firewall.py file, so this is at least structured to keep those changes in a single place.

Revision history for this message
Ed Leafe (ed-leafe) wrote :

Just in case I need to re-approve...

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

Hey Todd,

I was unable run Smokestack on this branch. Looks like you need to bump the version of the migration to '020':

019_add_provider_fw_rules.py

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

Actually. Make that '021' or whatever it needs to be with the latest trunk. Sorry. Hard to keep up with those migrations.

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

This looks good, and the extra tests are great.

Just a small correction:

You should move the "-j $provider" rule down below the "--state ESTABLISHED,RELEATED" rule. No need for these rules to be stateless, afaict.

review: Needs Fixing
Revision history for this message
Todd Willey (xtoddx) wrote :

I was thinking that if I block an IRC host or some-such I don't want communication to happen just because the initiating connection comes from an instance. The blocks should pretty much be unconditional, regardless of who initiates the connection.

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

> I was thinking that if I block an IRC host or some-such I don't want
> communication to happen just because the initiating connection comes from an
> instance. The blocks should pretty much be unconditional, regardless of who
> initiates the connection.

Then you shoulnd't be filtering the incoming response. You should be filtering the outgoing connection on its way out.

We want acceptable connections to have to go through has little processing as possible. This means the first two rules should be "block invalid stuff" followed by "allow existing connections through". Adding anything else before the "allow existing connections" rule means every single packet belonging to an existing connections has to waste time being filtered against things that it'll never match.

Revision history for this message
Todd Willey (xtoddx) wrote :

Soren, should this live in nova-filter-top or local?

Revision history for this message
Todd Willey (xtoddx) wrote :

Filed bug #796018 to address blocks for output to blacklisted hosts. Followed Soren's suggestion to make the common case easy.

Revision history for this message
Brian Lamar (blamar) wrote :

Sorry to mark all of your branches with "needs more testing" because the libvirt/network-level tests look great here, but should there be EC2 API tests as well?

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

> Soren, should this live in nova-filter-top or local?

Good question. I never completely understood what the "local" chain was for. It was there before I added the IptablesManager thing. Perhaps Vishy can shed some light on this?

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

9, 45: Would you mind using netaddr instead of IPy? There's a branch in to remove that dependency.

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

> Good question. I never completely understood what the "local" chain was for. It was there before I added the IptablesManager thing. Perhaps Vishy can shed some light on this?

Dim memory here, but I think the split of top and local was so that we could add rules to the "beginning" without interfering with predefined rules outside of nova and make sure they all got hit before the instance specific rules.

Revision history for this message
Todd Willey (xtoddx) wrote :

I've skirted the top vs. local chain discussion by leaving the rules as they are now to just apply to instance ingress. I'll follow bug #796018 to make output work (so I don't keep holding this merge up). Moving to WIP to change to using netaddr and look at testing again.

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

tests pass for me and look complete.

review: Approve
Revision history for this message
Devin Carlen (devcamcar) wrote :

lgtm

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

Marking approved with two core lgtms.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/ec2/admin.py'
2--- nova/api/ec2/admin.py 2011-06-06 15:41:04 +0000
3+++ nova/api/ec2/admin.py 2011-06-23 18:04:36 +0000
4@@ -21,7 +21,11 @@
5 """
6
7 import base64
8+import datetime
9+import netaddr
10+import urllib
11
12+from nova import compute
13 from nova import db
14 from nova import exception
15 from nova import flags
16@@ -117,6 +121,9 @@
17 def __str__(self):
18 return 'AdminController'
19
20+ def __init__(self):
21+ self.compute_api = compute.API()
22+
23 def describe_instance_types(self, context, **_kwargs):
24 """Returns all active instance types data (vcpus, memory, etc.)"""
25 return {'instanceTypeSet': [instance_dict(v) for v in
26@@ -324,3 +331,41 @@
27 rv.append(host_dict(host, compute, instances, volume, volumes,
28 now))
29 return {'hosts': rv}
30+
31+ def _provider_fw_rule_exists(self, context, rule):
32+ # TODO(todd): we call this repeatedly, can we filter by protocol?
33+ for old_rule in db.provider_fw_rule_get_all(context):
34+ if all([rule[k] == old_rule[k] for k in ('cidr', 'from_port',
35+ 'to_port', 'protocol')]):
36+ return True
37+ return False
38+
39+ def block_external_addresses(self, context, cidr):
40+ """Add provider-level firewall rules to block incoming traffic."""
41+ LOG.audit(_('Blocking traffic to all projects incoming from %s'),
42+ cidr, context=context)
43+ cidr = urllib.unquote(cidr).decode()
44+ # raise if invalid
45+ netaddr.IPNetwork(cidr)
46+ rule = {'cidr': cidr}
47+ tcp_rule = rule.copy()
48+ tcp_rule.update({'protocol': 'tcp', 'from_port': 1, 'to_port': 65535})
49+ udp_rule = rule.copy()
50+ udp_rule.update({'protocol': 'udp', 'from_port': 1, 'to_port': 65535})
51+ icmp_rule = rule.copy()
52+ icmp_rule.update({'protocol': 'icmp', 'from_port': -1,
53+ 'to_port': None})
54+ rules_added = 0
55+ if not self._provider_fw_rule_exists(context, tcp_rule):
56+ db.provider_fw_rule_create(context, tcp_rule)
57+ rules_added += 1
58+ if not self._provider_fw_rule_exists(context, udp_rule):
59+ db.provider_fw_rule_create(context, udp_rule)
60+ rules_added += 1
61+ if not self._provider_fw_rule_exists(context, icmp_rule):
62+ db.provider_fw_rule_create(context, icmp_rule)
63+ rules_added += 1
64+ if not rules_added:
65+ raise exception.ApiError(_('Duplicate rule'))
66+ self.compute_api.trigger_provider_fw_rules_refresh(context)
67+ return {'status': 'OK', 'message': 'Added %s rules' % rules_added}
68
69=== modified file 'nova/compute/api.py'
70--- nova/compute/api.py 2011-06-20 20:55:16 +0000
71+++ nova/compute/api.py 2011-06-23 18:04:36 +0000
72@@ -496,6 +496,16 @@
73 {"method": "refresh_security_group_members",
74 "args": {"security_group_id": group_id}})
75
76+ def trigger_provider_fw_rules_refresh(self, context):
77+ """Called when a rule is added to or removed from a security_group"""
78+
79+ hosts = [x['host'] for (x, idx)
80+ in db.service_get_all_compute_sorted(context)]
81+ for host in hosts:
82+ rpc.cast(context,
83+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
84+ {'method': 'refresh_provider_fw_rules', 'args': {}})
85+
86 def update(self, context, instance_id, **kwargs):
87 """Updates the instance in the datastore.
88
89
90=== modified file 'nova/compute/manager.py'
91--- nova/compute/manager.py 2011-06-23 00:27:17 +0000
92+++ nova/compute/manager.py 2011-06-23 18:04:36 +0000
93@@ -215,6 +215,11 @@
94 """
95 return self.driver.refresh_security_group_members(security_group_id)
96
97+ @exception.wrap_exception
98+ def refresh_provider_fw_rules(self, context, **_kwargs):
99+ """This call passes straight through to the virtualization driver."""
100+ return self.driver.refresh_provider_fw_rules()
101+
102 def _setup_block_device_mapping(self, context, instance_id):
103 """setup volumes for block device mapping"""
104 self.db.instance_set_state(context,
105
106=== modified file 'nova/db/api.py'
107--- nova/db/api.py 2011-06-20 20:55:16 +0000
108+++ nova/db/api.py 2011-06-23 18:04:36 +0000
109@@ -1034,6 +1034,19 @@
110 ###################
111
112
113+def provider_fw_rule_create(context, rule):
114+ """Add a firewall rule at the provider level (all hosts & instances)."""
115+ return IMPL.provider_fw_rule_create(context, rule)
116+
117+
118+def provider_fw_rule_get_all(context):
119+ """Get all provider-level firewall rules."""
120+ return IMPL.provider_fw_rule_get_all(context)
121+
122+
123+###################
124+
125+
126 def user_get(context, id):
127 """Get user by id."""
128 return IMPL.user_get(context, id)
129
130=== modified file 'nova/db/sqlalchemy/api.py'
131--- nova/db/sqlalchemy/api.py 2011-06-20 20:55:16 +0000
132+++ nova/db/sqlalchemy/api.py 2011-06-23 18:04:36 +0000
133@@ -2180,6 +2180,25 @@
134
135 ###################
136
137+
138+@require_admin_context
139+def provider_fw_rule_create(context, rule):
140+ fw_rule_ref = models.ProviderFirewallRule()
141+ fw_rule_ref.update(rule)
142+ fw_rule_ref.save()
143+ return fw_rule_ref
144+
145+
146+def provider_fw_rule_get_all(context):
147+ session = get_session()
148+ return session.query(models.ProviderFirewallRule).\
149+ filter_by(deleted=can_read_deleted(context)).\
150+ all()
151+
152+
153+###################
154+
155+
156 @require_admin_context
157 def user_get(context, id, session=None):
158 if not session:
159
160=== added file 'nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py'
161--- nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py 1970-01-01 00:00:00 +0000
162+++ nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py 2011-06-23 18:04:36 +0000
163@@ -0,0 +1,75 @@
164+# vim: tabstop=4 shiftwidth=4 softtabstop=4
165+
166+# Copyright 2010 United States Government as represented by the
167+# Administrator of the National Aeronautics and Space Administration.
168+# All Rights Reserved.
169+#
170+# Licensed under the Apache License, Version 2.0 (the "License"); you may
171+# not use this file except in compliance with the License. You may obtain
172+# a copy of the License at
173+#
174+# http://www.apache.org/licenses/LICENSE-2.0
175+#
176+# Unless required by applicable law or agreed to in writing, software
177+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
178+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
179+# License for the specific language governing permissions and limitations
180+# under the License.
181+
182+from sqlalchemy import *
183+from migrate import *
184+
185+from nova import log as logging
186+
187+
188+meta = MetaData()
189+
190+
191+# Just for the ForeignKey and column creation to succeed, these are not the
192+# actual definitions of instances or services.
193+instances = Table('instances', meta,
194+ Column('id', Integer(), primary_key=True, nullable=False),
195+ )
196+
197+
198+services = Table('services', meta,
199+ Column('id', Integer(), primary_key=True, nullable=False),
200+ )
201+
202+
203+networks = Table('networks', meta,
204+ Column('id', Integer(), primary_key=True, nullable=False),
205+ )
206+
207+
208+#
209+# New Tables
210+#
211+provider_fw_rules = Table('provider_fw_rules', meta,
212+ Column('created_at', DateTime(timezone=False)),
213+ Column('updated_at', DateTime(timezone=False)),
214+ Column('deleted_at', DateTime(timezone=False)),
215+ Column('deleted', Boolean(create_constraint=True, name=None)),
216+ Column('id', Integer(), primary_key=True, nullable=False),
217+ Column('protocol',
218+ String(length=5, convert_unicode=False, assert_unicode=None,
219+ unicode_error=None, _warn_on_bytestring=False)),
220+ Column('from_port', Integer()),
221+ Column('to_port', Integer()),
222+ Column('cidr',
223+ String(length=255, convert_unicode=False, assert_unicode=None,
224+ unicode_error=None, _warn_on_bytestring=False))
225+ )
226+
227+
228+def upgrade(migrate_engine):
229+ # Upgrade operations go here. Don't create your own engine;
230+ # bind migrate_engine to your metadata
231+ meta.bind = migrate_engine
232+ for table in (provider_fw_rules,):
233+ try:
234+ table.create()
235+ except Exception:
236+ logging.info(repr(table))
237+ logging.exception('Exception while creating table')
238+ raise
239
240=== modified file 'nova/db/sqlalchemy/models.py'
241--- nova/db/sqlalchemy/models.py 2011-06-20 20:55:16 +0000
242+++ nova/db/sqlalchemy/models.py 2011-06-23 18:04:36 +0000
243@@ -493,6 +493,17 @@
244 group_id = Column(Integer, ForeignKey('security_groups.id'))
245
246
247+class ProviderFirewallRule(BASE, NovaBase):
248+ """Represents a rule in a security group."""
249+ __tablename__ = 'provider_fw_rules'
250+ id = Column(Integer, primary_key=True)
251+
252+ protocol = Column(String(5)) # "tcp", "udp", or "icmp"
253+ from_port = Column(Integer)
254+ to_port = Column(Integer)
255+ cidr = Column(String(255))
256+
257+
258 class KeyPair(BASE, NovaBase):
259 """Represents a public key pair for ssh."""
260 __tablename__ = 'key_pairs'
261
262=== modified file 'nova/network/linux_net.py'
263--- nova/network/linux_net.py 2011-05-21 07:00:58 +0000
264+++ nova/network/linux_net.py 2011-06-23 18:04:36 +0000
265@@ -191,6 +191,13 @@
266 {'chain': chain, 'rule': rule,
267 'top': top, 'wrap': wrap})
268
269+ def empty_chain(self, chain, wrap=True):
270+ """Remove all rules from a chain."""
271+ chained_rules = [rule for rule in self.rules
272+ if rule.chain == chain and rule.wrap == wrap]
273+ for rule in chained_rules:
274+ self.rules.remove(rule)
275+
276
277 class IptablesManager(object):
278 """Wrapper for iptables.
279
280=== added file 'nova/tests/test_adminapi.py'
281--- nova/tests/test_adminapi.py 1970-01-01 00:00:00 +0000
282+++ nova/tests/test_adminapi.py 2011-06-23 18:04:36 +0000
283@@ -0,0 +1,89 @@
284+# vim: tabstop=4 shiftwidth=4 softtabstop=4
285+
286+# Copyright 2010 United States Government as represented by the
287+# Administrator of the National Aeronautics and Space Administration.
288+# All Rights Reserved.
289+#
290+# Licensed under the Apache License, Version 2.0 (the "License"); you may
291+# not use this file except in compliance with the License. You may obtain
292+# a copy of the License at
293+#
294+# http://www.apache.org/licenses/LICENSE-2.0
295+#
296+# Unless required by applicable law or agreed to in writing, software
297+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
298+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
299+# License for the specific language governing permissions and limitations
300+# under the License.
301+
302+from eventlet import greenthread
303+
304+from nova import context
305+from nova import db
306+from nova import flags
307+from nova import log as logging
308+from nova import rpc
309+from nova import test
310+from nova import utils
311+from nova.auth import manager
312+from nova.api.ec2 import admin
313+from nova.image import fake
314+
315+
316+FLAGS = flags.FLAGS
317+LOG = logging.getLogger('nova.tests.adminapi')
318+
319+
320+class AdminApiTestCase(test.TestCase):
321+ def setUp(self):
322+ super(AdminApiTestCase, self).setUp()
323+ self.flags(connection_type='fake')
324+
325+ self.conn = rpc.Connection.instance()
326+
327+ # set up our cloud
328+ self.api = admin.AdminController()
329+
330+ # set up services
331+ self.compute = self.start_service('compute')
332+ self.scheduter = self.start_service('scheduler')
333+ self.network = self.start_service('network')
334+ self.volume = self.start_service('volume')
335+ self.image_service = utils.import_object(FLAGS.image_service)
336+
337+ self.manager = manager.AuthManager()
338+ self.user = self.manager.create_user('admin', 'admin', 'admin', True)
339+ self.project = self.manager.create_project('proj', 'admin', 'proj')
340+ self.context = context.RequestContext(user=self.user,
341+ project=self.project)
342+ host = self.network.get_network_host(self.context.elevated())
343+
344+ def fake_show(meh, context, id):
345+ return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
346+ 'type': 'machine', 'image_state': 'available'}}
347+
348+ self.stubs.Set(fake._FakeImageService, 'show', fake_show)
349+ self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show)
350+
351+ # NOTE(vish): set up a manual wait so rpc.cast has a chance to finish
352+ rpc_cast = rpc.cast
353+
354+ def finish_cast(*args, **kwargs):
355+ rpc_cast(*args, **kwargs)
356+ greenthread.sleep(0.2)
357+
358+ self.stubs.Set(rpc, 'cast', finish_cast)
359+
360+ def tearDown(self):
361+ network_ref = db.project_get_network(self.context,
362+ self.project.id)
363+ db.network_disassociate(self.context, network_ref['id'])
364+ self.manager.delete_project(self.project)
365+ self.manager.delete_user(self.user)
366+ super(AdminApiTestCase, self).tearDown()
367+
368+ def test_block_external_ips(self):
369+ """Make sure provider firewall rules are created."""
370+ result = self.api.block_external_addresses(self.context, '1.1.1.1/32')
371+ self.assertEqual('OK', result['status'])
372+ self.assertEqual('Added 3 rules', result['message'])
373
374=== modified file 'nova/tests/test_libvirt.py'
375--- nova/tests/test_libvirt.py 2011-06-07 14:47:29 +0000
376+++ nova/tests/test_libvirt.py 2011-06-23 18:04:36 +0000
377@@ -799,7 +799,9 @@
378 self.network = utils.import_object(FLAGS.network_manager)
379
380 class FakeLibvirtConnection(object):
381- pass
382+ def nwfilterDefineXML(*args, **kwargs):
383+ """setup_basic_rules in nwfilter calls this."""
384+ pass
385 self.fake_libvirt_connection = FakeLibvirtConnection()
386 self.fw = firewall.IptablesFirewallDriver(
387 get_connection=lambda: self.fake_libvirt_connection)
388@@ -1035,7 +1037,6 @@
389 fakefilter.filterDefineXMLMock
390 self.fw.nwfilter._conn.nwfilterLookupByName =\
391 fakefilter.nwfilterLookupByName
392-
393 instance_ref = self._create_instance_ref()
394 inst_id = instance_ref['id']
395 instance = db.instance_get(self.context, inst_id)
396@@ -1057,6 +1058,63 @@
397
398 db.instance_destroy(admin_ctxt, instance_ref['id'])
399
400+ def test_provider_firewall_rules(self):
401+ # setup basic instance data
402+ instance_ref = self._create_instance_ref()
403+ nw_info = _create_network_info(1)
404+ ip = '10.11.12.13'
405+ network_ref = db.project_get_network(self.context, 'fake')
406+ admin_ctxt = context.get_admin_context()
407+ fixed_ip = {'address': ip, 'network_id': network_ref['id']}
408+ db.fixed_ip_create(admin_ctxt, fixed_ip)
409+ db.fixed_ip_update(admin_ctxt, ip, {'allocated': True,
410+ 'instance_id': instance_ref['id']})
411+ # FRAGILE: peeks at how the firewall names chains
412+ chain_name = 'inst-%s' % instance_ref['id']
413+
414+ # create a firewall via setup_basic_filtering like libvirt_conn.spawn
415+ # should have a chain with 0 rules
416+ self.fw.setup_basic_filtering(instance_ref, network_info=nw_info)
417+ self.assertTrue('provider' in self.fw.iptables.ipv4['filter'].chains)
418+ rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
419+ if rule.chain == 'provider']
420+ self.assertEqual(0, len(rules))
421+
422+ # add a rule and send the update message, check for 1 rule
423+ provider_fw0 = db.provider_fw_rule_create(admin_ctxt,
424+ {'protocol': 'tcp',
425+ 'cidr': '10.99.99.99/32',
426+ 'from_port': 1,
427+ 'to_port': 65535})
428+ self.fw.refresh_provider_fw_rules()
429+ rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
430+ if rule.chain == 'provider']
431+ self.assertEqual(1, len(rules))
432+
433+ # Add another, refresh, and make sure number of rules goes to two
434+ provider_fw1 = db.provider_fw_rule_create(admin_ctxt,
435+ {'protocol': 'udp',
436+ 'cidr': '10.99.99.99/32',
437+ 'from_port': 1,
438+ 'to_port': 65535})
439+ self.fw.refresh_provider_fw_rules()
440+ rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
441+ if rule.chain == 'provider']
442+ self.assertEqual(2, len(rules))
443+
444+ # create the instance filter and make sure it has a jump rule
445+ self.fw.prepare_instance_filter(instance_ref, network_info=nw_info)
446+ self.fw.apply_instance_filter(instance_ref)
447+ inst_rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
448+ if rule.chain == chain_name]
449+ jump_rules = [rule for rule in inst_rules if '-j' in rule.rule]
450+ provjump_rules = []
451+ # IptablesTable doesn't make rules unique internally
452+ for rule in jump_rules:
453+ if 'provider' in rule.rule and rule not in provjump_rules:
454+ provjump_rules.append(rule)
455+ self.assertEqual(1, len(provjump_rules))
456+
457
458 class NWFilterTestCase(test.TestCase):
459 def setUp(self):
460
461=== modified file 'nova/tests/test_network.py'
462--- nova/tests/test_network.py 2011-03-23 05:29:32 +0000
463+++ nova/tests/test_network.py 2011-06-23 18:04:36 +0000
464@@ -164,3 +164,33 @@
465 self.assertTrue('-A %s -j run_tests.py-%s' \
466 % (chain, chain) in new_lines,
467 "Built-in chain %s not wrapped" % (chain,))
468+
469+ def test_will_empty_chain(self):
470+ self.manager.ipv4['filter'].add_chain('test-chain')
471+ self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP')
472+ old_count = len(self.manager.ipv4['filter'].rules)
473+ self.manager.ipv4['filter'].empty_chain('test-chain')
474+ self.assertEqual(old_count - 1, len(self.manager.ipv4['filter'].rules))
475+
476+ def test_will_empty_unwrapped_chain(self):
477+ self.manager.ipv4['filter'].add_chain('test-chain', wrap=False)
478+ self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP',
479+ wrap=False)
480+ old_count = len(self.manager.ipv4['filter'].rules)
481+ self.manager.ipv4['filter'].empty_chain('test-chain', wrap=False)
482+ self.assertEqual(old_count - 1, len(self.manager.ipv4['filter'].rules))
483+
484+ def test_will_not_empty_wrapped_when_unwrapped(self):
485+ self.manager.ipv4['filter'].add_chain('test-chain')
486+ self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP')
487+ old_count = len(self.manager.ipv4['filter'].rules)
488+ self.manager.ipv4['filter'].empty_chain('test-chain', wrap=False)
489+ self.assertEqual(old_count, len(self.manager.ipv4['filter'].rules))
490+
491+ def test_will_not_empty_unwrapped_when_wrapped(self):
492+ self.manager.ipv4['filter'].add_chain('test-chain', wrap=False)
493+ self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP',
494+ wrap=False)
495+ old_count = len(self.manager.ipv4['filter'].rules)
496+ self.manager.ipv4['filter'].empty_chain('test-chain')
497+ self.assertEqual(old_count, len(self.manager.ipv4['filter'].rules))
498
499=== modified file 'nova/virt/driver.py'
500--- nova/virt/driver.py 2011-06-20 20:55:16 +0000
501+++ nova/virt/driver.py 2011-06-23 18:04:36 +0000
502@@ -191,6 +191,10 @@
503 def refresh_security_group_members(self, security_group_id):
504 raise NotImplementedError()
505
506+ def refresh_provider_fw_rules(self, security_group_id):
507+ """See: nova/virt/fake.py for docs."""
508+ raise NotImplementedError()
509+
510 def reset_network(self, instance):
511 """reset networking for specified instance"""
512 raise NotImplementedError()
513
514=== modified file 'nova/virt/fake.py'
515--- nova/virt/fake.py 2011-06-20 20:55:16 +0000
516+++ nova/virt/fake.py 2011-06-23 18:04:36 +0000
517@@ -466,6 +466,22 @@
518 """
519 return True
520
521+ def refresh_provider_fw_rules(self):
522+ """This triggers a firewall update based on database changes.
523+
524+ When this is called, rules have either been added or removed from the
525+ datastore. You can retrieve rules with
526+ :method:`nova.db.api.provider_fw_rule_get_all`.
527+
528+ Provider rules take precedence over security group rules. If an IP
529+ would be allowed by a security group ingress rule, but blocked by
530+ a provider rule, then packets from the IP are dropped. This includes
531+ intra-project traffic in the case of the allow_project_net_traffic
532+ flag for the libvirt-derived classes.
533+
534+ """
535+ pass
536+
537 def update_available_resource(self, ctxt, host):
538 """This method is supported only by libvirt."""
539 return
540
541=== modified file 'nova/virt/libvirt/connection.py'
542--- nova/virt/libvirt/connection.py 2011-06-15 16:46:24 +0000
543+++ nova/virt/libvirt/connection.py 2011-06-23 18:04:36 +0000
544@@ -1383,6 +1383,9 @@
545 def refresh_security_group_members(self, security_group_id):
546 self.firewall_driver.refresh_security_group_members(security_group_id)
547
548+ def refresh_provider_fw_rules(self):
549+ self.firewall_driver.refresh_provider_fw_rules()
550+
551 def update_available_resource(self, ctxt, host):
552 """Updates compute manager resource info on ComputeNode table.
553
554
555=== modified file 'nova/virt/libvirt/firewall.py'
556--- nova/virt/libvirt/firewall.py 2011-06-03 21:43:12 +0000
557+++ nova/virt/libvirt/firewall.py 2011-06-23 18:04:36 +0000
558@@ -76,6 +76,15 @@
559 the security group."""
560 raise NotImplementedError()
561
562+ def refresh_provider_fw_rules(self):
563+ """Refresh common rules for all hosts/instances from data store.
564+
565+ Gets called when a rule has been added to or removed from
566+ the list of rules (via admin api).
567+
568+ """
569+ raise NotImplementedError()
570+
571 def setup_basic_filtering(self, instance, network_info=None):
572 """Create rules to block spoofing and allow dhcp.
573
574@@ -207,6 +216,13 @@
575 [base_filter]))
576
577 def _ensure_static_filters(self):
578+ """Static filters are filters that have no need to be IP aware.
579+
580+ There is no configuration or tuneability of these filters, so they
581+ can be set up once and forgotten about.
582+
583+ """
584+
585 if self.static_filters_configured:
586 return
587
588@@ -310,19 +326,21 @@
589 'for %(instance_name)s is not found.') % locals())
590
591 def prepare_instance_filter(self, instance, network_info=None):
592- """
593- Creates an NWFilter for the given instance. In the process,
594- it makes sure the filters for the security groups as well as
595- the base filter are all in place.
596+ """Creates an NWFilter for the given instance.
597+
598+ In the process, it makes sure the filters for the provider blocks,
599+ security groups, and base filter are all in place.
600+
601 """
602 if not network_info:
603 network_info = netutils.get_network_info(instance)
604
605+ self.refresh_provider_fw_rules()
606+
607 ctxt = context.get_admin_context()
608
609 instance_secgroup_filter_name = \
610 '%s-secgroup' % (self._instance_filter_name(instance))
611- #% (instance_filter_name,)
612
613 instance_secgroup_filter_children = ['nova-base-ipv4',
614 'nova-base-ipv6',
615@@ -366,7 +384,7 @@
616 for (_n, mapping) in network_info:
617 nic_id = mapping['mac'].replace(':', '')
618 instance_filter_name = self._instance_filter_name(instance, nic_id)
619- instance_filter_children = [base_filter,
620+ instance_filter_children = [base_filter, 'nova-provider-rules',
621 instance_secgroup_filter_name]
622
623 if FLAGS.allow_project_net_traffic:
624@@ -388,6 +406,19 @@
625 return self._define_filter(
626 self.security_group_to_nwfilter_xml(security_group_id))
627
628+ def refresh_provider_fw_rules(self):
629+ """Update rules for all instances.
630+
631+ This is part of the FirewallDriver API and is called when the
632+ provider firewall rules change in the database. In the
633+ `prepare_instance_filter` we add a reference to the
634+ 'nova-provider-rules' filter for each instance's firewall, and
635+ by changing that filter we update them all.
636+
637+ """
638+ xml = self.provider_fw_to_nwfilter_xml()
639+ return self._define_filter(xml)
640+
641 def security_group_to_nwfilter_xml(self, security_group_id):
642 security_group = db.security_group_get(context.get_admin_context(),
643 security_group_id)
644@@ -426,6 +457,43 @@
645 xml += "chain='ipv4'>%s</filter>" % rule_xml
646 return xml
647
648+ def provider_fw_to_nwfilter_xml(self):
649+ """Compose a filter of drop rules from specified cidrs."""
650+ rule_xml = ""
651+ v6protocol = {'tcp': 'tcp-ipv6', 'udp': 'udp-ipv6', 'icmp': 'icmpv6'}
652+ rules = db.provider_fw_rule_get_all(context.get_admin_context())
653+ for rule in rules:
654+ rule_xml += "<rule action='block' direction='in' priority='150'>"
655+ version = netutils.get_ip_version(rule.cidr)
656+ if(FLAGS.use_ipv6 and version == 6):
657+ net, prefixlen = netutils.get_net_and_prefixlen(rule.cidr)
658+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
659+ (v6protocol[rule.protocol], net, prefixlen)
660+ else:
661+ net, mask = netutils.get_net_and_mask(rule.cidr)
662+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
663+ (rule.protocol, net, mask)
664+ if rule.protocol in ['tcp', 'udp']:
665+ rule_xml += "dstportstart='%s' dstportend='%s' " % \
666+ (rule.from_port, rule.to_port)
667+ elif rule.protocol == 'icmp':
668+ LOG.info('rule.protocol: %r, rule.from_port: %r, '
669+ 'rule.to_port: %r', rule.protocol,
670+ rule.from_port, rule.to_port)
671+ if rule.from_port != -1:
672+ rule_xml += "type='%s' " % rule.from_port
673+ if rule.to_port != -1:
674+ rule_xml += "code='%s' " % rule.to_port
675+
676+ rule_xml += '/>\n'
677+ rule_xml += "</rule>\n"
678+ xml = "<filter name='nova-provider-rules' "
679+ if(FLAGS.use_ipv6):
680+ xml += "chain='root'>%s</filter>" % rule_xml
681+ else:
682+ xml += "chain='ipv4'>%s</filter>" % rule_xml
683+ return xml
684+
685 def _instance_filter_name(self, instance, nic_id=None):
686 if not nic_id:
687 return 'nova-instance-%s' % (instance['name'])
688@@ -453,6 +521,7 @@
689 self.iptables = linux_net.iptables_manager
690 self.instances = {}
691 self.nwfilter = NWFilterFirewall(kwargs['get_connection'])
692+ self.basicly_filtered = False
693
694 self.iptables.ipv4['filter'].add_chain('sg-fallback')
695 self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP')
696@@ -460,10 +529,14 @@
697 self.iptables.ipv6['filter'].add_rule('sg-fallback', '-j DROP')
698
699 def setup_basic_filtering(self, instance, network_info=None):
700- """Use NWFilter from libvirt for this."""
701+ """Set up provider rules and basic NWFilter."""
702 if not network_info:
703 network_info = netutils.get_network_info(instance)
704- return self.nwfilter.setup_basic_filtering(instance, network_info)
705+ self.nwfilter.setup_basic_filtering(instance, network_info)
706+ if not self.basicly_filtered:
707+ LOG.debug(_('iptables firewall: Setup Basic Filtering'))
708+ self.refresh_provider_fw_rules()
709+ self.basicly_filtered = True
710
711 def apply_instance_filter(self, instance):
712 """No-op. Everything is done in prepare_instance_filter"""
713@@ -543,6 +616,10 @@
714 ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
715 ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
716
717+ # Pass through provider-wide drops
718+ ipv4_rules += ['-j $provider']
719+ ipv6_rules += ['-j $provider']
720+
721 dhcp_servers = [network['gateway'] for (network, _m) in network_info]
722
723 for dhcp_server in dhcp_servers:
724@@ -560,7 +637,7 @@
725 # they're not worth the clutter.
726 if FLAGS.use_ipv6:
727 # Allow RA responses
728- gateways_v6 = [network['gateway_v6'] for (network, _) in
729+ gateways_v6 = [network['gateway_v6'] for (network, _m) in
730 network_info]
731 for gateway_v6 in gateways_v6:
732 ipv6_rules.append(
733@@ -583,7 +660,7 @@
734 security_group['id'])
735
736 for rule in rules:
737- logging.info('%r', rule)
738+ LOG.debug(_('Adding security group rule: %r'), rule)
739
740 if not rule.cidr:
741 # Eventually, a mechanism to grant access for security
742@@ -592,9 +669,9 @@
743
744 version = netutils.get_ip_version(rule.cidr)
745 if version == 4:
746- rules = ipv4_rules
747+ fw_rules = ipv4_rules
748 else:
749- rules = ipv6_rules
750+ fw_rules = ipv6_rules
751
752 protocol = rule.protocol
753 if version == 6 and rule.protocol == 'icmp':
754@@ -629,7 +706,7 @@
755 icmp_type_arg]
756
757 args += ['-j ACCEPT']
758- rules += [' '.join(args)]
759+ fw_rules += [' '.join(args)]
760
761 ipv4_rules += ['-j $sg-fallback']
762 ipv6_rules += ['-j $sg-fallback']
763@@ -657,6 +734,85 @@
764 network_info = netutils.get_network_info(instance)
765 self.add_filters_for_instance(instance, network_info)
766
767+ def refresh_provider_fw_rules(self):
768+ """See class:FirewallDriver: docs."""
769+ self._do_refresh_provider_fw_rules()
770+ self.iptables.apply()
771+
772+ @utils.synchronized('iptables', external=True)
773+ def _do_refresh_provider_fw_rules(self):
774+ """Internal, synchronized version of refresh_provider_fw_rules."""
775+ self._purge_provider_fw_rules()
776+ self._build_provider_fw_rules()
777+
778+ def _purge_provider_fw_rules(self):
779+ """Remove all rules from the provider chains."""
780+ self.iptables.ipv4['filter'].empty_chain('provider')
781+ if FLAGS.use_ipv6:
782+ self.iptables.ipv6['filter'].empty_chain('provider')
783+
784+ def _build_provider_fw_rules(self):
785+ """Create all rules for the provider IP DROPs."""
786+ self.iptables.ipv4['filter'].add_chain('provider')
787+ if FLAGS.use_ipv6:
788+ self.iptables.ipv6['filter'].add_chain('provider')
789+ ipv4_rules, ipv6_rules = self._provider_rules()
790+ for rule in ipv4_rules:
791+ self.iptables.ipv4['filter'].add_rule('provider', rule)
792+
793+ if FLAGS.use_ipv6:
794+ for rule in ipv6_rules:
795+ self.iptables.ipv6['filter'].add_rule('provider', rule)
796+
797+ def _provider_rules(self):
798+ """Generate a list of rules from provider for IP4 & IP6."""
799+ ctxt = context.get_admin_context()
800+ ipv4_rules = []
801+ ipv6_rules = []
802+ rules = db.provider_fw_rule_get_all(ctxt)
803+ for rule in rules:
804+ LOG.debug(_('Adding provider rule: %s'), rule['cidr'])
805+ version = netutils.get_ip_version(rule['cidr'])
806+ if version == 4:
807+ fw_rules = ipv4_rules
808+ else:
809+ fw_rules = ipv6_rules
810+
811+ protocol = rule['protocol']
812+ if version == 6 and protocol == 'icmp':
813+ protocol = 'icmpv6'
814+
815+ args = ['-p', protocol, '-s', rule['cidr']]
816+
817+ if protocol in ['udp', 'tcp']:
818+ if rule['from_port'] == rule['to_port']:
819+ args += ['--dport', '%s' % (rule['from_port'],)]
820+ else:
821+ args += ['-m', 'multiport',
822+ '--dports', '%s:%s' % (rule['from_port'],
823+ rule['to_port'])]
824+ elif protocol == 'icmp':
825+ icmp_type = rule['from_port']
826+ icmp_code = rule['to_port']
827+
828+ if icmp_type == -1:
829+ icmp_type_arg = None
830+ else:
831+ icmp_type_arg = '%s' % icmp_type
832+ if not icmp_code == -1:
833+ icmp_type_arg += '/%s' % icmp_code
834+
835+ if icmp_type_arg:
836+ if version == 4:
837+ args += ['-m', 'icmp', '--icmp-type',
838+ icmp_type_arg]
839+ elif version == 6:
840+ args += ['-m', 'icmp6', '--icmpv6-type',
841+ icmp_type_arg]
842+ args += ['-j DROP']
843+ fw_rules += [' '.join(args)]
844+ return ipv4_rules, ipv6_rules
845+
846 def _security_group_chain_name(self, security_group_id):
847 return 'nova-sg-%s' % (security_group_id,)
848