Adds support for Glance Tasks calls
Add tasks operations on client side to support task create, list all and show. DocImpact Implement blueprint async-glance-workers Change-Id: Ib4b8e347a8a47817e3b427c8ba024e8c32f65155
This commit is contained in:
parent
49feed7cba
commit
7ef1b7c9b1
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
|
120
glanceclient/v2/tasks.py
Normal file
120
glanceclient/v2/tasks.py
Normal file
@ -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)
|
282
tests/v2/test_tasks.py
Normal file
282
tests/v2/test_tasks.py
Normal file
@ -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')
|
Loading…
x
Reference in New Issue
Block a user