diff --git a/glareclient/osc/v1/artifacts.py b/glareclient/osc/v1/artifacts.py index e482c86..1b1d4c5 100644 --- a/glareclient/osc/v1/artifacts.py +++ b/glareclient/osc/v1/artifacts.py @@ -374,3 +374,44 @@ class TypeList(command.Lister): column_headers = [c.capitalize() for c in columns] return (column_headers, data) + + +class TypeSchema(command.Lister): + """Schema of type name.""" + + def get_parser(self, prog_name): + parser = super(TypeSchema, self).get_parser(prog_name) + parser.add_argument( + 'type_name', + metavar='', + action=TypeMapperAction, + help='Name of artifact type.', + ) + return parser + + def take_action(self, parsed_args): + LOG.debug('take_action({0})'.format(parsed_args)) + client = self.app.client_manager.artifact + data = client.artifacts.get_type_schema( + type_name=parsed_args.type_name)['properties'] + + columns = ('name', 'glare_type', 'mutable', 'required', + 'sortable', 'filters', 'available_values') + column_headers = [c.capitalize() for c in columns] + + table = [] + + for name, values in six.iteritems(data): + row = ( + name, + values.get('glareType'), + values.get('mutable', False), + values.get('required_on_activate', True), + values.get('sortable', False), + values.get('filter_ops'), + values.get('enum', '') + ) + table.append(row) + + return (column_headers, + table) diff --git a/glareclient/tests/unit/osc/v1/fakes.py b/glareclient/tests/unit/osc/v1/fakes.py index 1820839..39c2666 100644 --- a/glareclient/tests/unit/osc/v1/fakes.py +++ b/glareclient/tests/unit/osc/v1/fakes.py @@ -15,11 +15,12 @@ import sys - -from glareclient.common import utils as g_utils import mock from osc_lib.tests import utils +from glareclient.common import utils as g_utils +from glareclient.tests.unit.osc.v1 import fakes_schemas + blob_fixture = { "status": "active", "url": "fake_url", @@ -70,6 +71,10 @@ def mock_g_servs(*args, **kwargs): 'status': 'active'} +def mock_g_schema(*args, **kwargs): + return fakes_schemas.FIXTURE_SCHEMA + + def mock_get_data_file(*args, **kwargs): return 'data' @@ -90,6 +95,8 @@ class TestArtifacts(utils.TestCommand): self.app.client_manager.artifact.artifacts.publish = mock_g_servs self.app.client_manager.artifact.blobs.upload_blob = mock_g_servs self.app.client_manager.artifact.blobs.download_blob = mock_g_servs + self.app.client_manager.artifact.artifacts.get_type_schema = \ + mock_g_schema g_utils.get_data_file = mock.MagicMock() g_utils.get_data_file = mock_get_data_file g_utils.save_blob = mock.MagicMock() diff --git a/glareclient/tests/unit/osc/v1/fakes_schemas.py b/glareclient/tests/unit/osc/v1/fakes_schemas.py new file mode 100644 index 0000000..45c5c99 --- /dev/null +++ b/glareclient/tests/unit/osc/v1/fakes_schemas.py @@ -0,0 +1,372 @@ +# Copyright 2016 OpenStack Foundation +# 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. + +FIXTURE_SCHEMA = { + u'name': u'sample_artifact', + u'properties': { + u'activated_at': { + u'description': u'Datetime when artifact has became active.', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'format': u'date-time', + u'glareType': u'DateTime', + u'readOnly': True, + u'required_on_activate': False, + u'sortable': True, + u'type': [u'string', + u'null']}, + u'created_at': { + u'description': u'Datetime when artifact has been created.', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'format': u'date-time', + u'glareType': u'DateTime', + u'readOnly': True, + u'sortable': True, + u'type': u'string'}, + u'description': {u'default': u'', + u'description': u'Artifact description.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'String', + u'maxLength': 4096, + u'mutable': True, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'icon': {u'additionalProperties': False, + u'description': u'Artifact icon.', + u'filter_ops': [], + u'glareType': u'Blob', + u'properties': {u'md5': {u'type': [u'string', u'null']}, + u'sha1': {u'type': [u'string', u'null']}, + u'sha256': {u'type': [u'string', u'null']}, + u'content_type': {u'type': u'string'}, + u'external': {u'type': u'boolean'}, + u'size': {u'type': [u'number', + u'null']}, + u'status': {u'enum': [u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'md5', u'sha1', u'sha256', + u'external', + u'status', + u'content_type'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'id': {u'description': u'Artifact UUID.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'String', + u'maxLength': 255, + u'pattern': u'^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-' + u'fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$', + u'readOnly': True, + u'sortable': True, + u'type': u'string'}, + u'license': {u'description': u'Artifact license type.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'String', + u'maxLength': 255, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'license_url': {u'description': u'URL to artifact license.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'String', + u'maxLength': 255, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'metadata': {u'additionalProperties': {u'type': u'string'}, + u'default': {}, + u'description': u'Key-value dict with useful information' + u'about an artifact.', + u'filter_ops': [u'eq', + u'neq'], + u'glareType': u'StringDict', + u'maxProperties': 255, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'name': {u'description': u'Artifact Name.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'String', + u'maxLength': 255, + u'required_on_activate': False, + u'sortable': True, + u'type': u'string'}, + u'owner': {u'description': u'ID of user/tenant who uploaded artifact.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'String', + u'maxLength': 255, + u'readOnly': True, + u'required_on_activate': False, + u'sortable': True, + u'type': u'string'}, + u'provided_by': {u'additionalProperties': False, + u'description': u'Info about artifact authors.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'StringDict', + u'maxProperties': 255, + u'properties': {u'company': {u'type': u'string'}, + u'href': {u'type': u'string'}, + u'name': {u'type': u'string'}}, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'release': {u'default': [], + u'description': u'Target OpenStack release for artifact. ' + u'It is usually the same when artifact ' + u'was uploaded.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'StringList', + u'items': {u'type': u'string'}, + u'maxItems': 255, + u'required_on_activate': False, + u'type': [u'array', + u'null'], + u'unique': True}, + u'status': {u'default': u'drafted', + u'description': u'Artifact status.', + u'enum': [u'drafted', + u'active', + u'deactivated', + u'deleted'], + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'String', + u'sortable': True, + u'type': u'string'}, + u'supported_by': {u'additionalProperties': {u'type': u'string'}, + u'description': u'Info about persons who ' + u'responsible for artifact support', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'StringDict', + u'maxProperties': 255, + u'required': [u'name'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'tags': {u'default': [], + u'description': u'List of tags added to Artifact.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'glareType': u'StringList', + u'items': {u'type': u'string'}, + u'maxItems': 255, + u'mutable': True, + u'required_on_activate': False, + u'type': [u'array', + u'null']}, + u'updated_at': { + u'description': u'Datetime when artifact has been updated ' + u'last time.', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'format': u'date-time', + u'glareType': u'DateTime', + u'readOnly': True, + u'sortable': True, + u'type': u'string'}, + u'version': {u'default': u'0.0.0', + u'description': u'Artifact version(semver).', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'glareType': u'String', + u'pattern': u'/^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-' + u'([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?' + u'(?:\\+[0-9A-Za-z-]+)?$/', + u'required_on_activate': False, + u'sortable': True, + u'type': u'string'}, + u'visibility': {u'default': u'private', + u'description': u'Artifact visibility that defines if ' + u'artifact can be available to other ' + u'users.', + u'filter_ops': [u'eq'], + u'glareType': u'String', + u'maxLength': 255, + u'sortable': True, + u'type': u'string'}, + u'image': {u'additionalProperties': False, + u'description': u'Image binary.', + u'filter_ops': [], + u'glareType': u'Blob', + u'properties': { + u'md5': {u'type': [u'string', u'null']}, + u'sha1': {u'type': [u'string', u'null']}, + u'sha256': {u'type': [u'string', u'null']}, + u'content_type': {u'type': u'string'}, + u'external': {u'type': u'boolean'}, + u'size': {u'type': [u'number', + u'null']}, + u'status': {u'enum': [u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'md5', u'sha1', u'sha256', + u'external', + u'status', + u'content_type'], + u'required_on_activate': False, + u'type': [u'object', u'null']}, + u'package': { + u'additionalProperties': False, + u'description': u'Murano Package binary.', + u'filter_ops': [], + u'glareType': u'Blob', + u'properties': {u'md5': {u'type': [u'string', u'null']}, + u'sha1': {u'type': [u'string', u'null']}, + u'sha256': {u'type': [u'string', u'null']}, + u'content_type': {u'type': u'string'}, + u'external': {u'type': u'boolean'}, + u'size': {u'type': [u'number', + u'null']}, + u'status': {u'enum': [u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'md5', u'sha1', u'sha256', + u'external', + u'status', + u'content_type'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'environment': { + u'additionalProperties': False, + u'description': u'Heat Environment text body.', + u'filter_ops': [], + u'glareType': u'Blob', + u'properties': {u'md5': {u'type': [u'string', u'null']}, + u'sha1': {u'type': [u'string', u'null']}, + u'sha256': {u'type': [u'string', u'null']}, + u'content_type': {u'type': u'string'}, + u'external': {u'type': u'boolean'}, + u'size': {u'type': [u'number', + u'null']}, + u'status': {u'enum': [u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'md5', u'sha1', u'sha256', + u'external', + u'status', + u'content_type'], + u'type': [u'object', + u'null']}, + u'blob': { + u'additionalProperties': False, + u'description': u'I am Blob', + u'filter_ops': [], + u'glareType': u'Blob', + u'mutable': True, + u'properties': { + u'md5': {u'type': [u'string', u'null']}, + u'sha1': {u'type': [u'string', u'null']}, + u'sha256': {u'type': [u'string', u'null']}, + u'content_type': { + u'type': u'string'}, + u'external': { + u'type': u'boolean'}, + u'size': {u'type': [ + u'number', + u'null']}, + u'status': { + u'enum': [ + u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'md5', u'sha1', u'sha256', + u'external', + u'status', + u'content_type'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'template': { + u'additionalProperties': False, + u'description': u'TOSCA template body.', + u'filter_ops': [], + u'glareType': u'Blob', + u'properties': { + u'md5': {u'type': [u'string', u'null']}, + u'sha1': {u'type': [u'string', u'null']}, + u'sha256': {u'type': [u'string', u'null']}, + u'content_type': { + u'type': u'string'}, + u'external': {u'type': u'boolean'}, + u'size': {u'type': [u'number', + u'null']}, + u'status': {u'enum': [u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'md5', u'sha1', u'sha256', + u'external', + u'status', + u'content_type'], + u'type': [u'object', + u'null']}, + } +} diff --git a/glareclient/tests/unit/osc/v1/test_artifacts.py b/glareclient/tests/unit/osc/v1/test_artifacts.py index e59c2f2..cbef3d7 100644 --- a/glareclient/tests/unit/osc/v1/test_artifacts.py +++ b/glareclient/tests/unit/osc/v1/test_artifacts.py @@ -341,3 +341,69 @@ class TestPublishArtifacts(TestArtifacts): name_fields = set([column[0] for column in data]) # Check that columns are correct self.assertEqual(self.COLUMNS, name_fields) + + +class TypeSchema(TestArtifacts): + + def setUp(self): + super(TypeSchema, self).setUp() + self.artifact_mock.call.return_value = \ + api_art.Controller(self.http, type_name='sample_artifact') + + # Command to test + self.cmd = osc_art.TypeSchema(self.app, None) + + def test_get_schema(self): + arglist = ['sample_artifact'] + verify = [('type_name', 'sample_artifact')] + parsed_args = self.check_parser(self.cmd, arglist, verify) + columns, data = self.cmd.take_action(parsed_args) + + exp_columns = ['Name', 'Glare_type', 'Mutable', 'Required', + 'Sortable', 'Filters', 'Available_values'] + exp_data = [ + (u'image', u'Blob', False, False, False, [], ''), + (u'updated_at', u'DateTime', False, True, True, + [u'eq', u'neq', u'in', u'gt', u'gte', u'lt', u'lte'], ''), + (u'owner', u'String', False, False, True, + [u'eq', u'neq', u'in'], ''), + (u'provided_by', u'StringDict', False, False, False, + [u'eq', u'neq', u'in'], ''), + (u'id', u'String', False, True, True, [u'eq', u'neq', u'in'], ''), + (u'environment', u'Blob', False, True, False, [], ''), + (u'version', u'String', False, False, True, + [u'eq', u'neq', u'in', u'gt', u'gte', u'lt', u'lte'], ''), + (u'blob', u'Blob', True, False, False, [], ''), + (u'template', u'Blob', False, True, False, [], ''), + (u'metadata', u'StringDict', False, False, False, + [u'eq', u'neq'], ''), + (u'status', u'String', False, True, True, [u'eq', u'neq', u'in'], + [u'drafted', u'active', u'deactivated', u'deleted']), + (u'description', u'String', True, False, False, + [u'eq', u'neq', u'in'], ''), + (u'tags', u'StringList', True, False, False, + [u'eq', u'neq', u'in'], ''), + (u'activated_at', u'DateTime', False, False, True, + [u'eq', u'neq', u'in', u'gt', u'gte', u'lt', u'lte'], ''), + (u'supported_by', u'StringDict', False, False, False, + [u'eq', u'neq', u'in'], ''), + (u'visibility', u'String', False, True, True, [u'eq'], ''), + (u'icon', u'Blob', False, False, False, [], ''), + (u'name', u'String', False, False, True, + [u'eq', u'neq', u'in'], ''), + (u'license', u'String', False, False, False, + [u'eq', u'neq', u'in'], ''), + (u'package', u'Blob', False, False, False, [], ''), + (u'created_at', u'DateTime', False, True, True, + [u'eq', u'neq', u'in', u'gt', u'gte', u'lt', u'lte'], ''), + (u'license_url', u'String', False, False, False, + [u'eq', u'neq', u'in'], ''), + (u'release', u'StringList', False, False, False, + [u'eq', u'neq', u'in'], '')] + + data.sort(key=lambda x: x[0]) + exp_data.sort(key=lambda x: x[0]) + + # Check that columns are correct + self.assertEqual(exp_columns, columns) + self.assertEqual(exp_data, data) diff --git a/glareclient/tests/unit/v1/fixtures.py b/glareclient/tests/unit/v1/fixtures.py index b814e0c..e9fc3e5 100644 --- a/glareclient/tests/unit/v1/fixtures.py +++ b/glareclient/tests/unit/v1/fixtures.py @@ -198,5 +198,16 @@ data_fixtures = { 'heat_environments': {'name': 'heat_environments', 'version': '1.0'}}} ) + }, + '/schemas/images': { + 'GET': ( + {}, + {'schemas': { + 'images': {'name': 'images', + 'version': '1.0', + 'properties': {'foo': 'bar'} + }}} + ) } + } diff --git a/glareclient/tests/unit/v1/test_artifacts.py b/glareclient/tests/unit/v1/test_artifacts.py index 2dbca53..3ffae12 100644 --- a/glareclient/tests/unit/v1/test_artifacts.py +++ b/glareclient/tests/unit/v1/test_artifacts.py @@ -374,3 +374,11 @@ class TestController(testtools.TestCase): expect_call = [('GET', '/schemas', {}, None)] self.assertEqual(expect_call, self.api.calls) self.assertEqual(expect_data, data) + + def test_get_schema(self): + data = self.controller.get_type_schema(type_name='images') + expect_data = {'name': 'images', 'version': '1.0', + 'properties': {'foo': 'bar'}} + expect_call = [('GET', '/schemas/images', {}, None)] + self.assertEqual(expect_call, self.api.calls) + self.assertEqual(expect_data, data) diff --git a/setup.cfg b/setup.cfg index 4fde8de..76d4f10 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ openstack.artifact.v1 = artifact_upload = glareclient.osc.v1.blobs:UploadBlob artifact_download = glareclient.osc.v1.blobs:DownloadBlob artifact_type-list = glareclient.osc.v1.artifacts:TypeList + artifact_schema = glareclient.osc.v1.artifacts:TypeSchema [build_sphinx] source-dir = doc/source