Begin low-level API for Image v1 and v2

image list for v1 and v2:
* Add --public|--private to command parsers
* Implement local public/private filtering for v1 image_list()
* Pass public/private filter to server for v2 image_list()

Change-Id: Ie7c24ea2d1bf2b3b1b7fa342eb45fee45894634d
This commit is contained in:
Dean Troyer 2014-09-18 00:56:38 -05:00
parent 95fe3fda3d
commit 1ecf1bee2d
11 changed files with 623 additions and 60 deletions

View File

@ -139,12 +139,21 @@ List available images
os image list
[--page-size <size>]
[--public|--private]
[--long]
.. option:: --page-size <size>
Number of images to request in each paginated request
.. option:: --public
List only public images
.. option:: --private
List only private images
.. option:: --long
List additional fields in output

View File

@ -161,7 +161,7 @@ class BaseAPI(KeystoneSession):
):
"""Return a list of resources
GET ${ENDPOINT}/${PATH}
GET ${ENDPOINT}/${PATH}?${PARAMS}
path is often the object's plural resource type

View File

@ -0,0 +1,68 @@
# 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.
#
"""Image v1 API Library"""
from openstackclient.api import api
class APIv1(api.BaseAPI):
"""Image v1 API"""
def __init__(self, endpoint=None, **kwargs):
super(APIv1, self).__init__(endpoint=endpoint, **kwargs)
# Hack this until discovery is up
self.endpoint = '/'.join([self.endpoint.rstrip('/'), 'v1'])
def image_list(
self,
detailed=False,
public=False,
private=False,
**filter
):
"""Get available images
:param detailed:
Retrieve detailed response from server if True
:param public:
Return public images if True
:param private:
Return private images if True
If public and private are both True or both False then all images are
returned. Both arguments False is equivalent to no filter and all
images are returned. Both arguments True is a filter that includes
both public and private images which is the same set as all images.
http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-a-list-of-public-vm-images.html
http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-detailed-metadata-on-public-vm-images.html
http://docs.openstack.org/api/openstack-image-service/1.1/content/filtering-images-returned-via-get-images-and-get-imagesdetail.html
TODO(dtroyer): Implement filtering
"""
url = "/images"
if detailed or public or private:
# Because we can't all use /details
url += "/detail"
image_list = self.list(url, **filter)['images']
if public != private:
# One is True and one is False, so public represents the filter
# state in either case
image_list = [i for i in image_list if i['is_public'] == public]
return image_list

View File

@ -0,0 +1,69 @@
# 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.
#
"""Image v2 API Library"""
from openstackclient.api import image_v1
class APIv2(image_v1.APIv1):
"""Image v2 API"""
def __init__(self, endpoint=None, **kwargs):
super(APIv2, self).__init__(endpoint=endpoint, **kwargs)
# Hack this until discovery is up, and ignore parent endpoint setting
self.endpoint = '/'.join([endpoint.rstrip('/'), 'v2'])
def image_list(
self,
detailed=False,
public=False,
private=False,
**filter
):
"""Get available images
can add limit/marker
:param detailed:
For v1 compatibility only, ignored as v2 is always 'detailed'
:param public:
Return public images if True
:param private:
Return private images if True
If public and private are both True or both False then all images are
returned. Both arguments False is equivalent to no filter and all
images are returned. Both arguments True is a filter that includes
both public and private images which is the same set as all images.
http://docs.openstack.org/api/openstack-image-service/2.0/content/list-images.html
TODO(dtroyer): Implement filtering
"""
if public == private:
# No filtering for both False and both True cases
filter.pop('visibility', None)
elif public:
filter['visibility'] = 'public'
elif private:
filter['visibility'] = 'private'
url = "/images"
if detailed:
# Because we can't all use /details
url += "/detail"
return self.list(url, **filter)['images']

View File

@ -31,9 +31,15 @@ API_VERSIONS = {
"2": "glanceclient.v2.client.Client",
}
IMAGE_API_TYPE = 'image'
IMAGE_API_VERSIONS = {
'1': 'openstackclient.api.image_v1.APIv1',
'2': 'openstackclient.api.image_v2.APIv2',
}
def make_client(instance):
"""Returns an image service client."""
"""Returns an image service client"""
image_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
@ -45,13 +51,31 @@ def make_client(instance):
region_name=instance._region_name,
)
return image_client(
client = image_client(
endpoint,
token=instance.auth.get_token(instance.session),
cacert=instance._cacert,
insecure=instance._insecure,
)
# Create the low-level API
image_api = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
IMAGE_API_VERSIONS)
LOG.debug('Instantiating image api: %s', image_api)
client.api = image_api(
session=instance.session,
endpoint=instance.get_endpoint_for_service_type(
IMAGE_API_TYPE,
region_name=instance._region_name,
)
)
return client
def build_option_parser(parser):
"""Hook to add global options"""

View File

@ -300,6 +300,21 @@ class ListImage(lister.Lister):
metavar="<size>",
help="Number of images to request in each paginated request",
)
public_group = parser.add_mutually_exclusive_group()
public_group.add_argument(
"--public",
dest="public",
action="store_true",
default=False,
help="List only public images",
)
public_group.add_argument(
"--private",
dest="private",
action="store_true",
default=False,
help="List only private images",
)
parser.add_argument(
'--long',
action='store_true',
@ -316,15 +331,21 @@ class ListImage(lister.Lister):
kwargs = {}
if parsed_args.page_size is not None:
kwargs["page_size"] = parsed_args.page_size
if parsed_args.public:
kwargs['public'] = True
if parsed_args.private:
kwargs['private'] = True
kwargs['detailed'] = parsed_args.long
data = image_client.images.list(**kwargs)
if parsed_args.long:
columns = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
else:
columns = ("ID", "Name")
return (columns, (utils.get_item_properties(s, columns) for s in data))
data = image_client.api.image_list(**kwargs)
return (columns, (utils.get_dict_properties(s, columns) for s in data))
class SaveImage(command.Command):

View File

@ -65,6 +65,21 @@ class ListImage(lister.Lister):
metavar="<size>",
help="Number of images to request in each paginated request",
)
public_group = parser.add_mutually_exclusive_group()
public_group.add_argument(
"--public",
dest="public",
action="store_true",
default=False,
help="List only public images",
)
public_group.add_argument(
"--private",
dest="private",
action="store_true",
default=False,
help="List only private images",
)
parser.add_argument(
'--long',
action='store_true',
@ -81,15 +96,21 @@ class ListImage(lister.Lister):
kwargs = {}
if parsed_args.page_size is not None:
kwargs["page_size"] = parsed_args.page_size
if parsed_args.public:
kwargs['public'] = True
if parsed_args.private:
kwargs['private'] = True
kwargs['detailed'] = parsed_args.long
data = image_client.images.list(**kwargs)
if parsed_args.long:
columns = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
else:
columns = ("ID", "Name")
return (columns, (utils.get_item_properties(s, columns) for s in data))
data = image_client.api.image_list(**kwargs)
return (columns, (utils.get_dict_properties(s, columns) for s in data))
class SaveImage(command.Command):

View File

@ -0,0 +1,98 @@
# 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.
#
"""Image v1 API Library Tests"""
from requests_mock.contrib import fixture
from keystoneclient import session
from openstackclient.api import image_v1
from openstackclient.tests import utils
FAKE_PROJECT = 'xyzpdq'
FAKE_URL = 'http://gopher.com'
class TestImageAPIv1(utils.TestCase):
def setUp(self):
super(TestImageAPIv1, self).setUp()
sess = session.Session()
self.api = image_v1.APIv1(session=sess, endpoint=FAKE_URL)
self.requests_mock = self.useFixture(fixture.Fixture())
class TestImage(TestImageAPIv1):
PUB_PROT = {
'id': '1',
'name': 'pub1',
'is_public': True,
'protected': True,
}
PUB_NOPROT = {
'id': '2',
'name': 'pub2-noprot',
'is_public': True,
'protected': False,
}
NOPUB_PROT = {
'id': '3',
'name': 'priv3',
'is_public': False,
'protected': True,
}
NOPUB_NOPROT = {
'id': '4',
'name': 'priv4-noprot',
'is_public': False,
'protected': False,
}
LIST_IMAGE_RESP = [
PUB_PROT,
PUB_NOPROT,
NOPUB_PROT,
NOPUB_NOPROT,
]
def test_image_list_no_options(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + '/v1/images',
json={'images': self.LIST_IMAGE_RESP},
status_code=200,
)
ret = self.api.image_list()
self.assertEqual(self.LIST_IMAGE_RESP, ret)
def test_image_list_public(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + '/v1/images/detail',
json={'images': self.LIST_IMAGE_RESP},
status_code=200,
)
ret = self.api.image_list(public=True)
self.assertEqual([self.PUB_PROT, self.PUB_NOPROT], ret)
def test_image_list_private(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + '/v1/images/detail',
json={'images': self.LIST_IMAGE_RESP},
status_code=200,
)
ret = self.api.image_list(private=True)
self.assertEqual([self.NOPUB_PROT, self.NOPUB_NOPROT], ret)

View File

@ -0,0 +1,98 @@
# 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.
#
"""Image v2 API Library Tests"""
from requests_mock.contrib import fixture
from keystoneclient import session
from openstackclient.api import image_v2
from openstackclient.tests import utils
FAKE_PROJECT = 'xyzpdq'
FAKE_URL = 'http://gopher.com'
class TestImageAPIv2(utils.TestCase):
def setUp(self):
super(TestImageAPIv2, self).setUp()
sess = session.Session()
self.api = image_v2.APIv2(session=sess, endpoint=FAKE_URL)
self.requests_mock = self.useFixture(fixture.Fixture())
class TestImage(TestImageAPIv2):
PUB_PROT = {
'id': '1',
'name': 'pub1',
'visibility': 'public',
'protected': True,
}
PUB_NOPROT = {
'id': '2',
'name': 'pub2-noprot',
'visibility': 'public',
'protected': False,
}
NOPUB_PROT = {
'id': '3',
'name': 'priv3',
'visibility': 'private',
'protected': True,
}
NOPUB_NOPROT = {
'id': '4',
'name': 'priv4-noprot',
'visibility': 'private',
'protected': False,
}
LIST_IMAGE_RESP = [
PUB_PROT,
PUB_NOPROT,
NOPUB_PROT,
NOPUB_NOPROT,
]
def test_image_list_no_options(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + '/v2/images',
json={'images': self.LIST_IMAGE_RESP},
status_code=200,
)
ret = self.api.image_list()
self.assertEqual(self.LIST_IMAGE_RESP, ret)
def test_image_list_public(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + '/v2/images',
json={'images': [self.PUB_PROT, self.PUB_NOPROT]},
status_code=200,
)
ret = self.api.image_list(public=True)
self.assertEqual([self.PUB_PROT, self.PUB_NOPROT], ret)
def test_image_list_private(self):
self.requests_mock.register_uri(
'GET',
FAKE_URL + '/v2/images',
json={'images': [self.NOPUB_PROT, self.NOPUB_NOPROT]},
status_code=200,
)
ret = self.api.image_list(public=True)
self.assertEqual([self.NOPUB_PROT, self.NOPUB_NOPROT], ret)

View File

@ -300,6 +300,128 @@ class TestImageDelete(TestImage):
)
class TestImageList(TestImage):
def setUp(self):
super(TestImageList, self).setUp()
self.api_mock = mock.Mock()
self.api_mock.image_list.return_value = [
copy.deepcopy(image_fakes.IMAGE),
]
self.app.client_manager.image.api = self.api_mock
# Get the command object to test
self.cmd = image.ListImage(self.app, None)
def test_image_list_no_options(self):
arglist = []
verifylist = [
('public', False),
('private', False),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_public_option(self):
arglist = [
'--public',
]
verifylist = [
('public', True),
('private', False),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
public=True,
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_private_option(self):
arglist = [
'--private',
]
verifylist = [
('public', False),
('private', True),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
private=True,
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_long_option(self):
arglist = [
'--long',
]
verifylist = [
('long', True),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=True,
)
collist = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
'',
'',
'',
'',
), )
self.assertEqual(datalist, tuple(data))
class TestImageSet(TestImage):
def setUp(self):
@ -453,48 +575,3 @@ class TestImageSet(TestImage):
image_fakes.image_id,
**kwargs
)
class TestImageList(TestImage):
def setUp(self):
super(TestImageList, self).setUp()
# This is the return value for utils.find_resource()
self.images_mock.list.return_value = [
fakes.FakeResource(
None,
copy.deepcopy(image_fakes.IMAGE),
loaded=True,
),
]
# Get the command object to test
self.cmd = image.ListImage(self.app, None)
def test_image_list_long_option(self):
arglist = [
'--long',
]
verifylist = [
('long', True),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.images_mock.list.assert_called_with()
collist = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
'',
'',
'',
'',
), )
self.assertEqual(datalist, tuple(data))

View File

@ -14,6 +14,7 @@
#
import copy
import mock
from openstackclient.image.v2 import image
from openstackclient.tests import fakes
@ -68,18 +69,93 @@ class TestImageList(TestImage):
def setUp(self):
super(TestImageList, self).setUp()
# This is the return value for utils.find_resource()
self.images_mock.list.return_value = [
fakes.FakeResource(
None,
copy.deepcopy(image_fakes.IMAGE),
loaded=True,
),
self.api_mock = mock.Mock()
self.api_mock.image_list.return_value = [
copy.deepcopy(image_fakes.IMAGE),
]
self.app.client_manager.image.api = self.api_mock
# Get the command object to test
self.cmd = image.ListImage(self.app, None)
def test_image_list_no_options(self):
arglist = []
verifylist = [
('public', False),
('private', False),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_public_option(self):
arglist = [
'--public',
]
verifylist = [
('public', True),
('private', False),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
public=True,
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_private_option(self):
arglist = [
'--private',
]
verifylist = [
('public', False),
('private', True),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
private=True,
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_long_option(self):
arglist = [
'--long',
@ -91,7 +167,9 @@ class TestImageList(TestImage):
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.images_mock.list.assert_called_with()
self.api_mock.image_list.assert_called_with(
detailed=True,
)
collist = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')