diff options
author | Jenkins <jenkins@review.openstack.org> | 2014-10-16 03:37:48 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2014-10-16 03:37:48 +0000 |
commit | cfe06235207d50d6d36b3d8b3e87608412038c80 (patch) | |
tree | ae2fbeb2f5353d595360a3996637f3bf12de7574 | |
parent | 1781c5d9ae1f4a0c60f70a72f5139eb2eebbce0d (diff) | |
parent | 7ef1b7c9b1c087c9786b5b37aa386098e81f048d (diff) | |
download | python-glanceclient-cfe06235207d50d6d36b3d8b3e87608412038c80.tar.gz |
Merge "Adds support for Glance Tasks calls"
-rw-r--r-- | glanceclient/v2/client.py | 3 | ||||
-rw-r--r-- | glanceclient/v2/shell.py | 65 | ||||
-rw-r--r-- | glanceclient/v2/tasks.py | 120 | ||||
-rw-r--r-- | tests/v2/test_tasks.py | 282 |
4 files changed, 470 insertions, 0 deletions
diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py index 4aa2e83..f11cb45 100644 --- a/glanceclient/v2/client.py +++ b/glanceclient/v2/client.py @@ -21,6 +21,7 @@ from glanceclient.v2 import image_tags from glanceclient.v2 import images from glanceclient.v2 import metadefs from glanceclient.v2 import schemas +from glanceclient.v2 import tasks class Client(object): @@ -44,6 +45,8 @@ class Client(object): self.image_members = image_members.Controller(self.http_client, self.schemas) + self.tasks = tasks.Controller(self.http_client, self.schemas) + self.metadefs_resource_type = ( metadefs.ResourceTypeController(self.http_client, self.schemas)) diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index bc862c2..7f56609 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -16,6 +16,7 @@ from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient import exc +from glanceclient.v2 import tasks import json import os from os.path import expanduser @@ -689,3 +690,67 @@ def do_md_object_list(gc, args): } } utils.print_list(objects, columns, field_settings=column_settings) + + +@utils.arg('--sort-key', default='status', + choices=tasks.SORT_KEY_VALUES, + help='Sort task list by specified field.') +@utils.arg('--sort-dir', default='desc', + choices=tasks.SORT_DIR_VALUES, + help='Sort task list in specified direction.') +@utils.arg('--page-size', metavar='<SIZE>', default=None, type=int, + help='Number of tasks to request in each paginated request.') +@utils.arg('--type', metavar='<TYPE>', + help='Filter tasks to those that have this type.') +@utils.arg('--status', metavar='<STATUS>', + help='Filter tasks to those that have this status.') +def do_task_list(gc, args): + """List tasks you can access.""" + filter_keys = ['type', 'status'] + filter_items = [(key, getattr(args, key)) for key in filter_keys] + filters = dict([item for item in filter_items if item[1] is not None]) + + kwargs = {'filters': filters} + if args.page_size is not None: + kwargs['page_size'] = args.page_size + + kwargs['sort_key'] = args.sort_key + kwargs['sort_dir'] = args.sort_dir + + tasks = gc.tasks.list(**kwargs) + + columns = ['ID', 'Type', 'Status', 'Owner'] + utils.print_list(tasks, columns) + + +@utils.arg('id', metavar='<TASK_ID>', help='ID of task to describe.') +def do_task_show(gc, args): + """Describe a specific task.""" + task = gc.tasks.get(args.id) + ignore = ['self', 'schema'] + task = dict([item for item in task.iteritems() if item[0] not in ignore]) + utils.print_dict(task) + + +@utils.arg('--type', metavar='<TYPE>', + help='Type of Task. Please refer to Glance schema or documentation' + ' to see which tasks are supported.') +@utils.arg('--input', metavar='<STRING>', default='{}', + help='Parameters of the task to be launched') +def do_task_create(gc, args): + """Create a new task.""" + if not (args.type and args.input): + utils.exit('Unable to create task. Specify task type and input.') + else: + try: + input = json.loads(args.input) + except ValueError: + utils.exit('Failed to parse the "input" parameter. Must be a ' + 'valid JSON object.') + + task_values = {'type': args.type, 'input': input} + task = gc.tasks.create(**task_values) + ignore = ['self', 'schema'] + task = dict([item for item in task.iteritems() + if item[0] not in ignore]) + utils.print_dict(task) diff --git a/glanceclient/v2/tasks.py b/glanceclient/v2/tasks.py new file mode 100644 index 0000000..730e130 --- /dev/null +++ b/glanceclient/v2/tasks.py @@ -0,0 +1,120 @@ +# Copyright 2013 OpenStack LLC. +# Copyright 2013 IBM Corp. +# 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 six + +import warlock + +from glanceclient.common import utils +from glanceclient.openstack.common import strutils +from glanceclient.v2 import schemas + +DEFAULT_PAGE_SIZE = 20 + +SORT_DIR_VALUES = ('asc', 'desc') +SORT_KEY_VALUES = ('id', 'type', 'status') + + +class Controller(object): + def __init__(self, http_client, schema_client): + self.http_client = http_client + self.schema_client = schema_client + + @utils.memoized_property + def model(self): + schema = self.schema_client.get('task') + return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel) + + def list(self, **kwargs): + """Retrieve a listing of Task objects + + :param page_size: Number of tasks to request in each paginated request + :returns generator over list of Tasks + """ + def paginate(url): + resp, body = self.http_client.get(url) + for task in body['tasks']: + yield task + try: + next_url = body['next'] + except KeyError: + return + else: + for task in paginate(next_url): + yield task + + filters = kwargs.get('filters', {}) + + if not kwargs.get('page_size'): + filters['limit'] = DEFAULT_PAGE_SIZE + else: + filters['limit'] = kwargs['page_size'] + + if 'marker' in kwargs: + filters['marker'] = kwargs['marker'] + + sort_key = kwargs.get('sort_key') + if sort_key is not None: + if sort_key in SORT_KEY_VALUES: + filters['sort_key'] = sort_key + else: + raise ValueError('sort_key must be one of the following: %s.' + % ', '.join(SORT_KEY_VALUES)) + + sort_dir = kwargs.get('sort_dir') + if sort_dir is not None: + if sort_dir in SORT_DIR_VALUES: + filters['sort_dir'] = sort_dir + else: + raise ValueError('sort_dir must be one of the following: %s.' + % ', '.join(SORT_DIR_VALUES)) + + for param, value in filters.items(): + if isinstance(value, six.string_types): + filters[param] = strutils.safe_encode(value) + + url = '/v2/tasks?%s' % six.moves.urllib.parse.urlencode(filters) + for task in paginate(url): + #NOTE(flwang): remove 'self' for now until we have an elegant + # way to pass it into the model constructor without conflict + task.pop('self', None) + yield self.model(**task) + + def get(self, task_id): + """Get a task based on given task id.""" + url = '/v2/tasks/%s' % task_id + resp, body = self.http_client.get(url) + #NOTE(flwang): remove 'self' for now until we have an elegant + # way to pass it into the model constructor without conflict + body.pop('self', None) + return self.model(**body) + + def create(self, **kwargs): + """Create a new task.""" + url = '/v2/tasks' + task = self.model() + + for (key, value) in kwargs.items(): + try: + setattr(task, key, value) + except warlock.InvalidOperation as e: + raise TypeError(unicode(e)) + + resp, body = self.http_client.post(url, data=task) + #NOTE(flwang): remove 'self' for now until we have an elegant + # way to pass it into the model constructor without conflict + body.pop('self', None) + return self.model(**body) diff --git a/tests/v2/test_tasks.py b/tests/v2/test_tasks.py new file mode 100644 index 0000000..e4f2a1e --- /dev/null +++ b/tests/v2/test_tasks.py @@ -0,0 +1,282 @@ +# Copyright 2013 OpenStack Foundation. +# Copyright 2013 IBM Corp. +# 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 six +import testtools + +from glanceclient.v2 import tasks +from tests import utils + + +_OWNED_TASK_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf' +_OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f' +_FAKE_OWNER_ID = '63e7f218-29de-4477-abdc-8db7c9533188' + + +fixtures = { + '/v2/tasks?limit=%d' % tasks.DEFAULT_PAGE_SIZE: { + 'GET': ( + {}, + {'tasks': [ + { + 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', + 'type': 'import', + 'status': 'pending', + }, + { + 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', + 'type': 'import', + 'status': 'processing', + }, + ]}, + ), + }, + '/v2/tasks?limit=1': { + 'GET': ( + {}, + { + 'tasks': [ + { + 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', + 'type': 'import', + 'status': 'pending', + }, + ], + 'next': ('/v2/tasks?limit=1&' + 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'), + }, + ), + }, + ('/v2/tasks?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): { + 'GET': ( + {}, + {'tasks': [ + { + 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', + 'type': 'import', + 'status': 'pending', + }, + ]}, + ), + }, + '/v2/tasks/3a4560a1-e585-443e-9b39-553b46ec92d1': { + 'GET': ( + {}, + { + 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', + 'type': 'import', + 'status': 'pending', + }, + ), + 'PATCH': ( + {}, + '', + ), + }, + '/v2/tasks/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': { + 'GET': ( + {}, + { + 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3', + 'type': 'import', + 'status': 'pending', + }, + ), + 'PATCH': ( + {}, + '', + ), + }, + '/v2/tasks': { + 'POST': ( + {}, + { + 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', + 'type': 'import', + 'status': 'pending', + 'input': '{"import_from": "file:///", ' + '"import_from_format": "qcow2"}' + }, + ), + }, + '/v2/tasks?owner=%s&limit=%d' % (_OWNER_ID, tasks.DEFAULT_PAGE_SIZE): { + 'GET': ( + {}, + {'tasks': [ + { + 'id': _OWNED_TASK_ID, + }, + ]}, + ), + }, + '/v2/tasks?status=processing&limit=%d' % (tasks.DEFAULT_PAGE_SIZE): { + 'GET': ( + {}, + {'tasks': [ + { + 'id': _OWNED_TASK_ID, + }, + ]}, + ), + }, + '/v2/tasks?type=import&limit=%d' % (tasks.DEFAULT_PAGE_SIZE): { + 'GET': ( + {}, + {'tasks': [ + { + 'id': _OWNED_TASK_ID, + }, + ]}, + ), + }, + '/v2/tasks?limit=%d&type=fake' % (tasks.DEFAULT_PAGE_SIZE): { + 'GET': ( + {}, + {'tasks': [ + ]}, + ), + }, + '/v2/tasks?status=fake&limit=%d' % (tasks.DEFAULT_PAGE_SIZE): { + 'GET': ( + {}, + {'tasks': [ + ]}, + ), + }, + '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): { + 'GET': ( + {}, + {'tasks': [ + { + 'id': _OWNED_TASK_ID, + }, + ]}, + ), + }, + '/v2/tasks?owner=%s&limit=%d' % (_FAKE_OWNER_ID, + tasks.DEFAULT_PAGE_SIZE): + { + 'GET': ({}, + {'tasks': []}, + ), + } +} + +schema_fixtures = { + 'task': { + 'GET': ( + {}, + { + 'name': 'task', + 'properties': { + 'id': {}, + 'type': {}, + 'status': {}, + 'input': {}, + 'result': {}, + 'message': {}, + }, + } + ) + } +} + + +class TestController(testtools.TestCase): + def setUp(self): + super(TestController, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.schema_api = utils.FakeSchemaAPI(schema_fixtures) + self.controller = tasks.Controller(self.api, self.schema_api) + + def test_list_tasks(self): + #NOTE(flwang): cast to list since the controller returns a generator + tasks = list(self.controller.list()) + self.assertEqual(tasks[0].id, '3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(tasks[0].type, 'import') + self.assertEqual(tasks[0].status, 'pending') + self.assertEqual(tasks[1].id, '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810') + self.assertEqual(tasks[1].type, 'import') + self.assertEqual(tasks[1].status, 'processing') + + def test_list_tasks_paginated(self): + #NOTE(flwang): cast to list since the controller returns a generator + tasks = list(self.controller.list(page_size=1)) + self.assertEqual(tasks[0].id, '3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(tasks[0].type, 'import') + self.assertEqual(tasks[1].id, '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810') + self.assertEqual(tasks[1].type, 'import') + + def test_list_tasks_with_status(self): + filters = {'filters': dict([('status', 'processing')])} + tasks = list(self.controller.list(**filters)) + self.assertEqual(tasks[0].id, _OWNED_TASK_ID) + + def test_list_tasks_with_wrong_status(self): + filters = {'filters': dict([('status', 'fake')])} + tasks = list(self.controller.list(**filters)) + self.assertEqual(len(tasks), 0) + + def test_list_tasks_with_type(self): + filters = {'filters': dict([('type', 'import')])} + tasks = list(self.controller.list(**filters)) + self.assertEqual(tasks[0].id, _OWNED_TASK_ID) + + def test_list_tasks_with_wrong_type(self): + filters = {'filters': dict([('type', 'fake')])} + tasks = list(self.controller.list(**filters)) + self.assertEqual(len(tasks), 0) + + def test_list_tasks_for_owner(self): + filters = {'filters': dict([('owner', _OWNER_ID)])} + tasks = list(self.controller.list(**filters)) + self.assertEqual(tasks[0].id, _OWNED_TASK_ID) + + def test_list_tasks_for_fake_owner(self): + filters = {'filters': dict([('owner', _FAKE_OWNER_ID)])} + tasks = list(self.controller.list(**filters)) + self.assertEqual(tasks, []) + + def test_list_tasks_filters_encoding(self): + filters = {"owner": u"ni\xf1o"} + try: + list(self.controller.list(filters=filters)) + except KeyError: + # NOTE(flaper87): It raises KeyError because there's + # no fixture supporting this query: + # /v2/tasks?owner=ni%C3%B1o&limit=20 + # We just want to make sure filters are correctly encoded. + pass + + if six.PY2: + self.assertEqual("ni\xc3\xb1o", filters["owner"]) + else: + self.assertEqual("ni\xf1o", filters["owner"]) + + def test_get_task(self): + task = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(task.id, '3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(task.type, 'import') + + def test_create_task(self): + properties = { + 'type': 'import', + 'input': {'import_from_format': 'ovf', 'import_from': + 'swift://cloud.foo/myaccount/mycontainer/path'}, + } + task = self.controller.create(**properties) + self.assertEqual(task.id, '3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(task.type, 'import') |