diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index 9a214b015..1b020c17a 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -1042,9 +1042,8 @@ class Proxy(proxy.Proxy): """Add a tag to an image :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance - that the member will be created for. - :param str tag: The tag to be added + :class:`~openstack.image.v2.image.Image` instance. + :param tag: The tag to be added. :returns: None """ @@ -1052,12 +1051,11 @@ class Proxy(proxy.Proxy): image.add_tag(self, tag) def remove_tag(self, image, tag): - """Remove a tag to an image + """Remove a tag from an image :param image: The value can be the ID of a image or a - :class:`~openstack.image.v2.image.Image` instance - that the member will be created for. - :param str tag: The tag to be removed + :class:`~openstack.image.v2.image.Image` instance. + :param tag: The tag to be removed. :returns: None """ @@ -1278,6 +1276,50 @@ class Proxy(proxy.Proxy): **attrs, ) + def add_tag_to_metadef_namespace(self, namespace, tag): + """Add a tag to a metadef namespace + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param str tag: The tag to be added. + + :returns: None + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, namespace + ) + namespace.add_tag(self, tag) + + def remove_tag_from_metadef_namespace(self, namespace, tag): + """Remove a tag from a metadef namespace + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + :param str tag: The tag to be removed. + + :returns: None + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, namespace + ) + namespace.remove_tag(self, tag) + + def remove_tags_from_metadef_namespace(self, namespace): + """Remove all tags from a metadef namespace + + :param metadef_namespace: Either the name of a metadef namespace or an + :class:`~openstack.image.v2.metadef_namespace.MetadefNamespace` + instance. + + :returns: None + """ + namespace = self._get_resource( + _metadef_namespace.MetadefNamespace, namespace + ) + namespace.remove_all_tags(self) + # ====== METADEF OBJECT ====== def create_metadef_object(self, namespace, **attrs): """Create a new object from namespace diff --git a/openstack/image/v2/metadef_namespace.py b/openstack/image/v2/metadef_namespace.py index 88e028f8e..b7f3e50ac 100644 --- a/openstack/image/v2/metadef_namespace.py +++ b/openstack/image/v2/metadef_namespace.py @@ -10,12 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. + +import typing_extensions as ty_ext + +from openstack.common import tag from openstack import exceptions from openstack import resource from openstack import utils -class MetadefNamespace(resource.Resource): +class MetadefNamespace(resource.Resource, tag.TagMixin): resources_key = 'namespaces' base_path = '/metadefs/namespaces' @@ -98,3 +102,50 @@ class MetadefNamespace(resource.Resource): """ url = utils.urljoin(self.base_path, self.id, 'objects') return self._delete_all(session, url) + + # NOTE(mrjoshi): This method is re-implemented as we require a ``POST`` + # call while the original method does a ``PUT`` call. + def add_tag(self, session: resource.AdapterT, tag: str) -> ty_ext.Self: + """Adds a single tag to the resource. + + :param session: The session to use for making this request. + :param tag: The tag as a string. + """ + url = utils.urljoin(self.base_path, self.id, 'tags', tag) + session = self._get_session(session) + response = session.post(url) + exceptions.raise_from_response(response) + # we do not want to update tags directly + tags = self.tags + tags.append(tag) + self._body.attributes.update({'tags': tags}) + return self + + # NOTE(mrjoshi): This method is re-implemented to add support for the + # 'append' option. This method uses a ``POST`` call rather than the + # standard ``PUT`` call. + def set_tags( + self, session: resource.AdapterT, tags: list[str], append: bool = False + ) -> ty_ext.Self: + """Sets/Replaces all tags on the resource. + + :param session: The session to use for making this request. + :param list tags: List with tags to be set on the resource + :param append: If set to true, adds new tags to existing tags, + else overwrites the existing tags with new ones. + """ + url = utils.urljoin(self.base_path, self.id, 'tags') + session = self._get_session(session) + + headers = {'X-OpenStack-Append': 'False'} + if append: + headers['X-Openstack-Append'] = 'True' + + response = session.post( + url, headers=headers, json={'tags': [{'name': x} for x in tags]} + ) + exceptions.raise_from_response(response) + + self._body.attributes.update({'tags': tags}) + + return self diff --git a/openstack/tests/functional/image/v2/test_metadef_namespace.py b/openstack/tests/functional/image/v2/test_metadef_namespace.py index 641926cc9..c340b33d5 100644 --- a/openstack/tests/functional/image/v2/test_metadef_namespace.py +++ b/openstack/tests/functional/image/v2/test_metadef_namespace.py @@ -90,3 +90,48 @@ class TestMetadefNamespace(base.BaseImageTest): metadef_namespace_description, metadef_namespace.description, ) + + def test_tags(self): + # add tag + metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + self.metadef_namespace.namespace + ) + metadef_namespace.add_tag(self.operator_cloud.image, 't1') + metadef_namespace.add_tag(self.operator_cloud.image, 't2') + + # list tags + metadef_namespace.fetch_tags(self.operator_cloud.image) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t1', md_tags) + self.assertIn('t2', md_tags) + + # remove tag + metadef_namespace.remove_tag(self.operator_cloud.image, 't1') + metadef_namespace = self.operator_cloud.image.get_metadef_namespace( + self.metadef_namespace.namespace + ) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t2', md_tags) + self.assertNotIn('t1', md_tags) + + # add tags without append + metadef_namespace.set_tags(self.operator_cloud.image, ["t1", "t2"]) + metadef_namespace.fetch_tags(self.operator_cloud.image) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t1', md_tags) + self.assertIn('t2', md_tags) + + # add tags with append + metadef_namespace.set_tags( + self.operator_cloud.image, ["t3", "t4"], append=True + ) + metadef_namespace.fetch_tags(self.operator_cloud.image) + md_tags = [tag['name'] for tag in metadef_namespace.tags] + self.assertIn('t1', md_tags) + self.assertIn('t2', md_tags) + self.assertIn('t3', md_tags) + self.assertIn('t4', md_tags) + + # remove all tags + metadef_namespace.remove_all_tags(self.operator_cloud.image) + self.assertEqual([], metadef_namespace.tags) diff --git a/openstack/tests/unit/image/v2/test_metadef_namespace.py b/openstack/tests/unit/image/v2/test_metadef_namespace.py index cf614f2a8..b8a829055 100644 --- a/openstack/tests/unit/image/v2/test_metadef_namespace.py +++ b/openstack/tests/unit/image/v2/test_metadef_namespace.py @@ -18,6 +18,7 @@ from keystoneauth1 import adapter from openstack import exceptions from openstack.image.v2 import metadef_namespace from openstack.tests.unit import base +from openstack.tests.unit.test_resource import FakeResponse EXAMPLE = { @@ -97,3 +98,55 @@ class TestMetadefNamespace(base.TestCase): session.delete.assert_called_with( 'metadefs/namespaces/OS::Cinder::Volumetype/objects' ) + + +class TestMetadefNamespaceTags(base.TestCase): + # The tests in this class are very similar to those provided by + # TestTagMixin. The main differences are: + # - test_add_tag uses a ``PUT`` call instead of a ``POST`` call + # - test_set_tag uses a ``PUT`` call instead of a ``POST`` call + # - test_set_tag uses an optional ``X-OpenStack-Append`` header + def setUp(self): + super().setUp() + self.base_path = 'metadefs/namespaces' + self.response = FakeResponse({}) + + self.session = mock.Mock(spec=adapter.Adapter) + self.session.post = mock.Mock(return_value=self.response) + + def test_add_tag(self): + res = metadef_namespace.MetadefNamespace(**EXAMPLE) + sess = self.session + + # Set some initial value to check add + res.tags = ['blue', 'green'] + + result = res.add_tag(sess, 'lila') + # Check tags attribute is updated + self.assertEqual(['blue', 'green', 'lila'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags/lila' + sess.post.assert_called_once_with(url) + + def test_set_tags(self): + res = metadef_namespace.MetadefNamespace(**EXAMPLE) + sess = self.session + + # Set some initial value to check rewrite + res.tags = ['blue_old', 'green_old'] + + result = res.set_tags(sess, ['blue', 'green']) + # Check tags attribute is updated + self.assertEqual(['blue', 'green'], res.tags) + # Check the passed resource is returned + self.assertEqual(res, result) + url = self.base_path + '/' + res.id + '/tags' + headers = {'X-OpenStack-Append': 'False'} + jsonargs = { + 'tags': [ + {'name': 'blue'}, + {'name': 'green'}, + ] + } + sess.post.assert_called_once_with(url, headers=headers, json=jsonargs) diff --git a/releasenotes/notes/add-image-metadef-tags-c980ec5e6502d76c.yaml b/releasenotes/notes/add-image-metadef-tags-c980ec5e6502d76c.yaml new file mode 100644 index 000000000..11ac84232 --- /dev/null +++ b/releasenotes/notes/add-image-metadef-tags-c980ec5e6502d76c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for Image Metadef Tags to create, remove + create-multiple, update tags.