image: Add support for cache commands
Depends-on: https://review.opendev.org/c/openstack/openstacksdk/+/874372 Depends-on: https://review.opendev.org/c/openstack/openstacksdk/+/874940 Change-Id: I96b95cb93d298602b6d4b0cd35a213478babff5f
This commit is contained in:
parent
08faf81d0d
commit
c628c2dcd3
@ -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)"
|
||||
|
|
218
openstackclient/image/v2/cache.py
Normal file
218
openstackclient/image/v2/cache.py
Normal 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)
|
@ -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
|
||||
@ -238,6 +239,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.
|
||||
|
||||
|
214
openstackclient/tests/unit/image/v2/test_cache.py
Normal file
214
openstackclient/tests/unit/image/v2/test_cache.py
Normal 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')
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add commands for the image Cache API, to list, queue,
|
||||
delete and clear images in the cache.
|
@ -399,6 +399,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
|
||||
|
Loading…
x
Reference in New Issue
Block a user