trunk merge
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
# TODO(vish) check to make sure the availability zone matches
|
||||
self._update_state(context, instance_id, power_state.BUILDING)
|
||||
|
||||
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)
|
||||
# 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 = []
|
||||
|
||||
self._update_launched_at(context, instance_id)
|
||||
self._update_state(context, instance_id)
|
||||
bd_mapping = self._setup_block_device_mapping(context, instance_id)
|
||||
|
||||
# TODO(vish) check to make sure the availability zone matches
|
||||
self._update_state(context, instance_id, power_state.BUILDING)
|
||||
|
||||
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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2413,6 +2413,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:
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 = '{"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.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(200, res.status_int)
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEqual('zz', res_dict['key1'])
|
||||
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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
89
nova/tests/test_adminapi.py
Normal file
89
nova/tests/test_adminapi.py
Normal file
@@ -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'])
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user