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-clear,cached image list,"Clear all images from cache, queue or both."
|
||||||
cache-delete,,Delete image from cache/caching queue.
|
cache-delete,cached image delete,Delete image from cache/caching queue.
|
||||||
cache-list,,Get cache state.
|
cache-list,cached image list,Get cache state.
|
||||||
cache-queue,,Queue image(s) for caching.
|
cache-queue,cached image queue,Queue image(s) for caching.
|
||||||
explain,WONTFIX,Describe a specific model.
|
explain,WONTFIX,Describe a specific model.
|
||||||
image-create,image create,Create a new image.
|
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)"
|
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
|
import uuid
|
||||||
|
|
||||||
from openstack.image.v2 import _proxy
|
from openstack.image.v2 import _proxy
|
||||||
|
from openstack.image.v2 import cache
|
||||||
from openstack.image.v2 import image
|
from openstack.image.v2 import image
|
||||||
from openstack.image.v2 import member
|
from openstack.image.v2 import member
|
||||||
from openstack.image.v2 import metadef_namespace
|
from openstack.image.v2 import metadef_namespace
|
||||||
@ -238,6 +239,28 @@ def create_tasks(attrs=None, count=2):
|
|||||||
return tasks
|
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):
|
def create_one_metadef_namespace(attrs=None):
|
||||||
"""Create a fake MetadefNamespace member.
|
"""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
|
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 =
|
openstack.network.v2 =
|
||||||
address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup
|
address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup
|
||||||
address_group_delete = openstackclient.network.v2.address_group:DeleteAddressGroup
|
address_group_delete = openstackclient.network.v2.address_group:DeleteAddressGroup
|
||||||
|
Loading…
x
Reference in New Issue
Block a user