Merge lp:~soren/nova/iptables-concurrency into lp:~hudson-openstack/nova/trunk

Proposed by Soren Hansen
Status: Superseded
Proposed branch: lp:~soren/nova/iptables-concurrency
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 1296 lines (+720/-285)
7 files modified
nova/flags.py (+2/-0)
nova/network/linux_net.py (+350/-111)
nova/tests/test_misc.py (+36/-1)
nova/tests/test_network.py (+142/-0)
nova/tests/test_virt.py (+36/-19)
nova/utils.py (+49/-26)
nova/virt/libvirt_conn.py (+105/-128)
To merge this branch: bzr merge lp:~soren/nova/iptables-concurrency
Reviewer Review Type Date Requested Status
Christian Berendt (community) Approve
Vish Ishaya (community) Approve
Rick Harris (community) Needs Fixing
Review via email: mp+50597@code.launchpad.net

This proposal has been superseded by a proposal from 2011-02-28.

Description of the change

Add a new IptablesManager that takes care of all uses of iptables.

Port all uses of iptables (in linux_net and libvirt_conn) over to use this new manager.

It wraps all uses of iptables so that each component can maintain its own set of rules without interfering with other components and/or existing system rules.

iptables-restore is an atomic interface to netfilter in the kernel. This means we can make a bunch of changes at a time, minimising the number of calls to iptables.

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

Seems like a lot of voodoo to me. Without any docstrings or documentation in these modules, it's all just Greek to me, not knowing the ins and outs of iptables...

Revision history for this message
Rick Harris (rconradharris) wrote :

lgtm, just a few minor-nits:

> 626 + print rule

Might be better to log here (or remove if now unnecessary)

> 227 + new_filter = self._modify_rules(current_lines,
> 228 + tables[table])

228 needs to be indented 1 space to the right.

> 238 + new_filter = filter(lambda l: binary_name not in l, current_lines)

Super-minor, but I'd lean against using a single letter variable name here. Aside from the fact that 'l' (ell) and '1' (one) look similar, I think it reads better if we use var name like 'line'. (Though, in most cases, I think 'i' and 'j' are fine for counting variables).

Of course this is personal preference, so, if you disagree here, feel free to disregard :)

> 245 + elif seen_chains == 1:

`seen_chains` is a boolean, so unless I'm misunderstanding, this should just be `elif seen_chains`. Related, perhaps that block should be rewritten as

  if seen_chains:
    <blah>
  else:
    <blah>

> 241 + for rules_index in range(len(new_filter)):

This might be clearer if rewritten as

  for rule_index, rule in enumerate(new_filter):

> + new_filter[rules_index:rules_index] = [str(rule) for rule in rules]

`rules_index` may end up uninitialized if the len(new_filter) == 0. We should probably handle that case even if it's unlikely (or even impossible) in practice.

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

So nova-local is now used for FORWARD and OUTPUT, whereas before it was only used for FORWARD, correct? Why the change?

Is use_nova_chains used anymore? It seems to have been replaced with the 'wrap' parameter. It may be prettier to have a helper function wrapped_chain('INPUT') or nova_chain('FORWARD') that we could use as the first parameter to add_rule, since we'd have the name in one spot, instead of having to look at two different parameters to determine what the actual chain name is. That also might let us get rid of the IptablesRule class and just store rules as tuples of (chain, rule).

I'm not quite done with this review, but I wanted to give you my early comments.

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

This is awesome stuff soren. I'm testing and there are a few issues:

1. The purpose of SNATTING is so that all the snatting rules can run AFTER all of the postrouting rules. It looks like you've done it the other way around. Also this seems to break with multiple workers on the same host.

    1 60 nova-network-SNATTING all -- * * 0.0.0.0/0 0.0.0.0/0
    1 60 nova-network-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0
    1 60 nova-compute-SNATTING all -- * * 0.0.0.0/0 0.0.0.0/0
    1 60 nova-compute-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0

2. The snatting rule for public ips is broken:
404 + ("SNATTING", "-d %s -j SNAT --to %s" % (fixed_ip, floating_ip))]
should be: "-s %s..."

3. I think you added a rule to both SNATTING and POSTROUTING by accident:
333 + iptables_manager.ipv4['nat'].add_rule("SNATTING",
334 + "-s %s -j SNAT --to-source %s" % \
335 + (FLAGS.fixed_range,
336 + FLAGS.routing_source_ip))
337 +
338 + iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
339 + "-s %s -j SNAT --to-source %s" % \
340 + (FLAGS.fixed_range,
341 + FLAGS.routing_source_ip))

This rule should only be in SNATTING

4. The old code added the above rule to the SNATTING chain, but added all other floating ips to the beginning of the chain. The purpose of this is so that it is a fallback. If an instance has a public ip it snats to its public ip, otherwise it uses the routing source ip. The current ordering means that all instances will always use the routing_source_ip. Consider the following:

Chain nova-network-SNATTING (1 references)
 pkts bytes target prot opt in out source destination
    0 0 SNAT all -- * * 10.0.0.0/8 0.0.0.0/0 to:10.0.0.1
    0 0 SNAT all -- * * 10.0.0.2 0.0.0.0/0 to:10.6.6.0

(the 10.0.0.2 was in destination, i moved it manually, because it is addressed by item 2 above)
10.0.0.2 is the fixed ip of the instance
10.6.6.0 is the public_ip of the instance
10.0.0.1 is the routing_source_ip
note that the snat rule at the bottom will never get hit because the first rule will catch all of them. In order to make this work properly you either need to add another chain that always runs after SNATTING, or you need a way to ensure that the floating rules get added to the beginning of the SNATTING chain.

Vish

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

2011/2/22 Rick Harris <email address hidden>:
>> 245   +            elif seen_chains == 1:
> `seen_chains` is a boolean, so unless I'm misunderstanding, this should just be `elif seen_chains`.

Actually, it could just be rewritten as "else:" :) It's a leftover
from a point where seen_chains had 4 states (and was named
differently). Good catch, thanks.

Related, perhaps that block should be rewritten as

>
>  if seen_chains:
>    <blah>
>  else:
>    <blah>

I prefer it the way it is. It matches the order of the output from
iptables-save (which is first
a comment, then a specification of the table to which the following
output pertains, then the chains,
then the rules).

>> 241   +        for rules_index in range(len(new_filter)):
>
> This might be clearer if rewritten as
>
>  for rule_index, rule in enumerate(new_filter):

Done.

>> +        new_filter[rules_index:rules_index] = [str(rule) for rule in rules]
> `rules_index` may end up uninitialized if the len(new_filter) == 0. We should probably handle that case even if it's unlikely (or even impossible) in practice.

I now initialise rules_index to 0.

--
Soren Hansen        | http://linux2go.dk/
Ubuntu Developer    | http://www.ubuntu.com/
OpenStack Developer | http://www.openstack.org/

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

2011/2/22 Todd Willey <email address hidden>:
> So nova-local is now used for FORWARD and OUTPUT, whereas before it was only used for FORWARD, correct?  Why the change?

This is the original code that is being replaced:

858 - our_chains += [':nova-local - [0:0]']
859 - our_rules += ['-A FORWARD -j nova-local']
860 - our_rules += ['-A OUTPUT -j nova-local']

So no change.

> Is use_nova_chains used anymore?  It seems to have been replaced with the 'wrap' parameter.

True. I've removed the flag now.

>  It may be prettier to have a helper function wrapped_chain('INPUT') or nova_chain('FORWARD') that we could use as the first parameter to add_rule, since we'd have the name in one spot, instead of having to look at two different parameters to determine what the actual chain name is.

Why/when do you have to look in two places to work this out?

I really like that consumers of this IptablesManager need not worry
about the the naming at all, but can just define whatever name they
want, and IptablesManager ensures that it doesn't collide with
anything else.

>  That also might let us get rid of the IptablesRule class and just store rules as tuples of (chain, rule).

I started out that way, actually, but ended up rewriting it like this.
I found it more readable this way.

> I'm not quite done with this review, but I wanted to give you my early comments.

Np. Thanks!

--
Soren Hansen        | http://linux2go.dk/
Ubuntu Developer    | http://www.ubuntu.com/
OpenStack Developer | http://www.openstack.org/

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

2011/2/22 Vish Ishaya <email address hidden>:
> This is awesome stuff soren. I'm testing and there are a few issues:
>
> 1. The purpose of SNATTING is so that all the snatting rules can run AFTER all of the postrouting rules.  It looks like you've done it the other way around.

Ah, I see. Yeah, that intent wasn't entirely clear to me. Fixed, thanks!

> Also this seems to break with multiple workers on the same host.

Yes, the "local" chain has the same problem. I realised this as I was
falling asleep last night. I'll try to come up with a way to address
this.

> 2. The snatting rule for public ips is broken:
> 404     +            ("SNATTING", "-d %s -j SNAT --to %s" % (fixed_ip, floating_ip))]
> should be: "-s %s..."

Wow. Good catch!

> 3. I think you added a rule to both SNATTING and POSTROUTING by accident:
> 333     +    iptables_manager.ipv4['nat'].add_rule("SNATTING",
> 334     +                                          "-s %s -j SNAT --to-source %s" % \
> 335     +                                           (FLAGS.fixed_range,
> 336     +                                            FLAGS.routing_source_ip))
> 337     +
> 338     +    iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
> 339     +                                          "-s %s -j SNAT --to-source %s" % \
> 340     +                                           (FLAGS.fixed_range,
> 341     +                                            FLAGS.routing_source_ip))
>
> This rule should only be in SNATTING

Fixed, thanks!

> 4. The old code added the above rule to the SNATTING chain, but added all other floating ips to the beginning of the chain.  The purpose of this is so that it is a fallback.  If an instance has a public ip it snats to its public ip, otherwise it uses the routing source ip.  The current ordering means that all instances will always use the routing_source_ip.  Consider the following:

That makes perfect sense. Thanks! I didn't realise this was how it
worked before. I'll fix this.

--
Soren Hansen        | http://linux2go.dk/
Ubuntu Developer    | http://www.ubuntu.com/
OpenStack Developer | http://www.openstack.org/

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

The rules seem to be working correctly now. One final suggestion:

The SNATTING chain is user created. I capitalized it originally because all of the default chains are capitalized. Now that we are using it along with a bunch of other custom chains (like floating-snat), i'm thinking for consistency it should be "snatting" or even "snat".

I'll go ahead and approve and leave it up to you if you want to change it.

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

I did consider that at one point, but I think I decided not to because I was
tired of adjusting the unit tests. ;) It's a good idea, though. I'll do
that once I'm back near my laptop.

lp:~soren/nova/iptables-concurrency updated
721. By Soren Hansen

Rename "SNATTING" chain to "snat".

Revision history for this message
Christian Berendt (berendt) wrote :

I tried spawning > 20 instances (hypervisor KVM) at the same time on one nova-compute node and I got 2 failed instances. I think the problem is, that there are only 15 (or 16?) available NBD devices at the same time.

(nova.compute.manager): TRACE: Error: No free nbd devices

So there has to be a hard limit somewhere while using KVM as hypervisor. Is there a flag to limit the number of spawning instances on one node at the same time? If not we should introduce one. Or is it possible to use more than 15 NBD devices at the same time?

I tested the code against rev 713 and I don't get the error described in #722477. So it seems working for me :) (but i didn't read the code)

Revision history for this message
Christian Berendt (berendt) wrote :

It's possible to load the nbd module using more devices

example: modprobe nbd nbds_max=32

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

Iirc, you can actually just mknod more devices, and the driver creates the
kernel structs on the fly.
Den 23/02/2011 13.29 skrev "Christian Berendt" <email address hidden>:
> It's possible to load the nbd module using more devices
>
> example: modprobe nbd nbds_max=32
> --
> https://code.launchpad.net/~soren/nova/iptables-concurrency/+merge/50597
> You are the owner of lp:~soren/nova/iptables-concurrency.

Revision history for this message
Christian Berendt (berendt) wrote :

> Iirc, you can actually just mknod more devices, and the driver creates the
> kernel structs on the fly.

Problem is, that the number of the devices is hardcoded to 16 in nova/virt/disk.py. But this is offtopic, I'll create a bug for this issue later...

Revision history for this message
Christian Berendt (berendt) wrote :

lgtm

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

I've spotted another concurrency problem that I might as well fix now..

lp:~soren/nova/iptables-concurrency updated
722. By Soren Hansen

Merge sync branch.

723. By Soren Hansen

Wrap IptablesManager.apply() calls in utils.synchronized to avoid having different workers step on each other's toes.

724. By Soren Hansen

Merge lock_path change.

725. By Soren Hansen

Merge sync branch.

726. By Soren Hansen

Merge sync branch and trunk

727. By Soren Hansen

Merge trunk

728. By Soren Hansen

Make iptables rules class __ne__ just be inverted __eq__.

729. By Soren Hansen

Log failed command execution if there are more retry attempts left.

730. By Soren Hansen

Use IptablesManager.semapahore from securitygroups driver to ensure we don't apply half a rule set.

731. By Soren Hansen

Merge trunk

732. By Soren Hansen

Merge trunk

733. By Soren Hansen

PEP8

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/flags.py'
2--- nova/flags.py 2011-02-25 01:04:25 +0000
3+++ nova/flags.py 2011-02-28 22:36:03 +0000
4@@ -321,6 +321,8 @@
5
6 DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../'),
7 "Top-level directory for maintaining nova's state")
8+DEFINE_string('lock_path', os.path.join(os.path.dirname(__file__), '../'),
9+ "Directory for lock files")
10 DEFINE_string('logdir', None, 'output to a per-service log file in named '
11 'directory')
12
13
14=== modified file 'nova/network/linux_net.py'
15--- nova/network/linux_net.py 2011-02-17 12:46:24 +0000
16+++ nova/network/linux_net.py 2011-02-28 22:36:03 +0000
17@@ -17,15 +17,17 @@
18 Implements vlans, bridges, and iptables rules using linux utilities.
19 """
20
21+import inspect
22 import os
23
24+from eventlet import semaphore
25+
26 from nova import db
27 from nova import exception
28 from nova import flags
29 from nova import log as logging
30 from nova import utils
31
32-
33 LOG = logging.getLogger("nova.linux_net")
34
35
36@@ -52,8 +54,6 @@
37 'location of nova-dhcpbridge')
38 flags.DEFINE_string('routing_source_ip', '$my_ip',
39 'Public IP of network host')
40-flags.DEFINE_bool('use_nova_chains', False,
41- 'use the nova_ routing chains instead of default')
42 flags.DEFINE_string('input_chain', 'INPUT',
43 'chain to add nova_input to')
44
45@@ -63,73 +63,334 @@
46 'dmz range that should be accepted')
47
48
49+binary_name = os.path.basename(inspect.stack()[-1][1])
50+
51+
52+class IptablesRule(object):
53+ """An iptables rule
54+
55+ You shouldn't need to use this class directly, it's only used by
56+ IptablesManager
57+ """
58+ def __init__(self, chain, rule, wrap=True, top=False):
59+ self.chain = chain
60+ self.rule = rule
61+ self.wrap = wrap
62+ self.top = top
63+
64+ def __eq__(self, other):
65+ return ((self.chain == other.chain) and
66+ (self.rule == other.rule) and
67+ (self.top == other.top) and
68+ (self.wrap == other.wrap))
69+
70+ def __ne__(self, other):
71+ return ((self.chain != other.chain) or
72+ (self.rule != other.rule) or
73+ (self.top != other.top) or
74+ (self.wrap != other.wrap))
75+
76+ def __str__(self):
77+ if self.wrap:
78+ chain = '%s-%s' % (binary_name, self.chain)
79+ else:
80+ chain = self.chain
81+ return '-A %s %s' % (chain, self.rule)
82+
83+
84+class IptablesTable(object):
85+ """An iptables table"""
86+
87+ def __init__(self):
88+ self.rules = []
89+ self.chains = set()
90+ self.unwrapped_chains = set()
91+
92+ def add_chain(self, name, wrap=True):
93+ """Adds a named chain to the table
94+
95+ The chain name is wrapped to be unique for the component creating
96+ it, so different components of Nova can safely create identically
97+ named chains without interfering with one another.
98+
99+ At the moment, its wrapped name is <binary name>-<chain name>,
100+ so if nova-compute creates a chain named "OUTPUT", it'll actually
101+ end up named "nova-compute-OUTPUT".
102+ """
103+ if wrap:
104+ self.chains.add(name)
105+ else:
106+ self.unwrapped_chains.add(name)
107+
108+ def remove_chain(self, name, wrap=True):
109+ """Remove named chain
110+
111+ This removal "cascades". All rule in the chain are removed, as are
112+ all rules in other chains that jump to it.
113+
114+ If the chain is not found, this is merely logged.
115+ """
116+ if wrap:
117+ chain_set = self.chains
118+ else:
119+ chain_set = self.unwrapped_chains
120+
121+ if name not in chain_set:
122+ LOG.debug(_("Attempted to remove chain %s which doesn't exist"),
123+ name)
124+ return
125+
126+ chain_set.remove(name)
127+ self.rules = filter(lambda r: r.chain != name, self.rules)
128+
129+ if wrap:
130+ jump_snippet = '-j %s-%s' % (binary_name, name)
131+ else:
132+ jump_snippet = '-j %s' % (name,)
133+
134+ self.rules = filter(lambda r: jump_snippet not in r.rule, self.rules)
135+
136+ def add_rule(self, chain, rule, wrap=True, top=False):
137+ """Add a rule to the table
138+
139+ This is just like what you'd feed to iptables, just without
140+ the "-A <chain name>" bit at the start.
141+
142+ However, if you need to jump to one of your wrapped chains,
143+ prepend its name with a '$' which will ensure the wrapping
144+ is applied correctly.
145+ """
146+ if wrap and chain not in self.chains:
147+ raise ValueError(_("Unknown chain: %r") % chain)
148+
149+ if '$' in rule:
150+ rule = ' '.join(map(self._wrap_target_chain, rule.split(' ')))
151+
152+ self.rules.append(IptablesRule(chain, rule, wrap, top))
153+
154+ def _wrap_target_chain(self, s):
155+ if s.startswith('$'):
156+ return '%s-%s' % (binary_name, s[1:])
157+ return s
158+
159+ def remove_rule(self, chain, rule, wrap=True, top=False):
160+ """Remove a rule from a chain
161+
162+ Note: The rule must be exactly identical to the one that was added.
163+ You cannot switch arguments around like you can with the iptables
164+ CLI tool.
165+ """
166+ try:
167+ self.rules.remove(IptablesRule(chain, rule, wrap, top))
168+ except ValueError:
169+ LOG.debug(_("Tried to remove rule that wasn't there:"
170+ " %(chain)r %(rule)r %(wrap)r %(top)r"),
171+ {'chain': chain, 'rule': rule,
172+ 'top': top, 'wrap': wrap})
173+
174+
175+class IptablesManager(object):
176+ """Wrapper for iptables
177+
178+ See IptablesTable for some usage docs
179+
180+ A number of chains are set up to begin with.
181+
182+ First, nova-filter-top. It's added at the top of FORWARD and OUTPUT. Its
183+ name is not wrapped, so it's shared between the various nova workers. It's
184+ intended for rules that need to live at the top of the FORWARD and OUTPUT
185+ chains. It's in both the ipv4 and ipv6 set of tables.
186+
187+ For ipv4 and ipv6, the builtin INPUT, OUTPUT, and FORWARD filter chains are
188+ wrapped, meaning that the "real" INPUT chain has a rule that jumps to the
189+ wrapped INPUT chain, etc. Additionally, there's a wrapped chain named
190+ "local" which is jumped to from nova-filter-top.
191+
192+ For ipv4, the builtin PREROUTING, OUTPUT, and POSTROUTING nat chains are
193+ wrapped in the same was as the builtin filter chains. Additionally, there's
194+ a snat chain that is applied after the POSTROUTING chain.
195+ """
196+ def __init__(self, execute=None):
197+ if not execute:
198+ if FLAGS.fake_network:
199+ self.execute = lambda *args, **kwargs: ('', '')
200+ else:
201+ self.execute = utils.execute
202+ else:
203+ self.execute = execute
204+
205+ self.ipv4 = {'filter': IptablesTable(),
206+ 'nat': IptablesTable()}
207+ self.ipv6 = {'filter': IptablesTable()}
208+
209+ # Add a nova-filter-top chain. It's intended to be shared
210+ # among the various nova components. It sits at the very top
211+ # of FORWARD and OUTPUT.
212+ for tables in [self.ipv4, self.ipv6]:
213+ tables['filter'].add_chain('nova-filter-top', wrap=False)
214+ tables['filter'].add_rule('FORWARD', '-j nova-filter-top',
215+ wrap=False, top=True)
216+ tables['filter'].add_rule('OUTPUT', '-j nova-filter-top',
217+ wrap=False, top=True)
218+
219+ tables['filter'].add_chain('local')
220+ tables['filter'].add_rule('nova-filter-top', '-j $local',
221+ wrap=False)
222+
223+ # Wrap the builtin chains
224+ builtin_chains = {4: {'filter': ['INPUT', 'OUTPUT', 'FORWARD'],
225+ 'nat': ['PREROUTING', 'OUTPUT', 'POSTROUTING']},
226+ 6: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']}}
227+
228+ for ip_version in builtin_chains:
229+ if ip_version == 4:
230+ tables = self.ipv4
231+ elif ip_version == 6:
232+ tables = self.ipv6
233+
234+ for table, chains in builtin_chains[ip_version].iteritems():
235+ for chain in chains:
236+ tables[table].add_chain(chain)
237+ tables[table].add_rule(chain, '-j $%s' % (chain,),
238+ wrap=False)
239+
240+ # Add a nova-postrouting-bottom chain. It's intended to be shared
241+ # among the various nova components. We set it as the last chain
242+ # of POSTROUTING chain.
243+ self.ipv4['nat'].add_chain('nova-postrouting-bottom', wrap=False)
244+ self.ipv4['nat'].add_rule('POSTROUTING', '-j nova-postrouting-bottom',
245+ wrap=False)
246+
247+ # We add a snat chain to the shared nova-postrouting-bottom chain
248+ # so that it's applied last.
249+ self.ipv4['nat'].add_chain('snat')
250+ self.ipv4['nat'].add_rule('nova-postrouting-bottom', '-j $snat',
251+ wrap=False)
252+
253+ # And then we add a floating-snat chain and jump to first thing in
254+ # the snat chain.
255+ self.ipv4['nat'].add_chain('floating-snat')
256+ self.ipv4['nat'].add_rule('snat', '-j $floating-snat')
257+
258+ self.semaphore = semaphore.Semaphore()
259+
260+ @utils.synchronized('iptables')
261+ def apply(self):
262+ """Apply the current in-memory set of iptables rules
263+
264+ This will blow away any rules left over from previous runs of the
265+ same component of Nova, and replace them with our current set of
266+ rules. This happens atomically, thanks to iptables-restore.
267+
268+ We wrap the call in a semaphore lock, so that we don't race with
269+ ourselves. In the event of a race with another component running
270+ an iptables-* command at the same time, we retry up to 5 times.
271+ """
272+ with self.semaphore:
273+ s = [('iptables', self.ipv4)]
274+ if FLAGS.use_ipv6:
275+ s += [('ip6tables', self.ipv6)]
276+
277+ for cmd, tables in s:
278+ for table in tables:
279+ current_table, _ = self.execute('sudo %s-save -t %s' %
280+ (cmd, table), attempts=5)
281+ current_lines = current_table.split('\n')
282+ new_filter = self._modify_rules(current_lines,
283+ tables[table])
284+ self.execute('sudo %s-restore' % (cmd,),
285+ process_input='\n'.join(new_filter),
286+ attempts=5)
287+
288+ def _modify_rules(self, current_lines, table, binary=None):
289+ unwrapped_chains = table.unwrapped_chains
290+ chains = table.chains
291+ rules = table.rules
292+
293+ # Remove any trace of our rules
294+ new_filter = filter(lambda line: binary_name not in line,
295+ current_lines)
296+
297+ seen_chains = False
298+ rules_index = 0
299+ for rules_index, rule in enumerate(new_filter):
300+ if not seen_chains:
301+ if rule.startswith(':'):
302+ seen_chains = True
303+ else:
304+ if not rule.startswith(':'):
305+ break
306+
307+ our_rules = []
308+ for rule in rules:
309+ rule_str = str(rule)
310+ if rule.top:
311+ # rule.top == True means we want this rule to be at the top.
312+ # Further down, we weed out duplicates from the bottom of the
313+ # list, so here we remove the dupes ahead of time.
314+ new_filter = filter(lambda s: s.strip() != rule_str.strip(),
315+ new_filter)
316+ our_rules += [rule_str]
317+
318+ new_filter[rules_index:rules_index] = our_rules
319+
320+ new_filter[rules_index:rules_index] = [':%s - [0:0]' % \
321+ (name,) \
322+ for name in unwrapped_chains]
323+ new_filter[rules_index:rules_index] = [':%s-%s - [0:0]' % \
324+ (binary_name, name,) \
325+ for name in chains]
326+
327+ seen_lines = set()
328+
329+ def _weed_out_duplicates(line):
330+ line = line.strip()
331+ if line in seen_lines:
332+ return False
333+ else:
334+ seen_lines.add(line)
335+ return True
336+
337+ # We filter duplicates, letting the *last* occurrence take
338+ # precendence.
339+ new_filter.reverse()
340+ new_filter = filter(_weed_out_duplicates, new_filter)
341+ new_filter.reverse()
342+ return new_filter
343+
344+
345+iptables_manager = IptablesManager()
346+
347+
348 def metadata_forward():
349 """Create forwarding rule for metadata"""
350- _confirm_rule("PREROUTING", "-t nat -s 0.0.0.0/0 "
351- "-d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT "
352- "--to-destination %s:%s" % (FLAGS.ec2_dmz_host, FLAGS.ec2_port))
353+ iptables_manager.ipv4['nat'].add_rule("PREROUTING",
354+ "-s 0.0.0.0/0 -d 169.254.169.254/32 "
355+ "-p tcp -m tcp --dport 80 -j DNAT "
356+ "--to-destination %s:%s" % \
357+ (FLAGS.ec2_dmz_host, FLAGS.ec2_port))
358+ iptables_manager.apply()
359
360
361 def init_host():
362 """Basic networking setup goes here"""
363
364- if FLAGS.use_nova_chains:
365- _execute("sudo iptables -N nova_input", check_exit_code=False)
366- _execute("sudo iptables -D %s -j nova_input" % FLAGS.input_chain,
367- check_exit_code=False)
368- _execute("sudo iptables -A %s -j nova_input" % FLAGS.input_chain)
369-
370- _execute("sudo iptables -N nova_forward", check_exit_code=False)
371- _execute("sudo iptables -D FORWARD -j nova_forward",
372- check_exit_code=False)
373- _execute("sudo iptables -A FORWARD -j nova_forward")
374-
375- _execute("sudo iptables -N nova_output", check_exit_code=False)
376- _execute("sudo iptables -D OUTPUT -j nova_output",
377- check_exit_code=False)
378- _execute("sudo iptables -A OUTPUT -j nova_output")
379-
380- _execute("sudo iptables -t nat -N nova_prerouting",
381- check_exit_code=False)
382- _execute("sudo iptables -t nat -D PREROUTING -j nova_prerouting",
383- check_exit_code=False)
384- _execute("sudo iptables -t nat -A PREROUTING -j nova_prerouting")
385-
386- _execute("sudo iptables -t nat -N nova_postrouting",
387- check_exit_code=False)
388- _execute("sudo iptables -t nat -D POSTROUTING -j nova_postrouting",
389- check_exit_code=False)
390- _execute("sudo iptables -t nat -A POSTROUTING -j nova_postrouting")
391-
392- _execute("sudo iptables -t nat -N nova_snatting",
393- check_exit_code=False)
394- _execute("sudo iptables -t nat -D POSTROUTING -j nova_snatting",
395- check_exit_code=False)
396- _execute("sudo iptables -t nat -A POSTROUTING -j nova_snatting")
397-
398- _execute("sudo iptables -t nat -N nova_output", check_exit_code=False)
399- _execute("sudo iptables -t nat -D OUTPUT -j nova_output",
400- check_exit_code=False)
401- _execute("sudo iptables -t nat -A OUTPUT -j nova_output")
402- else:
403- # NOTE(vish): This makes it easy to ensure snatting rules always
404- # come after the accept rules in the postrouting chain
405- _execute("sudo iptables -t nat -N SNATTING",
406- check_exit_code=False)
407- _execute("sudo iptables -t nat -D POSTROUTING -j SNATTING",
408- check_exit_code=False)
409- _execute("sudo iptables -t nat -A POSTROUTING -j SNATTING")
410-
411 # NOTE(devcamcar): Cloud public SNAT entries and the default
412 # SNAT rule for outbound traffic.
413- _confirm_rule("SNATTING", "-t nat -s %s "
414- "-j SNAT --to-source %s"
415- % (FLAGS.fixed_range, FLAGS.routing_source_ip), append=True)
416-
417- _confirm_rule("POSTROUTING", "-t nat -s %s -d %s -j ACCEPT" %
418- (FLAGS.fixed_range, FLAGS.dmz_cidr))
419- _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" %
420- {'range': FLAGS.fixed_range})
421+ iptables_manager.ipv4['nat'].add_rule("snat",
422+ "-s %s -j SNAT --to-source %s" % \
423+ (FLAGS.fixed_range,
424+ FLAGS.routing_source_ip))
425+
426+ iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
427+ "-s %s -d %s -j ACCEPT" % \
428+ (FLAGS.fixed_range, FLAGS.dmz_cidr))
429+
430+ iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
431+ "-s %(range)s -d %(range)s "
432+ "-j ACCEPT" % \
433+ {'range': FLAGS.fixed_range})
434+ iptables_manager.apply()
435
436
437 def bind_floating_ip(floating_ip, check_exit_code=True):
438@@ -147,31 +408,36 @@
439
440 def ensure_vlan_forward(public_ip, port, private_ip):
441 """Sets up forwarding rules for vlan"""
442- _confirm_rule("FORWARD", "-d %s -p udp --dport 1194 -j ACCEPT" %
443- private_ip)
444- _confirm_rule("PREROUTING",
445- "-t nat -d %s -p udp --dport %s -j DNAT --to %s:1194"
446- % (public_ip, port, private_ip))
447+ iptables_manager.ipv4['filter'].add_rule("FORWARD",
448+ "-d %s -p udp "
449+ "--dport 1194 "
450+ "-j ACCEPT" % private_ip)
451+ iptables_manager.ipv4['nat'].add_rule("PREROUTING",
452+ "-d %s -p udp "
453+ "--dport %s -j DNAT --to %s:1194" %
454+ (public_ip, port, private_ip))
455+ iptables_manager.apply()
456
457
458 def ensure_floating_forward(floating_ip, fixed_ip):
459 """Ensure floating ip forwarding rule"""
460- _confirm_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
461- % (floating_ip, fixed_ip))
462- _confirm_rule("OUTPUT", "-t nat -d %s -j DNAT --to %s"
463- % (floating_ip, fixed_ip))
464- _confirm_rule("SNATTING", "-t nat -s %s -j SNAT --to %s"
465- % (fixed_ip, floating_ip))
466+ for chain, rule in floating_forward_rules(floating_ip, fixed_ip):
467+ iptables_manager.ipv4['nat'].add_rule(chain, rule)
468+ iptables_manager.apply()
469
470
471 def remove_floating_forward(floating_ip, fixed_ip):
472 """Remove forwarding for floating ip"""
473- _remove_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
474- % (floating_ip, fixed_ip))
475- _remove_rule("OUTPUT", "-t nat -d %s -j DNAT --to %s"
476- % (floating_ip, fixed_ip))
477- _remove_rule("SNATTING", "-t nat -s %s -j SNAT --to %s"
478- % (fixed_ip, floating_ip))
479+ for chain, rule in floating_forward_rules(floating_ip, fixed_ip):
480+ iptables_manager.ipv4['nat'].remove_rule(chain, rule)
481+ iptables_manager.apply()
482+
483+
484+def floating_forward_rules(floating_ip, fixed_ip):
485+ return [("PREROUTING", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)),
486+ ("OUTPUT", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)),
487+ ("floating-snat",
488+ "-s %s -j SNAT --to %s" % (fixed_ip, floating_ip))]
489
490
491 def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
492@@ -258,19 +524,12 @@
493 "enslave it to bridge %s.\n" % (interface, bridge)):
494 raise exception.Error("Failed to add interface: %s" % err)
495
496- if FLAGS.use_nova_chains:
497- (out, err) = _execute("sudo iptables -N nova_forward",
498- check_exit_code=False)
499- if err != 'iptables: Chain already exists.\n':
500- # NOTE(vish): chain didn't exist link chain
501- _execute("sudo iptables -D FORWARD -j nova_forward",
502- check_exit_code=False)
503- _execute("sudo iptables -A FORWARD -j nova_forward")
504-
505- _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge)
506- _confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge)
507- _execute("sudo iptables -N nova-local", check_exit_code=False)
508- _confirm_rule("FORWARD", "-j nova-local")
509+ iptables_manager.ipv4['filter'].add_rule("FORWARD",
510+ "--in-interface %s -j ACCEPT" % \
511+ bridge)
512+ iptables_manager.ipv4['filter'].add_rule("FORWARD",
513+ "--out-interface %s -j ACCEPT" % \
514+ bridge)
515
516
517 def get_dhcp_hosts(context, network_id):
518@@ -390,26 +649,6 @@
519 return not err
520
521
522-def _confirm_rule(chain, cmd, append=False):
523- """Delete and re-add iptables rule"""
524- if FLAGS.use_nova_chains:
525- chain = "nova_%s" % chain.lower()
526- if append:
527- loc = "-A"
528- else:
529- loc = "-I"
530- _execute("sudo iptables --delete %s %s" % (chain, cmd),
531- check_exit_code=False)
532- _execute("sudo iptables %s %s %s" % (loc, chain, cmd))
533-
534-
535-def _remove_rule(chain, cmd):
536- """Remove iptables rule"""
537- if FLAGS.use_nova_chains:
538- chain = "%s" % chain.lower()
539- _execute("sudo iptables --delete %s %s" % (chain, cmd))
540-
541-
542 def _dnsmasq_cmd(net):
543 """Builds dnsmasq command"""
544 cmd = ['sudo -E dnsmasq',
545
546=== modified file 'nova/tests/test_misc.py'
547--- nova/tests/test_misc.py 2011-02-21 18:55:25 +0000
548+++ nova/tests/test_misc.py 2011-02-28 22:36:03 +0000
549@@ -14,10 +14,14 @@
550 # License for the specific language governing permissions and limitations
551 # under the License.
552
553+from datetime import datetime
554+import errno
555 import os
556+import select
557+import time
558
559 from nova import test
560-from nova.utils import parse_mailmap, str_dict_replace
561+from nova.utils import parse_mailmap, str_dict_replace, synchronized
562
563
564 class ProjectTestCase(test.TestCase):
565@@ -55,3 +59,34 @@
566 '%r not listed in Authors' % missing)
567 finally:
568 tree.unlock()
569+
570+
571+class LockTestCase(test.TestCase):
572+ def test_synchronized(self):
573+ rpipe, wpipe = os.pipe()
574+ pid = os.fork()
575+ if pid > 0:
576+ os.close(wpipe)
577+
578+ @synchronized('testlock')
579+ def f():
580+ rfds, _, __ = select.select([rpipe], [], [], 1)
581+ self.assertEquals(len(rfds), 0, "The other process, which was"
582+ " supposed to be locked, "
583+ "wrote on its end of the "
584+ "pipe")
585+ os.close(rpipe)
586+
587+ f()
588+ else:
589+ os.close(rpipe)
590+
591+ @synchronized('testlock')
592+ def g():
593+ try:
594+ os.write(wpipe, "foo")
595+ except OSError, e:
596+ self.assertEquals(e.errno, errno.EPIPE)
597+ return
598+ g()
599+ os._exit(0)
600
601=== modified file 'nova/tests/test_network.py'
602--- nova/tests/test_network.py 2011-02-23 19:20:07 +0000
603+++ nova/tests/test_network.py 2011-02-28 22:36:03 +0000
604@@ -29,11 +29,153 @@
605 from nova import test
606 from nova import utils
607 from nova.auth import manager
608+from nova.network import linux_net
609
610 FLAGS = flags.FLAGS
611 LOG = logging.getLogger('nova.tests.network')
612
613
614+class IptablesManagerTestCase(test.TestCase):
615+ sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011',
616+ '*filter',
617+ ':INPUT ACCEPT [2223527:305688874]',
618+ ':FORWARD ACCEPT [0:0]',
619+ ':OUTPUT ACCEPT [2172501:140856656]',
620+ ':nova-compute-FORWARD - [0:0]',
621+ ':nova-compute-INPUT - [0:0]',
622+ ':nova-compute-local - [0:0]',
623+ ':nova-compute-OUTPUT - [0:0]',
624+ ':nova-filter-top - [0:0]',
625+ '-A FORWARD -j nova-filter-top ',
626+ '-A OUTPUT -j nova-filter-top ',
627+ '-A nova-filter-top -j nova-compute-local ',
628+ '-A INPUT -j nova-compute-INPUT ',
629+ '-A OUTPUT -j nova-compute-OUTPUT ',
630+ '-A FORWARD -j nova-compute-FORWARD ',
631+ '-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ',
632+ '-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ',
633+ '-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ',
634+ '-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ',
635+ '-A FORWARD -s 192.168.122.0/24 -i virbr0 -j ACCEPT ',
636+ '-A FORWARD -i virbr0 -o virbr0 -j ACCEPT ',
637+ '-A FORWARD -o virbr0 -j REJECT --reject-with '
638+ 'icmp-port-unreachable ',
639+ '-A FORWARD -i virbr0 -j REJECT --reject-with '
640+ 'icmp-port-unreachable ',
641+ 'COMMIT',
642+ '# Completed on Fri Feb 18 15:17:05 2011']
643+
644+ sample_nat = ['# Generated by iptables-save on Fri Feb 18 15:17:05 2011',
645+ '*nat',
646+ ':PREROUTING ACCEPT [3936:762355]',
647+ ':INPUT ACCEPT [2447:225266]',
648+ ':OUTPUT ACCEPT [63491:4191863]',
649+ ':POSTROUTING ACCEPT [63112:4108641]',
650+ ':nova-compute-OUTPUT - [0:0]',
651+ ':nova-compute-floating-ip-snat - [0:0]',
652+ ':nova-compute-SNATTING - [0:0]',
653+ ':nova-compute-PREROUTING - [0:0]',
654+ ':nova-compute-POSTROUTING - [0:0]',
655+ ':nova-postrouting-bottom - [0:0]',
656+ '-A PREROUTING -j nova-compute-PREROUTING ',
657+ '-A OUTPUT -j nova-compute-OUTPUT ',
658+ '-A POSTROUTING -j nova-compute-POSTROUTING ',
659+ '-A POSTROUTING -j nova-postrouting-bottom ',
660+ '-A nova-postrouting-bottom -j nova-compute-SNATTING ',
661+ '-A nova-compute-SNATTING -j nova-compute-floating-ip-snat ',
662+ 'COMMIT',
663+ '# Completed on Fri Feb 18 15:17:05 2011']
664+
665+ def setUp(self):
666+ super(IptablesManagerTestCase, self).setUp()
667+ self.manager = linux_net.IptablesManager()
668+
669+ def test_filter_rules_are_wrapped(self):
670+ current_lines = self.sample_filter
671+
672+ table = self.manager.ipv4['filter']
673+ table.add_rule('FORWARD', '-s 1.2.3.4/5 -j DROP')
674+ new_lines = self.manager._modify_rules(current_lines, table)
675+ self.assertTrue('-A run_tests.py-FORWARD '
676+ '-s 1.2.3.4/5 -j DROP' in new_lines)
677+
678+ table.remove_rule('FORWARD', '-s 1.2.3.4/5 -j DROP')
679+ new_lines = self.manager._modify_rules(current_lines, table)
680+ self.assertTrue('-A run_tests.py-FORWARD '
681+ '-s 1.2.3.4/5 -j DROP' not in new_lines)
682+
683+ def test_nat_rules(self):
684+ current_lines = self.sample_nat
685+ new_lines = self.manager._modify_rules(current_lines,
686+ self.manager.ipv4['nat'])
687+
688+ for line in [':nova-compute-OUTPUT - [0:0]',
689+ ':nova-compute-floating-ip-snat - [0:0]',
690+ ':nova-compute-SNATTING - [0:0]',
691+ ':nova-compute-PREROUTING - [0:0]',
692+ ':nova-compute-POSTROUTING - [0:0]']:
693+ self.assertTrue(line in new_lines, "One of nova-compute's chains "
694+ "went missing.")
695+
696+ seen_lines = set()
697+ for line in new_lines:
698+ line = line.strip()
699+ self.assertTrue(line not in seen_lines,
700+ "Duplicate line: %s" % line)
701+ seen_lines.add(line)
702+
703+ last_postrouting_line = ''
704+
705+ for line in new_lines:
706+ if line.startswith('-A POSTROUTING'):
707+ last_postrouting_line = line
708+
709+ self.assertTrue('-j nova-postrouting-bottom' in last_postrouting_line,
710+ "Last POSTROUTING rule does not jump to "
711+ "nova-postouting-bottom: %s" % last_postrouting_line)
712+
713+ for chain in ['POSTROUTING', 'PREROUTING', 'OUTPUT']:
714+ self.assertTrue('-A %s -j run_tests.py-%s' \
715+ % (chain, chain) in new_lines,
716+ "Built-in chain %s not wrapped" % (chain,))
717+
718+ def test_filter_rules(self):
719+ current_lines = self.sample_filter
720+ new_lines = self.manager._modify_rules(current_lines,
721+ self.manager.ipv4['filter'])
722+
723+ for line in [':nova-compute-FORWARD - [0:0]',
724+ ':nova-compute-INPUT - [0:0]',
725+ ':nova-compute-local - [0:0]',
726+ ':nova-compute-OUTPUT - [0:0]']:
727+ self.assertTrue(line in new_lines, "One of nova-compute's chains"
728+ " went missing.")
729+
730+ seen_lines = set()
731+ for line in new_lines:
732+ line = line.strip()
733+ self.assertTrue(line not in seen_lines,
734+ "Duplicate line: %s" % line)
735+ seen_lines.add(line)
736+
737+ for chain in ['FORWARD', 'OUTPUT']:
738+ for line in new_lines:
739+ if line.startswith('-A %s' % chain):
740+ self.assertTrue('-j nova-filter-top' in line,
741+ "First %s rule does not "
742+ "jump to nova-filter-top" % chain)
743+ break
744+
745+ self.assertTrue('-A nova-filter-top '
746+ '-j run_tests.py-local' in new_lines,
747+ "nova-filter-top does not jump to wrapped local chain")
748+
749+ for chain in ['INPUT', 'OUTPUT', 'FORWARD']:
750+ self.assertTrue('-A %s -j run_tests.py-%s' \
751+ % (chain, chain) in new_lines,
752+ "Built-in chain %s not wrapped" % (chain,))
753+
754+
755 class NetworkTestCase(test.TestCase):
756 """Test cases for network code"""
757 def setUp(self):
758
759=== modified file 'nova/tests/test_virt.py'
760--- nova/tests/test_virt.py 2011-02-23 19:56:37 +0000
761+++ nova/tests/test_virt.py 2011-02-28 22:36:03 +0000
762@@ -14,6 +14,7 @@
763 # License for the specific language governing permissions and limitations
764 # under the License.
765
766+import re
767 from xml.etree.ElementTree import fromstring as xml_to_tree
768 from xml.dom.minidom import parseString as xml_to_dom
769
770@@ -234,16 +235,22 @@
771 self.manager.delete_user(self.user)
772 super(IptablesFirewallTestCase, self).tearDown()
773
774- in_rules = [
775+ in_nat_rules = [
776+ '# Generated by iptables-save v1.4.10 on Sat Feb 19 00:03:19 2011',
777+ '*nat',
778+ ':PREROUTING ACCEPT [1170:189210]',
779+ ':INPUT ACCEPT [844:71028]',
780+ ':OUTPUT ACCEPT [5149:405186]',
781+ ':POSTROUTING ACCEPT [5063:386098]'
782+ ]
783+
784+ in_filter_rules = [
785 '# Generated by iptables-save v1.4.4 on Mon Dec 6 11:54:13 2010',
786 '*filter',
787 ':INPUT ACCEPT [969615:281627771]',
788 ':FORWARD ACCEPT [0:0]',
789 ':OUTPUT ACCEPT [915599:63811649]',
790 ':nova-block-ipv4 - [0:0]',
791- '-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ',
792- '-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ',
793- '-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ',
794 '-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ',
795 '-A FORWARD -d 192.168.122.0/24 -o virbr0 -m state --state RELATED'
796 ',ESTABLISHED -j ACCEPT ',
797@@ -255,7 +262,7 @@
798 '# Completed on Mon Dec 6 11:54:13 2010',
799 ]
800
801- in6_rules = [
802+ in6_filter_rules = [
803 '# Generated by ip6tables-save v1.4.4 on Tue Jan 18 23:47:56 2011',
804 '*filter',
805 ':INPUT ACCEPT [349155:75810423]',
806@@ -315,23 +322,32 @@
807 instance_ref = db.instance_get(admin_ctxt, instance_ref['id'])
808
809 # self.fw.add_instance(instance_ref)
810- def fake_iptables_execute(cmd, process_input=None):
811+ def fake_iptables_execute(cmd, process_input=None, attempts=5):
812 if cmd == 'sudo ip6tables-save -t filter':
813- return '\n'.join(self.in6_rules), None
814+ return '\n'.join(self.in6_filter_rules), None
815 if cmd == 'sudo iptables-save -t filter':
816- return '\n'.join(self.in_rules), None
817+ return '\n'.join(self.in_filter_rules), None
818+ if cmd == 'sudo iptables-save -t nat':
819+ return '\n'.join(self.in_nat_rules), None
820 if cmd == 'sudo iptables-restore':
821- self.out_rules = process_input.split('\n')
822+ lines = process_input.split('\n')
823+ if '*filter' in lines:
824+ self.out_rules = lines
825 return '', ''
826 if cmd == 'sudo ip6tables-restore':
827- self.out6_rules = process_input.split('\n')
828+ lines = process_input.split('\n')
829+ if '*filter' in lines:
830+ self.out6_rules = lines
831 return '', ''
832- self.fw.execute = fake_iptables_execute
833+
834+ from nova.network import linux_net
835+ linux_net.iptables_manager.execute = fake_iptables_execute
836
837 self.fw.prepare_instance_filter(instance_ref)
838 self.fw.apply_instance_filter(instance_ref)
839
840- in_rules = filter(lambda l: not l.startswith('#'), self.in_rules)
841+ in_rules = filter(lambda l: not l.startswith('#'),
842+ self.in_filter_rules)
843 for rule in in_rules:
844 if not 'nova' in rule:
845 self.assertTrue(rule in self.out_rules,
846@@ -354,17 +370,18 @@
847 self.assertTrue(security_group_chain,
848 "The security group chain wasn't added")
849
850- self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -j ACCEPT' % \
851- security_group_chain in self.out_rules,
852+ regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -j ACCEPT')
853+ self.assertTrue(len(filter(regex.match, self.out_rules)) > 0,
854 "ICMP acceptance rule wasn't added")
855
856- self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -m icmp --icmp-type '
857- '8 -j ACCEPT' % security_group_chain in self.out_rules,
858+ regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -m icmp '
859+ '--icmp-type 8 -j ACCEPT')
860+ self.assertTrue(len(filter(regex.match, self.out_rules)) > 0,
861 "ICMP Echo Request acceptance rule wasn't added")
862
863- self.assertTrue('-A %s -p tcp -s 192.168.10.0/24 -m multiport '
864- '--dports 80:81 -j ACCEPT' % security_group_chain \
865- in self.out_rules,
866+ regex = re.compile('-A .* -p tcp -s 192.168.10.0/24 -m multiport '
867+ '--dports 80:81 -j ACCEPT')
868+ self.assertTrue(len(filter(regex.match, self.out_rules)) > 0,
869 "TCP port 80/81 acceptance rule wasn't added")
870 db.instance_destroy(admin_ctxt, instance_ref['id'])
871
872
873=== modified file 'nova/utils.py'
874--- nova/utils.py 2011-02-23 22:50:33 +0000
875+++ nova/utils.py 2011-02-28 22:36:03 +0000
876@@ -25,6 +25,7 @@
877 import datetime
878 import inspect
879 import json
880+import lockfile
881 import os
882 import random
883 import socket
884@@ -43,11 +44,13 @@
885
886 from nova import exception
887 from nova.exception import ProcessExecutionError
888+from nova import flags
889 from nova import log as logging
890
891
892 LOG = logging.getLogger("nova.utils")
893 TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
894+FLAGS = flags.FLAGS
895
896
897 def import_class(import_str):
898@@ -128,32 +131,41 @@
899 execute("curl --fail %s -o %s" % (url, target))
900
901
902-def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
903- LOG.debug(_("Running cmd (subprocess): %s"), cmd)
904- env = os.environ.copy()
905- if addl_env:
906- env.update(addl_env)
907- obj = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
908- stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
909- result = None
910- if process_input != None:
911- result = obj.communicate(process_input)
912- else:
913- result = obj.communicate()
914- obj.stdin.close()
915- if obj.returncode:
916- LOG.debug(_("Result was %s") % obj.returncode)
917- if check_exit_code and obj.returncode != 0:
918- (stdout, stderr) = result
919- raise ProcessExecutionError(exit_code=obj.returncode,
920- stdout=stdout,
921- stderr=stderr,
922- cmd=cmd)
923- # NOTE(termie): this appears to be necessary to let the subprocess call
924- # clean something up in between calls, without it two
925- # execute calls in a row hangs the second one
926- greenthread.sleep(0)
927- return result
928+def execute(cmd, process_input=None, addl_env=None, check_exit_code=True,
929+ attempts=1):
930+ while attempts > 0:
931+ attempts -= 1
932+ try:
933+ LOG.debug(_("Running cmd (subprocess): %s"), cmd)
934+ env = os.environ.copy()
935+ if addl_env:
936+ env.update(addl_env)
937+ obj = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
938+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
939+ result = None
940+ if process_input != None:
941+ result = obj.communicate(process_input)
942+ else:
943+ result = obj.communicate()
944+ obj.stdin.close()
945+ if obj.returncode:
946+ LOG.debug(_("Result was %s") % obj.returncode)
947+ if check_exit_code and obj.returncode != 0:
948+ (stdout, stderr) = result
949+ raise ProcessExecutionError(exit_code=obj.returncode,
950+ stdout=stdout,
951+ stderr=stderr,
952+ cmd=cmd)
953+ # NOTE(termie): this appears to be necessary to let the subprocess
954+ # call clean something up in between calls, without
955+ # it two execute calls in a row hangs the second one
956+ greenthread.sleep(0)
957+ return result
958+ except ProcessExecutionError:
959+ if not attempts:
960+ raise
961+ else:
962+ greenthread.sleep(random.randint(20, 200) / 100.0)
963
964
965 def ssh_execute(ssh, cmd, process_input=None,
966@@ -491,6 +503,17 @@
967 return json.loads(s)
968
969
970+def synchronized(name):
971+ def wrap(f):
972+ def inner(*args, **kwargs):
973+ lock = lockfile.FileLock(os.path.join(FLAGS.lock_path,
974+ 'nova-%s.lock' % name))
975+ with lock:
976+ return f(*args, **kwargs)
977+ return inner
978+ return wrap
979+
980+
981 def ensure_b64_encoding(val):
982 """Safety method to ensure that values expected to be base64-encoded
983 actually are. If they are, the value is returned unchanged. Otherwise,
984
985=== modified file 'nova/virt/libvirt_conn.py'
986--- nova/virt/libvirt_conn.py 2011-01-27 23:58:22 +0000
987+++ nova/virt/libvirt_conn.py 2011-02-28 22:36:03 +0000
988@@ -44,8 +44,6 @@
989 from xml.dom import minidom
990
991
992-from eventlet import greenthread
993-from eventlet import event
994 from eventlet import tpool
995
996 import IPy
997@@ -56,7 +54,6 @@
998 from nova import flags
999 from nova import log as logging
1000 from nova import utils
1001-#from nova.api import context
1002 from nova.auth import manager
1003 from nova.compute import instance_types
1004 from nova.compute import power_state
1005@@ -1206,10 +1203,14 @@
1006
1007 class IptablesFirewallDriver(FirewallDriver):
1008 def __init__(self, execute=None, **kwargs):
1009- self.execute = execute or utils.execute
1010+ from nova.network import linux_net
1011+ self.iptables = linux_net.iptables_manager
1012 self.instances = {}
1013 self.nwfilter = NWFilterFirewall(kwargs['get_connection'])
1014
1015+ self.iptables.ipv4['filter'].add_chain('sg-fallback')
1016+ self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP')
1017+
1018 def setup_basic_filtering(self, instance):
1019 """Use NWFilter from libvirt for this."""
1020 return self.nwfilter.setup_basic_filtering(instance)
1021@@ -1218,127 +1219,98 @@
1022 """No-op. Everything is done in prepare_instance_filter"""
1023 pass
1024
1025- def remove_instance(self, instance):
1026+ def unfilter_instance(self, instance):
1027 if instance['id'] in self.instances:
1028 del self.instances[instance['id']]
1029+ self.remove_filters_for_instance(instance)
1030+ self.iptables.apply()
1031 else:
1032 LOG.info(_('Attempted to unfilter instance %s which is not '
1033 'filtered'), instance['id'])
1034
1035- def add_instance(self, instance):
1036+ def prepare_instance_filter(self, instance):
1037 self.instances[instance['id']] = instance
1038-
1039- def unfilter_instance(self, instance):
1040- self.remove_instance(instance)
1041- self.apply_ruleset()
1042-
1043- def prepare_instance_filter(self, instance):
1044- self.add_instance(instance)
1045- self.apply_ruleset()
1046-
1047- def apply_ruleset(self):
1048- current_filter, _ = self.execute('sudo iptables-save -t filter')
1049- current_lines = current_filter.split('\n')
1050- new_filter = self.modify_rules(current_lines, 4)
1051- self.execute('sudo iptables-restore',
1052- process_input='\n'.join(new_filter))
1053- if(FLAGS.use_ipv6):
1054- current_filter, _ = self.execute('sudo ip6tables-save -t filter')
1055- current_lines = current_filter.split('\n')
1056- new_filter = self.modify_rules(current_lines, 6)
1057- self.execute('sudo ip6tables-restore',
1058- process_input='\n'.join(new_filter))
1059-
1060- def modify_rules(self, current_lines, ip_version=4):
1061+ self.add_filters_for_instance(instance)
1062+ self.iptables.apply()
1063+
1064+ def add_filters_for_instance(self, instance):
1065+ chain_name = self._instance_chain_name(instance)
1066+
1067+ self.iptables.ipv4['filter'].add_chain(chain_name)
1068+ ipv4_address = self._ip_for_instance(instance)
1069+ self.iptables.ipv4['filter'].add_rule('local',
1070+ '-d %s -j $%s' %
1071+ (ipv4_address, chain_name))
1072+
1073+ if FLAGS.use_ipv6:
1074+ self.iptables.ipv6['filter'].add_chain(chain_name)
1075+ ipv6_address = self._ip_for_instance_v6(instance)
1076+ self.iptables.ipv6['filter'].add_rule('local',
1077+ '-d %s -j $%s' %
1078+ (ipv6_address,
1079+ chain_name))
1080+
1081+ ipv4_rules, ipv6_rules = self.instance_rules(instance)
1082+
1083+ for rule in ipv4_rules:
1084+ self.iptables.ipv4['filter'].add_rule(chain_name, rule)
1085+
1086+ if FLAGS.use_ipv6:
1087+ for rule in ipv6_rules:
1088+ self.iptables.ipv6['filter'].add_rule(chain_name, rule)
1089+
1090+ def remove_filters_for_instance(self, instance):
1091+ chain_name = self._instance_chain_name(instance)
1092+
1093+ self.iptables.ipv4['filter'].remove_chain(chain_name)
1094+ if FLAGS.use_ipv6:
1095+ self.iptables.ipv6['filter'].remove_chain(chain_name)
1096+
1097+ def instance_rules(self, instance):
1098 ctxt = context.get_admin_context()
1099- # Remove any trace of nova rules.
1100- new_filter = filter(lambda l: 'nova-' not in l, current_lines)
1101-
1102- seen_chains = False
1103- for rules_index in range(len(new_filter)):
1104- if not seen_chains:
1105- if new_filter[rules_index].startswith(':'):
1106- seen_chains = True
1107- elif seen_chains == 1:
1108- if not new_filter[rules_index].startswith(':'):
1109- break
1110-
1111- our_chains = [':nova-fallback - [0:0]']
1112- our_rules = ['-A nova-fallback -j DROP']
1113-
1114- our_chains += [':nova-local - [0:0]']
1115- our_rules += ['-A FORWARD -j nova-local']
1116- our_rules += ['-A OUTPUT -j nova-local']
1117-
1118- security_groups = {}
1119- # Add our chains
1120- # First, we add instance chains and rules
1121- for instance_id in self.instances:
1122- instance = self.instances[instance_id]
1123- chain_name = self._instance_chain_name(instance)
1124- if(ip_version == 4):
1125- ip_address = self._ip_for_instance(instance)
1126- elif(ip_version == 6):
1127- ip_address = self._ip_for_instance_v6(instance)
1128-
1129- our_chains += [':%s - [0:0]' % chain_name]
1130-
1131- # Jump to the per-instance chain
1132- our_rules += ['-A nova-local -d %s -j %s' % (ip_address,
1133- chain_name)]
1134-
1135- # Always drop invalid packets
1136- our_rules += ['-A %s -m state --state '
1137- 'INVALID -j DROP' % (chain_name,)]
1138-
1139- # Allow established connections
1140- our_rules += ['-A %s -m state --state '
1141- 'ESTABLISHED,RELATED -j ACCEPT' % (chain_name,)]
1142-
1143- # Jump to each security group chain in turn
1144- for security_group in \
1145- db.security_group_get_by_instance(ctxt,
1146- instance['id']):
1147- security_groups[security_group['id']] = security_group
1148-
1149- sg_chain_name = self._security_group_chain_name(
1150+
1151+ ipv4_rules = []
1152+ ipv6_rules = []
1153+
1154+ # Always drop invalid packets
1155+ ipv4_rules += ['-m state --state ' 'INVALID -j DROP']
1156+ ipv6_rules += ['-m state --state ' 'INVALID -j DROP']
1157+
1158+ # Allow established connections
1159+ ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
1160+ ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
1161+
1162+ dhcp_server = self._dhcp_server_for_instance(instance)
1163+ ipv4_rules += ['-s %s -p udp --sport 67 --dport 68 '
1164+ '-j ACCEPT' % (dhcp_server,)]
1165+
1166+ #Allow project network traffic
1167+ if FLAGS.allow_project_net_traffic:
1168+ cidr = self._project_cidr_for_instance(instance)
1169+ ipv4_rules += ['-s %s -j ACCEPT' % (cidr,)]
1170+
1171+ # We wrap these in FLAGS.use_ipv6 because they might cause
1172+ # a DB lookup. The other ones are just list operations, so
1173+ # they're not worth the clutter.
1174+ if FLAGS.use_ipv6:
1175+ # Allow RA responses
1176+ ra_server = self._ra_server_for_instance(instance)
1177+ if ra_server:
1178+ ipv6_rules += ['-s %s/128 -p icmpv6 -j ACCEPT' % (ra_server,)]
1179+
1180+ #Allow project network traffic
1181+ if FLAGS.allow_project_net_traffic:
1182+ cidrv6 = self._project_cidrv6_for_instance(instance)
1183+ ipv6_rules += ['-s %s -j ACCEPT' % (cidrv6,)]
1184+
1185+ security_groups = db.security_group_get_by_instance(ctxt,
1186+ instance['id'])
1187+
1188+ # then, security group chains and rules
1189+ for security_group in security_groups:
1190+ rules = db.security_group_rule_get_by_security_group(ctxt,
1191 security_group['id'])
1192
1193- our_rules += ['-A %s -j %s' % (chain_name, sg_chain_name)]
1194-
1195- if(ip_version == 4):
1196- # Allow DHCP responses
1197- dhcp_server = self._dhcp_server_for_instance(instance)
1198- our_rules += ['-A %s -s %s -p udp --sport 67 --dport 68 '
1199- '-j ACCEPT ' % (chain_name, dhcp_server)]
1200- #Allow project network traffic
1201- if (FLAGS.allow_project_net_traffic):
1202- cidr = self._project_cidr_for_instance(instance)
1203- our_rules += ['-A %s -s %s -j ACCEPT' % (chain_name, cidr)]
1204- elif(ip_version == 6):
1205- # Allow RA responses
1206- ra_server = self._ra_server_for_instance(instance)
1207- if ra_server:
1208- our_rules += ['-A %s -s %s -p icmpv6 -j ACCEPT' %
1209- (chain_name, ra_server + "/128")]
1210- #Allow project network traffic
1211- if (FLAGS.allow_project_net_traffic):
1212- cidrv6 = self._project_cidrv6_for_instance(instance)
1213- our_rules += ['-A %s -s %s -j ACCEPT' %
1214- (chain_name, cidrv6)]
1215-
1216- # If nothing matches, jump to the fallback chain
1217- our_rules += ['-A %s -j nova-fallback' % (chain_name,)]
1218-
1219- # then, security group chains and rules
1220- for security_group_id in security_groups:
1221- chain_name = self._security_group_chain_name(security_group_id)
1222- our_chains += [':%s - [0:0]' % chain_name]
1223-
1224- rules = \
1225- db.security_group_rule_get_by_security_group(ctxt,
1226- security_group_id)
1227-
1228 for rule in rules:
1229 logging.info('%r', rule)
1230
1231@@ -1348,14 +1320,16 @@
1232 continue
1233
1234 version = _get_ip_version(rule.cidr)
1235- if version != ip_version:
1236- continue
1237+ if version == 4:
1238+ rules = ipv4_rules
1239+ else:
1240+ rules = ipv6_rules
1241
1242 protocol = rule.protocol
1243 if version == 6 and rule.protocol == 'icmp':
1244 protocol = 'icmpv6'
1245
1246- args = ['-A', chain_name, '-p', protocol, '-s', rule.cidr]
1247+ args = ['-p', protocol, '-s', rule.cidr]
1248
1249 if rule.protocol in ['udp', 'tcp']:
1250 if rule.from_port == rule.to_port:
1251@@ -1376,32 +1350,35 @@
1252 icmp_type_arg += '/%s' % icmp_code
1253
1254 if icmp_type_arg:
1255- if(ip_version == 4):
1256+ if version == 4:
1257 args += ['-m', 'icmp', '--icmp-type',
1258 icmp_type_arg]
1259- elif(ip_version == 6):
1260+ elif version == 6:
1261 args += ['-m', 'icmp6', '--icmpv6-type',
1262 icmp_type_arg]
1263
1264 args += ['-j ACCEPT']
1265- our_rules += [' '.join(args)]
1266-
1267- new_filter[rules_index:rules_index] = our_rules
1268- new_filter[rules_index:rules_index] = our_chains
1269- logging.info('new_filter: %s', '\n'.join(new_filter))
1270- return new_filter
1271+ rules += [' '.join(args)]
1272+
1273+ ipv4_rules += ['-j $sg-fallback']
1274+ ipv6_rules += ['-j $sg-fallback']
1275+
1276+ return ipv4_rules, ipv6_rules
1277
1278 def refresh_security_group_members(self, security_group):
1279 pass
1280
1281 def refresh_security_group_rules(self, security_group):
1282- self.apply_ruleset()
1283+ for instance in self.instances.values():
1284+ self.remove_filters_for_instance(instance)
1285+ self.add_filters_for_instance(instance)
1286+ self.iptables.apply()
1287
1288 def _security_group_chain_name(self, security_group_id):
1289 return 'nova-sg-%s' % (security_group_id,)
1290
1291 def _instance_chain_name(self, instance):
1292- return 'nova-inst-%s' % (instance['id'],)
1293+ return 'inst-%s' % (instance['id'],)
1294
1295 def _ip_for_instance(self, instance):
1296 return db.instance_get_fixed_address(context.get_admin_context(),