diff --git a/reddwarf/common/context.py b/reddwarf/common/context.py index 3763d8cff7..04dc75fc66 100644 --- a/reddwarf/common/context.py +++ b/reddwarf/common/context.py @@ -34,6 +34,10 @@ class ReddwarfContext(context.RequestContext): """ def __init__(self, **kwargs): + self.limit = kwargs['limit'] + self.marker = kwargs['marker'] + del kwargs['limit'] + del kwargs['marker'] super(ReddwarfContext, self).__init__(**kwargs) def to_dict(self): @@ -43,6 +47,8 @@ class ReddwarfContext(context.RequestContext): 'show_deleted': self.show_deleted, 'read_only': self.read_only, 'auth_tok': self.auth_tok, + 'limit': self.limit, + 'marker': self.marker } @classmethod diff --git a/reddwarf/common/pagination.py b/reddwarf/common/pagination.py new file mode 100644 index 0000000000..832b7cd392 --- /dev/null +++ b/reddwarf/common/pagination.py @@ -0,0 +1,97 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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 urllib +import urlparse +from xml.dom import minidom + + +class PaginatedDataView(object): + + def __init__(self, collection_type, collection, current_page_url, + next_page_marker=None): + self.collection_type = collection_type + self.collection = collection + self.current_page_url = current_page_url + self.next_page_marker = next_page_marker + + def data(self): + return {self.collection_type: self.collection, + 'links': self._links, + } + + def _links(self): + if not self.next_page_marker: + return [] + app_url = AppUrl(self.current_page_url) + next_url = app_url.change_query_params(marker=self.next_page_marker) + next_link = {'rel': 'next', + 'href': str(next_url), + } + return [next_link] + + +class SimplePaginatedDataView(object): + # In some cases, we can't create a PaginatedDataView because + # we don't have a collection query object to create a view on. + # In that case, we have to supply the URL and collection manually. + + def __init__(self, url, name, view, marker): + self.url = url + self.name = name + self.view = view + self.marker = marker + + def data(self): + if not self.marker: + return self.view.data() + + app_url = AppUrl(self.url) + next_url = str(app_url.change_query_params(marker=self.marker)) + next_link = {'rel': 'next', + 'href': next_url} + view_data = {self.name: self.view.data()[self.name], + 'links': [next_link]} + return view_data + + +class AppUrl(object): + + def __init__(self, url): + self.url = url + + def __str__(self): + return self.url + + def change_query_params(self, **kwargs): + # Seeks out the query params in a URL and changes/appends to them + # from the kwargs given. So change_query_params(foo='bar') + # would remove from the URL any old instance of foo=something and + # then add &foo=bar to the URL. + parsed_url = urlparse.urlparse(self.url) + # Build a dictionary out of the query parameters in the URL + query_params = dict(urlparse.parse_qsl(parsed_url.query)) + # Use kwargs to change or update any values in the query dict. + query_params.update(kwargs) + + # Build a new query based on the updated query dict. + new_query_params = urllib.urlencode(query_params) + return self.__class__( + urlparse.ParseResult(parsed_url.scheme, + parsed_url.netloc, parsed_url.path, + parsed_url.params, new_query_params, + parsed_url.fragment).geturl()) diff --git a/reddwarf/common/remote.py b/reddwarf/common/remote.py index 70ba0dd0c3..05ab61a2b9 100644 --- a/reddwarf/common/remote.py +++ b/reddwarf/common/remote.py @@ -38,7 +38,7 @@ def create_nova_client(context): 'http://0.0.0.0:5000/v2.0') client = Client(context.user, context.auth_tok, project_id=context.tenant, auth_url=PROXY_AUTH_URL) - client.client.auth_token=context.auth_tok + client.client.auth_token = context.auth_tok client.client.management_url = "%s/%s/" % (COMPUTE_URL, context.tenant) return client @@ -52,8 +52,8 @@ def create_nova_volume_client(context): 'http://0.0.0.0:5000/v2.0') client = Client(context.user, context.auth_tok, project_id=context.tenant, auth_url=PROXY_AUTH_URL) - client.client.auth_token=context.auth_tok - client.client.management_url="%s/%s/" % (VOLUME_URL, context.tenant) + client.client.auth_token = context.auth_tok + client.client.management_url = "%s/%s/" % (VOLUME_URL, context.tenant) return client diff --git a/reddwarf/common/wsgi.py b/reddwarf/common/wsgi.py index 3c3c4ccdbe..ab60921622 100644 --- a/reddwarf/common/wsgi.py +++ b/reddwarf/common/wsgi.py @@ -265,12 +265,18 @@ class ContextMiddleware(openstack_wsgi.Middleware): def __init__(self, application): super(ContextMiddleware, self).__init__(application) + def _extract_limits(self, params): + return dict([(key, params[key]) for key in params.keys() + if key in ["limit", "marker"]]) + def process_request(self, request): tenant_id = request.headers.get('X-Tenant-Id', None) - auth_tok = request.headers['X-Auth-Token'] - user = request.headers.get('X-User', None) - context = rd_context.ReddwarfContext(auth_tok=auth_tok, user=user, - tenant=tenant_id) + auth_tok = request.headers["X-Auth-Token"] + limits = self._extract_limits(request.params) + context = rd_context.ReddwarfContext(auth_tok=auth_tok, + tenant=tenant_id, + limit=limits.get('limit'), + marker=limits.get('marker')) request.environ[CONTEXT_KEY] = context @classmethod diff --git a/reddwarf/db/__init__.py b/reddwarf/db/__init__.py index cdf9bf1797..623910d763 100644 --- a/reddwarf/db/__init__.py +++ b/reddwarf/db/__init__.py @@ -54,21 +54,19 @@ class Query(object): def delete(self): db_api.delete_all(self._query_func, self._model, **self._conditions) - #TODO(hub-cap): Reenable pagination when we have a need for it - # def limit(self, limit=200, marker=None, marker_column=None): - # return db_api.find_all_by_limit(self._query_func, - # self._model, - # self._conditions, - # limit=limit, - # marker=marker, - # marker_column=marker_column) - # - # def paginated_collection(self, limit=200, marker=None, - # marker_column=None): - # collection = self.limit(int(limit) + 1, marker, marker_column) - # if len(collection) > int(limit): - # return (collection[0:-1], collection[-2]['id']) - # return (collection, None) + def limit(self, limit=200, marker=None, marker_column=None): + return db_api.find_all_by_limit(self._query_func, + self._model, + self._conditions, + limit=limit, + marker=marker, + marker_column=marker_column) + + def paginated_collection(self, limit=200, marker=None, marker_column=None): + collection = self.limit(int(limit) + 1, marker, marker_column) + if len(collection) > int(limit): + return (collection[0:-1], collection[-2]['id']) + return (collection, None) class Queryable(object): diff --git a/reddwarf/extensions/mysql/models.py b/reddwarf/extensions/mysql/models.py index bfeb58b3ef..ca4cec206b 100644 --- a/reddwarf/extensions/mysql/models.py +++ b/reddwarf/extensions/mysql/models.py @@ -157,10 +157,16 @@ class RootHistory(object): class Users(object): + DEFAULT_LIMIT = int(config.Config.get('users_page_size', '20')) + @classmethod def load(cls, context, instance_id): load_and_verify(context, instance_id) - user_list = create_guest_client(context, instance_id).list_users() + limit = int(context.limit or Users.DEFAULT_LIMIT) + limit = Users.DEFAULT_LIMIT if limit > Users.DEFAULT_LIMIT else limit + client = create_guest_client(context, instance_id) + user_list, next_marker = client.list_users(limit=limit, + marker=context.marker) model_users = [] for user in user_list: mysql_user = guest_models.MySQLUser() @@ -173,7 +179,7 @@ class Users(object): model_users.append(User(mysql_user.name, mysql_user.password, dbs)) - return model_users + return model_users, next_marker class Schema(object): @@ -198,10 +204,17 @@ class Schema(object): class Schemas(object): + DEFAULT_LIMIT = int(config.Config.get('databases_page_size', '20')) + @classmethod def load(cls, context, instance_id): load_and_verify(context, instance_id) - schemas = create_guest_client(context, instance_id).list_databases() + limit = int(context.limit or Schemas.DEFAULT_LIMIT) + if limit > Schemas.DEFAULT_LIMIT: + limit = Schemas.DEFAULT_LIMIT + client = create_guest_client(context, instance_id) + schemas, next_marker = client.list_databases(limit=limit, + marker=context.marker) model_schemas = [] for schema in schemas: mysql_schema = guest_models.MySQLDatabase() @@ -209,4 +222,4 @@ class Schemas(object): model_schemas.append(Schema(mysql_schema.name, mysql_schema.collate, mysql_schema.character_set)) - return model_schemas + return model_schemas, next_marker diff --git a/reddwarf/extensions/mysql/service.py b/reddwarf/extensions/mysql/service.py index df8129a318..3aedec9411 100644 --- a/reddwarf/extensions/mysql/service.py +++ b/reddwarf/extensions/mysql/service.py @@ -19,6 +19,7 @@ import logging import webob.exc from reddwarf.common import exception +from reddwarf.common import pagination from reddwarf.common import wsgi from reddwarf.guestagent.db import models as guest_models from reddwarf.instance import models as instance_models @@ -56,6 +57,10 @@ class BaseController(wsgi.Controller): return utils.stringify_keys(utils.exclude(model_params, *self.exclude_attr)) + def _extract_limits(self, params): + return dict([(key, params[key]) for key in params.keys() + if key in ["limit", "marker"]]) + class RootController(BaseController): """Controller for instance functionality""" @@ -101,8 +106,11 @@ class UserController(BaseController): LOG.info(_("Listing users for instance '%s'") % instance_id) LOG.info(_("req : '%s'\n\n") % req) context = req.environ[wsgi.CONTEXT_KEY] - users = models.Users.load(context, instance_id) - return wsgi.Result(views.UsersView(users).data(), 200) + users, next_marker = models.Users.load(context, instance_id) + view = views.UsersView(users) + paged = pagination.SimplePaginatedDataView(req.url, 'users', view, + next_marker) + return wsgi.Result(paged.data(), 200) def create(self, req, body, tenant_id, instance_id): """Creates a set of users""" @@ -145,9 +153,11 @@ class SchemaController(BaseController): LOG.info(_("Listing schemas for instance '%s'") % instance_id) LOG.info(_("req : '%s'\n\n") % req) context = req.environ[wsgi.CONTEXT_KEY] - schemas = models.Schemas.load(context, instance_id) - # Not exactly sure why we cant return a wsgi.Result() here - return wsgi.Result(views.SchemasView(schemas).data(), 200) + schemas, next_marker = models.Schemas.load(context, instance_id) + view = views.SchemasView(schemas) + paged = pagination.SimplePaginatedDataView(req.url, 'databases', view, + next_marker) + return wsgi.Result(paged.data(), 200) def create(self, req, body, tenant_id, instance_id): """Creates a set of schemas""" diff --git a/reddwarf/guestagent/api.py b/reddwarf/guestagent/api.py index 4a7f0f31c2..66de33977b 100644 --- a/reddwarf/guestagent/api.py +++ b/reddwarf/guestagent/api.py @@ -39,9 +39,12 @@ class API(object): self.id = id def _call(self, method_name, **kwargs): + LOG.debug("Calling %s" % method_name) try: - return rpc.call(self.context, self._get_routing_key(), + result = rpc.call(self.context, self._get_routing_key(), {"method": method_name, "args": kwargs}) + LOG.debug("Result is %s" % result) + return result except Exception as e: LOG.error(e) raise exception.GuestError(original_message=str(e)) @@ -72,10 +75,10 @@ class API(object): LOG.debug(_("Creating Users for Instance %s"), self.id) self._cast("create_user", users=users) - def list_users(self): + def list_users(self, limit=None, marker=None): """Make an asynchronous call to list database users""" LOG.debug(_("Listing Users for Instance %s"), self.id) - return self._call("list_users") + return self._call("list_users", limit=limit, marker=marker) def delete_user(self, user): """Make an asynchronous call to delete an existing database user""" @@ -88,10 +91,10 @@ class API(object): LOG.debug(_("Creating databases for Instance %s"), self.id) self._cast("create_database", databases=databases) - def list_databases(self): + def list_databases(self, limit=None, marker=None): """Make an asynchronous call to list databases""" LOG.debug(_("Listing databases for Instance %s"), self.id) - return self._call("list_databases") + return self._call("list_databases", limit=limit, marker=marker) def delete_database(self, database): """Make an asynchronous call to delete an existing database diff --git a/reddwarf/guestagent/dbaas.py b/reddwarf/guestagent/dbaas.py index d118b42504..80013e021c 100644 --- a/reddwarf/guestagent/dbaas.py +++ b/reddwarf/guestagent/dbaas.py @@ -47,6 +47,7 @@ from reddwarf.common import config from reddwarf.common import utils from reddwarf.guestagent.db import models from reddwarf.guestagent.volume import VolumeDevice +from reddwarf.guestagent.query import Query from reddwarf.instance import models as rd_models @@ -390,7 +391,7 @@ class MySqlAdmin(object): LOG.debug("result = " + str(result)) return result.rowcount != 0 - def list_databases(self): + def list_databases(self, limit=None, marker=None): """List databases the user created on this mysql instance""" LOG.debug(_("---Listing Databases---")) databases = [] @@ -400,51 +401,74 @@ class MySqlAdmin(object): # the lost+found directory will show up in mysql as a database # which will create errors if you try to do any database ops # on it. So we remove it here if it exists. - t = text(''' - SELECT - schema_name as name, - default_character_set_name as charset, - default_collation_name as collation - FROM - information_schema.schemata - WHERE - schema_name not in - ('mysql', 'information_schema', - 'lost+found', '#mysql50#lost+found') - ORDER BY - schema_name ASC; - ''') + q = Query() + q.columns = [ + 'schema_name as name', + 'default_character_set_name as charset', + 'default_collation_name as collation', + ] + q.tables = ['information_schema.schemata'] + q.where = ['''schema_name not in ( + 'mysql', 'information_schema', + 'lost+found', '#mysql50#lost+found' + )'''] + q.order = ['schema_name ASC'] + if limit: + q.limit = limit + 1 + if marker: + q.where.append("schema_name > '%s'" % marker) + t = text(str(q)) database_names = client.execute(t) + next_marker = None LOG.debug(_("database_names = %r") % database_names) - for database in database_names: + for count, database in enumerate(database_names): + if count >= limit: + break LOG.debug(_("database = %s ") % str(database)) mysql_db = models.MySQLDatabase() mysql_db.name = database[0] + next_marker = mysql_db.name mysql_db.character_set = database[1] mysql_db.collate = database[2] databases.append(mysql_db.serialize()) LOG.debug(_("databases = ") + str(databases)) - return databases + if database_names.rowcount <= limit: + next_marker = None + return databases, next_marker - def list_users(self): + def list_users(self, limit=None, marker=None): """List users that have access to the database""" LOG.debug(_("---Listing Users---")) users = [] client = LocalSqlClient(get_engine()) with client: mysql_user = models.MySQLUser() - t = text("""select User from mysql.user where host != - 'localhost';""") + q = Query() + q.columns = ['User'] + q.tables = ['mysql.user'] + q.where = ["host != 'localhost'"] + q.order = ['User'] + if marker: + q.where.append("User > '%s'" % marker) + if limit: + q.limit = limit + 1 + t = text(str(q)) result = client.execute(t) + next_marker = None LOG.debug("result = " + str(result)) - for row in result: + for count, row in enumerate(result): + if count >= limit: + break LOG.debug("user = " + str(row)) mysql_user = models.MySQLUser() mysql_user.name = row['User'] + next_marker = row['User'] # Now get the databases - t = text("""SELECT grantee, table_schema - from information_schema.SCHEMA_PRIVILEGES - group by grantee, table_schema;""") + q = Query() + q.columns = ['grantee', 'table_schema'] + q.tables = ['information_schema.SCHEMA_PRIVILEGES'] + q.group = ['grantee', 'table_schema'] + t = text(str(q)) db_result = client.execute(t) for db in db_result: matches = re.match("^'(.+)'@", db['grantee']) @@ -454,8 +478,10 @@ class MySqlAdmin(object): mysql_db.name = db['table_schema'] mysql_user.databases.append(mysql_db.serialize()) users.append(mysql_user.serialize()) + if result.rowcount <= limit: + next_marker = None LOG.debug("users = " + str(users)) - return users + return users, next_marker class DBaaSAgent(object): @@ -479,11 +505,11 @@ class DBaaSAgent(object): def delete_user(self, user): MySqlAdmin().delete_user(user) - def list_databases(self): - return MySqlAdmin().list_databases() + def list_databases(self, limit=None, marker=None): + return MySqlAdmin().list_databases(limit, marker) - def list_users(self): - return MySqlAdmin().list_users() + def list_users(self, limit=None, marker=None): + return MySqlAdmin().list_users(limit, marker) def enable_root(self): return MySqlAdmin().enable_root() diff --git a/reddwarf/guestagent/query.py b/reddwarf/guestagent/query.py new file mode 100644 index 0000000000..406dfb74ac --- /dev/null +++ b/reddwarf/guestagent/query.py @@ -0,0 +1,71 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 OpenStack, LLC. +# 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. + +""" + +Intermediary class for building SQL queries for use by the guest agent. + +""" + + +class Query(object): + + def __init__(self, columns=[], tables=[], where=[], order=[], limit=0): + self.columns = columns + self.tables = tables + self.where = where + self.order = order + self.limit = limit + + @property + def _columns(self): + return ', '.join(self.columns) if self.columns else "*" + + @property + def _tables(self): + return ', '.join(self.tables) + + @property + def _where(self): + if not self.where: + return "" + return "WHERE %s" % (" AND ".join(self.where)) + + @property + def _order(self): + if not self.order: + return '' + return "ORDER BY %s" % (', '.join(self.order)) + + @property + def _limit(self): + if not self.limit: + return '' + return "LIMIT %s" % str(self.limit) + + def __str__(self): + query = [ + "SELECT %s" % self._columns, + "FROM %s" % self._tables, + self._where, + self._order, + self._limit + ] + return '\n'.join(query) + + def __repr__(self): + return str(self) diff --git a/reddwarf/instance/models.py b/reddwarf/instance/models.py index 4f5a372ad5..70ca6da466 100644 --- a/reddwarf/instance/models.py +++ b/reddwarf/instance/models.py @@ -24,17 +24,20 @@ import time from reddwarf import db +from novaclient import exceptions as nova_exceptions from reddwarf.common import config from reddwarf.common import exception as rd_exceptions +from reddwarf.common import pagination from reddwarf.common import utils -from reddwarf.instance.tasks import InstanceTask -from reddwarf.instance.tasks import InstanceTasks from reddwarf.common.models import ModelBase from novaclient import exceptions as nova_exceptions from reddwarf.common.remote import create_dns_client +from reddwarf.common.remote import create_guest_client from reddwarf.common.remote import create_nova_client from reddwarf.common.remote import create_nova_volume_client -from reddwarf.common.remote import create_guest_client +from reddwarf.guestagent import api as guest_api +from reddwarf.instance.tasks import InstanceTask +from reddwarf.instance.tasks import InstanceTasks from eventlet import greenthread @@ -273,13 +276,13 @@ class Instance(object): if volume_size: volume_info = cls._create_volume(context, db_info, volume_size) block_device_mapping = volume_info['block_device'] - device_path=volume_info['device_path'] - mount_point=volume_info['mount_point'] + device_path = volume_info['device_path'] + mount_point = volume_info['mount_point'] volumes = volume_info['volumes'] else: block_device_mapping = None - device_path=None - mount_point=None + device_path = None + mount_point = None volumes = [] client = create_nova_client(context) @@ -384,9 +387,7 @@ class Instance(object): @property def links(self): - #TODO(tim.simpson): Review whether we should be returning the server - # links. - return self._build_links(self.server.links) + return self.server.links @property def addresses(self): @@ -580,18 +581,31 @@ def create_server_list_matcher(server_list): class Instances(object): + DEFAULT_LIMIT = int(config.Config.get('instances_page_size', '20')) + @staticmethod def load(context): if context is None: raise TypeError("Argument context not defined.") client = create_nova_client(context) servers = client.servers.list() + db_infos = DBInstance.find_all() + limit = int(context.limit or Instances.DEFAULT_LIMIT) + if limit > Instances.DEFAULT_LIMIT: + limit = Instances.DEFAULT_LIMIT + data_view = DBInstance.find_by_pagination('instances', db_infos, "foo", + limit=limit, + marker=context.marker) + next_marker = data_view.next_page_marker + ret = [] find_server = create_server_list_matcher(servers) for db in db_infos: LOG.debug("checking for db [id=%s, compute_instance_id=%s]" % (db.id, db.compute_instance_id)) + for db in data_view.collection: + status = InstanceServiceStatus.find_by(instance_id=db.id) try: # TODO(hub-cap): Figure out if this is actually correct. # We are not sure if we should be doing some validation. @@ -622,7 +636,7 @@ class Instances(object): "or instance was deleted")) continue ret.append(Instance(context, db, server, status, volumes)) - return ret + return ret, next_marker class DatabaseModelBase(ModelBase): @@ -681,6 +695,16 @@ class DatabaseModelBase(ModelBase): """Override in inheritors to format/modify any conditions.""" return raw_conditions + @classmethod + def find_by_pagination(cls, collection_type, collection_query, + paginated_url, **kwargs): + elements, next_marker = collection_query.paginated_collection(**kwargs) + + return pagination.PaginatedDataView(collection_type, + elements, + paginated_url, + next_marker) + class DBInstance(DatabaseModelBase): """Defines the task being executed plus the start time.""" diff --git a/reddwarf/instance/service.py b/reddwarf/instance/service.py index f23b01acf8..dffec4bf86 100644 --- a/reddwarf/instance/service.py +++ b/reddwarf/instance/service.py @@ -21,10 +21,10 @@ import webob.exc from reddwarf.common import config from reddwarf.common import exception +from reddwarf.common import pagination from reddwarf.common import utils from reddwarf.common import wsgi from reddwarf.instance import models, views -from reddwarf.common import exception as rd_exceptions #TODO(ed-): Import these properly after this is restructured from reddwarf.flavor import models as flavormodels @@ -70,6 +70,10 @@ class BaseController(wsgi.Controller): config.Config.get('reddwarf_volume_support', 'False')) pass + def _extract_limits(self, params): + return dict([(key, params[key]) for key in params.keys() + if key in ["limit", "marker"]]) + def _extract_required_params(self, params, model_name): params = params or {} model_params = params.get(model_name, {}) @@ -112,16 +116,16 @@ class InstanceController(BaseController): if key in _actions: if selected_action is not None: msg = _("Only one action can be specified per request.") - raise rd_exceptions.BadRequest(msg) + raise exception.BadRequest(msg) selected_action = _actions[key] else: msg = _("Invalid instance action: %s") % key - raise rd_exceptions.BadRequest(msg) + raise exception.BadRequest(msg) if selected_action: return selected_action(instance, body) else: - raise rd_exceptions.BadRequest(_("Invalid request body.")) + raise exception.BadRequest(_("Invalid request body.")) def _action_restart(self, instance, body): instance.validate_can_perform_restart_or_reboot() @@ -150,20 +154,20 @@ class InstanceController(BaseController): if selected_option is not None: msg = _("Not allowed to resize volume and flavor at the " "same time.") - raise rd_exceptions.BadRequest(msg) + raise exception.BadRequest(msg) selected_option = options[key] args = body['resize'][key] else: - raise rd_exceptions.BadRequest("Invalid resize argument %s" + raise exception.BadRequest("Invalid resize argument %s" % key) if selected_option: return selected_option(instance, args) else: - raise rd_exceptions.BadRequest(_("Missing resize arguments.")) + raise exception.BadRequest(_("Missing resize arguments.")) def _action_resize_volume(self, instance, volume): if 'size' not in volume: - raise rd_exceptions.BadRequest( + raise exception.BadRequest( "Missing 'size' property of 'volume' in request body.") new_size = volume['size'] instance.resize_volume(new_size) @@ -186,13 +190,17 @@ class InstanceController(BaseController): LOG.info(_("req : '%s'\n\n") % req) LOG.info(_("Indexing a database instance for tenant '%s'") % tenant_id) context = req.environ[wsgi.CONTEXT_KEY] - servers = models.Instances.load(context) + servers, marker = models.Instances.load(context) # TODO(cp16net): need to set the return code correctly - view_cls = views.InstancesDetailView if detailed \ - else views.InstancesView - return wsgi.Result(view_cls(servers, - add_addresses=self.add_addresses, - add_volumes=self.add_volumes).data(), 200) + view_cls = (views.InstancesDetailView if detailed + else views.InstancesView) + + view = view_cls(servers, req=req, add_addresses=self.add_addresses, + add_volumes=self.add_volumes) + + paged = pagination.SimplePaginatedDataView(req.url, 'instances', view, + marker) + return wsgi.Result(paged.data(), 200) def show(self, req, tenant_id, id): """Return a single instance.""" @@ -210,7 +218,7 @@ class InstanceController(BaseController): LOG.error(e) return wsgi.Result(str(e), 404) # TODO(cp16net): need to set the return code correctly - return wsgi.Result(views.InstanceDetailView(server, + return wsgi.Result(views.InstanceDetailView(server, req=req, add_addresses=self.add_addresses, add_volumes=self.add_volumes).data(), 200) @@ -274,7 +282,7 @@ class InstanceController(BaseController): image_id, databases, service_type, volume_size) - return wsgi.Result(views.InstanceDetailView(instance, + return wsgi.Result(views.InstanceDetailView(instance, req=req, add_volumes=self.add_volumes).data(), 200) @staticmethod @@ -282,7 +290,7 @@ class InstanceController(BaseController): """Check that the body is not empty""" if not body: msg = "The request contains an empty body" - raise rd_exceptions.ReddwarfError(msg) + raise exception.ReddwarfError(msg) @staticmethod def _validate_volume_size(size): @@ -293,19 +301,19 @@ class InstanceController(BaseController): LOG.error(err) msg = ("Required element/key - instance volume 'size' was not " "specified as a number (value was %s)." % size) - raise rd_exceptions.ReddwarfError(msg) + raise exception.ReddwarfError(msg) if int(volume_size) != volume_size or int(volume_size) < 1: msg = ("Volume 'size' needs to be a positive " "integer value, %s cannot be accepted." % volume_size) - raise rd_exceptions.ReddwarfError(msg) + raise exception.ReddwarfError(msg) #TODO(cp16net) add in the volume validation when volumes are supported # max_size = FLAGS.reddwarf_max_accepted_volume_size # if int(volume_size) > max_size: # msg = ("Volume 'size' cannot exceed maximum " # "of %d Gb, %s cannot be accepted." # % (max_size, volume_size)) -# raise rd_exceptions.ReddwarfError(msg) +# raise exception.ReddwarfError(msg) @staticmethod def _validate(body): @@ -320,7 +328,7 @@ class InstanceController(BaseController): volume_size = body['instance']['volume']['size'] except KeyError as e: LOG.error(_("Create Instance Required field(s) - %s") % e) - raise rd_exceptions.ReddwarfError("Required element/key - %s " + raise exception.ReddwarfError("Required element/key - %s " "was not specified" % e) @staticmethod @@ -331,7 +339,7 @@ class InstanceController(BaseController): body['resize']['flavorRef'] except KeyError as e: LOG.error(_("Resize Instance Required field(s) - %s") % e) - raise rd_exceptions.ReddwarfError("Required element/key - %s " + raise exception.ReddwarfError("Required element/key - %s " "was not specified" % e) diff --git a/reddwarf/instance/views.py b/reddwarf/instance/views.py index 2d4de1dfdb..c245e0f48d 100644 --- a/reddwarf/instance/views.py +++ b/reddwarf/instance/views.py @@ -41,10 +41,12 @@ def get_volumes(volumes): class InstanceView(object): - def __init__(self, instance, add_addresses=False, add_volumes=False): + def __init__(self, instance, req=None, add_addresses=False, + add_volumes=False): self.instance = instance self.add_addresses = add_addresses self.add_volumes = add_volumes + self.req = req def data(self): ip = get_ip_address(self.instance.addresses) @@ -53,7 +55,7 @@ class InstanceView(object): "id": self.instance.id, "name": self.instance.name, "status": self.instance.status, - "links": self.instance.links + "links": self._build_links() } dns_support = config.Config.get("reddwarf_dns_support", 'False') if utils.bool_from_string(dns_support): @@ -65,12 +67,51 @@ class InstanceView(object): LOG.debug(instance_dict) return {"instance": instance_dict} + def _build_links(self): + # TODO(ed-): Make generic, move to common? + result = [] + scheme = 'https' # Forcing https + links = [link for link in self.instance.links] + links = [link['href'] for link in links if link['rel'] == 'self'] + href_link = links[0] + splitpath = href_link.split('/') + endpoint = '' + if self.req: + endpoint = self.req.host + splitpath = self.req.path.split('/') + + detailed = '' + if splitpath[-1] == 'detail': + detailed = '/detail' + splitpath.pop(-1) + + instance_id = self.instance.id + if str(splitpath[-1]) == str(instance_id): + splitpath.pop(-1) + href_template = "%(scheme)s://%(endpoint)s%(path)s/%(instance_id)s" + for link in self.instance.links: + rlink = link + href = rlink['href'] + if rlink['rel'] == 'self': + path = '/'.join(splitpath) + href = href_template % locals() + elif rlink['rel'] == 'bookmark': + splitpath.pop(2) # Remove the version. + splitpath.pop(1) # Remove the tenant id. + path = '/'.join(splitpath) + href = href_template % locals() + + rlink['href'] = href + result.append(rlink) + return result + class InstanceDetailView(InstanceView): - def __init__(self, instance, add_addresses=False, + def __init__(self, instance, req=None, add_addresses=False, add_volumes=False): super(InstanceDetailView, self).__init__(instance, + req=req, add_addresses=add_addresses, add_volumes=add_volumes) @@ -84,10 +125,12 @@ class InstanceDetailView(InstanceView): class InstancesView(object): - def __init__(self, instances, add_addresses=False, add_volumes=False): + def __init__(self, instances, req=None, add_addresses=False, + add_volumes=False): self.instances = instances self.add_addresses = add_addresses self.add_volumes = add_volumes + self.req = req def data(self): data = [] @@ -97,13 +140,14 @@ class InstancesView(object): return {'instances': data} def data_for_instance(self, instance): - return InstanceView(instance, - self.add_addresses).data()['instance'] + view = InstanceView(instance, req=self.req, + add_addresses=self.add_addresses) + return view.data()['instance'] class InstancesDetailView(InstancesView): def data_for_instance(self, instance): - return InstanceDetailView(instance, + return InstanceDetailView(instance, req=self.req, add_addresses=self.add_addresses, add_volumes=self.add_volumes).data()['instance'] diff --git a/reddwarf/tests/fakes/guestagent.py b/reddwarf/tests/fakes/guestagent.py index 3a47d88ea0..872a513de6 100644 --- a/reddwarf/tests/fakes/guestagent.py +++ b/reddwarf/tests/fakes/guestagent.py @@ -63,11 +63,30 @@ class FakeGuest(object): def is_root_enabled(self): return self.root_was_enabled - def list_databases(self): - return [self.dbs[name] for name in self.dbs] + def list_databases(self, limit=None, marker=None): + dbs = [self.dbs[name] for name in self.dbs] + names = [db['_name'] for db in dbs] + if marker in names: + # Cut off everything left of and including the marker item. + dbs = dbs[names.index(marker) + 1:] + next_marker = None + if limit: + if len(dbs) > limit: + next_marker = dbs[limit - 1]['_name'] + dbs = dbs[:limit] + return dbs, next_marker - def list_users(self): - return [self.users[name] for name in self.users] + def list_users(self, limit=None, marker=None): + users = [self.users[name] for name in self.users] + names = [user['_name'] for user in users] + if marker in names: + users = users[names.index(marker) + 1:] + next_marker = None + if limit: + if len(users) > limit: + next_marker = users[limit - 1]['_name'] + users = users[:limit] + return users, next_marker def prepare(self, databases, memory_mb, users, device_path=None, mount_point=None): @@ -100,6 +119,7 @@ class FakeGuest(object): status.status = ServiceStatuses.SHUTDOWN status.save() + def get_or_create(id): if id not in DB: DB[id] = FakeGuest(id) diff --git a/reddwarf/tests/fakes/nova.py b/reddwarf/tests/fakes/nova.py index e11a46b070..69f1ced1e1 100644 --- a/reddwarf/tests/fakes/nova.py +++ b/reddwarf/tests/fakes/nova.py @@ -108,7 +108,6 @@ class FakeServer(object): raise RuntimeError("Not in resize confirm mode.") self._current_status = "ACTIVE" - def delete(self): self.schedule_status = [] self._current_status = "SHUTDOWN" @@ -127,12 +126,15 @@ class FakeServer(object): def resize(self, new_flavor_id): self._current_status = "RESIZE" + def set_to_confirm_mode(): self._current_status = "VERIFY_RESIZE" + def set_flavor(): flavor = self.parent.flavors.get(new_flavor_id) self.flavor_ref = flavor.links[0]['href'] self.events.add_event(1, set_to_confirm_mode) + self.events.add_event(1, set_flavor) def schedule_status(self, new_status, time_from_now): @@ -248,10 +250,6 @@ class FakeServerVolumes(object): return [ServerVolumes(server.block_device_mapping)] - - - - class FakeVolume(object): def __init__(self, parent, owner, id, size, display_name, @@ -332,6 +330,7 @@ class FakeVolumes(object): FLAVORS = FakeFlavors() + class FakeClient(object): def __init__(self, context):