summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-10-16 03:37:48 +0000
committerGerrit Code Review <review@openstack.org>2014-10-16 03:37:48 +0000
commitcfe06235207d50d6d36b3d8b3e87608412038c80 (patch)
treeae2fbeb2f5353d595360a3996637f3bf12de7574
parent1781c5d9ae1f4a0c60f70a72f5139eb2eebbce0d (diff)
parent7ef1b7c9b1c087c9786b5b37aa386098e81f048d (diff)
downloadpython-glanceclient-cfe06235207d50d6d36b3d8b3e87608412038c80.tar.gz
Merge "Adds support for Glance Tasks calls"
-rw-r--r--glanceclient/v2/client.py3
-rw-r--r--glanceclient/v2/shell.py65
-rw-r--r--glanceclient/v2/tasks.py120
-rw-r--r--tests/v2/test_tasks.py282
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')