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): | ||||
|     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,16 +282,17 @@ class ComputeManager(manager.SchedulerDependentManager): | ||||
|                                    'networking') | ||||
|  | ||||
|         is_vpn = instance['image_ref'] == str(FLAGS.vpn_image_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) | ||||
|                                                          instance, vpn=is_vpn) | ||||
|                 LOG.debug(_("instance network_info: |%s|"), network_info) | ||||
|             self.network_manager.setup_compute_network(context, instance_id) | ||||
|                 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 | ||||
| @@ -294,14 +300,13 @@ class ComputeManager(manager.SchedulerDependentManager): | ||||
|                 # all vif creation and network injection, maybe this is correct | ||||
|                 network_info = [] | ||||
|  | ||||
|         block_device_mapping = self._setup_block_device_mapping(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, block_device_mapping) | ||||
|                 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: " | ||||
| @@ -310,6 +315,12 @@ class ComputeManager(manager.SchedulerDependentManager): | ||||
|  | ||||
|             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,6 +821,8 @@ class IptablesFirewallTestCase(test.TestCase): | ||||
|         self.network = utils.import_object(FLAGS.network_manager) | ||||
|  | ||||
|         class FakeLibvirtConnection(object): | ||||
|             def nwfilterDefineXML(*args, **kwargs): | ||||
|                 """setup_basic_rules in nwfilter calls this.""" | ||||
|                 pass | ||||
|         self.fake_libvirt_connection = FakeLibvirtConnection() | ||||
|         self.fw = firewall.IptablesFirewallDriver( | ||||
| @@ -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
	 Trey Morris
					Trey Morris