diff options
| author | Salvatore Orlando <salvatore.orlando@eu.citrix.com> | 2011-03-13 16:36:38 +0000 |
|---|---|---|
| committer | Salvatore Orlando <salvatore.orlando@eu.citrix.com> | 2011-03-13 16:36:38 +0000 |
| commit | 7aca22f407915d95c53ee65f4aac79cd4e6ab301 (patch) | |
| tree | e2e64304245bde3f0e89b450b90cd1539683cb67 | |
| parent | 3e3da6619a946db843533fafe33d6280948632fc (diff) | |
| parent | 7ca1669603132e3afd14606dda3f95ccbce08a41 (diff) | |
Merged with trunk
Fixed testing instrastructure:
- stubbed out LoopingCall.start
- unstubbed db.instance_create
- now using fake context in spawn tests
- moved fake xenstore ops into fake driver to avoid code duplication
Fixed pep8 errors
| -rw-r--r-- | nova/api/openstack/__init__.py | 6 | ||||
| -rw-r--r-- | nova/api/openstack/zones.py | 24 | ||||
| -rw-r--r-- | nova/compute/api.py | 1 | ||||
| -rw-r--r-- | nova/flags.py | 4 | ||||
| -rw-r--r-- | nova/image/s3.py | 31 | ||||
| -rw-r--r-- | nova/network/linux_net.py | 510 | ||||
| -rw-r--r-- | nova/objectstore/image.py | 33 | ||||
| -rw-r--r-- | nova/scheduler/api.py | 49 | ||||
| -rw-r--r-- | nova/scheduler/manager.py | 10 | ||||
| -rw-r--r-- | nova/scheduler/zone_manager.py | 143 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_zones.py | 52 | ||||
| -rw-r--r-- | nova/tests/db/fakes.py | 26 | ||||
| -rw-r--r-- | nova/tests/fake_utils.py | 22 | ||||
| -rw-r--r-- | nova/tests/test_network.py | 142 | ||||
| -rw-r--r-- | nova/tests/test_virt.py | 54 | ||||
| -rw-r--r-- | nova/tests/test_xenapi.py | 69 | ||||
| -rw-r--r-- | nova/tests/test_zones.py | 172 | ||||
| -rw-r--r-- | nova/tests/xenapi/stubs.py | 16 | ||||
| -rw-r--r-- | nova/utils.py | 62 | ||||
| -rw-r--r-- | nova/virt/disk.py | 5 | ||||
| -rw-r--r-- | nova/virt/libvirt_conn.py | 227 | ||||
| -rw-r--r-- | nova/virt/xenapi/fake.py | 39 | ||||
| -rw-r--r-- | nova/virt/xenapi/vm_utils.py | 9 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 5 | ||||
| -rw-r--r-- | tools/pip-requires | 1 |
25 files changed, 1258 insertions, 454 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 197fcc619..ab9dbb780 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -77,8 +77,8 @@ class APIRouter(wsgi.Router): server_members['pause'] = 'POST' server_members['unpause'] = 'POST' - server_members["diagnostics"] = "GET" - server_members["actions"] = "GET" + server_members['diagnostics'] = 'GET' + server_members['actions'] = 'GET' server_members['suspend'] = 'POST' server_members['resume'] = 'POST' server_members['rescue'] = 'POST' @@ -87,7 +87,7 @@ class APIRouter(wsgi.Router): server_members['inject_network_info'] = 'POST' mapper.resource("zone", "zones", controller=zones.Controller(), - collection={'detail': 'GET'}) + collection={'detail': 'GET', 'info': 'GET'}), mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}, diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index cf6cd789f..8fe84275a 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -1,4 +1,4 @@ -# Copyright 2010 OpenStack LLC. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -18,6 +18,7 @@ import common from nova import flags from nova import wsgi from nova import db +from nova.scheduler import api FLAGS = flags.FLAGS @@ -32,6 +33,10 @@ def _filter_keys(item, keys): return dict((k, v) for k, v in item.iteritems() if k in keys) +def _exclude_keys(item, keys): + return dict((k, v) for k, v in item.iteritems() if k not in keys) + + def _scrub_zone(zone): return _filter_keys(zone, ('id', 'api_url')) @@ -41,19 +46,30 @@ class Controller(wsgi.Controller): _serialization_metadata = { 'application/xml': { "attributes": { - "zone": ["id", "api_url"]}}} + "zone": ["id", "api_url", "name", "capabilities"]}}} def index(self, req): """Return all zones in brief""" - items = db.zone_get_all(req.environ['nova.context']) + # Ask the ZoneManager in the Scheduler for most recent data, + # or fall-back to the database ... + items = api.API().get_zone_list(req.environ['nova.context']) + if not items: + items = db.zone_get_all(req.environ['nova.context']) + items = common.limited(items, req) - items = [_scrub_zone(item) for item in items] + items = [_exclude_keys(item, ['username', 'password']) + for item in items] return dict(zones=items) def detail(self, req): """Return all zones in detail""" return self.index(req) + def info(self, req): + """Return name and capabilities for this zone.""" + return dict(zone=dict(name=FLAGS.zone_name, + capabilities=FLAGS.zone_capabilities)) + def show(self, req, id): """Return data about the given zone id""" zone_id = int(id) diff --git a/nova/compute/api.py b/nova/compute/api.py index 301f9ee18..f5638ba0b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -165,6 +165,7 @@ class API(base.Base): 'image_id': image_id, 'kernel_id': kernel_id or '', 'ramdisk_id': ramdisk_id or '', + 'state': 0, 'state_description': 'scheduling', 'user_id': context.user_id, 'project_id': context.project_id, diff --git a/nova/flags.py b/nova/flags.py index 62cbf30ca..9123e9ac7 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -356,3 +356,7 @@ DEFINE_string('host', socket.gethostname(), DEFINE_string('node_availability_zone', 'nova', 'availability zone of this node') + +DEFINE_string('zone_name', 'nova', 'name of this zone') +DEFINE_string('zone_capabilities', 'kypervisor:xenserver;os:linux', + 'Key/Value tags which represent capabilities of this zone') diff --git a/nova/image/s3.py b/nova/image/s3.py index bbc54c263..85a2c651c 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -236,25 +236,32 @@ class S3ImageService(service.BaseImageService): @staticmethod def _decrypt_image(encrypted_filename, encrypted_key, encrypted_iv, cloud_private_key, decrypted_filename): - key, err = utils.execute( - 'openssl rsautl -decrypt -inkey %s' % cloud_private_key, - process_input=encrypted_key, - check_exit_code=False) + key, err = utils.execute('openssl', + 'rsautl', + '-decrypt', + '-inkey', '%s' % cloud_private_key, + process_input=encrypted_key, + check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt private key: %s") % err) - iv, err = utils.execute( - 'openssl rsautl -decrypt -inkey %s' % cloud_private_key, - process_input=encrypted_iv, - check_exit_code=False) + iv, err = utils.execute('openssl', + 'rsautl', + '-decrypt', + '-inkey', '%s' % cloud_private_key, + process_input=encrypted_iv, + check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt initialization " "vector: %s") % err) - _out, err = utils.execute( - 'openssl enc -d -aes-128-cbc -in %s -K %s -iv %s -out %s' - % (encrypted_filename, key, iv, decrypted_filename), - check_exit_code=False) + _out, err = utils.execute('openssl', 'enc', + '-d', '-aes-128-cbc', + '-in', '%s' % (encrypted_filename,), + '-K', '%s' % (key,), + '-iv', '%s' % (iv,), + '-out', '%s' % (decrypted_filename,), + check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt image file " "%(image_file)s: %(err)s") % diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 228a4d9ea..9f9d282b6 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -17,15 +17,17 @@ Implements vlans, bridges, and iptables rules using linux utilities. """ +import inspect import os +from eventlet import semaphore + from nova import db from nova import exception from nova import flags from nova import log as logging from nova import utils - LOG = logging.getLogger("nova.linux_net") @@ -52,8 +54,6 @@ flags.DEFINE_string('dhcpbridge', _bin_file('nova-dhcpbridge'), 'location of nova-dhcpbridge') flags.DEFINE_string('routing_source_ip', '$my_ip', 'Public IP of network host') -flags.DEFINE_bool('use_nova_chains', False, - 'use the nova_ routing chains instead of default') flags.DEFINE_string('input_chain', 'INPUT', 'chain to add nova_input to') @@ -63,79 +63,332 @@ flags.DEFINE_string('dmz_cidr', '10.128.0.0/24', 'dmz range that should be accepted') +binary_name = os.path.basename(inspect.stack()[-1][1]) + + +class IptablesRule(object): + """An iptables rule + + You shouldn't need to use this class directly, it's only used by + IptablesManager + """ + def __init__(self, chain, rule, wrap=True, top=False): + self.chain = chain + self.rule = rule + self.wrap = wrap + self.top = top + + def __eq__(self, other): + return ((self.chain == other.chain) and + (self.rule == other.rule) and + (self.top == other.top) and + (self.wrap == other.wrap)) + + def __ne__(self, other): + return not self == other + + def __str__(self): + if self.wrap: + chain = '%s-%s' % (binary_name, self.chain) + else: + chain = self.chain + return '-A %s %s' % (chain, self.rule) + + +class IptablesTable(object): + """An iptables table""" + + def __init__(self): + self.rules = [] + self.chains = set() + self.unwrapped_chains = set() + + def add_chain(self, name, wrap=True): + """Adds a named chain to the table + + The chain name is wrapped to be unique for the component creating + it, so different components of Nova can safely create identically + named chains without interfering with one another. + + At the moment, its wrapped name is <binary name>-<chain name>, + so if nova-compute creates a chain named "OUTPUT", it'll actually + end up named "nova-compute-OUTPUT". + """ + if wrap: + self.chains.add(name) + else: + self.unwrapped_chains.add(name) + + def remove_chain(self, name, wrap=True): + """Remove named chain + + This removal "cascades". All rule in the chain are removed, as are + all rules in other chains that jump to it. + + If the chain is not found, this is merely logged. + """ + if wrap: + chain_set = self.chains + else: + chain_set = self.unwrapped_chains + + if name not in chain_set: + LOG.debug(_("Attempted to remove chain %s which doesn't exist"), + name) + return + + chain_set.remove(name) + self.rules = filter(lambda r: r.chain != name, self.rules) + + if wrap: + jump_snippet = '-j %s-%s' % (binary_name, name) + else: + jump_snippet = '-j %s' % (name,) + + self.rules = filter(lambda r: jump_snippet not in r.rule, self.rules) + + def add_rule(self, chain, rule, wrap=True, top=False): + """Add a rule to the table + + This is just like what you'd feed to iptables, just without + the "-A <chain name>" bit at the start. + + However, if you need to jump to one of your wrapped chains, + prepend its name with a '$' which will ensure the wrapping + is applied correctly. + """ + if wrap and chain not in self.chains: + raise ValueError(_("Unknown chain: %r") % chain) + + if '$' in rule: + rule = ' '.join(map(self._wrap_target_chain, rule.split(' '))) + + self.rules.append(IptablesRule(chain, rule, wrap, top)) + + def _wrap_target_chain(self, s): + if s.startswith('$'): + return '%s-%s' % (binary_name, s[1:]) + return s + + def remove_rule(self, chain, rule, wrap=True, top=False): + """Remove a rule from a chain + + Note: The rule must be exactly identical to the one that was added. + You cannot switch arguments around like you can with the iptables + CLI tool. + """ + try: + self.rules.remove(IptablesRule(chain, rule, wrap, top)) + except ValueError: + LOG.debug(_("Tried to remove rule that wasn't there:" + " %(chain)r %(rule)r %(wrap)r %(top)r"), + {'chain': chain, 'rule': rule, + 'top': top, 'wrap': wrap}) + + +class IptablesManager(object): + """Wrapper for iptables + + See IptablesTable for some usage docs + + A number of chains are set up to begin with. + + First, nova-filter-top. It's added at the top of FORWARD and OUTPUT. Its + name is not wrapped, so it's shared between the various nova workers. It's + intended for rules that need to live at the top of the FORWARD and OUTPUT + chains. It's in both the ipv4 and ipv6 set of tables. + + For ipv4 and ipv6, the builtin INPUT, OUTPUT, and FORWARD filter chains are + wrapped, meaning that the "real" INPUT chain has a rule that jumps to the + wrapped INPUT chain, etc. Additionally, there's a wrapped chain named + "local" which is jumped to from nova-filter-top. + + For ipv4, the builtin PREROUTING, OUTPUT, and POSTROUTING nat chains are + wrapped in the same was as the builtin filter chains. Additionally, there's + a snat chain that is applied after the POSTROUTING chain. + """ + def __init__(self, execute=None): + if not execute: + if FLAGS.fake_network: + self.execute = lambda *args, **kwargs: ('', '') + else: + self.execute = utils.execute + else: + self.execute = execute + + self.ipv4 = {'filter': IptablesTable(), + 'nat': IptablesTable()} + self.ipv6 = {'filter': IptablesTable()} + + # Add a nova-filter-top chain. It's intended to be shared + # among the various nova components. It sits at the very top + # of FORWARD and OUTPUT. + for tables in [self.ipv4, self.ipv6]: + tables['filter'].add_chain('nova-filter-top', wrap=False) + tables['filter'].add_rule('FORWARD', '-j nova-filter-top', + wrap=False, top=True) + tables['filter'].add_rule('OUTPUT', '-j nova-filter-top', + wrap=False, top=True) + + tables['filter'].add_chain('local') + tables['filter'].add_rule('nova-filter-top', '-j $local', + wrap=False) + + # Wrap the builtin chains + builtin_chains = {4: {'filter': ['INPUT', 'OUTPUT', 'FORWARD'], + 'nat': ['PREROUTING', 'OUTPUT', 'POSTROUTING']}, + 6: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']}} + + for ip_version in builtin_chains: + if ip_version == 4: + tables = self.ipv4 + elif ip_version == 6: + tables = self.ipv6 + + for table, chains in builtin_chains[ip_version].iteritems(): + for chain in chains: + tables[table].add_chain(chain) + tables[table].add_rule(chain, '-j $%s' % (chain,), + wrap=False) + + # Add a nova-postrouting-bottom chain. It's intended to be shared + # among the various nova components. We set it as the last chain + # of POSTROUTING chain. + self.ipv4['nat'].add_chain('nova-postrouting-bottom', wrap=False) + self.ipv4['nat'].add_rule('POSTROUTING', '-j nova-postrouting-bottom', + wrap=False) + + # We add a snat chain to the shared nova-postrouting-bottom chain + # so that it's applied last. + self.ipv4['nat'].add_chain('snat') + self.ipv4['nat'].add_rule('nova-postrouting-bottom', '-j $snat', + wrap=False) + + # And then we add a floating-snat chain and jump to first thing in + # the snat chain. + self.ipv4['nat'].add_chain('floating-snat') + self.ipv4['nat'].add_rule('snat', '-j $floating-snat') + + self.semaphore = semaphore.Semaphore() + + @utils.synchronized('iptables') + def apply(self): + """Apply the current in-memory set of iptables rules + + This will blow away any rules left over from previous runs of the + same component of Nova, and replace them with our current set of + rules. This happens atomically, thanks to iptables-restore. + + We wrap the call in a semaphore lock, so that we don't race with + ourselves. In the event of a race with another component running + an iptables-* command at the same time, we retry up to 5 times. + """ + with self.semaphore: + s = [('iptables', self.ipv4)] + if FLAGS.use_ipv6: + s += [('ip6tables', self.ipv6)] + + for cmd, tables in s: + for table in tables: + current_table, _ = self.execute('sudo', + '%s-save' % (cmd,), + '-t', '%s' % (table,), + attempts=5) + current_lines = current_table.split('\n') + new_filter = self._modify_rules(current_lines, + tables[table]) + self.execute('sudo', '%s-restore' % (cmd,), + process_input='\n'.join(new_filter), + attempts=5) + + def _modify_rules(self, current_lines, table, binary=None): + unwrapped_chains = table.unwrapped_chains + chains = table.chains + rules = table.rules + + # Remove any trace of our rules + new_filter = filter(lambda line: binary_name not in line, + current_lines) + + seen_chains = False + rules_index = 0 + for rules_index, rule in enumerate(new_filter): + if not seen_chains: + if rule.startswith(':'): + seen_chains = True + else: + if not rule.startswith(':'): + break + + our_rules = [] + for rule in rules: + rule_str = str(rule) + if rule.top: + # rule.top == True means we want this rule to be at the top. + # Further down, we weed out duplicates from the bottom of the + # list, so here we remove the dupes ahead of time. + new_filter = filter(lambda s: s.strip() != rule_str.strip(), + new_filter) + our_rules += [rule_str] + + new_filter[rules_index:rules_index] = our_rules + + new_filter[rules_index:rules_index] = [':%s - [0:0]' % \ + (name,) \ + for name in unwrapped_chains] + new_filter[rules_index:rules_index] = [':%s-%s - [0:0]' % \ + (binary_name, name,) \ + for name in chains] + + seen_lines = set() + + def _weed_out_duplicates(line): + line = line.strip() + if line in seen_lines: + return False + else: + seen_lines.add(line) + return True + + # We filter duplicates, letting the *last* occurrence take + # precendence. + new_filter.reverse() + new_filter = filter(_weed_out_duplicates, new_filter) + new_filter.reverse() + return new_filter + + +iptables_manager = IptablesManager() + + def metadata_forward(): """Create forwarding rule for metadata""" - _confirm_rule("PREROUTING", '-t', 'nat', '-s', '0.0.0.0/0', - '-d', '169.254.169.254/32', '-p', 'tcp', '-m', 'tcp', - '--dport', '80', '-j', 'DNAT', - '--to-destination', - '%s:%s' % (FLAGS.ec2_dmz_host, FLAGS.ec2_port)) + iptables_manager.ipv4['nat'].add_rule("PREROUTING", + "-s 0.0.0.0/0 -d 169.254.169.254/32 " + "-p tcp -m tcp --dport 80 -j DNAT " + "--to-destination %s:%s" % \ + (FLAGS.ec2_dmz_host, FLAGS.ec2_port)) + iptables_manager.apply() def init_host(): """Basic networking setup goes here""" - - if FLAGS.use_nova_chains: - _execute('sudo', 'iptables', '-N', 'nova_input', check_exit_code=False) - _execute('sudo', 'iptables', '-D', FLAGS.input_chain, - '-j', 'nova_input', - check_exit_code=False) - _execute('sudo', 'iptables', '-A', FLAGS.input_chain, - '-j', 'nova_input') - _execute('sudo', 'iptables', '-N', 'nova_forward', - check_exit_code=False) - _execute('sudo', 'iptables', '-D', 'FORWARD', '-j', 'nova_forward', - check_exit_code=False) - _execute('sudo', 'iptables', '-A', 'FORWARD', '-j', 'nova_forward') - _execute('sudo', 'iptables', '-N', 'nova_output', - check_exit_code=False) - _execute('sudo', 'iptables', '-D', 'OUTPUT', '-j', 'nova_output', - check_exit_code=False) - _execute('sudo', 'iptables', '-A', 'OUTPUT', '-j', 'nova_output') - _execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_prerouting', - check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-D', 'PREROUTING', - '-j', 'nova_prerouting', check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-A', 'PREROUTING', - '-j', 'nova_prerouting') - _execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_postrouting', - check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-D', 'POSTROUTING', - '-j', 'nova_postrouting', check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-A', 'POSTROUTING', - '-j', 'nova_postrouting') - _execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_snatting', - check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-D', 'POSTROUTING', - '-j nova_snatting', check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-A', 'POSTROUTING', - '-j', 'nova_snatting') - _execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_output', - check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-D', 'OUTPUT', - '-j nova_output', check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-A', 'OUTPUT', - '-j', 'nova_output') - else: - # NOTE(vish): This makes it easy to ensure snatting rules always - # come after the accept rules in the postrouting chain - _execute('sudo', 'iptables', '-t', 'nat', '-N', 'SNATTING', - check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-D', 'POSTROUTING', - '-j', 'SNATTING', check_exit_code=False) - _execute('sudo', 'iptables', '-t', 'nat', '-A', 'POSTROUTING', - '-j', 'SNATTING') - # NOTE(devcamcar): Cloud public SNAT entries and the default # SNAT rule for outbound traffic. - _confirm_rule("SNATTING", '-t', 'nat', '-s', FLAGS.fixed_range, - '-j', 'SNAT', '--to-source', FLAGS.routing_source_ip, - append=True) + iptables_manager.ipv4['nat'].add_rule("snat", + "-s %s -j SNAT --to-source %s" % \ + (FLAGS.fixed_range, + FLAGS.routing_source_ip)) - _confirm_rule("POSTROUTING", '-t', 'nat', '-s', FLAGS.fixed_range, - '-d', FLAGS.dmz_cidr, '-j', 'ACCEPT') - _confirm_rule("POSTROUTING", '-t', 'nat', '-s', FLAGS.fixed_range, - '-d', FLAGS.fixed_range, '-j', 'ACCEPT') + iptables_manager.ipv4['nat'].add_rule("POSTROUTING", + "-s %s -d %s -j ACCEPT" % \ + (FLAGS.fixed_range, FLAGS.dmz_cidr)) + + iptables_manager.ipv4['nat'].add_rule("POSTROUTING", + "-s %(range)s -d %(range)s " + "-j ACCEPT" % \ + {'range': FLAGS.fixed_range}) + iptables_manager.apply() def bind_floating_ip(floating_ip, check_exit_code=True): @@ -153,31 +406,36 @@ def unbind_floating_ip(floating_ip): def ensure_vlan_forward(public_ip, port, private_ip): """Sets up forwarding rules for vlan""" - _confirm_rule("FORWARD", '-d', private_ip, '-p', 'udp', - '--dport', '1194', '-j', 'ACCEPT') - _confirm_rule("PREROUTING", '-t', 'nat', '-d', public_ip, '-p', 'udp', - '--dport', port, '-j', 'DNAT', '--to', '%s:1194' - % private_ip) + iptables_manager.ipv4['filter'].add_rule("FORWARD", + "-d %s -p udp " + "--dport 1194 " + "-j ACCEPT" % private_ip) + iptables_manager.ipv4['nat'].add_rule("PREROUTING", + "-d %s -p udp " + "--dport %s -j DNAT --to %s:1194" % + (public_ip, port, private_ip)) + iptables_manager.apply() def ensure_floating_forward(floating_ip, fixed_ip): """Ensure floating ip forwarding rule""" - _confirm_rule("PREROUTING", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT', - '--to', fixed_ip) - _confirm_rule("OUTPUT", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT', - '--to', fixed_ip) - _confirm_rule("SNATTING", '-t', 'nat', '-s', fixed_ip, '-j', 'SNAT', - '--to', floating_ip) + for chain, rule in floating_forward_rules(floating_ip, fixed_ip): + iptables_manager.ipv4['nat'].add_rule(chain, rule) + iptables_manager.apply() def remove_floating_forward(floating_ip, fixed_ip): """Remove forwarding for floating ip""" - _remove_rule("PREROUTING", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT', - '--to', fixed_ip) - _remove_rule("OUTPUT", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT', - '--to', fixed_ip) - _remove_rule("SNATTING", '-t', 'nat', '-s', fixed_ip, '-j', 'SNAT', - '--to', floating_ip) + for chain, rule in floating_forward_rules(floating_ip, fixed_ip): + iptables_manager.ipv4['nat'].remove_rule(chain, rule) + iptables_manager.apply() + + +def floating_forward_rules(floating_ip, fixed_ip): + return [("PREROUTING", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)), + ("OUTPUT", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)), + ("floating-snat", + "-s %s -j SNAT --to %s" % (fixed_ip, floating_ip))] def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None): @@ -216,7 +474,7 @@ def ensure_bridge(bridge, interface, net_attrs=None): _execute('sudo', 'brctl', 'setfd', bridge, 0) # _execute("sudo brctl setageing %s 10" % bridge) _execute('sudo', 'brctl', 'stp', bridge, 'off') - _execute('sudo', 'ip', 'link', 'set', bridge, up) + _execute('sudo', 'ip', 'link', 'set', bridge, 'up') if net_attrs: # NOTE(vish): The ip for dnsmasq has to be the first address on the # bridge for it to respond to reqests properly @@ -269,19 +527,12 @@ def ensure_bridge(bridge, interface, net_attrs=None): "enslave it to bridge %s.\n" % (interface, bridge)): raise exception.Error("Failed to add interface: %s" % err) - if FLAGS.use_nova_chains: - (out, err) = _execute('sudo', 'iptables', '-N', 'nova_forward', - check_exit_code=False) - if err != 'iptables: Chain already exists.\n': - # NOTE(vish): chain didn't exist link chain - _execute('sudo', 'iptables', '-D', 'FORWARD', '-j', 'nova_forward', - check_exit_code=False) - _execute('sudo', 'iptables', '-A', 'FORWARD', '-j', 'nova_forward') - - _confirm_rule("FORWARD", '--in-interface', bridge, '-j', 'ACCEPT') - _confirm_rule("FORWARD", '--out-interface', bridge, '-j', 'ACCEPT') - _execute('sudo', 'iptables', '-N', 'nova-local', check_exit_code=False) - _confirm_rule("FORWARD", '-j', 'nova-local') + iptables_manager.ipv4['filter'].add_rule("FORWARD", + "--in-interface %s -j ACCEPT" % \ + bridge) + iptables_manager.ipv4['filter'].add_rule("FORWARD", + "--out-interface %s -j ACCEPT" % \ + bridge) def get_dhcp_hosts(context, network_id): @@ -330,7 +581,7 @@ def update_dhcp(context, network_id): env = {'FLAGFILE': FLAGS.dhcpbridge_flagfile, 'DNSMASQ_INTERFACE': network_ref['bridge']} command = _dnsmasq_cmd(network_ref) - _execute(command, addl_env=env) + _execute(*command, addl_env=env) def update_ra(context, network_id): @@ -370,7 +621,7 @@ interface %s else: LOG.debug(_("Pid %d is stale, relaunching radvd"), pid) command = _ra_cmd(network_ref) - _execute(command) + _execute(*command) db.network_update(context, network_id, {"ra_server": utils.get_my_linklocal(network_ref['bridge'])}) @@ -401,53 +652,32 @@ def _device_exists(device): return not err -def _confirm_rule(chain, *cmd, **kwargs): - append = kwargs.get('append', False) - """Delete and re-add iptables rule""" - if FLAGS.use_nova_chains: - chain = "nova_%s" % chain.lower() - if append: - loc = "-A" - else: - loc = "-I" - _execute('sudo', 'iptables', '--delete', chain, *cmd, - check_exit_code=False) - _execute('sudo', 'iptables', loc, chain, *cmd) - - -def _remove_rule(chain, *cmd): - """Remove iptables rule""" - if FLAGS.use_nova_chains: - chain = "%s" % chain.lower() - _execute('sudo', 'iptables', '--delete', chain, *cmd) - - def _dnsmasq_cmd(net): """Builds dnsmasq command""" - cmd = ['sudo -E dnsmasq', - ' --strict-order', - ' --bind-interfaces', - ' --conf-file=', - ' --domain=%s' % FLAGS.dhcp_domain, - ' --pid-file=%s' % _dhcp_file(net['bridge'], 'pid'), - ' --listen-address=%s' % net['gateway'], - ' --except-interface=lo', - ' --dhcp-range=%s,static,120s' % net['dhcp_start'], - ' --dhcp-hostsfile=%s' % _dhcp_file(net['bridge'], 'conf'), - ' --dhcp-script=%s' % FLAGS.dhcpbridge, - ' --leasefile-ro'] + cmd = ['sudo', '-E', 'dnsmasq', + '--strict-order', + '--bind-interfaces', + '--conf-file=', + '--domain=%s' % FLAGS.dhcp_domain, + '--pid-file=%s' % _dhcp_file(net['bridge'], 'pid'), + '--listen-address=%s' % net['gateway'], + '--except-interface=lo', + '--dhcp-range=%s,static,120s' % net['dhcp_start'], + '--dhcp-hostsfile=%s' % _dhcp_file(net['bridge'], 'conf'), + '--dhcp-script=%s' % FLAGS.dhcpbridge, + '--leasefile-ro'] if FLAGS.dns_server: - cmd.append(' -h -R --server=%s' % FLAGS.dns_server) - return ''.join(cmd) + cmd += ['-h', '-R', '--server=%s' % FLAGS.dns_server] + return cmd def _ra_cmd(net): """Builds radvd command""" - cmd = ['sudo -E radvd', -# ' -u nobody', - ' -C %s' % _ra_file(net['bridge'], 'conf'), - ' -p %s' % _ra_file(net['bridge'], 'pid')] - return ''.join(cmd) + cmd = ['sudo', '-E', 'radvd', +# '-u', 'nobody', + '-C', '%s' % _ra_file(net['bridge'], 'conf'), + '-p', '%s' % _ra_file(net['bridge'], 'pid')] + return cmd def _stop_dnsmasq(network): diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index 8013cbd9c..c90b5b54b 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -253,25 +253,34 @@ class Image(object): @staticmethod def decrypt_image(encrypted_filename, encrypted_key, encrypted_iv, cloud_private_key, decrypted_filename): - key, err = utils.execute( - 'openssl rsautl -decrypt -inkey %s' % cloud_private_key, - process_input=encrypted_key, - check_exit_code=False) + key, err = utils.execute('openssl', + 'rsautl', + '-decrypt', + '-inkey', '%s' % cloud_private_key, + process_input=encrypted_key, + check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt private key: %s") % err) - iv, err = utils.execute( - 'openssl rsautl -decrypt -inkey %s' % cloud_private_key, - process_input=encrypted_iv, - check_exit_code=False) + iv, err = utils.execute('openssl', + 'rsautl', + '-decrypt', + '-inkey', '%s' % cloud_private_key, + process_input=encrypted_iv, + check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt initialization " "vector: %s") % err) - _out, err = utils.execute( - 'openssl enc -d -aes-128-cbc -in %s -K %s -iv %s -out %s' - % (encrypted_filename, key, iv, decrypted_filename), - check_exit_code=False) + _out, err = utils.execute('openssl', + 'enc', + '-d', + '-aes-128-cbc', + '-in', '%s' % (encrypted_filename,), + '-K', '%s' % (key,), + '-iv', '%s' % (iv,), + '-out', '%s' % (decrypted_filename,), + check_exit_code=False) if err: raise exception.Error(_("Failed to decrypt image file " "%(image_file)s: %(err)s") % diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py new file mode 100644 index 000000000..2405f1343 --- /dev/null +++ b/nova/scheduler/api.py @@ -0,0 +1,49 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Handles all requests relating to schedulers. +""" + +from nova import flags +from nova import log as logging +from nova import rpc + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.scheduler.api') + + +class API(object): + """API for interacting with the scheduler.""" + + def _call_scheduler(self, method, context, params=None): + """Generic handler for RPC calls to the scheduler. + + :param params: Optional dictionary of arguments to be passed to the + scheduler worker + + :retval: Result returned by scheduler worker + """ + if not params: + params = {} + queue = FLAGS.scheduler_topic + kwargs = {'method': method, 'args': params} + return rpc.call(context, queue, kwargs) + + def get_zone_list(self, context): + items = self._call_scheduler('get_zone_list', context) + for item in items: + item['api_url'] = item['api_url'].replace('\\/', '/') + return items diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index e9b47512e..c94397210 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -29,6 +29,7 @@ from nova import log as logging from nova import manager from nova import rpc from nova import utils +from nova.scheduler import zone_manager LOG = logging.getLogger('nova.scheduler.manager') FLAGS = flags.FLAGS @@ -43,12 +44,21 @@ class SchedulerManager(manager.Manager): if not scheduler_driver: scheduler_driver = FLAGS.scheduler_driver self.driver = utils.import_object(scheduler_driver) + self.zone_manager = zone_manager.ZoneManager() super(SchedulerManager, self).__init__(*args, **kwargs) def __getattr__(self, key): """Converts all method calls to use the schedule method""" return functools.partial(self._schedule, key) + def periodic_tasks(self, context=None): + """Poll child zones periodically to get status.""" + self.zone_manager.ping(context) + + def get_zone_list(self, context=None): + """Get a list of zones from the ZoneManager.""" + return self.zone_manager.get_zone_list() + def _schedule(self, method, context, topic, *args, **kwargs): """Tries to call schedule_* method on the driver to retrieve host. diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py new file mode 100644 index 000000000..edf9000cc --- /dev/null +++ b/nova/scheduler/zone_manager.py @@ -0,0 +1,143 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +ZoneManager oversees all communications with child Zones. +""" + +import novaclient +import thread +import traceback + +from datetime import datetime +from eventlet import greenpool + +from nova import db +from nova import flags +from nova import log as logging + +FLAGS = flags.FLAGS +flags.DEFINE_integer('zone_db_check_interval', 60, + 'Seconds between getting fresh zone info from db.') +flags.DEFINE_integer('zone_failures_to_offline', 3, + 'Number of consecutive errors before marking zone offline') + + +class ZoneState(object): + """Holds the state of all connected child zones.""" + def __init__(self): + self.is_active = True + self.name = None + self.capabilities = None + self.attempt = 0 + self.last_seen = datetime.min + self.last_exception = None + self.last_exception_time = None + + def update_credentials(self, zone): + """Update zone credentials from db""" + self.zone_id = zone.id + self.api_url = zone.api_url + self.username = zone.username + self.password = zone.password + + def update_metadata(self, zone_metadata): + """Update zone metadata after successful communications with + child zone.""" + self.last_seen = datetime.now() + self.attempt = 0 + self.name = zone_metadata["name"] + self.capabilities = zone_metadata["capabilities"] + self.is_active = True + + def to_dict(self): + return dict(name=self.name, capabilities=self.capabilities, + is_active=self.is_active, api_url=self.api_url, + id=self.zone_id) + + def log_error(self, exception): + """Something went wrong. Check to see if zone should be + marked as offline.""" + self.last_exception = exception + self.last_exception_time = datetime.now() + api_url = self.api_url + logging.warning(_("'%(exception)s' error talking to " + "zone %(api_url)s") % locals()) + + max_errors = FLAGS.zone_failures_to_offline + self.attempt += 1 + if self.attempt >= max_errors: + self.is_active = False + logging.error(_("No answer from zone %(api_url)s " + "after %(max_errors)d " + "attempts. Marking inactive.") % locals()) + + +def _call_novaclient(zone): + """Call novaclient. Broken out for testing purposes.""" + client = novaclient.OpenStack(zone.username, zone.password, zone.api_url) + return client.zones.info()._info + + +def _poll_zone(zone): + """Eventlet worker to poll a zone.""" + logging.debug(_("Polling zone: %s") % zone.api_url) + try: + zone.update_metadata(_call_novaclient(zone)) + except Exception, e: + zone.log_error(traceback.format_exc()) + + +class ZoneManager(object): + """Keeps the zone states updated.""" + def __init__(self): + self.last_zone_db_check = datetime.min + self.zone_states = {} + self.green_pool = greenpool.GreenPool() + + def get_zone_list(self): + """Return the list of zones we know about.""" + return [zone.to_dict() for zone in self.zone_states.values()] + + def _refresh_from_db(self, context): + """Make our zone state map match the db.""" + # Add/update existing zones ... + zones = db.zone_get_all(context) + existing = self.zone_states.keys() + db_keys = [] + for zone in zones: + db_keys.append(zone.id) + if zone.id not in existing: + self.zone_states[zone.id] = ZoneState() + self.zone_states[zone.id].update_credentials(zone) + + # Cleanup zones removed from db ... + keys = self.zone_states.keys() # since we're deleting + for zone_id in keys: + if zone_id not in db_keys: + del self.zone_states[zone_id] + + def _poll_zones(self, context): + """Try to connect to each child zone and get update.""" + self.green_pool.imap(_poll_zone, self.zone_states.values()) + + def ping(self, context=None): + """Ping should be called periodically to update zone status.""" + diff = datetime.now() - self.last_zone_db_check + if diff.seconds >= FLAGS.zone_db_check_interval: + logging.debug(_("Updating zone cache from db.")) + self.last_zone_db_check = datetime.now() + self._refresh_from_db(context) + self._poll_zones(context) diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py index d0da8eaaf..9ea72020e 100644 --- a/nova/tests/api/openstack/test_zones.py +++ b/nova/tests/api/openstack/test_zones.py @@ -1,4 +1,4 @@ -# Copyright 2010 OpenStack LLC. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -24,6 +24,7 @@ from nova import flags from nova import test from nova.api.openstack import zones from nova.tests.api.openstack import fakes +from nova.scheduler import api FLAGS = flags.FLAGS @@ -31,7 +32,7 @@ FLAGS.verbose = True def zone_get(context, zone_id): - return dict(id=1, api_url='http://foo.com', username='bob', + return dict(id=1, api_url='http://example.com', username='bob', password='xxx') @@ -42,7 +43,7 @@ def zone_create(context, values): def zone_update(context, zone_id, values): - zone = dict(id=zone_id, api_url='http://foo.com', username='bob', + zone = dict(id=zone_id, api_url='http://example.com', username='bob', password='xxx') zone.update(values) return zone @@ -52,12 +53,26 @@ def zone_delete(context, zone_id): pass -def zone_get_all(context): +def zone_get_all_scheduler(*args): return [ - dict(id=1, api_url='http://foo.com', username='bob', + dict(id=1, api_url='http://example.com', username='bob', password='xxx'), - dict(id=2, api_url='http://blah.com', username='alice', - password='qwerty')] + dict(id=2, api_url='http://example.org', username='alice', + password='qwerty'), + ] + + +def zone_get_all_scheduler_empty(*args): + return [] + + +def zone_get_all_db(context): + return [ + dict(id=1, api_url='http://example.com', username='bob', + password='xxx'), + dict(id=2, api_url='http://example.org', username='alice', + password='qwerty'), + ] class ZonesTest(test.TestCase): @@ -74,7 +89,6 @@ class ZonesTest(test.TestCase): FLAGS.allow_admin_api = True self.stubs.Set(nova.db, 'zone_get', zone_get) - self.stubs.Set(nova.db, 'zone_get_all', zone_get_all) self.stubs.Set(nova.db, 'zone_update', zone_update) self.stubs.Set(nova.db, 'zone_create', zone_create) self.stubs.Set(nova.db, 'zone_delete', zone_delete) @@ -84,7 +98,19 @@ class ZonesTest(test.TestCase): FLAGS.allow_admin_api = self.allow_admin super(ZonesTest, self).tearDown() - def test_get_zone_list(self): + def test_get_zone_list_scheduler(self): + self.stubs.Set(api.API, '_call_scheduler', zone_get_all_scheduler) + req = webob.Request.blank('/v1.0/zones') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(len(res_dict['zones']), 2) + + def test_get_zone_list_db(self): + self.stubs.Set(api.API, '_call_scheduler', + zone_get_all_scheduler_empty) + self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db) req = webob.Request.blank('/v1.0/zones') req.headers["Content-Type"] = "application/json" res = req.get_response(fakes.wsgi_app()) @@ -101,7 +127,7 @@ class ZonesTest(test.TestCase): self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) self.assertEqual(res_dict['zone']['id'], 1) - self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com') + self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') self.assertFalse('password' in res_dict['zone']) def test_zone_delete(self): @@ -112,7 +138,7 @@ class ZonesTest(test.TestCase): self.assertEqual(res.status_int, 200) def test_zone_create(self): - body = dict(zone=dict(api_url='http://blah.zoo', username='fred', + body = dict(zone=dict(api_url='http://example.com', username='fred', password='fubar')) req = webob.Request.blank('/v1.0/zones') req.headers["Content-Type"] = "application/json" @@ -124,7 +150,7 @@ class ZonesTest(test.TestCase): self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) self.assertEqual(res_dict['zone']['id'], 1) - self.assertEqual(res_dict['zone']['api_url'], 'http://blah.zoo') + self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') self.assertFalse('username' in res_dict['zone']) def test_zone_update(self): @@ -139,5 +165,5 @@ class ZonesTest(test.TestCase): self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) self.assertEqual(res_dict['zone']['id'], 1) - self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com') + self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') self.assertFalse('username' in res_dict['zone']) diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py index f7fc1d245..1527b4ed2 100644 --- a/nova/tests/db/fakes.py +++ b/nova/tests/db/fakes.py @@ -74,31 +74,6 @@ def stub_out_db_instance_api(stubs, injected=True): def fake_instance_type_get_by_name(context, name): return INSTANCE_TYPES[name] - def fake_instance_create(values): - """ Stubs out the db.instance_create method """ - - type_data = INSTANCE_TYPES[values['instance_type']] - - base_options = { - 'name': values['name'], - 'id': values['id'], - 'reservation_id': utils.generate_uid('r'), - 'image_id': values['image_id'], - 'kernel_id': values['kernel_id'], - 'ramdisk_id': values['ramdisk_id'], - 'state_description': 'scheduling', - 'user_id': values['user_id'], - 'project_id': values['project_id'], - 'launch_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), - 'instance_type': values['instance_type'], - 'memory_mb': type_data['memory_mb'], - 'mac_address': values['mac_address'], - 'vcpus': type_data['vcpus'], - 'local_gb': type_data['local_gb'], - 'key_data': '', - } - return FakeModel(base_options) - def fake_network_get_by_instance(context, instance_id): return FakeModel(network_fields) @@ -115,7 +90,6 @@ def stub_out_db_instance_api(stubs, injected=True): l.append(FakeModel(fixed_ip_fields)) return l - stubs.Set(db, 'instance_create', fake_instance_create) stubs.Set(db, 'network_get_by_instance', fake_network_get_by_instance) stubs.Set(db, 'instance_type_get_all', fake_instance_type_get_all) stubs.Set(db, 'instance_type_get_by_name', fake_instance_type_get_by_name) diff --git a/nova/tests/fake_utils.py b/nova/tests/fake_utils.py index f51d31e0c..8982f50be 100644 --- a/nova/tests/fake_utils.py +++ b/nova/tests/fake_utils.py @@ -48,26 +48,32 @@ def fake_execute_set_repliers(repliers): _fake_execute_repliers = repliers -def fake_execute_default_reply_handler(*ignore_args): +def fake_execute_default_reply_handler(*ignore_args, **ignore_kwargs): """A reply handler for commands that haven't been added to the reply list. Returns empty strings for stdout and stderr """ return '', '' -def fake_execute(cmd, process_input=None, addl_env=None, check_exit_code=True): +def fake_execute(*cmd, **kwargs): """This function stubs out execute, optionally executing a preconfigued function to return expected data """ global _fake_execute_repliers - LOG.debug(_("Faking execution of cmd (subprocess): %s"), cmd) - _fake_execute_log.append(cmd) + process_input = kwargs.get('process_input', None) + addl_env = kwargs.get('addl_env', None) + check_exit_code = kwargs.get('check_exit_code', 0) + cmd_map = map(str, cmd) + cmd_str = ' '.join(cmd_map) + + LOG.debug(_("Faking execution of cmd (subprocess): %s"), cmd_str) + _fake_execute_log.append(cmd_str) reply_handler = fake_execute_default_reply_handler for fake_replier in _fake_execute_repliers: - if re.match(fake_replier[0], cmd): + if re.match(fake_replier[0], cmd_str): reply_handler = fake_replier[1] LOG.debug(_('Faked command matched %s') % fake_replier[0]) break @@ -78,8 +84,10 @@ def fake_execute(cmd, process_input=None, addl_env=None, check_exit_code=True): else: try: # Alternative is a function, so call it - reply = reply_handler(cmd, process_input, addl_env, - check_exit_code) + reply = reply_handler(cmd, + process_input=process_input, + addl_env=addl_env, + check_exit_code=check_exit_code) except exception.ProcessExecutionError as e: LOG.debug(_('Faked command raised an exception %s' % str(e))) raise diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 19099ff4c..53e35ce7e 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -29,11 +29,153 @@ from nova import log as logging from nova import test from nova import utils from nova.auth import manager +from nova.network import linux_net FLAGS = flags.FLAGS LOG = logging.getLogger('nova.tests.network') +class IptablesManagerTestCase(test.TestCase): + sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011', + '*filter', + ':INPUT ACCEPT [2223527:305688874]', + ':FORWARD ACCEPT [0:0]', + ':OUTPUT ACCEPT [2172501:140856656]', + ':nova-compute-FORWARD - [0:0]', + ':nova-compute-INPUT - [0:0]', + ':nova-compute-local - [0:0]', + ':nova-compute-OUTPUT - [0:0]', + ':nova-filter-top - [0:0]', + '-A FORWARD -j nova-filter-top ', + '-A OUTPUT -j nova-filter-top ', + '-A nova-filter-top -j nova-compute-local ', + '-A INPUT -j nova-compute-INPUT ', + '-A OUTPUT -j nova-compute-OUTPUT ', + '-A FORWARD -j nova-compute-FORWARD ', + '-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ', + '-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ', + '-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ', + '-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ', + '-A FORWARD -s 192.168.122.0/24 -i virbr0 -j ACCEPT ', + '-A FORWARD -i virbr0 -o virbr0 -j ACCEPT ', + '-A FORWARD -o virbr0 -j REJECT --reject-with ' + 'icmp-port-unreachable ', + '-A FORWARD -i virbr0 -j REJECT --reject-with ' + 'icmp-port-unreachable ', + 'COMMIT', + '# Completed on Fri Feb 18 15:17:05 2011'] + + sample_nat = ['# Generated by iptables-save on Fri Feb 18 15:17:05 2011', + '*nat', + ':PREROUTING ACCEPT [3936:762355]', + ':INPUT ACCEPT [2447:225266]', + ':OUTPUT ACCEPT [63491:4191863]', + ':POSTROUTING ACCEPT [63112:4108641]', + ':nova-compute-OUTPUT - [0:0]', + ':nova-compute-floating-ip-snat - [0:0]', + ':nova-compute-SNATTING - [0:0]', + ':nova-compute-PREROUTING - [0:0]', + ':nova-compute-POSTROUTING - [0:0]', + ':nova-postrouting-bottom - [0:0]', + '-A PREROUTING -j nova-compute-PREROUTING ', + '-A OUTPUT -j nova-compute-OUTPUT ', + '-A POSTROUTING -j nova-compute-POSTROUTING ', + '-A POSTROUTING -j nova-postrouting-bottom ', + '-A nova-postrouting-bottom -j nova-compute-SNATTING ', + '-A nova-compute-SNATTING -j nova-compute-floating-ip-snat ', + 'COMMIT', + '# Completed on Fri Feb 18 15:17:05 2011'] + + def setUp(self): + super(IptablesManagerTestCase, self).setUp() + self.manager = linux_net.IptablesManager() + + def test_filter_rules_are_wrapped(self): + current_lines = self.sample_filter + + table = self.manager.ipv4['filter'] + table.add_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') + new_lines = self.manager._modify_rules(current_lines, table) + self.assertTrue('-A run_tests.py-FORWARD ' + '-s 1.2.3.4/5 -j DROP' in new_lines) + + table.remove_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') + new_lines = self.manager._modify_rules(current_lines, table) + self.assertTrue('-A run_tests.py-FORWARD ' + '-s 1.2.3.4/5 -j DROP' not in new_lines) + + def test_nat_rules(self): + current_lines = self.sample_nat + new_lines = self.manager._modify_rules(current_lines, + self.manager.ipv4['nat']) + + for line in [':nova-compute-OUTPUT - [0:0]', + ':nova-compute-floating-ip-snat - [0:0]', + ':nova-compute-SNATTING - [0:0]', + ':nova-compute-PREROUTING - [0:0]', + ':nova-compute-POSTROUTING - [0:0]']: + self.assertTrue(line in new_lines, "One of nova-compute's chains " + "went missing.") + + seen_lines = set() + for line in new_lines: + line = line.strip() + self.assertTrue(line not in seen_lines, + "Duplicate line: %s" % line) + seen_lines.add(line) + + last_postrouting_line = '' + + for line in new_lines: + if line.startswith('-A POSTROUTING'): + last_postrouting_line = line + + self.assertTrue('-j nova-postrouting-bottom' in last_postrouting_line, + "Last POSTROUTING rule does not jump to " + "nova-postouting-bottom: %s" % last_postrouting_line) + + for chain in ['POSTROUTING', 'PREROUTING', 'OUTPUT']: + self.assertTrue('-A %s -j run_tests.py-%s' \ + % (chain, chain) in new_lines, + "Built-in chain %s not wrapped" % (chain,)) + + def test_filter_rules(self): + current_lines = self.sample_filter + new_lines = self.manager._modify_rules(current_lines, + self.manager.ipv4['filter']) + + for line in [':nova-compute-FORWARD - [0:0]', + ':nova-compute-INPUT - [0:0]', + ':nova-compute-local - [0:0]', + ':nova-compute-OUTPUT - [0:0]']: + self.assertTrue(line in new_lines, "One of nova-compute's chains" + " went missing.") + + seen_lines = set() + for line in new_lines: + line = line.strip() + self.assertTrue(line not in seen_lines, + "Duplicate line: %s" % line) + seen_lines.add(line) + + for chain in ['FORWARD', 'OUTPUT']: + for line in new_lines: + if line.startswith('-A %s' % chain): + self.assertTrue('-j nova-filter-top' in line, + "First %s rule does not " + "jump to nova-filter-top" % chain) + break + + self.assertTrue('-A nova-filter-top ' + '-j run_tests.py-local' in new_lines, + "nova-filter-top does not jump to wrapped local chain") + + for chain in ['INPUT', 'OUTPUT', 'FORWARD']: + self.assertTrue('-A %s -j run_tests.py-%s' \ + % (chain, chain) in new_lines, + "Built-in chain %s not wrapped" % (chain,)) + + class NetworkTestCase(test.TestCase): """Test cases for network code""" def setUp(self): diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 0625296a4..27893043a 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import re import os import eventlet @@ -301,16 +302,22 @@ class IptablesFirewallTestCase(test.TestCase): self.manager.delete_user(self.user) super(IptablesFirewallTestCase, self).tearDown() - in_rules = [ + in_nat_rules = [ + '# Generated by iptables-save v1.4.10 on Sat Feb 19 00:03:19 2011', + '*nat', + ':PREROUTING ACCEPT [1170:189210]', + ':INPUT ACCEPT [844:71028]', + ':OUTPUT ACCEPT [5149:405186]', + ':POSTROUTING ACCEPT [5063:386098]', + ] + + in_filter_rules = [ '# Generated by iptables-save v1.4.4 on Mon Dec 6 11:54:13 2010', '*filter', ':INPUT ACCEPT [969615:281627771]', ':FORWARD ACCEPT [0:0]', ':OUTPUT ACCEPT [915599:63811649]', ':nova-block-ipv4 - [0:0]', - '-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ', - '-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ', - '-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ', '-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ', '-A FORWARD -d 192.168.122.0/24 -o virbr0 -m state --state RELATED' ',ESTABLISHED -j ACCEPT ', @@ -322,7 +329,7 @@ class IptablesFirewallTestCase(test.TestCase): '# Completed on Mon Dec 6 11:54:13 2010', ] - in6_rules = [ + in6_filter_rules = [ '# Generated by ip6tables-save v1.4.4 on Tue Jan 18 23:47:56 2011', '*filter', ':INPUT ACCEPT [349155:75810423]', @@ -385,21 +392,31 @@ class IptablesFirewallTestCase(test.TestCase): def fake_iptables_execute(*cmd, **kwargs): process_input = kwargs.get('process_input', None) if cmd == ('sudo', 'ip6tables-save', '-t', 'filter'): - return '\n'.join(self.in6_rules), None + return '\n'.join(self.in6_filter_rules), None if cmd == ('sudo', 'iptables-save', '-t', 'filter'): - return '\n'.join(self.in_rules), None + return '\n'.join(self.in_filter_rules), None + if cmd == ('sudo', 'iptables-save', '-t', 'nat'): + return '\n'.join(self.in_nat_rules), None if cmd == ('sudo', 'iptables-restore'): - self.out_rules = process_input.split('\n') + lines = process_input.split('\n') + if '*filter' in lines: + self.out_rules = lines return '', '' if cmd == ('sudo', 'ip6tables-restore'): - self.out6_rules = process_input.split('\n') + lines = process_input.split('\n') + if '*filter' in lines: + self.out6_rules = lines return '', '' - self.fw.execute = fake_iptables_execute + print cmd, kwargs + + from nova.network import linux_net + linux_net.iptables_manager.execute = fake_iptables_execute self.fw.prepare_instance_filter(instance_ref) self.fw.apply_instance_filter(instance_ref) - in_rules = filter(lambda l: not l.startswith('#'), self.in_rules) + in_rules = filter(lambda l: not l.startswith('#'), + self.in_filter_rules) for rule in in_rules: if not 'nova' in rule: self.assertTrue(rule in self.out_rules, @@ -422,17 +439,18 @@ class IptablesFirewallTestCase(test.TestCase): self.assertTrue(security_group_chain, "The security group chain wasn't added") - self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -j ACCEPT' % \ - security_group_chain in self.out_rules, + regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -j ACCEPT') + self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, "ICMP acceptance rule wasn't added") - self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -m icmp --icmp-type ' - '8 -j ACCEPT' % security_group_chain in self.out_rules, + regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -m icmp ' + '--icmp-type 8 -j ACCEPT') + self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, "ICMP Echo Request acceptance rule wasn't added") - self.assertTrue('-A %s -p tcp -s 192.168.10.0/24 -m multiport ' - '--dports 80:81 -j ACCEPT' % security_group_chain \ - in self.out_rules, + regex = re.compile('-A .* -p tcp -s 192.168.10.0/24 -m multiport ' + '--dports 80:81 -j ACCEPT') + self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, "TCP port 80/81 acceptance rule wasn't added") db.instance_destroy(admin_ctxt, instance_ref['id']) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 083c0b202..ffd964c4d 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -55,6 +55,7 @@ class XenAPIVolumeTestCase(test.TestCase): def setUp(self): super(XenAPIVolumeTestCase, self).setUp() self.stubs = stubout.StubOutForTesting() + self.context = context.RequestContext('fake', 'fake', False) FLAGS.target_host = '127.0.0.1' FLAGS.xenapi_connection_url = 'test_url' FLAGS.xenapi_connection_password = 'test_pass' @@ -62,7 +63,7 @@ class XenAPIVolumeTestCase(test.TestCase): #db_fakes.stub_out_db_network_api(self.stubs) stubs.stub_out_get_target(self.stubs) xenapi_fake.reset() - self.values = {'name': 1, 'id': 1, + self.values = {'id': 1, 'project_id': 'fake', 'user_id': 'fake', 'image_id': 1, @@ -82,7 +83,7 @@ class XenAPIVolumeTestCase(test.TestCase): vol['availability_zone'] = FLAGS.storage_availability_zone vol['status'] = "creating" vol['attach_status'] = "detached" - return db.volume_create(context.get_admin_context(), vol) + return db.volume_create(self.context, vol) def test_create_iscsi_storage(self): """ This shows how to test helper classes' methods """ @@ -118,7 +119,7 @@ class XenAPIVolumeTestCase(test.TestCase): stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests) conn = xenapi_conn.get_connection(False) volume = self._create_volume() - instance = db.instance_create(self.values) + instance = db.instance_create(self.context, self.values) vm = xenapi_fake.create_vm(instance.name, 'Running') result = conn.attach_volume(instance.name, volume['id'], '/dev/sdc') @@ -138,7 +139,7 @@ class XenAPIVolumeTestCase(test.TestCase): stubs.FakeSessionForVolumeFailedTests) conn = xenapi_conn.get_connection(False) volume = self._create_volume() - instance = db.instance_create(self.values) + instance = db.instance_create(self.context, self.values) xenapi_fake.create_vm(instance.name, 'Running') self.assertRaises(Exception, conn.attach_volume, @@ -167,12 +168,12 @@ class XenAPIVMTestCase(test.TestCase): self.project = self.manager.create_project('fake', 'fake', 'fake') self.network = utils.import_object(FLAGS.network_manager) self.stubs = stubout.StubOutForTesting() - FLAGS.xenapi_connection_url = 'test_url' - FLAGS.xenapi_connection_password = 'test_pass' + self.flags(xenapi_connection_url='test_url', + xenapi_connection_password='test_pass', + instance_name_template='%d') xenapi_fake.reset() xenapi_fake.create_local_srs() db_fakes.stub_out_db_instance_api(self.stubs) - #db_fakes.stub_out_db_network_api(self.stubs) xenapi_fake.create_network('fake', FLAGS.flat_network_bridge) stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests) stubs.stubout_get_this_vm_uuid(self.stubs) @@ -182,6 +183,7 @@ class XenAPIVMTestCase(test.TestCase): glance_stubs.stubout_glance_client(self.stubs, glance_stubs.FakeGlance) fake_utils.stub_out_utils_execute(self.stubs) + self.context = context.RequestContext('fake', 'fake', False) self.conn = xenapi_conn.get_connection(False) def test_list_instances_0(self): @@ -235,8 +237,7 @@ class XenAPIVMTestCase(test.TestCase): # Get Nova record for VM vm_info = conn.get_info('1') - - # Get XenAPI record for VM + # Get XenAPI record for VM vms = [(ref, rec) for ref, rec in xenapi_fake.get_all_records('VM').iteritems() if not rec['is_control_domain']] @@ -262,7 +263,8 @@ class XenAPIVMTestCase(test.TestCase): self.assertEquals(vm['power_state'], 'Running') if check_injection: - xenstore_data = xenapi_fake.VM_get_xenstore_data(vm_ref) + session = xenapi_conn.XenAPISession('fake', 'fake', 'fake') + xenstore_data = session.call_xenapi('VM.get_xenstore_data', vm_ref) key = 'vm-data/networking/aabbccddeeff' xenstore_value = xenstore_data[key] tcpip_data = ast.literal_eval(xenstore_value) @@ -282,10 +284,8 @@ class XenAPIVMTestCase(test.TestCase): def _test_spawn(self, image_id, kernel_id, ramdisk_id, instance_type="m1.large", check_injection=False): - - stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests) - values = {'name': "1", - 'id': "1", + stubs.stubout_loopingcall_start(self.stubs) + values = {'id': 1, 'project_id': self.project.id, 'user_id': self.user.id, 'image_id': image_id, @@ -294,10 +294,9 @@ class XenAPIVMTestCase(test.TestCase): 'instance_type': instance_type, 'mac_address': 'aa:bb:cc:dd:ee:ff', } - conn = xenapi_conn.get_connection(False) - instance = db.instance_create(values) - conn.spawn(instance) - self.check_vm_record(conn, check_injection=check_injection) + instance = db.instance_create(self.context, values) + self.conn.spawn(instance) + self.check_vm_record(self.conn, check_injection=check_injection) def test_spawn_not_enough_memory(self): FLAGS.xenapi_image_service = 'glance' @@ -333,14 +332,12 @@ class XenAPIVMTestCase(test.TestCase): self._tee_executed = False - def _tee_handler(cmd, input, *ignore_args): + def _tee_handler(cmd, **kwargs): + input = kwargs.get('process_input', None) self.assertNotEqual(input, None) - config = [line.strip() for line in input.split("\n")] - # Find the start of eth0 configuration and check it index = config.index('auto eth0') - self.assertEquals(config[index + 1:index + 8], [ 'iface eth0 inet static', 'address 10.0.0.3', @@ -349,9 +346,7 @@ class XenAPIVMTestCase(test.TestCase): 'gateway 10.0.0.1', 'dns-nameservers 10.0.0.2', '']) - self._tee_executed = True - return '', '' fake_utils.fake_execute_set_repliers([ @@ -371,12 +366,14 @@ class XenAPIVMTestCase(test.TestCase): self._tee_executed = False - def _mount_handler(cmd, *ignore_args): + def _mount_handler(cmd, *ignore_args, **ignore_kwargs): # When mounting, create real files under the mountpoint to simulate # files in the mounted filesystem # RegExp extracts the path of the mountpoint - match = re.match(r'(sudo\s+)?mount[^"]*"[^"]*"\s+"([^"]*)"', cmd) + cmd_str = ' '.join(cmd) + match = re.match(r'(sudo\s+)?mount[^"]*"[^"]*"\s+"([^"]*)"', + cmd_str) self._tmpdir = match.group(2) LOG.debug(_('Creating files in %s to simulate guest agent' % self._tmpdir)) @@ -386,7 +383,7 @@ class XenAPIVMTestCase(test.TestCase): 'xe-update-networking'), 'w').close() return '', '' - def _umount_handler(cmd, *ignore_args): + def _umount_handler(cmd, *ignore_args, **ignore_kwargs): # Umount would normall make files in the m,ounted filesystem # disappear, so do that here LOG.debug(_('Removing simulated guest agent files in %s' % @@ -397,7 +394,7 @@ class XenAPIVMTestCase(test.TestCase): os.rmdir(os.path.join(self._tmpdir, 'usr')) return '', '' - def _tee_handler(cmd, input, *ignore_args): + def _tee_handler(cmd, *ignore_args, **ignore_kwargs): self._tee_executed = True return '', '' @@ -419,9 +416,9 @@ class XenAPIVMTestCase(test.TestCase): def _create_instance(self): """Creates and spawns a test instance""" + stubs.stubout_loopingcall_start(self.stubs) values = { - 'name': '1', - 'id': '1', + 'id': 1, 'project_id': self.project.id, 'user_id': self.user.id, 'image_id': 1, @@ -429,7 +426,7 @@ class XenAPIVMTestCase(test.TestCase): 'ramdisk_id': 3, 'instance_type': 'm1.large', 'mac_address': 'aa:bb:cc:dd:ee:ff'} - instance = db.instance_create(values) + instance = db.instance_create(self.context, values) self.conn.spawn(instance) return instance @@ -474,11 +471,13 @@ class XenAPIMigrateInstance(test.TestCase): db_fakes.stub_out_db_instance_api(self.stubs) stubs.stub_out_get_target(self.stubs) xenapi_fake.reset() + xenapi_fake.create_network('fake', FLAGS.flat_network_bridge) self.manager = manager.AuthManager() self.user = self.manager.create_user('fake', 'fake', 'fake', admin=True) self.project = self.manager.create_project('fake', 'fake', 'fake') - self.values = {'name': 1, 'id': 1, + self.context = context.RequestContext('fake', 'fake', False) + self.values = {'id': 1, 'project_id': self.project.id, 'user_id': self.user.id, 'image_id': 1, @@ -488,6 +487,7 @@ class XenAPIMigrateInstance(test.TestCase): 'mac_address': 'aa:bb:cc:dd:ee:ff', } stubs.stub_out_migration_methods(self.stubs) + stubs.stubout_get_this_vm_uuid(self.stubs) glance_stubs.stubout_glance_client(self.stubs, glance_stubs.FakeGlance) @@ -498,14 +498,15 @@ class XenAPIMigrateInstance(test.TestCase): self.stubs.UnsetAll() def test_migrate_disk_and_power_off(self): - instance = db.instance_create(self.values) + instance = db.instance_create(self.context, self.values) stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) conn = xenapi_conn.get_connection(False) conn.migrate_disk_and_power_off(instance, '127.0.0.1') def test_finish_resize(self): - instance = db.instance_create(self.values) + instance = db.instance_create(self.context, self.values) stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) + stubs.stubout_loopingcall_start(self.stubs) conn = xenapi_conn.get_connection(False) conn.finish_resize(instance, dict(base_copy='hurr', cow='durr')) diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py new file mode 100644 index 000000000..5a52a0506 --- /dev/null +++ b/nova/tests/test_zones.py @@ -0,0 +1,172 @@ +# Copyright 2010 United States Government as represented by the +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Tests For ZoneManager +""" + +import datetime +import mox +import novaclient + +from nova import context +from nova import db +from nova import flags +from nova import service +from nova import test +from nova import rpc +from nova import utils +from nova.auth import manager as auth_manager +from nova.scheduler import zone_manager + +FLAGS = flags.FLAGS + + +class FakeZone: + """Represents a fake zone from the db""" + def __init__(self, *args, **kwargs): + for k, v in kwargs.iteritems(): + setattr(self, k, v) + + +def exploding_novaclient(zone): + """Used when we want to simulate a novaclient call failing.""" + raise Exception("kaboom") + + +class ZoneManagerTestCase(test.TestCase): + """Test case for zone manager""" + def test_ping(self): + zm = zone_manager.ZoneManager() + self.mox.StubOutWithMock(zm, '_refresh_from_db') + self.mox.StubOutWithMock(zm, '_poll_zones') + zm._refresh_from_db(mox.IgnoreArg()) + zm._poll_zones(mox.IgnoreArg()) + + self.mox.ReplayAll() + zm.ping(None) + self.mox.VerifyAll() + + def test_refresh_from_db_new(self): + zm = zone_manager.ZoneManager() + + self.mox.StubOutWithMock(db, 'zone_get_all') + db.zone_get_all(mox.IgnoreArg()).AndReturn([ + FakeZone(id=1, api_url='http://foo.com', username='user1', + password='pass1'), + ]) + + self.assertEquals(len(zm.zone_states), 0) + + self.mox.ReplayAll() + zm._refresh_from_db(None) + self.mox.VerifyAll() + + self.assertEquals(len(zm.zone_states), 1) + self.assertEquals(zm.zone_states[1].username, 'user1') + + def test_refresh_from_db_replace_existing(self): + zm = zone_manager.ZoneManager() + zone_state = zone_manager.ZoneState() + zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com', + username='user1', password='pass1')) + zm.zone_states[1] = zone_state + + self.mox.StubOutWithMock(db, 'zone_get_all') + db.zone_get_all(mox.IgnoreArg()).AndReturn([ + FakeZone(id=1, api_url='http://foo.com', username='user2', + password='pass2'), + ]) + + self.assertEquals(len(zm.zone_states), 1) + + self.mox.ReplayAll() + zm._refresh_from_db(None) + self.mox.VerifyAll() + + self.assertEquals(len(zm.zone_states), 1) + self.assertEquals(zm.zone_states[1].username, 'user2') + + def test_refresh_from_db_missing(self): + zm = zone_manager.ZoneManager() + zone_state = zone_manager.ZoneState() + zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com', + username='user1', password='pass1')) + zm.zone_states[1] = zone_state + + self.mox.StubOutWithMock(db, 'zone_get_all') + db.zone_get_all(mox.IgnoreArg()).AndReturn([]) + + self.assertEquals(len(zm.zone_states), 1) + + self.mox.ReplayAll() + zm._refresh_from_db(None) + self.mox.VerifyAll() + + self.assertEquals(len(zm.zone_states), 0) + + def test_refresh_from_db_add_and_delete(self): + zm = zone_manager.ZoneManager() + zone_state = zone_manager.ZoneState() + zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com', + username='user1', password='pass1')) + zm.zone_states[1] = zone_state + + self.mox.StubOutWithMock(db, 'zone_get_all') + + db.zone_get_all(mox.IgnoreArg()).AndReturn([ + FakeZone(id=2, api_url='http://foo.com', username='user2', + password='pass2'), + ]) + self.assertEquals(len(zm.zone_states), 1) + + self.mox.ReplayAll() + zm._refresh_from_db(None) + self.mox.VerifyAll() + + self.assertEquals(len(zm.zone_states), 1) + self.assertEquals(zm.zone_states[2].username, 'user2') + + def test_poll_zone(self): + self.mox.StubOutWithMock(zone_manager, '_call_novaclient') + zone_manager._call_novaclient(mox.IgnoreArg()).AndReturn( + dict(name='zohan', capabilities='hairdresser')) + + zone_state = zone_manager.ZoneState() + zone_state.update_credentials(FakeZone(id=2, + api_url='http://foo.com', username='user2', + password='pass2')) + zone_state.attempt = 1 + + self.mox.ReplayAll() + zone_manager._poll_zone(zone_state) + self.mox.VerifyAll() + self.assertEquals(zone_state.attempt, 0) + self.assertEquals(zone_state.name, 'zohan') + + def test_poll_zone_fails(self): + self.stubs.Set(zone_manager, "_call_novaclient", exploding_novaclient) + + zone_state = zone_manager.ZoneState() + zone_state.update_credentials(FakeZone(id=2, + api_url='http://foo.com', username='user2', + password='pass2')) + zone_state.attempt = FLAGS.zone_failures_to_offline - 1 + + self.mox.ReplayAll() + zone_manager._poll_zone(zone_state) + self.mox.VerifyAll() + self.assertEquals(zone_state.attempt, 3) + self.assertFalse(zone_state.is_active) + self.assertEquals(zone_state.name, None) diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index adf10a00a..d278934c6 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -21,6 +21,7 @@ from nova.virt.xenapi import fake from nova.virt.xenapi import volume_utils from nova.virt.xenapi import vm_utils from nova.virt.xenapi import vmops +from nova import utils def stubout_instance_snapshot(stubs): @@ -137,14 +138,17 @@ def stubout_is_vdi_pv(stubs): stubs.Set(vm_utils, '_is_vdi_pv', f) +def stubout_loopingcall_start(stubs): + def f_1(self, interval, now=True): + self.f(*self.args, **self.kw) + stubs.Set(utils.LoopingCall, 'start', f_1) + + class FakeSessionForVMTests(fake.SessionBase): """ Stubs out a XenAPISession for VM tests """ def __init__(self, uri): super(FakeSessionForVMTests, self).__init__(uri) - def network_get_all_records_where(self, _1, _2): - return self.xenapi.network.get_all_records() - def host_call_plugin(self, _1, _2, _3, _4, _5): sr_ref = fake.get_all('SR')[0] vdi_ref = fake.create_vdi('', False, sr_ref, False) @@ -178,12 +182,6 @@ class FakeSessionForVMTests(fake.SessionBase): def VM_destroy(self, session_ref, vm_ref): fake.destroy_vm(vm_ref) - def VM_add_to_xenstore_data(self, session_ref, vm_ref, key, value): - fake.VM_add_to_xenstore_data(vm_ref, key, value) - - def VM_remove_from_xenstore_data(self, session_ref, vm_ref, key): - fake.VM_remove_from_xenstore_data(vm_ref, key) - def SR_scan(self, session_ref, sr_ref): pass diff --git a/nova/utils.py b/nova/utils.py index 0466fecf4..87e726394 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -139,34 +139,44 @@ def execute(*cmd, **kwargs): stdin = kwargs.get('stdin', subprocess.PIPE) stdout = kwargs.get('stdout', subprocess.PIPE) stderr = kwargs.get('stderr', subprocess.PIPE) + attempts = kwargs.get('attempts', 1) cmd = map(str, cmd) - LOG.debug(_("Running cmd (subprocess): %s"), ' '.join(cmd)) - env = os.environ.copy() - if addl_env: - env.update(addl_env) - obj = subprocess.Popen(cmd, stdin=stdin, - stdout=stdout, stderr=stderr, env=env) - result = None - if process_input != None: - result = obj.communicate(process_input) - else: - result = obj.communicate() - obj.stdin.close() - if obj.returncode: - LOG.debug(_("Result was %s") % obj.returncode) - if type(check_exit_code) == types.IntType \ - and obj.returncode != check_exit_code: - (stdout, stderr) = result - raise ProcessExecutionError(exit_code=obj.returncode, - stdout=stdout, - stderr=stderr, - cmd=' '.join(cmd)) - # NOTE(termie): this appears to be necessary to let the subprocess call - # clean something up in between calls, without it two - # execute calls in a row hangs the second one - greenthread.sleep(0) - return result + while attempts > 0: + attempts -= 1 + try: + LOG.debug(_("Running cmd (subprocess): %s"), ' '.join(cmd)) + env = os.environ.copy() + if addl_env: + env.update(addl_env) + obj = subprocess.Popen(cmd, stdin=stdin, + stdout=stdout, stderr=stderr, env=env) + result = None + if process_input != None: + result = obj.communicate(process_input) + else: + result = obj.communicate() + obj.stdin.close() + if obj.returncode: + LOG.debug(_("Result was %s") % obj.returncode) + if type(check_exit_code) == types.IntType \ + and obj.returncode != check_exit_code: + (stdout, stderr) = result + raise ProcessExecutionError(exit_code=obj.returncode, + stdout=stdout, + stderr=stderr, + cmd=' '.join(cmd)) + # NOTE(termie): this appears to be necessary to let the subprocess + # call clean something up in between calls, without + # it two execute calls in a row hangs the second one + greenthread.sleep(0) + return result + except ProcessExecutionError: + if not attempts: + raise + else: + LOG.debug(_("%r failed. Retrying."), cmd) + greenthread.sleep(random.randint(20, 200) / 100.0) def ssh_execute(ssh, cmd, process_input=None, diff --git a/nova/virt/disk.py b/nova/virt/disk.py index dea703d32..d80f3c21f 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -56,7 +56,7 @@ def extend(image, size): return utils.execute('truncate', '-s', size, image) # NOTE(vish): attempts to resize filesystem - utils.execute('e2fsck', '-fp', mage, check_exit_code=False) + utils.execute('e2fsck', '-fp', image, check_exit_code=False) utils.execute('resize2fs', image, check_exit_code=False) @@ -223,4 +223,5 @@ def _inject_net_into_fs(net, fs, execute=None): utils.execute('sudo', 'chown', 'root:root', netdir) utils.execute('sudo', 'chmod', 755, netdir) netfile = os.path.join(netdir, 'interfaces') - utils.execute('sudo', 'tee', netfile, net) + utils.execute('sudo', 'tee', netfile, + process_input=net) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 57f40acab..4bb9500b9 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -56,7 +56,6 @@ from nova import flags from nova import log as logging #from nova import test from nova import utils -#from nova.api import context from nova.auth import manager from nova.compute import instance_types from nova.compute import power_state @@ -461,7 +460,7 @@ class LibvirtConnection(object): console_log = os.path.join(FLAGS.instances_path, instance['name'], 'console.log') - utils.execute('sudo', 'chown', s.getuid(), console_log) + utils.execute('sudo', 'chown', os.getuid(), console_log) if FLAGS.libvirt_type == 'xen': # Xen is special @@ -1205,10 +1204,14 @@ class NWFilterFirewall(FirewallDriver): class IptablesFirewallDriver(FirewallDriver): def __init__(self, execute=None, **kwargs): - self.execute = execute or utils.execute + from nova.network import linux_net + self.iptables = linux_net.iptables_manager self.instances = {} self.nwfilter = NWFilterFirewall(kwargs['get_connection']) + self.iptables.ipv4['filter'].add_chain('sg-fallback') + self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP') + def setup_basic_filtering(self, instance): """Use NWFilter from libvirt for this.""" return self.nwfilter.setup_basic_filtering(instance) @@ -1217,128 +1220,97 @@ class IptablesFirewallDriver(FirewallDriver): """No-op. Everything is done in prepare_instance_filter""" pass - def remove_instance(self, instance): + def unfilter_instance(self, instance): if instance['id'] in self.instances: del self.instances[instance['id']] + self.remove_filters_for_instance(instance) + self.iptables.apply() else: LOG.info(_('Attempted to unfilter instance %s which is not ' 'filtered'), instance['id']) - def add_instance(self, instance): + def prepare_instance_filter(self, instance): self.instances[instance['id']] = instance + self.add_filters_for_instance(instance) + self.iptables.apply() - def unfilter_instance(self, instance): - self.remove_instance(instance) - self.apply_ruleset() + def add_filters_for_instance(self, instance): + chain_name = self._instance_chain_name(instance) - def prepare_instance_filter(self, instance): - self.add_instance(instance) - self.apply_ruleset() - - def apply_ruleset(self): - current_filter, _ = self.execute('sudo', 'iptables-save', - '-t', 'filter') - current_lines = current_filter.split('\n') - new_filter = self.modify_rules(current_lines, 4) - self.execute('sudo', 'iptables-restore', - process_input='\n'.join(new_filter)) - if(FLAGS.use_ipv6): - current_filter, _ = self.execute('sudo', 'ip6tables-save', - '-t', 'filter') - current_lines = current_filter.split('\n') - new_filter = self.modify_rules(current_lines, 6) - self.execute('sudo', 'ip6tables-restore', - process_input='\n'.join(new_filter)) - - def modify_rules(self, current_lines, ip_version=4): + self.iptables.ipv4['filter'].add_chain(chain_name) + ipv4_address = self._ip_for_instance(instance) + self.iptables.ipv4['filter'].add_rule('local', + '-d %s -j $%s' % + (ipv4_address, chain_name)) + + if FLAGS.use_ipv6: + self.iptables.ipv6['filter'].add_chain(chain_name) + ipv6_address = self._ip_for_instance_v6(instance) + self.iptables.ipv6['filter'].add_rule('local', + '-d %s -j $%s' % + (ipv6_address, + chain_name)) + + ipv4_rules, ipv6_rules = self.instance_rules(instance) + + for rule in ipv4_rules: + self.iptables.ipv4['filter'].add_rule(chain_name, rule) + + if FLAGS.use_ipv6: + for rule in ipv6_rules: + self.iptables.ipv6['filter'].add_rule(chain_name, rule) + + def remove_filters_for_instance(self, instance): + chain_name = self._instance_chain_name(instance) + + self.iptables.ipv4['filter'].remove_chain(chain_name) + if FLAGS.use_ipv6: + self.iptables.ipv6['filter'].remove_chain(chain_name) + + def instance_rules(self, instance): ctxt = context.get_admin_context() - # Remove any trace of nova rules. - new_filter = filter(lambda l: 'nova-' not in l, current_lines) - - seen_chains = False - for rules_index in range(len(new_filter)): - if not seen_chains: - if new_filter[rules_index].startswith(':'): - seen_chains = True - elif seen_chains == 1: - if not new_filter[rules_index].startswith(':'): - break - our_chains = [':nova-fallback - [0:0]'] - our_rules = ['-A nova-fallback -j DROP'] - - our_chains += [':nova-local - [0:0]'] - our_rules += ['-A FORWARD -j nova-local'] - our_rules += ['-A OUTPUT -j nova-local'] - - security_groups = {} - # Add our chains - # First, we add instance chains and rules - for instance_id in self.instances: - instance = self.instances[instance_id] - chain_name = self._instance_chain_name(instance) - if(ip_version == 4): - ip_address = self._ip_for_instance(instance) - elif(ip_version == 6): - ip_address = self._ip_for_instance_v6(instance) - - our_chains += [':%s - [0:0]' % chain_name] - - # Jump to the per-instance chain - our_rules += ['-A nova-local -d %s -j %s' % (ip_address, - chain_name)] - - # Always drop invalid packets - our_rules += ['-A %s -m state --state ' - 'INVALID -j DROP' % (chain_name,)] - - # Allow established connections - our_rules += ['-A %s -m state --state ' - 'ESTABLISHED,RELATED -j ACCEPT' % (chain_name,)] - - # Jump to each security group chain in turn - for security_group in \ - db.security_group_get_by_instance(ctxt, - instance['id']): - security_groups[security_group['id']] = security_group - - sg_chain_name = self._security_group_chain_name( - security_group['id']) + ipv4_rules = [] + ipv6_rules = [] - our_rules += ['-A %s -j %s' % (chain_name, sg_chain_name)] - - if(ip_version == 4): - # Allow DHCP responses - dhcp_server = self._dhcp_server_for_instance(instance) - our_rules += ['-A %s -s %s -p udp --sport 67 --dport 68 ' - '-j ACCEPT ' % (chain_name, dhcp_server)] - #Allow project network traffic - if (FLAGS.allow_project_net_traffic): - cidr = self._project_cidr_for_instance(instance) - our_rules += ['-A %s -s %s -j ACCEPT' % (chain_name, cidr)] - elif(ip_version == 6): - # Allow RA responses - ra_server = self._ra_server_for_instance(instance) - if ra_server: - our_rules += ['-A %s -s %s -p icmpv6 -j ACCEPT' % - (chain_name, ra_server + "/128")] - #Allow project network traffic - if (FLAGS.allow_project_net_traffic): - cidrv6 = self._project_cidrv6_for_instance(instance) - our_rules += ['-A %s -s %s -j ACCEPT' % - (chain_name, cidrv6)] - - # If nothing matches, jump to the fallback chain - our_rules += ['-A %s -j nova-fallback' % (chain_name,)] + # Always drop invalid packets + ipv4_rules += ['-m state --state ' 'INVALID -j DROP'] + ipv6_rules += ['-m state --state ' 'INVALID -j DROP'] - # then, security group chains and rules - for security_group_id in security_groups: - chain_name = self._security_group_chain_name(security_group_id) - our_chains += [':%s - [0:0]' % chain_name] + # Allow established connections + ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT'] + ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT'] + + dhcp_server = self._dhcp_server_for_instance(instance) + ipv4_rules += ['-s %s -p udp --sport 67 --dport 68 ' + '-j ACCEPT' % (dhcp_server,)] + + #Allow project network traffic + if FLAGS.allow_project_net_traffic: + cidr = self._project_cidr_for_instance(instance) + ipv4_rules += ['-s %s -j ACCEPT' % (cidr,)] + + # We wrap these in FLAGS.use_ipv6 because they might cause + # a DB lookup. The other ones are just list operations, so + # they're not worth the clutter. + if FLAGS.use_ipv6: + # Allow RA responses + ra_server = self._ra_server_for_instance(instance) + if ra_server: + ipv6_rules += ['-s %s/128 -p icmpv6 -j ACCEPT' % (ra_server,)] + + #Allow project network traffic + if FLAGS.allow_project_net_traffic: + cidrv6 = self._project_cidrv6_for_instance(instance) + ipv6_rules += ['-s %s -j ACCEPT' % (cidrv6,)] - rules = \ - db.security_group_rule_get_by_security_group(ctxt, - security_group_id) + security_groups = db.security_group_get_by_instance(ctxt, + instance['id']) + + # then, security group chains and rules + for security_group in security_groups: + rules = db.security_group_rule_get_by_security_group(ctxt, + security_group['id']) for rule in rules: logging.info('%r', rule) @@ -1349,14 +1321,16 @@ class IptablesFirewallDriver(FirewallDriver): continue version = _get_ip_version(rule.cidr) - if version != ip_version: - continue + if version == 4: + rules = ipv4_rules + else: + rules = ipv6_rules protocol = rule.protocol if version == 6 and rule.protocol == 'icmp': protocol = 'icmpv6' - args = ['-A', chain_name, '-p', protocol, '-s', rule.cidr] + args = ['-p', protocol, '-s', rule.cidr] if rule.protocol in ['udp', 'tcp']: if rule.from_port == rule.to_port: @@ -1377,32 +1351,39 @@ class IptablesFirewallDriver(FirewallDriver): icmp_type_arg += '/%s' % icmp_code if icmp_type_arg: - if(ip_version == 4): + if version == 4: args += ['-m', 'icmp', '--icmp-type', icmp_type_arg] - elif(ip_version == 6): + elif version == 6: args += ['-m', 'icmp6', '--icmpv6-type', icmp_type_arg] args += ['-j ACCEPT'] - our_rules += [' '.join(args)] + rules += [' '.join(args)] + + ipv4_rules += ['-j $sg-fallback'] + ipv6_rules += ['-j $sg-fallback'] - new_filter[rules_index:rules_index] = our_rules - new_filter[rules_index:rules_index] = our_chains - logging.info('new_filter: %s', '\n'.join(new_filter)) - return new_filter + return ipv4_rules, ipv6_rules def refresh_security_group_members(self, security_group): pass def refresh_security_group_rules(self, security_group): - self.apply_ruleset() + for instance in self.instances.values(): + # We use the semaphore to make sure noone applies the rule set + # after we've yanked the existing rules but before we've put in + # the new ones. + with self.iptables.semaphore: + self.remove_filters_for_instance(instance) + self.add_filters_for_instance(instance) + self.iptables.apply() def _security_group_chain_name(self, security_group_id): return 'nova-sg-%s' % (security_group_id,) def _instance_chain_name(self, instance): - return 'nova-inst-%s' % (instance['id'],) + return 'inst-%s' % (instance['id'],) def _ip_for_instance(self, instance): return db.instance_get_fixed_address(context.get_admin_context(), diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index eecfa20b1..60db86ecd 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -149,24 +149,6 @@ def create_vbd(vm_ref, vdi_ref): return vbd_ref -def VM_get_xenstore_data(vm_ref): - return _db_content['VM'][vm_ref].get('xenstore_data', '') - - -def VM_remove_from_xenstore_data(vm_ref, key): - db_ref = _db_content['VM'][vm_ref] - if not 'xenstore_data' in db_ref: - return - db_ref['xenstore_data'][key] = None - - -def VM_add_to_xenstore_data(vm_ref, key, value): - db_ref = _db_content['VM'][vm_ref] - if not 'xenstore_data' in db_ref: - db_ref['xenstore_data'] = {} - db_ref['xenstore_data'][key] = value - - def after_VBD_create(vbd_ref, vbd_rec): """Create read-only fields and backref from VM to VBD when VBD is created.""" @@ -310,6 +292,25 @@ class SessionBase(object): rec['currently_attached'] = False rec['device'] = '' + def VM_get_xenstore_data(self, _1, vm_ref): + return _db_content['VM'][vm_ref].get('xenstore_data', '') + + def VM_remove_from_xenstore_data(self, _1, vm_ref, key): + db_ref = _db_content['VM'][vm_ref] + if not 'xenstore_data' in db_ref: + return + db_ref['xenstore_data'][key] = None + + def network_get_all_records_where(self, _1, _2): + # TODO (salvatore-orlando):filter table on _2 + return _db_content['network'] + + def VM_add_to_xenstore_data(self, _1, vm_ref, key, value): + db_ref = _db_content['VM'][vm_ref] + if not 'xenstore_data' in db_ref: + db_ref['xenstore_data'] = {} + db_ref['xenstore_data'][key] = value + def host_compute_free_memory(self, _1, ref): #Always return 12GB available return 12 * 1024 * 1024 * 1024 @@ -501,7 +502,7 @@ class SessionBase(object): def _check_session(self, params): if (self._session is None or self._session not in _db_content['session']): - raise Failure(['HANDLE_INVALID', 'session', self._session]) + raise Failure(['HANDLE_INVALID', 'session', self._session]) if len(params) == 0 or params[0] != self._session: LOG.debug(_('Raising NotImplemented')) raise NotImplementedError('Call to XenAPI without using .xenapi') diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 21528fbcf..07d438b5e 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -626,9 +626,10 @@ class VMHelper(HelperBase): # Mount only Linux filesystems, to avoid disturbing # NTFS images try: - out, err = utils.execute( - 'sudo mount -t ext2,ext3 "%s" "%s"' % - (dev_path, tmpdir)) + out, err = utils.execute('sudo', 'mount', + '-t', 'ext2,ext3', + '"%s"' % dev_path, + '"%s"' % tmpdir) except exception.ProcessExecutionError as e: err = str(e) if err: @@ -664,7 +665,7 @@ class VMHelper(HelperBase): disk.inject_data_into_fs(tmpdir, key, net, utils.execute) finally: - utils.execute('sudo umount "%s"' % dev_path) + utils.execute('sudo', 'umount', dev_path) finally: # remove temporary directory os.rmdir(tmpdir) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 79b1842d5..26bb8ac0c 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -168,13 +168,14 @@ class VMOps(object): def _wait_for_boot(): try: + LOG.debug("ENTERING WAIT FOR BOOT!") state = self.get_info(instance_name)['state'] db.instance_set_state(context.get_admin_context(), instance['id'], state) if state == power_state.RUNNING: LOG.debug(_('Instance %s: booted'), instance_name) - timer.stop() _inject_onset_files() + timer.stop() return True except Exception, exc: LOG.warn(exc) @@ -836,7 +837,7 @@ class VMOps(object): """ instance_id = vm.id vm = self._get_vm_opaque_ref(vm) - rec = self._session.get_xenapi().VM.get_record(vm) + rec = self._session.call_xenapi('VM.get_record', vm) args = {'dom_id': rec['domid'], 'path': path} args.update(addl_args) try: diff --git a/tools/pip-requires b/tools/pip-requires index 3587df644..3c9047e04 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -10,6 +10,7 @@ boto==1.9b carrot==0.10.5 eventlet==0.9.12 lockfile==0.8 +python-novaclient==2.3 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0 |
