summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTrey Morris <trey.morris@rackspace.com>2011-06-23 18:10:30 -0500
committerTrey Morris <trey.morris@rackspace.com>2011-06-23 18:10:30 -0500
commit5b5cc6f42ef5cca33bbca65ef66e53862cf69f07 (patch)
tree2ac8e4694a842d224a077732f460dd711b99f57a
parentc33fc283c4f75b4de745484b53a818795ad80d96 (diff)
parent654350a1cf93e8ecf8d38f07802e0c3ed7039562 (diff)
downloadnova-5b5cc6f42ef5cca33bbca65ef66e53862cf69f07.tar.gz
nova-5b5cc6f42ef5cca33bbca65ef66e53862cf69f07.tar.xz
nova-5b5cc6f42ef5cca33bbca65ef66e53862cf69f07.zip
trunk merge
-rwxr-xr-xbin/nova-manage7
-rw-r--r--nova/api/ec2/admin.py45
-rw-r--r--nova/api/openstack/image_metadata.py64
-rw-r--r--nova/api/openstack/images.py45
-rw-r--r--nova/api/openstack/views/images.py30
-rw-r--r--nova/api/openstack/wsgi.py8
-rw-r--r--nova/compute/api.py13
-rw-r--r--nova/compute/manager.py71
-rw-r--r--nova/db/api.py13
-rw-r--r--nova/db/sqlalchemy/api.py18
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py75
-rw-r--r--nova/db/sqlalchemy/models.py11
-rw-r--r--nova/network/linux_net.py7
-rw-r--r--nova/tests/api/openstack/fakes.py7
-rw-r--r--nova/tests/api/openstack/test_image_metadata.py115
-rw-r--r--nova/tests/api/openstack/test_images.py41
-rw-r--r--nova/tests/api/openstack/test_limits.py3
-rw-r--r--nova/tests/api/openstack/test_servers.py2
-rw-r--r--nova/tests/api/openstack/test_wsgi.py4
-rw-r--r--nova/tests/test_adminapi.py89
-rw-r--r--nova/tests/test_libvirt.py62
-rw-r--r--nova/virt/driver.py4
-rw-r--r--nova/virt/fake.py16
-rw-r--r--nova/virt/libvirt/connection.py3
-rw-r--r--nova/virt/libvirt/firewall.py180
-rw-r--r--tools/pip-requires2
26 files changed, 831 insertions, 104 deletions
diff --git a/bin/nova-manage b/bin/nova-manage
index 1e2043296..9d67ed95b 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -263,6 +263,11 @@ class RoleCommands(object):
"""adds role to user
if project is specified, adds project specific role
arguments: user, role [project]"""
+ if project:
+ projobj = self.manager.get_project(project)
+ if not projobj.has_member(user):
+ print "%s not a member of %s" % (user, project)
+ return
self.manager.add_role(user, role, project)
def has(self, user, role, project=None):
@@ -900,7 +905,7 @@ class InstanceTypeCommands(object):
try:
instance_types.create(name, memory, vcpus, local_gb,
flavorid, swap, rxtx_quota, rxtx_cap)
- except exception.InvalidInputException:
+ except exception.InvalidInput:
print "Must supply valid parameters to create instance_type"
print e
sys.exit(1)
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index 57d0a0339..343bc61c4 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -21,7 +21,11 @@ Admin API controller, exposed through http via the api worker.
"""
import base64
+import datetime
+import netaddr
+import urllib
+from nova import compute
from nova import db
from nova import exception
from nova import flags
@@ -117,6 +121,9 @@ class AdminController(object):
def __str__(self):
return 'AdminController'
+ def __init__(self):
+ self.compute_api = compute.API()
+
def describe_instance_types(self, context, **_kwargs):
"""Returns all active instance types data (vcpus, memory, etc.)"""
return {'instanceTypeSet': [instance_dict(v) for v in
@@ -324,3 +331,41 @@ class AdminController(object):
rv.append(host_dict(host, compute, instances, volume, volumes,
now))
return {'hosts': rv}
+
+ def _provider_fw_rule_exists(self, context, rule):
+ # TODO(todd): we call this repeatedly, can we filter by protocol?
+ for old_rule in db.provider_fw_rule_get_all(context):
+ if all([rule[k] == old_rule[k] for k in ('cidr', 'from_port',
+ 'to_port', 'protocol')]):
+ return True
+ return False
+
+ def block_external_addresses(self, context, cidr):
+ """Add provider-level firewall rules to block incoming traffic."""
+ LOG.audit(_('Blocking traffic to all projects incoming from %s'),
+ cidr, context=context)
+ cidr = urllib.unquote(cidr).decode()
+ # raise if invalid
+ netaddr.IPNetwork(cidr)
+ rule = {'cidr': cidr}
+ tcp_rule = rule.copy()
+ tcp_rule.update({'protocol': 'tcp', 'from_port': 1, 'to_port': 65535})
+ udp_rule = rule.copy()
+ udp_rule.update({'protocol': 'udp', 'from_port': 1, 'to_port': 65535})
+ icmp_rule = rule.copy()
+ icmp_rule.update({'protocol': 'icmp', 'from_port': -1,
+ 'to_port': None})
+ rules_added = 0
+ if not self._provider_fw_rule_exists(context, tcp_rule):
+ db.provider_fw_rule_create(context, tcp_rule)
+ rules_added += 1
+ if not self._provider_fw_rule_exists(context, udp_rule):
+ db.provider_fw_rule_create(context, udp_rule)
+ rules_added += 1
+ if not self._provider_fw_rule_exists(context, icmp_rule):
+ db.provider_fw_rule_create(context, icmp_rule)
+ rules_added += 1
+ if not rules_added:
+ raise exception.ApiError(_('Duplicate rule'))
+ self.compute_api.trigger_provider_fw_rules_refresh(context)
+ return {'status': 'OK', 'message': 'Added %s rules' % rules_added}
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
index ebfe2bde9..c0e92f2fc 100644
--- a/nova/api/openstack/image_metadata.py
+++ b/nova/api/openstack/image_metadata.py
@@ -16,6 +16,7 @@
# under the License.
from webob import exc
+from xml.dom import minidom
from nova import flags
from nova import image
@@ -59,7 +60,7 @@ class Controller(object):
context = req.environ['nova.context']
metadata = self._get_metadata(context, image_id)
if id in metadata:
- return {id: metadata[id]}
+ return {'meta': {id: metadata[id]}}
else:
return faults.Fault(exc.HTTPNotFound())
@@ -77,15 +78,22 @@ class Controller(object):
def update(self, req, image_id, id, body):
context = req.environ['nova.context']
- if not id in body:
+
+ try:
+ meta = body['meta']
+ except KeyError:
+ expl = _('Incorrect request body format')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ if not id in meta:
expl = _('Request body and URI mismatch')
raise exc.HTTPBadRequest(explanation=expl)
- if len(body) > 1:
+ if len(meta) > 1:
expl = _('Request body contains too many items')
raise exc.HTTPBadRequest(explanation=expl)
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id, img)
- metadata[id] = body[id]
+ metadata[id] = meta[id]
self._check_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
@@ -103,9 +111,55 @@ class Controller(object):
self.image_service.update(context, image_id, img, None)
+class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
+ def __init__(self):
+ xmlns = wsgi.XMLNS_V11
+ super(ImageMetadataXMLSerializer, self).__init__(xmlns=xmlns)
+
+ def _meta_item_to_xml(self, doc, key, value):
+ node = doc.createElement('meta')
+ node.setAttribute('key', key)
+ text = doc.createTextNode(value)
+ node.appendChild(text)
+ return node
+
+ def _meta_list_to_xml(self, xml_doc, meta_items):
+ container_node = xml_doc.createElement('metadata')
+ for (key, value) in meta_items:
+ item_node = self._meta_item_to_xml(xml_doc, key, value)
+ container_node.appendChild(item_node)
+ return container_node
+
+ def _meta_list_to_xml_string(self, metadata_dict):
+ xml_doc = minidom.Document()
+ items = metadata_dict['metadata'].items()
+ container_node = self._meta_list_to_xml(xml_doc, items)
+ self._add_xmlns(container_node)
+ return container_node.toprettyxml(indent=' ')
+
+ def index(self, metadata_dict):
+ return self._meta_list_to_xml_string(metadata_dict)
+
+ def create(self, metadata_dict):
+ return self._meta_list_to_xml_string(metadata_dict)
+
+ def _meta_item_to_xml_string(self, meta_item_dict):
+ xml_doc = minidom.Document()
+ item_key, item_value = meta_item_dict.items()[0]
+ item_node = self._meta_item_to_xml(xml_doc, item_key, item_value)
+ self._add_xmlns(item_node)
+ return item_node.toprettyxml(indent=' ')
+
+ def show(self, meta_item_dict):
+ return self._meta_item_to_xml_string(meta_item_dict['meta'])
+
+ def update(self, meta_item_dict):
+ return self._meta_item_to_xml_string(meta_item_dict['meta'])
+
+
def create_resource():
serializers = {
- 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
+ 'application/xml': ImageMetadataXMLSerializer(),
}
return wsgi.Resource(Controller(), serializers=serializers)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 5ffd8e96a..d43340e10 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import os.path
+
import webob.exc
from nova import compute
@@ -99,21 +101,27 @@ class Controller(object):
raise webob.exc.HTTPBadRequest()
try:
- server_id = self._server_id_from_req_data(body)
+ server_id = self._server_id_from_req(req, body)
image_name = body["image"]["name"]
except KeyError:
raise webob.exc.HTTPBadRequest()
- image = self._compute_service.snapshot(context, server_id, image_name)
+ props = self._get_extra_properties(req, body)
+
+ image = self._compute_service.snapshot(context, server_id,
+ image_name, props)
return dict(image=self.get_builder(req).build(image, detail=True))
def get_builder(self, request):
"""Indicates that you must use a Controller subclass."""
raise NotImplementedError
- def _server_id_from_req_data(self, data):
+ def _server_id_from_req(self, req, data):
raise NotImplementedError()
+ def _get_extra_properties(self, req, data):
+ return {}
+
class ControllerV10(Controller):
"""Version 1.0 specific controller logic."""
@@ -149,8 +157,12 @@ class ControllerV10(Controller):
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
- def _server_id_from_req_data(self, data):
- return data['image']['serverId']
+ def _server_id_from_req(self, req, data):
+ try:
+ return data['image']['serverId']
+ except KeyError:
+ msg = _("Expected serverId attribute on server entity.")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
class ControllerV11(Controller):
@@ -189,8 +201,27 @@ class ControllerV11(Controller):
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
- def _server_id_from_req_data(self, data):
- return data['image']['serverRef']
+ def _server_id_from_req(self, req, data):
+ try:
+ server_ref = data['image']['serverRef']
+ except KeyError:
+ msg = _("Expected serverRef attribute on server entity.")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ head, tail = os.path.split(server_ref)
+
+ if head and head != os.path.join(req.application_url, 'servers'):
+ msg = _("serverRef must match request url")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ return tail
+
+ def _get_extra_properties(self, req, data):
+ server_ref = data['image']['serverRef']
+ if not server_ref.startswith('http'):
+ server_ref = os.path.join(req.application_url, 'servers',
+ server_ref)
+ return {'instance_ref': server_ref}
def create_resource(version='1.0'):
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index 2773c9c13..d6a054102 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -46,13 +46,9 @@ class ViewBuilder(object):
except KeyError:
image['status'] = image['status'].upper()
- def _build_server(self, image, instance_id):
+ def _build_server(self, image, image_obj):
"""Indicates that you must use a ViewBuilder subclass."""
- raise NotImplementedError
-
- def generate_server_ref(self, server_id):
- """Return an href string pointing to this server."""
- return os.path.join(self._url, "servers", str(server_id))
+ raise NotImplementedError()
def generate_href(self, image_id):
"""Return an href string pointing to this object."""
@@ -60,8 +56,6 @@ class ViewBuilder(object):
def build(self, image_obj, detail=False):
"""Return a standardized image structure for display by the API."""
- properties = image_obj.get("properties", {})
-
self._format_dates(image_obj)
if "status" in image_obj:
@@ -72,11 +66,7 @@ class ViewBuilder(object):
"name": image_obj.get("name"),
}
- if "instance_id" in properties:
- try:
- self._build_server(image, int(properties["instance_id"]))
- except ValueError:
- pass
+ self._build_server(image, image_obj)
if detail:
image.update({
@@ -94,15 +84,21 @@ class ViewBuilder(object):
class ViewBuilderV10(ViewBuilder):
"""OpenStack API v1.0 Image Builder"""
- def _build_server(self, image, instance_id):
- image["serverId"] = instance_id
+ def _build_server(self, image, image_obj):
+ try:
+ image['serverId'] = int(image_obj['properties']['instance_id'])
+ except (KeyError, ValueError):
+ pass
class ViewBuilderV11(ViewBuilder):
"""OpenStack API v1.1 Image Builder"""
- def _build_server(self, image, instance_id):
- image["serverRef"] = self.generate_server_ref(instance_id)
+ def _build_server(self, image, image_obj):
+ try:
+ image['serverRef'] = image_obj['properties']['instance_ref']
+ except KeyError:
+ return
def build(self, image_obj, detail=False):
"""Return a standardized image structure for display by the API."""
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index a57b7f72b..5d24b4cca 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -232,12 +232,14 @@ class XMLDictSerializer(DictSerializer):
doc = minidom.Document()
node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
- xmlns = node.getAttribute('xmlns')
- if not xmlns and self.xmlns:
- node.setAttribute('xmlns', self.xmlns)
+ self._add_xmlns(node)
return node.toprettyxml(indent=' ', encoding='utf-8')
+ def _add_xmlns(self, node):
+ if self.xmlns is not None:
+ node.setAttribute('xmlns', self.xmlns)
+
def _to_xml_node(self, doc, metadata, nodename, data):
"""Recursive method to convert data members to XML nodes."""
result = doc.createElement(nodename)
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 9f4d4899f..4eff7bac0 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -478,6 +478,16 @@ class API(base.Base):
{"method": "refresh_security_group_members",
"args": {"security_group_id": group_id}})
+ def trigger_provider_fw_rules_refresh(self, context):
+ """Called when a rule is added to or removed from a security_group"""
+
+ hosts = [x['host'] for (x, idx)
+ in db.service_get_all_compute_sorted(context)]
+ for host in hosts:
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {'method': 'refresh_provider_fw_rules', 'args': {}})
+
def update(self, context, instance_id, **kwargs):
"""Updates the instance in the datastore.
@@ -683,7 +693,7 @@ class API(base.Base):
raise exception.Error(_("Unable to find host for Instance %s")
% instance_id)
- def snapshot(self, context, instance_id, name):
+ def snapshot(self, context, instance_id, name, extra_properties=None):
"""Snapshot the given instance.
:returns: A dict containing image metadata
@@ -691,6 +701,7 @@ class API(base.Base):
properties = {'instance_id': str(instance_id),
'user_id': str(context.user_id),
'image_state': 'creating'}
+ properties.update(extra_properties or {})
sent_meta = {'name': name, 'is_public': False,
'status': 'creating', 'properties': properties}
recv_meta = self.image_service.create(context, sent_meta)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 8021154eb..102a3c5a7 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -201,6 +201,11 @@ class ComputeManager(manager.SchedulerDependentManager):
"""
return self.driver.refresh_security_group_members(security_group_id)
+ @exception.wrap_exception
+ def refresh_provider_fw_rules(self, context, **_kwargs):
+ """This call passes straight through to the virtualization driver."""
+ return self.driver.refresh_provider_fw_rules()
+
def _setup_block_device_mapping(self, context, instance_id):
"""setup volumes for block device mapping"""
self.db.instance_set_state(context,
@@ -277,39 +282,45 @@ class ComputeManager(manager.SchedulerDependentManager):
'networking')
is_vpn = instance['image_ref'] == str(FLAGS.vpn_image_id)
- # NOTE(vish): This could be a cast because we don't do anything
- # with the address currently, but I'm leaving it as
- # a call to ensure that network setup completes. We
- # will eventually also need to save the address here.
- if not FLAGS.stub_network:
- network_info = self.network_api.allocate_for_instance(context,
- instance,
- vpn=is_vpn)
- LOG.debug(_("instance network_info: |%s|"), network_info)
- self.network_manager.setup_compute_network(context, instance_id)
- else:
- # TODO(tr3buchet) not really sure how this should be handled.
- # virt requires network_info to be passed in but stub_network
- # is enabled. Setting to [] for now will cause virt to skip
- # all vif creation and network injection, maybe this is correct
- network_info = []
-
- block_device_mapping = self._setup_block_device_mapping(context,
- instance_id)
+ try:
+ # NOTE(vish): This could be a cast because we don't do anything
+ # with the address currently, but I'm leaving it as
+ # a call to ensure that network setup completes. We
+ # will eventually also need to save the address here.
+ if not FLAGS.stub_network:
+ network_info = self.network_api.allocate_for_instance(context,
+ instance, vpn=is_vpn)
+ LOG.debug(_("instance network_info: |%s|"), network_info)
+ self.network_manager.setup_compute_network(context,
+ instance_id)
+ else:
+ # TODO(tr3buchet) not really sure how this should be handled.
+ # virt requires network_info to be passed in but stub_network
+ # is enabled. Setting to [] for now will cause virt to skip
+ # all vif creation and network injection, maybe this is correct
+ network_info = []
- # TODO(vish) check to make sure the availability zone matches
- self._update_state(context, instance_id, power_state.BUILDING)
+ bd_mapping = self._setup_block_device_mapping(context, instance_id)
- try:
- self.driver.spawn(instance, network_info, block_device_mapping)
- except Exception as ex: # pylint: disable=W0702
- msg = _("Instance '%(instance_id)s' failed to spawn. Is "
- "virtualization enabled in the BIOS? Details: "
- "%(ex)s") % locals()
- LOG.exception(msg)
+ # TODO(vish) check to make sure the availability zone matches
+ self._update_state(context, instance_id, power_state.BUILDING)
- self._update_launched_at(context, instance_id)
- self._update_state(context, instance_id)
+ try:
+ self.driver.spawn(instance, network_info, bd_mapping)
+ except Exception as ex: # pylint: disable=W0702
+ msg = _("Instance '%(instance_id)s' failed to spawn. Is "
+ "virtualization enabled in the BIOS? Details: "
+ "%(ex)s") % locals()
+ LOG.exception(msg)
+
+ self._update_launched_at(context, instance_id)
+ self._update_state(context, instance_id)
+ except exception.InstanceNotFound:
+ # FIXME(wwolf): We are just ignoring InstanceNotFound
+ # exceptions here in case the instance was immediately
+ # deleted before it actually got created. This should
+ # be fixed once we have no-db-messaging
+ pass
@exception.wrap_exception
def run_instance(self, context, instance_id, **kwargs):
diff --git a/nova/db/api.py b/nova/db/api.py
index 4d036ac57..a3a3d2b6b 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -1084,6 +1084,19 @@ def security_group_rule_destroy(context, security_group_rule_id):
###################
+def provider_fw_rule_create(context, rule):
+ """Add a firewall rule at the provider level (all hosts & instances)."""
+ return IMPL.provider_fw_rule_create(context, rule)
+
+
+def provider_fw_rule_get_all(context):
+ """Get all provider-level firewall rules."""
+ return IMPL.provider_fw_rule_get_all(context)
+
+
+###################
+
+
def user_get(context, id):
"""Get user by id."""
return IMPL.user_get(context, id)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index ce8e9f39b..c3de412d7 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -2414,6 +2414,24 @@ def security_group_rule_destroy(context, security_group_rule_id):
@require_admin_context
+def provider_fw_rule_create(context, rule):
+ fw_rule_ref = models.ProviderFirewallRule()
+ fw_rule_ref.update(rule)
+ fw_rule_ref.save()
+ return fw_rule_ref
+
+
+def provider_fw_rule_get_all(context):
+ session = get_session()
+ return session.query(models.ProviderFirewallRule).\
+ filter_by(deleted=can_read_deleted(context)).\
+ all()
+
+
+###################
+
+
+@require_admin_context
def user_get(context, id, session=None):
if not session:
session = get_session()
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py
new file mode 100644
index 000000000..5aa30f7a8
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py
@@ -0,0 +1,75 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+from sqlalchemy import *
+from migrate import *
+
+from nova import log as logging
+
+
+meta = MetaData()
+
+
+# Just for the ForeignKey and column creation to succeed, these are not the
+# actual definitions of instances or services.
+instances = Table('instances', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+
+services = Table('services', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+
+networks = Table('networks', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+
+#
+# New Tables
+#
+provider_fw_rules = Table('provider_fw_rules', meta,
+ Column('created_at', DateTime(timezone=False)),
+ Column('updated_at', DateTime(timezone=False)),
+ Column('deleted_at', DateTime(timezone=False)),
+ Column('deleted', Boolean(create_constraint=True, name=None)),
+ Column('id', Integer(), primary_key=True, nullable=False),
+ Column('protocol',
+ String(length=5, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('from_port', Integer()),
+ Column('to_port', Integer()),
+ Column('cidr',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False))
+ )
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine;
+ # bind migrate_engine to your metadata
+ meta.bind = migrate_engine
+ for table in (provider_fw_rules,):
+ try:
+ table.create()
+ except Exception:
+ logging.info(repr(table))
+ logging.exception('Exception while creating table')
+ raise
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 250e88572..7f02ac532 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -493,6 +493,17 @@ class SecurityGroupIngressRule(BASE, NovaBase):
group_id = Column(Integer, ForeignKey('security_groups.id'))
+class ProviderFirewallRule(BASE, NovaBase):
+ """Represents a rule in a security group."""
+ __tablename__ = 'provider_fw_rules'
+ id = Column(Integer, primary_key=True)
+
+ protocol = Column(String(5)) # "tcp", "udp", or "icmp"
+ from_port = Column(Integer)
+ to_port = Column(Integer)
+ cidr = Column(String(255))
+
+
class KeyPair(BASE, NovaBase):
"""Represents a public key pair for ssh."""
__tablename__ = 'key_pairs'
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 4b998fbba..9a5f2c5e6 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -191,6 +191,13 @@ class IptablesTable(object):
{'chain': chain, 'rule': rule,
'top': top, 'wrap': wrap})
+ def empty_chain(self, chain, wrap=True):
+ """Remove all rules from a chain."""
+ chained_rules = [rule for rule in self.rules
+ if rule.chain == chain and rule.wrap == wrap]
+ for rule in chained_rules:
+ self.rules.remove(rule)
+
class IptablesManager(object):
"""Wrapper for iptables.
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index f8d158ddd..c74974b16 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -140,9 +140,10 @@ def stub_out_networking(stubs):
def stub_out_compute_api_snapshot(stubs):
- def snapshot(self, context, instance_id, name):
- return dict(id='123', status='ACTIVE',
- properties=dict(instance_id='123'))
+ def snapshot(self, context, instance_id, name, extra_properties=None):
+ props = dict(instance_id=instance_id, instance_ref=instance_id)
+ props.update(extra_properties or {})
+ return dict(id='123', status='ACTIVE', name=name, properties=props)
stubs.Set(nova.compute.API, 'snapshot', snapshot)
diff --git a/nova/tests/api/openstack/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py
index 56be0f1cc..efed44960 100644
--- a/nova/tests/api/openstack/test_image_metadata.py
+++ b/nova/tests/api/openstack/test_image_metadata.py
@@ -19,6 +19,7 @@ import json
import stubout
import unittest
import webob
+import xml.dom.minidom as minidom
from nova import flags
@@ -105,19 +106,62 @@ class ImageMetaDataTest(unittest.TestCase):
self.assertEqual(200, res.status_int)
self.assertEqual('value1', res_dict['metadata']['key1'])
+ def test_index_xml(self):
+ serializer = openstack.image_metadata.ImageMetadataXMLSerializer()
+ fixture = {
+ 'metadata': {
+ 'one': 'two',
+ 'three': 'four',
+ },
+ }
+ output = serializer.index(fixture)
+ actual = minidom.parseString(output.replace(" ", ""))
+
+ expected = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="three">
+ four
+ </meta>
+ <meta key="one">
+ two
+ </meta>
+ </metadata>
+ """.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
def test_show(self):
req = webob.Request.blank('/v1.1/images/1/meta/key1')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
- self.assertEqual('value1', res_dict['key1'])
+ self.assertTrue('meta' in res_dict)
+ self.assertEqual(len(res_dict['meta']), 1)
+ self.assertEqual('value1', res_dict['meta']['key1'])
+
+ def test_show_xml(self):
+ serializer = openstack.image_metadata.ImageMetadataXMLSerializer()
+ fixture = {
+ 'meta': {
+ 'one': 'two',
+ },
+ }
+ output = serializer.show(fixture)
+ actual = minidom.parseString(output.replace(" ", ""))
+
+ expected = minidom.parseString("""
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one">
+ two
+ </meta>
+ """.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
def test_show_not_found(self):
req = webob.Request.blank('/v1.1/images/1/meta/key9')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
- res_dict = json.loads(res.body)
self.assertEqual(404, res.status_int)
def test_create(self):
@@ -135,22 +179,79 @@ class ImageMetaDataTest(unittest.TestCase):
self.assertEqual('value2', res_dict['metadata']['key2'])
self.assertEqual(1, len(res_dict))
+ def test_create_xml(self):
+ serializer = openstack.image_metadata.ImageMetadataXMLSerializer()
+ fixture = {
+ 'metadata': {
+ 'key9': 'value9',
+ 'key2': 'value2',
+ 'key1': 'value1',
+ },
+ }
+ output = serializer.create(fixture)
+ actual = minidom.parseString(output.replace(" ", ""))
+
+ expected = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key2">
+ value2
+ </meta>
+ <meta key="key9">
+ value9
+ </meta>
+ <meta key="key1">
+ value1
+ </meta>
+ </metadata>
+ """.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
def test_update_item(self):
req = webob.Request.blank('/v1.1/images/1/meta/key1')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
- req.body = '{"key1": "zz"}'
+ req.body = '{"meta": {"key1": "zz"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(200, res.status_int)
res_dict = json.loads(res.body)
- self.assertEqual('zz', res_dict['key1'])
+ self.assertTrue('meta' in res_dict)
+ self.assertEqual(len(res_dict['meta']), 1)
+ self.assertEqual('zz', res_dict['meta']['key1'])
+
+ def test_update_item_bad_body(self):
+ req = webob.Request.blank('/v1.1/images/1/meta/key1')
+ req.environ['api.version'] = '1.1'
+ req.method = 'PUT'
+ req.body = '{"key1": "zz"}'
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, res.status_int)
+
+ def test_update_item_xml(self):
+ serializer = openstack.image_metadata.ImageMetadataXMLSerializer()
+ fixture = {
+ 'meta': {
+ 'one': 'two',
+ },
+ }
+ output = serializer.update(fixture)
+ actual = minidom.parseString(output.replace(" ", ""))
+
+ expected = minidom.parseString("""
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one">
+ two
+ </meta>
+ """.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
def test_update_item_too_many_keys(self):
req = webob.Request.blank('/v1.1/images/1/meta/key1')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
- req.body = '{"key1": "value1", "key2": "value2"}'
+ req.body = '{"meta": {"key1": "value1", "key2": "value2"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
@@ -159,7 +260,7 @@ class ImageMetaDataTest(unittest.TestCase):
req = webob.Request.blank('/v1.1/images/1/meta/bad')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
- req.body = '{"key1": "value1"}'
+ req.body = '{"meta": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
@@ -195,7 +296,7 @@ class ImageMetaDataTest(unittest.TestCase):
req = webob.Request.blank('/v1.1/images/3/meta/blah')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
- req.body = '{"blah": "blah"}'
+ req.body = '{"meta": {"blah": "blah"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index e4204809f..446d68e9e 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -618,7 +618,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 124,
'name': 'queued backup',
- 'serverId': 42,
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
@@ -626,7 +625,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 125,
'name': 'saving backup',
- 'serverId': 42,
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'SAVING',
@@ -635,7 +633,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 126,
'name': 'active backup',
- 'serverId': 42,
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE'
@@ -643,7 +640,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 127,
'name': 'killed backup',
- 'serverId': 42,
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
@@ -689,7 +685,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 124,
'name': 'queued backup',
- 'serverRef': "http://localhost/v1.1/servers/42",
+ 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
@@ -711,7 +707,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 125,
'name': 'saving backup',
- 'serverRef': "http://localhost/v1.1/servers/42",
+ 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'SAVING',
@@ -734,7 +730,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 126,
'name': 'active backup',
- 'serverRef': "http://localhost/v1.1/servers/42",
+ 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
@@ -756,7 +752,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{
'id': 127,
'name': 'killed backup',
- 'serverRef': "http://localhost/v1.1/servers/42",
+ 'serverRef': "http://localhost:8774/v1.1/servers/42",
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
@@ -1002,6 +998,30 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
self.assertEqual(200, response.status_int)
+ def test_create_image_v1_1_actual_server_ref(self):
+
+ serverRef = 'http://localhost/v1.1/servers/1'
+ body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
+ req = webob.Request.blank('/v1.1/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, response.status_int)
+ result = json.loads(response.body)
+ self.assertEqual(result['image']['serverRef'], serverRef)
+
+ def test_create_image_v1_1_server_ref_bad_hostname(self):
+
+ serverRef = 'http://asdf/v1.1/servers/1'
+ body = dict(image=dict(serverRef=serverRef, name='Backup 1'))
+ req = webob.Request.blank('/v1.1/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
def test_create_image_v1_1_xml_serialization(self):
body = dict(image=dict(serverRef='123', name='Backup 1'))
@@ -1018,7 +1038,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
<image
created="None"
id="123"
- name="None"
+ name="Backup 1"
serverRef="http://localhost/v1.1/servers/123"
status="ACTIVE"
updated="None"
@@ -1065,7 +1085,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
image_id += 1
# Backup for User 1
- backup_properties = {'instance_id': '42', 'user_id': '1'}
+ server_ref = 'http://localhost:8774/v1.1/servers/42'
+ backup_properties = {'instance_ref': server_ref, 'user_id': '1'}
for status in ('queued', 'saving', 'active', 'killed'):
add_fixture(id=image_id, name='%s backup' % status,
is_public=False, status=status,
diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py
index 01613d1d8..38c959fae 100644
--- a/nova/tests/api/openstack/test_limits.py
+++ b/nova/tests/api/openstack/test_limits.py
@@ -672,8 +672,7 @@ class WsgiLimiterTest(BaseLimitTestSuite):
"""Only POSTs should work."""
requests = []
for method in ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]:
- request = webob.Request.blank("/")
- request.body = self._request_data("GET", "/something")
+ request = webob.Request.blank("/", method=method)
response = request.get_response(self.app)
self.assertEqual(response.status_int, 405)
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index be9ecab1f..e318b7c57 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -1504,7 +1504,7 @@ class ServersTest(test.TestCase):
self.assertEqual(res.status_int, 400)
def test_resized_server_has_correct_status(self):
- req = self.webreq('/1', 'GET', dict(resize=dict(flavorId=3)))
+ req = self.webreq('/1', 'GET')
def fake_migration_get(*args):
return {}
diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py
index 2fa50ac9b..73a26a087 100644
--- a/nova/tests/api/openstack/test_wsgi.py
+++ b/nova/tests/api/openstack/test_wsgi.py
@@ -10,13 +10,13 @@ from nova.api.openstack import wsgi
class RequestTest(test.TestCase):
def test_content_type_missing(self):
- request = wsgi.Request.blank('/tests/123')
+ request = wsgi.Request.blank('/tests/123', method='POST')
request.body = "<body />"
self.assertRaises(exception.InvalidContentType,
request.get_content_type)
def test_content_type_unsupported(self):
- request = wsgi.Request.blank('/tests/123')
+ request = wsgi.Request.blank('/tests/123', method='POST')
request.headers["Content-Type"] = "text/html"
request.body = "asdf<br />"
self.assertRaises(exception.InvalidContentType,
diff --git a/nova/tests/test_adminapi.py b/nova/tests/test_adminapi.py
new file mode 100644
index 000000000..7ecaf1c09
--- /dev/null
+++ b/nova/tests/test_adminapi.py
@@ -0,0 +1,89 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+from eventlet import greenthread
+
+from nova import context
+from nova import db
+from nova import flags
+from nova import log as logging
+from nova import rpc
+from nova import test
+from nova import utils
+from nova.auth import manager
+from nova.api.ec2 import admin
+from nova.image import fake
+
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.tests.adminapi')
+
+
+class AdminApiTestCase(test.TestCase):
+ def setUp(self):
+ super(AdminApiTestCase, self).setUp()
+ self.flags(connection_type='fake')
+
+ self.conn = rpc.Connection.instance()
+
+ # set up our cloud
+ self.api = admin.AdminController()
+
+ # set up services
+ self.compute = self.start_service('compute')
+ self.scheduter = self.start_service('scheduler')
+ self.network = self.start_service('network')
+ self.volume = self.start_service('volume')
+ self.image_service = utils.import_object(FLAGS.image_service)
+
+ self.manager = manager.AuthManager()
+ self.user = self.manager.create_user('admin', 'admin', 'admin', True)
+ self.project = self.manager.create_project('proj', 'admin', 'proj')
+ self.context = context.RequestContext(user=self.user,
+ project=self.project)
+ host = self.network.get_network_host(self.context.elevated())
+
+ def fake_show(meh, context, id):
+ return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ 'type': 'machine', 'image_state': 'available'}}
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_show)
+ self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show)
+
+ # NOTE(vish): set up a manual wait so rpc.cast has a chance to finish
+ rpc_cast = rpc.cast
+
+ def finish_cast(*args, **kwargs):
+ rpc_cast(*args, **kwargs)
+ greenthread.sleep(0.2)
+
+ self.stubs.Set(rpc, 'cast', finish_cast)
+
+ def tearDown(self):
+ network_ref = db.project_get_network(self.context,
+ self.project.id)
+ db.network_disassociate(self.context, network_ref['id'])
+ self.manager.delete_project(self.project)
+ self.manager.delete_user(self.user)
+ super(AdminApiTestCase, self).tearDown()
+
+ def test_block_external_ips(self):
+ """Make sure provider firewall rules are created."""
+ result = self.api.block_external_addresses(self.context, '1.1.1.1/32')
+ self.assertEqual('OK', result['status'])
+ self.assertEqual('Added 3 rules', result['message'])
diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py
index bd1352c5e..1d61adb0e 100644
--- a/nova/tests/test_libvirt.py
+++ b/nova/tests/test_libvirt.py
@@ -821,7 +821,9 @@ class IptablesFirewallTestCase(test.TestCase):
self.network = utils.import_object(FLAGS.network_manager)
class FakeLibvirtConnection(object):
- pass
+ def nwfilterDefineXML(*args, **kwargs):
+ """setup_basic_rules in nwfilter calls this."""
+ pass
self.fake_libvirt_connection = FakeLibvirtConnection()
self.fw = firewall.IptablesFirewallDriver(
get_connection=lambda: self.fake_libvirt_connection)
@@ -1063,7 +1065,6 @@ class IptablesFirewallTestCase(test.TestCase):
fakefilter.filterDefineXMLMock
self.fw.nwfilter._conn.nwfilterLookupByName =\
fakefilter.nwfilterLookupByName
-
instance_ref = self._create_instance_ref()
inst_id = instance_ref['id']
instance = db.instance_get(self.context, inst_id)
@@ -1085,6 +1086,63 @@ class IptablesFirewallTestCase(test.TestCase):
db.instance_destroy(admin_ctxt, instance_ref['id'])
+ def test_provider_firewall_rules(self):
+ # setup basic instance data
+ instance_ref = self._create_instance_ref()
+ nw_info = _create_network_info(1)
+ ip = '10.11.12.13'
+ network_ref = db.project_get_network(self.context, 'fake')
+ admin_ctxt = context.get_admin_context()
+ fixed_ip = {'address': ip, 'network_id': network_ref['id']}
+ db.fixed_ip_create(admin_ctxt, fixed_ip)
+ db.fixed_ip_update(admin_ctxt, ip, {'allocated': True,
+ 'instance_id': instance_ref['id']})
+ # FRAGILE: peeks at how the firewall names chains
+ chain_name = 'inst-%s' % instance_ref['id']
+
+ # create a firewall via setup_basic_filtering like libvirt_conn.spawn
+ # should have a chain with 0 rules
+ self.fw.setup_basic_filtering(instance_ref, network_info=nw_info)
+ self.assertTrue('provider' in self.fw.iptables.ipv4['filter'].chains)
+ rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
+ if rule.chain == 'provider']
+ self.assertEqual(0, len(rules))
+
+ # add a rule and send the update message, check for 1 rule
+ provider_fw0 = db.provider_fw_rule_create(admin_ctxt,
+ {'protocol': 'tcp',
+ 'cidr': '10.99.99.99/32',
+ 'from_port': 1,
+ 'to_port': 65535})
+ self.fw.refresh_provider_fw_rules()
+ rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
+ if rule.chain == 'provider']
+ self.assertEqual(1, len(rules))
+
+ # Add another, refresh, and make sure number of rules goes to two
+ provider_fw1 = db.provider_fw_rule_create(admin_ctxt,
+ {'protocol': 'udp',
+ 'cidr': '10.99.99.99/32',
+ 'from_port': 1,
+ 'to_port': 65535})
+ self.fw.refresh_provider_fw_rules()
+ rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
+ if rule.chain == 'provider']
+ self.assertEqual(2, len(rules))
+
+ # create the instance filter and make sure it has a jump rule
+ self.fw.prepare_instance_filter(instance_ref, network_info=nw_info)
+ self.fw.apply_instance_filter(instance_ref)
+ inst_rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
+ if rule.chain == chain_name]
+ jump_rules = [rule for rule in inst_rules if '-j' in rule.rule]
+ provjump_rules = []
+ # IptablesTable doesn't make rules unique internally
+ for rule in jump_rules:
+ if 'provider' in rule.rule and rule not in provjump_rules:
+ provjump_rules.append(rule)
+ self.assertEqual(1, len(provjump_rules))
+
class NWFilterTestCase(test.TestCase):
def setUp(self):
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index 83ef2270d..1c9797973 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -191,6 +191,10 @@ class ComputeDriver(object):
def refresh_security_group_members(self, security_group_id):
raise NotImplementedError()
+ def refresh_provider_fw_rules(self, security_group_id):
+ """See: nova/virt/fake.py for docs."""
+ raise NotImplementedError()
+
def reset_network(self, instance):
"""reset networking for specified instance"""
raise NotImplementedError()
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 7ea2c9071..5fe9d674f 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -466,6 +466,22 @@ class FakeConnection(driver.ComputeDriver):
"""
return True
+ def refresh_provider_fw_rules(self):
+ """This triggers a firewall update based on database changes.
+
+ When this is called, rules have either been added or removed from the
+ datastore. You can retrieve rules with
+ :method:`nova.db.api.provider_fw_rule_get_all`.
+
+ Provider rules take precedence over security group rules. If an IP
+ would be allowed by a security group ingress rule, but blocked by
+ a provider rule, then packets from the IP are dropped. This includes
+ intra-project traffic in the case of the allow_project_net_traffic
+ flag for the libvirt-derived classes.
+
+ """
+ pass
+
def update_available_resource(self, ctxt, host):
"""This method is supported only by libvirt."""
return
diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py
index 9016933d6..394d60f71 100644
--- a/nova/virt/libvirt/connection.py
+++ b/nova/virt/libvirt/connection.py
@@ -1383,6 +1383,9 @@ class LibvirtConnection(driver.ComputeDriver):
def refresh_security_group_members(self, security_group_id):
self.firewall_driver.refresh_security_group_members(security_group_id)
+ def refresh_provider_fw_rules(self):
+ self.firewall_driver.refresh_provider_fw_rules()
+
def update_available_resource(self, ctxt, host):
"""Updates compute manager resource info on ComputeNode table.
diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py
index d990d6f15..379197398 100644
--- a/nova/virt/libvirt/firewall.py
+++ b/nova/virt/libvirt/firewall.py
@@ -76,6 +76,15 @@ class FirewallDriver(object):
the security group."""
raise NotImplementedError()
+ def refresh_provider_fw_rules(self):
+ """Refresh common rules for all hosts/instances from data store.
+
+ Gets called when a rule has been added to or removed from
+ the list of rules (via admin api).
+
+ """
+ raise NotImplementedError()
+
def setup_basic_filtering(self, instance, network_info=None):
"""Create rules to block spoofing and allow dhcp.
@@ -207,6 +216,13 @@ class NWFilterFirewall(FirewallDriver):
[base_filter]))
def _ensure_static_filters(self):
+ """Static filters are filters that have no need to be IP aware.
+
+ There is no configuration or tuneability of these filters, so they
+ can be set up once and forgotten about.
+
+ """
+
if self.static_filters_configured:
return
@@ -310,19 +326,21 @@ class NWFilterFirewall(FirewallDriver):
'for %(instance_name)s is not found.') % locals())
def prepare_instance_filter(self, instance, network_info=None):
- """
- Creates an NWFilter for the given instance. In the process,
- it makes sure the filters for the security groups as well as
- the base filter are all in place.
+ """Creates an NWFilter for the given instance.
+
+ In the process, it makes sure the filters for the provider blocks,
+ security groups, and base filter are all in place.
+
"""
if not network_info:
network_info = netutils.get_network_info(instance)
+ self.refresh_provider_fw_rules()
+
ctxt = context.get_admin_context()
instance_secgroup_filter_name = \
'%s-secgroup' % (self._instance_filter_name(instance))
- #% (instance_filter_name,)
instance_secgroup_filter_children = ['nova-base-ipv4',
'nova-base-ipv6',
@@ -366,7 +384,7 @@ class NWFilterFirewall(FirewallDriver):
for (_n, mapping) in network_info:
nic_id = mapping['mac'].replace(':', '')
instance_filter_name = self._instance_filter_name(instance, nic_id)
- instance_filter_children = [base_filter,
+ instance_filter_children = [base_filter, 'nova-provider-rules',
instance_secgroup_filter_name]
if FLAGS.allow_project_net_traffic:
@@ -388,6 +406,19 @@ class NWFilterFirewall(FirewallDriver):
return self._define_filter(
self.security_group_to_nwfilter_xml(security_group_id))
+ def refresh_provider_fw_rules(self):
+ """Update rules for all instances.
+
+ This is part of the FirewallDriver API and is called when the
+ provider firewall rules change in the database. In the
+ `prepare_instance_filter` we add a reference to the
+ 'nova-provider-rules' filter for each instance's firewall, and
+ by changing that filter we update them all.
+
+ """
+ xml = self.provider_fw_to_nwfilter_xml()
+ return self._define_filter(xml)
+
def security_group_to_nwfilter_xml(self, security_group_id):
security_group = db.security_group_get(context.get_admin_context(),
security_group_id)
@@ -426,6 +457,43 @@ class NWFilterFirewall(FirewallDriver):
xml += "chain='ipv4'>%s</filter>" % rule_xml
return xml
+ def provider_fw_to_nwfilter_xml(self):
+ """Compose a filter of drop rules from specified cidrs."""
+ rule_xml = ""
+ v6protocol = {'tcp': 'tcp-ipv6', 'udp': 'udp-ipv6', 'icmp': 'icmpv6'}
+ rules = db.provider_fw_rule_get_all(context.get_admin_context())
+ for rule in rules:
+ rule_xml += "<rule action='block' direction='in' priority='150'>"
+ version = netutils.get_ip_version(rule.cidr)
+ if(FLAGS.use_ipv6 and version == 6):
+ net, prefixlen = netutils.get_net_and_prefixlen(rule.cidr)
+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
+ (v6protocol[rule.protocol], net, prefixlen)
+ else:
+ net, mask = netutils.get_net_and_mask(rule.cidr)
+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
+ (rule.protocol, net, mask)
+ if rule.protocol in ['tcp', 'udp']:
+ rule_xml += "dstportstart='%s' dstportend='%s' " % \
+ (rule.from_port, rule.to_port)
+ elif rule.protocol == 'icmp':
+ LOG.info('rule.protocol: %r, rule.from_port: %r, '
+ 'rule.to_port: %r', rule.protocol,
+ rule.from_port, rule.to_port)
+ if rule.from_port != -1:
+ rule_xml += "type='%s' " % rule.from_port
+ if rule.to_port != -1:
+ rule_xml += "code='%s' " % rule.to_port
+
+ rule_xml += '/>\n'
+ rule_xml += "</rule>\n"
+ xml = "<filter name='nova-provider-rules' "
+ if(FLAGS.use_ipv6):
+ xml += "chain='root'>%s</filter>" % rule_xml
+ else:
+ xml += "chain='ipv4'>%s</filter>" % rule_xml
+ return xml
+
def _instance_filter_name(self, instance, nic_id=None):
if not nic_id:
return 'nova-instance-%s' % (instance['name'])
@@ -453,6 +521,7 @@ class IptablesFirewallDriver(FirewallDriver):
self.iptables = linux_net.iptables_manager
self.instances = {}
self.nwfilter = NWFilterFirewall(kwargs['get_connection'])
+ self.basicly_filtered = False
self.iptables.ipv4['filter'].add_chain('sg-fallback')
self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP')
@@ -460,10 +529,14 @@ class IptablesFirewallDriver(FirewallDriver):
self.iptables.ipv6['filter'].add_rule('sg-fallback', '-j DROP')
def setup_basic_filtering(self, instance, network_info=None):
- """Use NWFilter from libvirt for this."""
+ """Set up provider rules and basic NWFilter."""
if not network_info:
network_info = netutils.get_network_info(instance)
- return self.nwfilter.setup_basic_filtering(instance, network_info)
+ self.nwfilter.setup_basic_filtering(instance, network_info)
+ if not self.basicly_filtered:
+ LOG.debug(_('iptables firewall: Setup Basic Filtering'))
+ self.refresh_provider_fw_rules()
+ self.basicly_filtered = True
def apply_instance_filter(self, instance):
"""No-op. Everything is done in prepare_instance_filter"""
@@ -543,6 +616,10 @@ class IptablesFirewallDriver(FirewallDriver):
ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
+ # Pass through provider-wide drops
+ ipv4_rules += ['-j $provider']
+ ipv6_rules += ['-j $provider']
+
dhcp_servers = [info['gateway'] for (_n, info) in network_info]
for dhcp_server in dhcp_servers:
@@ -583,7 +660,7 @@ class IptablesFirewallDriver(FirewallDriver):
security_group['id'])
for rule in rules:
- logging.info('%r', rule)
+ LOG.debug(_('Adding security group rule: %r'), rule)
if not rule.cidr:
# Eventually, a mechanism to grant access for security
@@ -592,9 +669,9 @@ class IptablesFirewallDriver(FirewallDriver):
version = netutils.get_ip_version(rule.cidr)
if version == 4:
- rules = ipv4_rules
+ fw_rules = ipv4_rules
else:
- rules = ipv6_rules
+ fw_rules = ipv6_rules
protocol = rule.protocol
if version == 6 and rule.protocol == 'icmp':
@@ -629,7 +706,7 @@ class IptablesFirewallDriver(FirewallDriver):
icmp_type_arg]
args += ['-j ACCEPT']
- rules += [' '.join(args)]
+ fw_rules += [' '.join(args)]
ipv4_rules += ['-j $sg-fallback']
ipv6_rules += ['-j $sg-fallback']
@@ -657,6 +734,85 @@ class IptablesFirewallDriver(FirewallDriver):
network_info = netutils.get_network_info(instance)
self.add_filters_for_instance(instance, network_info)
+ def refresh_provider_fw_rules(self):
+ """See class:FirewallDriver: docs."""
+ self._do_refresh_provider_fw_rules()
+ self.iptables.apply()
+
+ @utils.synchronized('iptables', external=True)
+ def _do_refresh_provider_fw_rules(self):
+ """Internal, synchronized version of refresh_provider_fw_rules."""
+ self._purge_provider_fw_rules()
+ self._build_provider_fw_rules()
+
+ def _purge_provider_fw_rules(self):
+ """Remove all rules from the provider chains."""
+ self.iptables.ipv4['filter'].empty_chain('provider')
+ if FLAGS.use_ipv6:
+ self.iptables.ipv6['filter'].empty_chain('provider')
+
+ def _build_provider_fw_rules(self):
+ """Create all rules for the provider IP DROPs."""
+ self.iptables.ipv4['filter'].add_chain('provider')
+ if FLAGS.use_ipv6:
+ self.iptables.ipv6['filter'].add_chain('provider')
+ ipv4_rules, ipv6_rules = self._provider_rules()
+ for rule in ipv4_rules:
+ self.iptables.ipv4['filter'].add_rule('provider', rule)
+
+ if FLAGS.use_ipv6:
+ for rule in ipv6_rules:
+ self.iptables.ipv6['filter'].add_rule('provider', rule)
+
+ def _provider_rules(self):
+ """Generate a list of rules from provider for IP4 & IP6."""
+ ctxt = context.get_admin_context()
+ ipv4_rules = []
+ ipv6_rules = []
+ rules = db.provider_fw_rule_get_all(ctxt)
+ for rule in rules:
+ LOG.debug(_('Adding provider rule: %s'), rule['cidr'])
+ version = netutils.get_ip_version(rule['cidr'])
+ if version == 4:
+ fw_rules = ipv4_rules
+ else:
+ fw_rules = ipv6_rules
+
+ protocol = rule['protocol']
+ if version == 6 and protocol == 'icmp':
+ protocol = 'icmpv6'
+
+ args = ['-p', protocol, '-s', rule['cidr']]
+
+ if protocol in ['udp', 'tcp']:
+ if rule['from_port'] == rule['to_port']:
+ args += ['--dport', '%s' % (rule['from_port'],)]
+ else:
+ args += ['-m', 'multiport',
+ '--dports', '%s:%s' % (rule['from_port'],
+ rule['to_port'])]
+ elif protocol == 'icmp':
+ icmp_type = rule['from_port']
+ icmp_code = rule['to_port']
+
+ if icmp_type == -1:
+ icmp_type_arg = None
+ else:
+ icmp_type_arg = '%s' % icmp_type
+ if not icmp_code == -1:
+ icmp_type_arg += '/%s' % icmp_code
+
+ if icmp_type_arg:
+ if version == 4:
+ args += ['-m', 'icmp', '--icmp-type',
+ icmp_type_arg]
+ elif version == 6:
+ args += ['-m', 'icmp6', '--icmpv6-type',
+ icmp_type_arg]
+ args += ['-j DROP']
+ fw_rules += [' '.join(args)]
+ return ipv4_rules, ipv6_rules
+
def _security_group_chain_name(self, security_group_id):
return 'nova-sg-%s' % (security_group_id,)
diff --git a/tools/pip-requires b/tools/pip-requires
index 7849dbea9..0da473109 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -15,7 +15,7 @@ python-daemon==1.5.5
python-gflags==1.3
redis==2.0.0
routes==1.12.3
-WebOb==0.9.8
+WebOb==1.0.8
wsgiref==0.1.2
mox==0.5.3
greenlet==0.3.1