gce-api/gceapi/api/common.py

390 lines
14 KiB
Python

# Copyright 2013 Cloudscaling Group, Inc
#
# 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.
"""Base GCE API controller"""
import os.path
import re
from webob import exc
from gceapi.api import operation_api
from gceapi.api import operation_util
from gceapi.api import scopes
from gceapi.api import utils
from gceapi import exception
from gceapi.openstack.common.gettextutils import _
from gceapi.openstack.common import timeutils
class Controller(object):
"""Base controller
Implements base CRUD methods.
Individual GCE controllers should inherit this and:
- implement format_item() method,
- override _get_type() method,
- add necessary specific request handlers,
- use _api to hold instance of related GCE API (see base_api.py).
"""
_api = None
# Initialization
def __init__(self, api):
"""Base initialization.
Inherited classes should init _api and call super().
"""
self._api = api
self._type_name = self._api._get_type()
self._collection_name = utils.get_collection_name(self._type_name)
self._type_kind = utils.get_type_kind(self._type_name)
self._list_kind = utils.get_list_kind(self._type_name)
self._aggregated_kind = utils.get_aggregated_kind(self._type_name)
self._operation_api = operation_api.API()
def process_result(self, request, action, action_result):
context = self._get_context(request)
operation = operation_util.save_operaton(context, action_result)
if operation is not None:
scope = self._operation_api.get_scopes(context, operation)[0]
action_result = self._format_operation(request, operation, scope)
if isinstance(action_result, Exception):
return self._format_error(action_result)
if action_result is None:
return None, 204
return self._format_output(request, action, action_result), 200
# Base methods, should be overriden
def format_item(self, request, image, scope):
"""Main item resource conversion routine
Overriden in inherited classes should implement conversion of
OpenStack resource into GCE resource.
"""
raise exc.HTTPNotImplemented
# Actions
def index(self, req, scope_id=None):
"""GCE list requests, global or with zone/region specified."""
context = self._get_context(req)
scope = self._get_scope(req, scope_id)
items = self._api.get_items(context, scope)
items = [{
"scope": scope,
"item": self.format_item(req, i, scope)
} for i in items]
items = self._filter_items(req, items)
items, next_page_token = self._page_items(req, items)
items = [i["item"] for i in items]
return self._format_list(req, items, next_page_token, scope)
def show(self, req, id=None, scope_id=None):
"""GCE get requests, global or zone/region specified."""
context = self._get_context(req)
scope = self._get_scope(req, scope_id)
try:
item = self._api.get_item(context, id, scope)
return self.format_item(req, item, scope)
except (exception.NotFound, KeyError, IndexError):
msg = _("Resource '%s' could not be found") % id
raise exc.HTTPNotFound(explanation=msg)
def aggregated_list(self, req):
"""GCE aggregated list requests for all zones/regions."""
context = self._get_context(req)
items = list()
for item in self._api.get_items(context, None):
for scope in self._api.get_scopes(context, item):
items.append({
"scope": scope,
"item": self.format_item(req, item, scope)
})
items = self._filter_items(req, items)
items, next_page_token = self._page_items(req, items)
items_by_scopes = {}
for item in items:
scope_path = item["scope"].get_path()
items_by_scope = items_by_scopes.setdefault(scope_path,
{self._collection_name: []})[self._collection_name]
items_by_scope.append(item["item"])
return self._format_list(req, items_by_scopes, next_page_token,
scopes.AggregatedScope())
def delete(self, req, id, scope_id=None):
"""GCE delete requests."""
scope = self._get_scope(req, scope_id)
context = self._get_context(req)
operation_util.init_operation(context, "delete",
self._type_name, id, scope)
try:
self._api.delete_item(context, id, scope)
except (exception.NotFound, KeyError, IndexError):
msg = _("Resource '%s' could not be found") % id
raise exc.HTTPNotFound(explanation=msg)
def create(self, req, body, scope_id=None):
"""GCE add requests."""
scope = self._get_scope(req, scope_id)
context = self._get_context(req)
operation_util.init_operation(context, "insert",
self._type_name, body["name"], scope)
self._api.add_item(context, body['name'], body, scope)
# Filtering
def _filter_items(self, req, items):
"""Filtering result list
Only one filter is supported(eg. by one field)
Only two comparison strings are supported: 'eq' and 'ne'
There are no logical expressions with fields
"""
if not items:
return items
if "filter" not in req.params:
return items
filter_def = req.params["filter"].split()
if len(filter_def) != 3:
# TODO(apavlov): raise exception
return items
if filter_def[1] != "eq" and filter_def[1] != "ne":
# TODO(apavlov): raise exception
return items
if filter_def[0] not in items[0]["item"]:
# TODO(apavlov): raise exception
return items
filter_field = filter_def[0]
filter_cmp = filter_def[1] == "eq"
filter_pattern = filter_def[2]
if filter_pattern[0] == "'" and filter_pattern[-1] == "'":
filter_pattern = filter_pattern[1:-1]
result_list = list()
for item in items:
field = item["item"][filter_field]
result = re.match(filter_pattern, field)
if filter_cmp != (result is None):
result_list.append(item)
return result_list
# Paging
def _page_items(self, req, items):
if not items:
return items, None
if "maxResults" not in req.params:
return items, None
limit = int(req.params["maxResults"])
if limit >= len(items):
return items, None
page_index = int(req.params.get("pageToken", 0))
if page_index < 0 or page_index * limit > len(items):
# TODO(apavlov): raise exception
return [], None
items.sort(None, lambda x: x["item"].get("name"))
start = limit * page_index
if start + limit >= len(items):
return items[start:], None
return items[start:start + limit], str(page_index + 1)
# Utility
def _get_context(self, req):
return req.environ['gceapi.context']
def _get_scope(self, req, scope_id):
scope = scopes.construct_from_path(req.path_info, scope_id)
if scope is None:
return None
scope_api = scope.get_scope_api()
if scope_api is not None:
try:
context = self._get_context(req)
scope_api.get_item(context, scope.get_name(), None)
except ValueError as ex:
raise exc.HTTPNotFound(detail=ex)
return scope
# Result formatting
def _format_date(self, date_string):
"""Returns standard format for given date."""
if date_string is None:
return None
if isinstance(date_string, basestring):
date_string = timeutils.parse_isotime(date_string)
return date_string.strftime('%Y-%m-%dT%H:%M:%SZ')
def _get_id(self, link):
hashed_link = hash(link)
if hashed_link < 0:
hashed_link = -hashed_link
return str(hashed_link)
def _qualify(self, request, controller, identifier, scope):
"""Creates fully qualified selfLink for an item or collection
Specific formatting for projects and zones/regions,
'global' prefix For global resources,
'zones/zone_id' prefix for zone(similar for regions) resources.
"""
result = os.path.join(
request.application_url, self._get_context(request).project_name)
if controller:
if scope:
result = os.path.join(result, scope.get_path())
result = os.path.join(result, controller)
if identifier:
result = os.path.join(result, identifier)
return result
def _format_item(self, request, result_dict, scope):
return self._add_item_header(request, result_dict, scope,
self._type_kind, self._collection_name)
def _format_operation(self, request, operation, scope):
result_dict = {
"name": operation["name"],
"operationType": operation["type"],
"insertTime": operation["insert_time"],
"startTime": operation["start_time"],
"status": operation["status"],
"progress": operation["progress"],
"user": operation["user"],
}
result_dict["targetLink"] = self._qualify(
request, utils.get_collection_name(operation["target_type"]),
operation["target_name"], scope)
result_dict["targetId"] = self._get_id(result_dict["targetLink"])
if "end_time" in operation:
result_dict["endTime"] = operation["end_time"]
if "error_code" in operation:
result_dict.update({
"httpErrorStatusCode": operation["error_code"],
"httpErrorMessage": operation["error_message"],
"error": {"errors": operation["errors"]},
})
type_name = self._operation_api._get_type()
return self._add_item_header(request, result_dict, scope,
utils.get_type_kind(type_name),
utils.get_collection_name(type_name))
def _add_item_header(self, request, result_dict, scope,
_type_kind, _collection_name):
if scope is not None and scope.get_name() is not None:
result_dict[scope.get_type()] = self._qualify(
request, scope.get_collection(), scope.get_name(), None)
result_dict["kind"] = _type_kind
result_dict["selfLink"] = self._qualify(
request, _collection_name, result_dict.get("name"), scope)
result_dict["id"] = self._get_id(result_dict["selfLink"])
return result_dict
def _format_list(self, request, result_list, next_page_token, scope):
result_dict = {}
result_dict["items"] = result_list
if next_page_token:
result_dict["nextPageToken"] = next_page_token
result_dict["kind"] = (self._aggregated_kind
if scope and isinstance(scope, scopes.AggregatedScope)
else self._list_kind)
context = self._get_context(request)
list_id = os.path.join("projects", context.project_name)
if scope:
list_id = os.path.join(list_id, scope.get_path())
list_id = os.path.join(list_id, self._collection_name)
result_dict["id"] = list_id
result_dict["selfLink"] = self._qualify(
request, self._collection_name, None, scope)
return result_dict
def _format_error(self, ex_value):
if isinstance(ex_value, exception.NotAuthorized):
msg = _('Unauthorized')
code = 401
elif isinstance(ex_value, exc.HTTPException):
msg = ex_value.explanation
code = ex_value.code
elif isinstance(ex_value, exception.GceapiException):
msg = ex_value.args[0]
code = ex_value.code
else:
msg = _('Internal server error')
code = 500
return {
'error': {'errors': [{'message': msg}]},
'code': code,
'message': msg
}, code
def _format_output(self, request, action, action_result):
# TODO(ft): this metod must be safe and ignore unknown fields
fields = request.params.get('fields', None)
# TODO(ft): GCE can also format results of other action
if action not in ('index', 'show') or fields is None:
return action_result
if action == 'show':
action_result = utils.apply_template(fields, action_result)
return action_result
sp = utils.split_by_comma(fields)
top_level = []
items = []
for string in sp:
if 'items' in string:
items.append(string)
else:
top_level.append(string)
res = {}
if len(items) > 0:
res['items'] = []
for string in top_level:
dct = utils.apply_template(string, action_result)
for key, val in dct.items():
res[key] = val
for string in items:
if '(' in string:
dct = utils.apply_template(string, action_result)
for key, val in dct.items():
res[key] = val
elif string.startswith('items/'):
string = string[len('items/'):]
for element in action_result['items']:
dct = utils.apply_template(string, element)
res['items'].append(dct)
return res