trio2o/trio2o/nova_apigw/controllers/server.py

337 lines
13 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 pecan
from pecan import expose
from pecan import rest
import six
import oslo_log.log as logging
from trio2o.common import az_ag
import trio2o.common.client as t_client
from trio2o.common import constants
import trio2o.common.context as t_context
import trio2o.common.exceptions as t_exceptions
from trio2o.common.i18n import _
from trio2o.common.i18n import _LE
import trio2o.common.lock_handle as t_lock
from trio2o.common.quota import QUOTAS
from trio2o.common import utils
from trio2o.common import xrpcapi
import trio2o.db.api as db_api
from trio2o.db import core
from trio2o.db import models
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.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)
ret.extend(servers)
return ret
@staticmethod
def _construct_brief_server_entry(server):
return {'id': server['id'],
'name': server.get('name'),
'links': server.get('links')}
@expose(generic=True, template='json')
def get_one(self, _id, **kwargs):
context = t_context.extract_context_from_environ()
if _id == 'detail':
# return {'servers': [self._construct_brief_server_entry(
# server) for server in self._get_all(context, kwargs)]}
return {'servers': 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:
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)]}
return {'servers': 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)
security_groups = []
if 'security_groups' not in kw['server']:
security_groups = ['default']
else:
for sg in kw['server']['security_groups']:
if 'name' not in sg:
return utils.format_nova_error(
400, _('Invalid input for field/attribute'))
security_groups.append(sg['name'])
server_body['networks'] = []
if 'networks' in kw['server']:
for net_info in kw['server']['networks']:
if 'uuid' in net_info:
nic = {'net-id': net_info['uuid']}
server_body['networks'].append(nic)
elif 'port' in net_info:
nic = {'port-id': net_info['port']}
server_body['networks'].append(nic)
client = self._get_client(pod['pod_name'])
server = client.create_servers(
context,
name=server_body['name'],
image=server_body['imageRef'],
flavor=server_body['flavorRef'],
nics=server_body['networks'],
security_groups=security_groups)
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'])
try:
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}}
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}}
pecan.response.status = 204
return pecan.response
def _get_or_create_route(self, context, pod_name, _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_name, {'id': _id},
_type, list_resources)
@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_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)
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)