
1. What is the problem Shared vlan type driver has been merged, so we can run two VMs in the same network but across two pods. However if we attach a network to the router, the tricircle plugin still check if the network is bound to one AZ, so one network cannot cross different pods if we are going to attach it to a router. 2. What is the solution to the problem The reason we require network to be bound to AZ is that when the network is attaching the one router, we know where to create the bottom network resources. To support l3 networking in shared vlan network, we need to remove this restriction. In the previous patches[1, 2], we have already move bottom router setup to an asynchronouse job, so we just remove the AZ restriction and make the tricircle plugin and the nova_apigw to use the job. Floating ip association and disassociation are also moved to bottom router setup job. 3. What the features need to be implemented to the Tricircle to realize the solution Now network can be attached to a router without specifying AZ, so l3 networking can work with across-pod network. [1] https://review.openstack.org/#/c/343568/ [2] https://review.openstack.org/#/c/345863/ Change-Id: I9aaf908a5de55575d63533f1574a0a6edb3c66b8
680 lines
29 KiB
Python
680 lines
29 KiB
Python
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
|
|
# 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.
|
|
|
|
import netaddr
|
|
import pecan
|
|
from pecan import expose
|
|
from pecan import rest
|
|
import six
|
|
|
|
import oslo_log.log as logging
|
|
|
|
import neutronclient.common.exceptions as q_exceptions
|
|
|
|
from tricircle.common import az_ag
|
|
import tricircle.common.client as t_client
|
|
from tricircle.common import constants
|
|
import tricircle.common.context as t_context
|
|
import tricircle.common.exceptions as t_exceptions
|
|
from tricircle.common.i18n import _
|
|
from tricircle.common.i18n import _LE
|
|
import tricircle.common.lock_handle as t_lock
|
|
from tricircle.common.quota import QUOTAS
|
|
from tricircle.common import utils
|
|
from tricircle.common import xrpcapi
|
|
import tricircle.db.api as db_api
|
|
from tricircle.db import core
|
|
from tricircle.db import models
|
|
from tricircle.network import helper
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
MAX_METADATA_KEY_LENGTH = 255
|
|
MAX_METADATA_VALUE_LENGTH = 255
|
|
|
|
|
|
class ServerController(rest.RestController):
|
|
|
|
def __init__(self, project_id):
|
|
self.project_id = project_id
|
|
self.clients = {constants.TOP: t_client.Client()}
|
|
self.helper = helper.NetworkHelper()
|
|
self.xjob_handler = xrpcapi.XJobAPI()
|
|
|
|
def _get_client(self, pod_name=constants.TOP):
|
|
if pod_name not in self.clients:
|
|
self.clients[pod_name] = t_client.Client(pod_name)
|
|
return self.clients[pod_name]
|
|
|
|
def _get_all(self, context, params):
|
|
filters = [{'key': key,
|
|
'comparator': 'eq',
|
|
'value': value} for key, value in params.iteritems()]
|
|
ret = []
|
|
pods = db_api.list_pods(context)
|
|
for pod in pods:
|
|
if not pod['az_name']:
|
|
continue
|
|
client = self._get_client(pod['pod_name'])
|
|
servers = client.list_servers(context, filters=filters)
|
|
self._remove_fip_info(servers)
|
|
ret.extend(servers)
|
|
return ret
|
|
|
|
@staticmethod
|
|
def _construct_brief_server_entry(server):
|
|
return {'id': server['id'],
|
|
'name': server.get('name'),
|
|
'links': server.get('links')}
|
|
|
|
@staticmethod
|
|
def _transform_network_name(server):
|
|
if 'addresses' not in server:
|
|
return
|
|
keys = [key for key in server['addresses'].iterkeys()]
|
|
for key in keys:
|
|
value = server['addresses'].pop(key)
|
|
network_name = key.split('#')[1]
|
|
server['addresses'][network_name] = value
|
|
return server
|
|
|
|
@expose(generic=True, template='json')
|
|
def get_one(self, _id, **kwargs):
|
|
context = t_context.extract_context_from_environ()
|
|
|
|
if _id == 'detail':
|
|
return {'servers': [self._transform_network_name(
|
|
server) for server in self._get_all(context, kwargs)]}
|
|
|
|
mappings = db_api.get_bottom_mappings_by_top_id(
|
|
context, _id, constants.RT_SERVER)
|
|
if not mappings:
|
|
return utils.format_nova_error(
|
|
404, _('Instance %s could not be found.') % _id)
|
|
pod, bottom_id = mappings[0]
|
|
client = self._get_client(pod['pod_name'])
|
|
server = client.get_servers(context, bottom_id)
|
|
if not server:
|
|
return utils.format_nova_error(
|
|
404, _('Instance %s could not be found.') % _id)
|
|
else:
|
|
self._transform_network_name(server)
|
|
return {'server': server}
|
|
|
|
@expose(generic=True, template='json')
|
|
def get_all(self, **kwargs):
|
|
context = t_context.extract_context_from_environ()
|
|
return {'servers': [self._construct_brief_server_entry(
|
|
server) for server in self._get_all(context, kwargs)]}
|
|
|
|
@expose(generic=True, template='json')
|
|
def post(self, **kw):
|
|
context = t_context.extract_context_from_environ()
|
|
|
|
if 'server' not in kw:
|
|
return utils.format_nova_error(
|
|
400, _('server is not set'))
|
|
|
|
az = kw['server'].get('availability_zone', '')
|
|
|
|
pod, b_az = az_ag.get_pod_by_az_tenant(
|
|
context, az, self.project_id)
|
|
if not pod:
|
|
return utils.format_nova_error(
|
|
500, _('Pod not configured or scheduling failure'))
|
|
|
|
t_server_dict = kw['server']
|
|
self._process_metadata_quota(context, t_server_dict)
|
|
self._process_injected_file_quota(context, t_server_dict)
|
|
|
|
server_body = self._get_create_server_body(kw['server'], b_az)
|
|
|
|
top_client = self._get_client()
|
|
|
|
sg_filters = [{'key': 'tenant_id', 'comparator': 'eq',
|
|
'value': self.project_id}]
|
|
top_sgs = top_client.list_security_groups(context, sg_filters)
|
|
top_sg_map = dict((sg['name'], sg) for sg in top_sgs)
|
|
|
|
if 'security_groups' not in kw['server']:
|
|
security_groups = ['default']
|
|
else:
|
|
security_groups = []
|
|
for sg in kw['server']['security_groups']:
|
|
if 'name' not in sg:
|
|
return utils.format_nova_error(
|
|
400, _('Invalid input for field/attribute'))
|
|
if sg['name'] not in top_sg_map:
|
|
return utils.format_nova_error(
|
|
400, _('Unable to find security_group with name or id '
|
|
'%s') % sg['name'])
|
|
security_groups.append(sg['name'])
|
|
t_sg_ids, b_sg_ids, is_news = self._handle_security_group(
|
|
context, pod, top_sg_map, security_groups)
|
|
|
|
server_body['networks'] = []
|
|
if 'networks' in kw['server']:
|
|
for net_info in kw['server']['networks']:
|
|
if 'uuid' in net_info:
|
|
network = top_client.get_networks(context,
|
|
net_info['uuid'])
|
|
if not network:
|
|
return utils.format_nova_error(
|
|
400, _('Network %s could not be '
|
|
'found') % net_info['uuid'])
|
|
|
|
if not self._check_network_server_az_match(
|
|
context, network,
|
|
kw['server']['availability_zone']):
|
|
return utils.format_nova_error(
|
|
400, _('Network and server not in the same '
|
|
'availability zone'))
|
|
|
|
subnets = top_client.list_subnets(
|
|
context, [{'key': 'network_id',
|
|
'comparator': 'eq',
|
|
'value': network['id']}])
|
|
if not subnets:
|
|
return utils.format_nova_error(
|
|
400, _('Network does not contain any subnets'))
|
|
t_port_id, b_port_id = self._handle_network(
|
|
context, pod, network, subnets,
|
|
top_sg_ids=t_sg_ids, bottom_sg_ids=b_sg_ids)
|
|
elif 'port' in net_info:
|
|
port = top_client.get_ports(context, net_info['port'])
|
|
if not port:
|
|
return utils.format_nova_error(
|
|
400, _('Port %s could not be '
|
|
'found') % net_info['port'])
|
|
t_port_id, b_port_id = self._handle_port(
|
|
context, pod, port)
|
|
server_body['networks'].append({'port': b_port_id})
|
|
|
|
# only for security group first created in a pod, we invoke
|
|
# _handle_sg_rule_for_new_group to initialize rules in that group, this
|
|
# method removes all the rules in the new group then add new rules
|
|
top_sg_id_map = dict((sg['id'], sg) for sg in top_sgs)
|
|
new_top_sgs = []
|
|
new_bottom_sg_ids = []
|
|
default_sg = None
|
|
for t_id, b_id, is_new in zip(t_sg_ids, b_sg_ids, is_news):
|
|
sg_name = top_sg_id_map[t_id]['name']
|
|
if sg_name == 'default':
|
|
default_sg = top_sg_id_map[t_id]
|
|
continue
|
|
if not is_new:
|
|
continue
|
|
new_top_sgs.append(top_sg_id_map[t_id])
|
|
new_bottom_sg_ids.append(b_id)
|
|
self._handle_sg_rule_for_new_group(context, pod, new_top_sgs,
|
|
new_bottom_sg_ids)
|
|
if default_sg:
|
|
self._handle_sg_rule_for_default_group(
|
|
context, pod, default_sg, self.project_id)
|
|
|
|
client = self._get_client(pod['pod_name'])
|
|
nics = [
|
|
{'port-id': _port['port']} for _port in server_body['networks']]
|
|
|
|
server = client.create_servers(context,
|
|
name=server_body['name'],
|
|
image=server_body['imageRef'],
|
|
flavor=server_body['flavorRef'],
|
|
nics=nics,
|
|
security_groups=b_sg_ids)
|
|
with context.session.begin():
|
|
core.create_resource(context, models.ResourceRouting,
|
|
{'top_id': server['id'],
|
|
'bottom_id': server['id'],
|
|
'pod_id': pod['pod_id'],
|
|
'project_id': self.project_id,
|
|
'resource_type': constants.RT_SERVER})
|
|
pecan.response.status = 202
|
|
return {'server': server}
|
|
|
|
@expose(generic=True, template='json')
|
|
def delete(self, _id):
|
|
context = t_context.extract_context_from_environ()
|
|
|
|
mappings = db_api.get_bottom_mappings_by_top_id(context, _id,
|
|
constants.RT_SERVER)
|
|
if not mappings:
|
|
pecan.response.status = 404
|
|
return {'Error': {'message': _('Server not found'), 'code': 404}}
|
|
|
|
pod, bottom_id = mappings[0]
|
|
client = self._get_client(pod['pod_name'])
|
|
top_client = self._get_client()
|
|
try:
|
|
server_ports = top_client.list_ports(
|
|
context, filters=[{'key': 'device_id', 'comparator': 'eq',
|
|
'value': _id}])
|
|
ret = client.delete_servers(context, bottom_id)
|
|
# none return value indicates server not found
|
|
if ret is None:
|
|
self._remove_stale_mapping(context, _id)
|
|
pecan.response.status = 404
|
|
return {'Error': {'message': _('Server not found'),
|
|
'code': 404}}
|
|
for server_port in server_ports:
|
|
self.xjob_handler.delete_server_port(context,
|
|
server_port['id'])
|
|
except Exception as e:
|
|
code = 500
|
|
message = _('Delete server %(server_id)s fails') % {
|
|
'server_id': _id}
|
|
if hasattr(e, 'code'):
|
|
code = e.code
|
|
ex_message = str(e)
|
|
if ex_message:
|
|
message = ex_message
|
|
LOG.error(message)
|
|
|
|
pecan.response.status = code
|
|
return {'Error': {'message': message, 'code': code}}
|
|
|
|
# NOTE(zhiyuan) Security group rules for default security group are
|
|
# also kept until subnet is deleted.
|
|
pecan.response.status = 204
|
|
return pecan.response
|
|
|
|
def _get_or_create_route(self, context, pod, _id, _type):
|
|
def list_resources(t_ctx, q_ctx, pod_, ele, _type_):
|
|
client = self._get_client(pod_['pod_name'])
|
|
return client.list_resources(_type_, t_ctx, [{'key': 'name',
|
|
'comparator': 'eq',
|
|
'value': ele['id']}])
|
|
|
|
return t_lock.get_or_create_route(context, None,
|
|
self.project_id, pod, {'id': _id},
|
|
_type, list_resources)
|
|
|
|
def _handle_router(self, context, pod, net):
|
|
top_client = self._get_client()
|
|
|
|
interfaces = top_client.list_ports(
|
|
context, filters=[{'key': 'network_id',
|
|
'comparator': 'eq',
|
|
'value': net['id']},
|
|
{'key': 'device_owner',
|
|
'comparator': 'eq',
|
|
'value': 'network:router_interface'}])
|
|
interfaces = [inf for inf in interfaces if inf['device_id']]
|
|
if not interfaces:
|
|
return
|
|
# TODO(zhiyuan) change xjob invoking from "cast" to "call" to guarantee
|
|
# the job can be successfully registered
|
|
self.xjob_handler.setup_bottom_router(
|
|
context, net['id'], interfaces[0]['device_id'], pod['pod_id'])
|
|
|
|
def _handle_network(self, context, pod, net, subnets, port=None,
|
|
top_sg_ids=None, bottom_sg_ids=None):
|
|
(bottom_net_id,
|
|
subnet_map) = self.helper.prepare_bottom_network_subnets(
|
|
context, None, self.project_id, pod, net, subnets)
|
|
|
|
top_client = self._get_client()
|
|
top_port_body = {'port': {'network_id': net['id'],
|
|
'admin_state_up': True}}
|
|
if top_sg_ids:
|
|
top_port_body['port']['security_groups'] = top_sg_ids
|
|
|
|
# port
|
|
if not port:
|
|
port = top_client.create_ports(context, top_port_body)
|
|
port_body = self.helper.get_create_port_body(
|
|
self.project_id, port, subnet_map, bottom_net_id,
|
|
bottom_sg_ids)
|
|
else:
|
|
port_body = self.helper.get_create_port_body(
|
|
self.project_id, port, subnet_map, bottom_net_id)
|
|
_, bottom_port_id = self.helper.prepare_bottom_element(
|
|
context, self.project_id, pod, port, constants.RT_PORT, port_body)
|
|
|
|
self._handle_router(context, pod, net)
|
|
|
|
return port['id'], bottom_port_id
|
|
|
|
def _handle_port(self, context, pod, port):
|
|
top_client = self._get_client()
|
|
# NOTE(zhiyuan) at this moment, it is possible that the bottom port has
|
|
# been created. if user creates a port and associate it with a floating
|
|
# ip before booting a vm, tricircle plugin will create the bottom port
|
|
# first in order to setup floating ip in bottom pod. but it is still
|
|
# safe for us to use network id and subnet id in the returned port dict
|
|
# since tricircle plugin will do id mapping and guarantee ids in the
|
|
# dict are top id.
|
|
net = top_client.get_networks(context, port['network_id'])
|
|
subnets = []
|
|
for fixed_ip in port['fixed_ips']:
|
|
subnets.append(top_client.get_subnets(context,
|
|
fixed_ip['subnet_id']))
|
|
return self._handle_network(context, pod, net, subnets, port=port)
|
|
|
|
@staticmethod
|
|
def _safe_create_security_group_rule(context, client, body):
|
|
try:
|
|
client.create_security_group_rules(context, body)
|
|
except q_exceptions.Conflict:
|
|
return
|
|
|
|
@staticmethod
|
|
def _safe_delete_security_group_rule(context, client, _id):
|
|
try:
|
|
client.delete_security_group_rules(context, _id)
|
|
except q_exceptions.NotFound:
|
|
return
|
|
|
|
def _handle_security_group(self, context, pod, top_sg_map,
|
|
security_groups):
|
|
t_sg_ids = []
|
|
b_sg_ids = []
|
|
is_news = []
|
|
for sg_name in security_groups:
|
|
t_sg = top_sg_map[sg_name]
|
|
sg_body = {
|
|
'security_group': {
|
|
'name': t_sg['id'],
|
|
'description': t_sg['description']}}
|
|
is_new, b_sg_id = self.helper.prepare_bottom_element(
|
|
context, self.project_id, pod, t_sg, constants.RT_SG, sg_body)
|
|
t_sg_ids.append(t_sg['id'])
|
|
is_news.append(is_new)
|
|
b_sg_ids.append(b_sg_id)
|
|
|
|
return t_sg_ids, b_sg_ids, is_news
|
|
|
|
@staticmethod
|
|
def _construct_bottom_rule(rule, sg_id, ip=None):
|
|
ip = ip or rule['remote_ip_prefix']
|
|
# if ip is passed, this is a extended rule for remote group
|
|
return {'remote_group_id': None,
|
|
'direction': rule['direction'],
|
|
'remote_ip_prefix': ip,
|
|
'protocol': rule.get('protocol'),
|
|
'ethertype': rule['ethertype'],
|
|
'port_range_max': rule.get('port_range_max'),
|
|
'port_range_min': rule.get('port_range_min'),
|
|
'security_group_id': sg_id}
|
|
|
|
@staticmethod
|
|
def _compare_rule(rule1, rule2):
|
|
for key in ('direction', 'remote_ip_prefix', 'protocol', 'ethertype',
|
|
'port_range_max', 'port_range_min'):
|
|
if rule1[key] != rule2[key]:
|
|
return False
|
|
return True
|
|
|
|
def _handle_sg_rule_for_default_group(self, context, pod, default_sg,
|
|
project_id):
|
|
top_client = self._get_client()
|
|
new_b_rules = []
|
|
for t_rule in default_sg['security_group_rules']:
|
|
if not t_rule['remote_group_id']:
|
|
# leave sg_id empty here
|
|
new_b_rules.append(
|
|
self._construct_bottom_rule(t_rule, ''))
|
|
continue
|
|
if t_rule['ethertype'] != 'IPv4':
|
|
continue
|
|
subnets = top_client.list_subnets(
|
|
context, [{'key': 'tenant_id', 'comparator': 'eq',
|
|
'value': project_id}])
|
|
bridge_ip_net = netaddr.IPNetwork('100.0.0.0/8')
|
|
for subnet in subnets:
|
|
ip_net = netaddr.IPNetwork(subnet['cidr'])
|
|
if ip_net in bridge_ip_net:
|
|
continue
|
|
# leave sg_id empty here
|
|
new_b_rules.append(
|
|
self._construct_bottom_rule(t_rule, '',
|
|
subnet['cidr']))
|
|
|
|
mappings = db_api.get_bottom_mappings_by_top_id(
|
|
context, default_sg['id'], constants.RT_SG)
|
|
for pod, b_sg_id in mappings:
|
|
client = self._get_client(pod['pod_name'])
|
|
b_sg = client.get_security_groups(context, b_sg_id)
|
|
add_rules = []
|
|
del_rules = []
|
|
match_index = set()
|
|
for b_rule in b_sg['security_group_rules']:
|
|
match = False
|
|
for i, rule in enumerate(new_b_rules):
|
|
if self._compare_rule(b_rule, rule):
|
|
match = True
|
|
match_index.add(i)
|
|
break
|
|
if not match:
|
|
del_rules.append(b_rule)
|
|
for i, rule in enumerate(new_b_rules):
|
|
if i not in match_index:
|
|
add_rules.append(rule)
|
|
|
|
for del_rule in del_rules:
|
|
self._safe_delete_security_group_rule(
|
|
context, client, del_rule['id'])
|
|
if add_rules:
|
|
rule_body = {'security_group_rules': []}
|
|
for add_rule in add_rules:
|
|
add_rule['security_group_id'] = b_sg_id
|
|
rule_body['security_group_rules'].append(add_rule)
|
|
self._safe_create_security_group_rule(context,
|
|
client, rule_body)
|
|
|
|
def _handle_sg_rule_for_new_group(self, context, pod, top_sgs,
|
|
bottom_sg_ids):
|
|
client = self._get_client(pod['pod_name'])
|
|
for i, t_sg in enumerate(top_sgs):
|
|
b_sg_id = bottom_sg_ids[i]
|
|
new_b_rules = []
|
|
for t_rule in t_sg['security_group_rules']:
|
|
if t_rule['remote_group_id']:
|
|
# we do not handle remote group rule for non-default
|
|
# security group, actually tricircle plugin in neutron
|
|
# will reject such rule
|
|
# default security group is not passed with top_sgs so
|
|
# t_rule will not belong to default security group
|
|
continue
|
|
new_b_rules.append(
|
|
self._construct_bottom_rule(t_rule, b_sg_id))
|
|
try:
|
|
b_sg = client.get_security_groups(context, b_sg_id)
|
|
for b_rule in b_sg['security_group_rules']:
|
|
self._safe_delete_security_group_rule(
|
|
context, client, b_rule['id'])
|
|
if new_b_rules:
|
|
rule_body = {'security_group_rules': new_b_rules}
|
|
self._safe_create_security_group_rule(context, client,
|
|
rule_body)
|
|
except Exception:
|
|
# if we fails when operating bottom security group rule, we
|
|
# update the security group mapping to set bottom_id to None
|
|
# and expire the mapping, so next time the security group rule
|
|
# operations can be redone
|
|
with context.session.begin():
|
|
routes = core.query_resource(
|
|
context, models.ResourceRouting,
|
|
[{'key': 'top_id', 'comparator': 'eq',
|
|
'value': t_sg['id']},
|
|
{'key': 'bottom_id', 'comparator': 'eq',
|
|
'value': b_sg_id}], [])
|
|
update_dict = {'bottom_id': None,
|
|
'created_at': constants.expire_time,
|
|
'updated_at': constants.expire_time}
|
|
core.update_resource(context, models.ResourceRouting,
|
|
routes[0]['id'], update_dict)
|
|
raise
|
|
|
|
@staticmethod
|
|
def _get_create_server_body(origin, bottom_az):
|
|
body = {}
|
|
copy_fields = ['name', 'imageRef', 'flavorRef',
|
|
'max_count', 'min_count']
|
|
if bottom_az:
|
|
body['availability_zone'] = bottom_az
|
|
for field in copy_fields:
|
|
if field in origin:
|
|
body[field] = origin[field]
|
|
return body
|
|
|
|
@staticmethod
|
|
def _remove_fip_info(servers):
|
|
for server in servers:
|
|
if 'addresses' not in server:
|
|
continue
|
|
for addresses in server['addresses'].values():
|
|
remove_index = -1
|
|
for i, address in enumerate(addresses):
|
|
if address.get('OS-EXT-IPS:type') == 'floating':
|
|
remove_index = i
|
|
break
|
|
if remove_index >= 0:
|
|
del addresses[remove_index]
|
|
|
|
@staticmethod
|
|
def _remove_stale_mapping(context, server_id):
|
|
filters = [{'key': 'top_id', 'comparator': 'eq', 'value': server_id},
|
|
{'key': 'resource_type',
|
|
'comparator': 'eq',
|
|
'value': constants.RT_SERVER}]
|
|
with context.session.begin():
|
|
core.delete_resources(context,
|
|
models.ResourceRouting,
|
|
filters)
|
|
|
|
@staticmethod
|
|
def _check_network_server_az_match(context, network, server_az):
|
|
az_hints = 'availability_zone_hints'
|
|
network_type = 'provider:network_type'
|
|
|
|
# for local type network, we make sure it's created in only one az
|
|
|
|
# NOTE(zhiyuan) race condition exists when creating vms in the same
|
|
# local type network but different azs at the same time
|
|
if network.get(network_type) == constants.NT_LOCAL:
|
|
mappings = db_api.get_bottom_mappings_by_top_id(
|
|
context, network['id'], constants.RT_NETWORK)
|
|
if mappings:
|
|
pod, _ = mappings[0]
|
|
if pod['az_name'] != server_az:
|
|
return False
|
|
# if neutron az not assigned, server az is used
|
|
if not network.get(az_hints):
|
|
return True
|
|
if server_az in network[az_hints]:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def _process_injected_file_quota(self, context, t_server_dict):
|
|
try:
|
|
ctx = context.elevated()
|
|
injected_files = t_server_dict.get('injected_files', None)
|
|
self._check_injected_file_quota(ctx, injected_files)
|
|
except (t_exceptions.OnsetFileLimitExceeded,
|
|
t_exceptions.OnsetFilePathLimitExceeded,
|
|
t_exceptions.OnsetFileContentLimitExceeded) as e:
|
|
msg = str(e)
|
|
LOG.exception(_LE('Quota exceeded %(msg)s'),
|
|
{'msg': msg})
|
|
return utils.format_nova_error(400, _('Quota exceeded %s') % msg)
|
|
|
|
def _check_injected_file_quota(self, context, injected_files):
|
|
"""Enforce quota limits on injected files.
|
|
|
|
Raises a QuotaError if any limit is exceeded.
|
|
|
|
"""
|
|
|
|
if injected_files is None:
|
|
return
|
|
|
|
# Check number of files first
|
|
try:
|
|
QUOTAS.limit_check(context,
|
|
injected_files=len(injected_files))
|
|
except t_exceptions.OverQuota:
|
|
raise t_exceptions.OnsetFileLimitExceeded()
|
|
|
|
# OK, now count path and content lengths; we're looking for
|
|
# the max...
|
|
max_path = 0
|
|
max_content = 0
|
|
for path, content in injected_files:
|
|
max_path = max(max_path, len(path))
|
|
max_content = max(max_content, len(content))
|
|
|
|
try:
|
|
QUOTAS.limit_check(context,
|
|
injected_file_path_bytes=max_path,
|
|
injected_file_content_bytes=max_content)
|
|
except t_exceptions.OverQuota as exc:
|
|
# Favor path limit over content limit for reporting
|
|
# purposes
|
|
if 'injected_file_path_bytes' in exc.kwargs['overs']:
|
|
raise t_exceptions.OnsetFilePathLimitExceeded()
|
|
else:
|
|
raise t_exceptions.OnsetFileContentLimitExceeded()
|
|
|
|
def _process_metadata_quota(self, context, t_server_dict):
|
|
try:
|
|
ctx = context.elevated()
|
|
metadata = t_server_dict.get('metadata', None)
|
|
self._check_metadata_properties_quota(ctx, metadata)
|
|
except t_exceptions.InvalidMetadata as e1:
|
|
LOG.exception(_LE('Invalid metadata %(exception)s'),
|
|
{'exception': str(e1)})
|
|
return utils.format_nova_error(400, _('Invalid metadata'))
|
|
except t_exceptions.InvalidMetadataSize as e2:
|
|
LOG.exception(_LE('Invalid metadata size %(exception)s'),
|
|
{'exception': str(e2)})
|
|
return utils.format_nova_error(400, _('Invalid metadata size'))
|
|
except t_exceptions.MetadataLimitExceeded as e3:
|
|
LOG.exception(_LE('Quota exceeded %(exception)s'),
|
|
{'exception': str(e3)})
|
|
return utils.format_nova_error(400,
|
|
_('Quota exceeded in metadata'))
|
|
|
|
def _check_metadata_properties_quota(self, context, metadata=None):
|
|
"""Enforce quota limits on metadata properties."""
|
|
if not metadata:
|
|
metadata = {}
|
|
if not isinstance(metadata, dict):
|
|
msg = (_("Metadata type should be dict."))
|
|
raise t_exceptions.InvalidMetadata(reason=msg)
|
|
num_metadata = len(metadata)
|
|
try:
|
|
QUOTAS.limit_check(context, metadata_items=num_metadata)
|
|
except t_exceptions.OverQuota as exc:
|
|
quota_metadata = exc.kwargs['quotas']['metadata_items']
|
|
raise t_exceptions.MetadataLimitExceeded(allowed=quota_metadata)
|
|
|
|
# Because metadata is processed in the bottom pod, we just do
|
|
# parameter validation here to ensure quota management
|
|
for k, v in six.iteritems(metadata):
|
|
try:
|
|
utils.check_string_length(v)
|
|
utils.check_string_length(k, min_len=1)
|
|
except t_exceptions.InvalidInput as e:
|
|
raise t_exceptions.InvalidMetadata(reason=str(e))
|
|
|
|
if len(k) > MAX_METADATA_KEY_LENGTH:
|
|
msg = _("Metadata property key greater than 255 characters")
|
|
raise t_exceptions.InvalidMetadataSize(reason=msg)
|
|
if len(v) > MAX_METADATA_VALUE_LENGTH:
|
|
msg = _("Metadata property value greater than 255 characters")
|
|
raise t_exceptions.InvalidMetadataSize(reason=msg)
|