Merge "image: Add support for cache commands"

This commit is contained in:
Zuul 2023-11-07 09:31:24 +00:00 committed by Gerrit Code Review
commit 0439f17ed3
6 changed files with 470 additions and 4 deletions

View File

@ -1,7 +1,7 @@
cache-clear,,"Clear all images from cache, queue or both."
cache-delete,,Delete image from cache/caching queue.
cache-list,,Get cache state.
cache-queue,,Queue image(s) for caching.
cache-clear,cached image list,"Clear all images from cache, queue or both."
cache-delete,cached image delete,Delete image from cache/caching queue.
cache-list,cached image list,Get cache state.
cache-queue,cached image queue,Queue image(s) for caching.
explain,WONTFIX,Describe a specific model.
image-create,image create,Create a new image.
image-create-via-import, image create --import,"EXPERIMENTAL: Create a new image via image import using glance-direct import method. Missing support for web-download, copy-image and glance-download import methods. The OSC command is also missing support for importing image to specified store as well as all stores (--store, --stores, --all-stores) and skip or stop processing if import fails to one of the store (--allow-failure)"

1 cache-clear cached image list Clear all images from cache, queue or both.
2 cache-delete cached image delete Delete image from cache/caching queue.
3 cache-list cached image list Get cache state.
4 cache-queue cached image queue Queue image(s) for caching.
5 explain WONTFIX WONTFIX Describe a specific model.
6 image-create image create image create Create a new image.
7 image-create-via-import image create --import image create --import EXPERIMENTAL: Create a new image via image import using glance-direct import method. Missing support for web-download, copy-image and glance-download import methods. The OSC command is also missing support for importing image to specified store as well as all stores (--store, --stores, --all-stores) and skip or stop processing if import fails to one of the store (--allow-failure)

View File

@ -0,0 +1,218 @@
# Copyright 2023 Red Hat.
# 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 copy
import datetime
import logging
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
from openstackclient.i18n import _
LOG = logging.getLogger(__name__)
def _format_image_cache(cached_images):
"""Format image cache to make it more consistent with OSC operations."""
image_list = []
for item in cached_images:
if item == "cached_images":
for image in cached_images[item]:
image_obj = copy.deepcopy(image)
image_obj['state'] = 'cached'
image_obj[
'last_accessed'
] = datetime.datetime.utcfromtimestamp(
image['last_accessed']
).isoformat()
image_obj[
'last_modified'
] = datetime.datetime.utcfromtimestamp(
image['last_modified']
).isoformat()
image_list.append(image_obj)
elif item == "queued_images":
for image in cached_images[item]:
image = {'image_id': image}
image.update(
{
'state': 'queued',
'last_accessed': 'N/A',
'last_modified': 'N/A',
'size': 'N/A',
'hits': 'N/A',
}
)
image_list.append(image)
return image_list
class ListCachedImage(command.Lister):
_description = _("Get Cache State")
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
return parser
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
# List of Cache data received
data = _format_image_cache(dict(image_client.get_image_cache()))
columns = [
'image_id',
'state',
'last_accessed',
'last_modified',
'size',
'hits',
]
column_headers = [
"ID",
"State",
"Last Accessed (UTC)",
"Last Modified (UTC)",
"Size",
"Hits",
]
return (
column_headers,
(
utils.get_dict_properties(
image,
columns,
)
for image in data
),
)
class QueueCachedImage(command.Command):
_description = _("Queue image(s) for caching.")
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
"images",
metavar="<image>",
nargs="+",
help=_("Image to display (name or ID)"),
)
return parser
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
failures = 0
for image in parsed_args.images:
try:
image_obj = image_client.find_image(
image,
ignore_missing=False,
)
image_client.queue_image(image_obj.id)
except Exception as e:
failures += 1
msg = _(
"Failed to queue image with name or "
"ID '%(image)s': %(e)s"
)
LOG.error(msg, {'image': image, 'e': e})
if failures > 0:
total = len(parsed_args.images)
msg = _("Failed to queue %(failures)s of %(total)s images") % {
'failures': failures,
'total': total,
}
raise exceptions.CommandError(msg)
class DeleteCachedImage(command.Command):
_description = _("Delete image(s) from cache")
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
"images",
metavar="<image>",
nargs="+",
help=_("Image(s) to delete (name or ID)"),
)
return parser
def take_action(self, parsed_args):
failures = 0
image_client = self.app.client_manager.image
for image in parsed_args.images:
try:
image_obj = image_client.find_image(
image,
ignore_missing=False,
)
image_client.cache_delete_image(image_obj.id)
except Exception as e:
failures += 1
msg = _(
"Failed to delete image with name or "
"ID '%(image)s': %(e)s"
)
LOG.error(msg, {'image': image, 'e': e})
if failures > 0:
total = len(parsed_args.images)
msg = _("Failed to delete %(failures)s of %(total)s images.") % {
'failures': failures,
'total': total,
}
raise exceptions.CommandError(msg)
class ClearCachedImage(command.Command):
_description = _("Clear all images from cache, queue or both")
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument(
"--cache",
action="store_const",
const="cache",
dest="target",
help=_("Clears all the cached images"),
)
parser.add_argument(
"--queue",
action="store_const",
const="queue",
dest="target",
help=_("Clears all the queued images"),
)
return parser
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
target = parsed_args.target
try:
image_client.clear_cache(target)
except Exception:
msg = _("Failed to clear image cache")
LOG.error(msg)
raise exceptions.CommandError(msg)

View File

@ -17,6 +17,7 @@ from unittest import mock
import uuid
from openstack.image.v2 import _proxy
from openstack.image.v2 import cache
from openstack.image.v2 import image
from openstack.image.v2 import member
from openstack.image.v2 import metadef_namespace
@ -240,6 +241,28 @@ def create_tasks(attrs=None, count=2):
return tasks
def create_cache(attrs=None):
attrs = attrs or {}
cache_info = {
'cached_images': [
{
'hits': 0,
'image_id': '1a56983c-f71f-490b-a7ac-6b321a18935a',
'last_accessed': 1671699579.444378,
'last_modified': 1671699579.444378,
'size': 0,
},
],
'queued_images': [
'3a4560a1-e585-443e-9b39-553b46ec92d1',
'6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
],
}
cache_info.update(attrs)
return cache.Cache(**cache_info)
def create_one_metadef_namespace(attrs=None):
"""Create a fake MetadefNamespace member.

View File

@ -0,0 +1,214 @@
# Copyright 2023 Red Hat.
# 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.
from unittest.mock import call
from openstack import exceptions as sdk_exceptions
from osc_lib import exceptions
from openstackclient.image.v2 import cache
from openstackclient.tests.unit.image.v2 import fakes
class TestCacheList(fakes.TestImagev2):
_cache = fakes.create_cache()
columns = [
"ID",
"State",
"Last Accessed (UTC)",
"Last Modified (UTC)",
"Size",
"Hits",
]
cache_list = cache._format_image_cache(dict(fakes.create_cache()))
datalist = (
(
image['image_id'],
image['state'],
image['last_accessed'],
image['last_modified'],
image['size'],
image['hits'],
)
for image in cache_list
)
def setUp(self):
super().setUp()
# Get the command object to test
self.image_client.get_image_cache.return_value = self._cache
self.cmd = cache.ListCachedImage(self.app, None)
def test_image_cache_list(self):
arglist = []
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
self.image_client.get_image_cache.assert_called()
self.assertEqual(self.columns, columns)
self.assertEqual(tuple(self.datalist), tuple(data))
class TestQueueCache(fakes.TestImagev2):
def setUp(self):
super().setUp()
self.image_client.queue_image.return_value = None
self.cmd = cache.QueueCachedImage(self.app, None)
def test_cache_queue(self):
images = fakes.create_images(count=1)
arglist = [
images[0].id,
]
verifylist = [
('images', [images[0].id]),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.image_client.find_image.side_effect = images
self.cmd.take_action(parsed_args)
self.image_client.queue_image.assert_called_once_with(images[0].id)
def test_cache_queue_multiple_images(self):
images = fakes.create_images(count=3)
arglist = [i.id for i in images]
verifylist = [
('images', arglist),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.image_client.find_image.side_effect = images
self.cmd.take_action(parsed_args)
calls = [call(i.id) for i in images]
self.image_client.queue_image.assert_has_calls(calls)
class TestCacheDelete(fakes.TestImagev2):
def setUp(self):
super().setUp()
self.image_client.cache_delete_image.return_value = None
self.cmd = cache.DeleteCachedImage(self.app, None)
def test_cache_delete(self):
images = fakes.create_images(count=1)
arglist = [
images[0].id,
]
verifylist = [
('images', [images[0].id]),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.image_client.find_image.side_effect = images
self.cmd.take_action(parsed_args)
self.image_client.find_image.assert_called_once_with(
images[0].id, ignore_missing=False
)
self.image_client.cache_delete_image.assert_called_once_with(
images[0].id
)
def test_cache_delete_multiple_images(self):
images = fakes.create_images(count=3)
arglist = [i.id for i in images]
verifylist = [
('images', arglist),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.image_client.find_image.side_effect = images
self.cmd.take_action(parsed_args)
calls = [call(i.id) for i in images]
self.image_client.cache_delete_image.assert_has_calls(calls)
def test_cache_delete_multiple_images_exception(self):
images = fakes.create_images(count=2)
arglist = [
images[0].id,
images[1].id,
'x-y-x',
]
verifylist = [
('images', arglist),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
ret_find = [images[0], images[1], sdk_exceptions.ResourceNotFound()]
self.image_client.find_image.side_effect = ret_find
self.assertRaises(
exceptions.CommandError, self.cmd.take_action, parsed_args
)
calls = [call(i.id) for i in images]
self.image_client.cache_delete_image.assert_has_calls(calls)
class TestCacheClear(fakes.TestImagev2):
def setUp(self):
super().setUp()
self.image_client.clear_cache.return_value = None
self.cmd = cache.ClearCachedImage(self.app, None)
def test_cache_clear_no_option(self):
arglist = []
verifylist = [('target', None)]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertIsNone(
self.image_client.clear_cache.assert_called_with(None)
)
def test_cache_clear_queue_option(self):
arglist = ['--queue']
verifylist = [('target', 'queue')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.image_client.clear_cache.assert_called_once_with('queue')
def test_cache_clear_cache_option(self):
arglist = ['--cache']
verifylist = [('target', 'cache')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.image_client.clear_cache.assert_called_once_with('cache')

View File

@ -0,0 +1,5 @@
---
features:
- |
Add commands for the image Cache API, to list, queue,
delete and clear images in the cache.

View File

@ -405,6 +405,12 @@ openstack.image.v2 =
image_metadef_resource_type_list = openstackclient.image.v2.metadef_resource_types:ListMetadefResourceTypes
cached_image_list = openstackclient.image.v2.cache:ListCachedImage
cached_image_queue = openstackclient.image.v2.cache:QueueCachedImage
cached_image_delete = openstackclient.image.v2.cache:DeleteCachedImage
cached_image_clear = openstackclient.image.v2.cache:ClearCachedImage
openstack.network.v2 =
address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup
address_group_delete = openstackclient.network.v2.address_group:DeleteAddressGroup