# 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)