Glare 0.2.0 release

-----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJX89giAAoJEGvNeA1vV2/Yrg4IAK1icFWXWE7ikgtiFWsIQMZf
 ZRPGkNyG4kKuruFibJFUBZnlYgYYRV6QnyhrL6QH2bQmSD9s1ES6yV3UyxQP3Em3
 tFyi/CIUIyppkYsEA+BlBxoKpY8oXE/OIEDvo0Pi4SGmbKTMdidkvDGK7deUKsOl
 rftPBjTTlJXGX97ACnu9AY1yhIHYsfugTSXyVVfLIcZ2Jjxr+PglopRSnyoo9ejl
 sabjSQGF5cl2mtHmdXGbA5gBdWQN2A9wW6IjtClorkbDYJI7U3TbLBSP5C+Xkq7K
 2DKAJsSpv7N7YhixTH8ZVH/3N+tv7IPxu3PCwiu7VkfE6xF9H/tOASiOV9Sx0H0=
 =YFJg
 -----END PGP SIGNATURE-----

Merge tag '0.2.0' into debian/newton

Glare 0.2.0 release

Change-Id: I8ac5fc4af3367bdf2946c9733711cfb78ebc4d24
This commit is contained in:
Ivan Udovichenko 2016-10-06 12:39:49 +03:00
commit 6789bdd57a
27 changed files with 931 additions and 322 deletions

View File

@ -1,4 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/glare.git
project=openstack/deb-glare.git

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
glare (0.2.0-1) UNRELEASED; urgency=medium
* New upstream release.
-- Ivan Udovichenko <iudovichenko@mirantis.com> Thu, 06 Oct 2016 12:42:54 +0300
glare (0.1.0-1) unstable; urgency=medium
* Initial release. (Closes: #839231)

4
debian/control vendored
View File

@ -43,7 +43,7 @@ Build-Depends-Indep: python-alembic (>= 0.8.4),
python-oslo.log (>= 3.11.0),
python-oslo.messaging (>= 5.2.0),
python-oslo.middleware (>= 3.0.0),
python-oslo.policy (>= 1.9.0),
python-oslo.policy (>= 1.14.0),
python-oslo.serialization (>= 1.10.0),
python-oslo.service (>= 1.10.0),
python-oslo.utils (>= 3.16.0),
@ -101,7 +101,7 @@ Depends: python-alembic (>= 0.8.4),
python-oslo.log (>= 3.11.0),
python-oslo.messaging (>= 5.2.0),
python-oslo.middleware (>= 3.0.0),
python-oslo.policy (>= 1.9.0),
python-oslo.policy (>= 1.14.0),
python-oslo.serialization (>= 1.10.0),
python-oslo.service (>= 1.10.0),
python-oslo.utils (>= 3.16.0),

View File

@ -72,6 +72,9 @@ function configure_glare {
iniset $GLARE_CONF_FILE oslo_messaging_rabbit rabbit_userid $RABBIT_USERID
iniset $GLARE_CONF_FILE oslo_messaging_rabbit rabbit_password $RABBIT_PASSWORD
# Enable notifications support
iniset $GLARE_CONF_FILE oslo_messaging_notifications driver messaging
# Configure the database.
iniset $GLARE_CONF_FILE database connection `database_connection_url glare`
iniset $GLARE_CONF_FILE database max_overflow -1

View File

@ -70,6 +70,7 @@ def main():
config.parse_args()
wsgi.set_eventlet_hub()
logging.setup(CONF, 'glare')
notification.set_defaults()
if cfg.CONF.profiler.enabled:
_notifier = osprofiler.notifier.create(

View File

@ -33,8 +33,6 @@ artifact_policy_rules = [
'is_admin:True or project_id:%(owner)s'),
policy.RuleDefault("artifact:type_list", "",
"Policy to request list of artifact types"),
policy.RuleDefault("artifact:type_get", "",
"Policy to request artifact type definition"),
policy.RuleDefault("artifact:create", "", "Policy to create artifact."),
policy.RuleDefault("artifact:update_public",
"'public':%(visibility)s and rule:context_is_admin "

View File

@ -40,11 +40,13 @@ from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import excutils
from oslo_utils import timeutils
from oslo_versionedobjects import fields
import six
from webob import exc
from glare.common import exception
from glare.i18n import _, _LE, _LW
from glare.objects.meta import fields as glare_fields
CONF = cfg.CONF
@ -553,6 +555,54 @@ class error_handler(object):
return new_function
def get_schema_type(attr):
if isinstance(attr, fields.IntegerField):
return 'integer'
elif isinstance(attr, fields.FloatField):
return 'number'
elif isinstance(attr, fields.BooleanField):
return 'boolean'
elif isinstance(attr, glare_fields.List):
return 'array'
elif isinstance(attr, (glare_fields.Dict, glare_fields.BlobField)):
return 'object'
return 'string'
def get_glare_type(attr):
if isinstance(attr, fields.IntegerField):
return 'Integer'
elif isinstance(attr, fields.FloatField):
return 'Float'
elif isinstance(attr, fields.FlexibleBooleanField):
return 'Boolean'
elif isinstance(attr, fields.DateTimeField):
return 'DateTime'
elif isinstance(attr, glare_fields.BlobField):
return 'Blob'
elif isinstance(attr, glare_fields.Link):
return 'Link'
elif isinstance(attr, glare_fields.List):
return _get_element_type(attr.element_type) + 'List'
elif isinstance(attr, glare_fields.Dict):
return _get_element_type(attr.element_type) + 'Dict'
return 'String'
def _get_element_type(element_type):
if element_type is fields.FlexibleBooleanField:
return 'Boolean'
elif element_type is fields.Integer:
return 'Integer'
elif element_type is fields.Float:
return 'Float'
elif element_type is glare_fields.BlobFieldType:
return 'Blob'
elif element_type is glare_fields.LinkFieldType:
return 'Link'
return 'String'
class DictDiffer(object):
"""
Calculate the difference between two dictionaries as:

View File

@ -57,7 +57,8 @@ class ArtifactAPI(base_api.BaseDBAPI):
def list(self, context, filters, marker, limit, sort, latest):
session = api.get_session()
filters.append(('type_name', None, 'eq', None, self.type))
if self.type != 'all':
filters.append(('type_name', None, 'eq', None, self.type))
return api.get_all(context=context, session=session, filters=filters,
marker=marker, limit=limit, sort=sort,
latest=latest)

View File

@ -113,7 +113,7 @@ class Engine(object):
@classmethod
def show_type_schema(cls, context, type_name):
policy.authorize("artifact:type_get", {}, context)
policy.authorize("artifact:type_list", {}, context)
schemas = cls._get_schemas(cls.registry)
if type_name not in schemas:
msg = _("Artifact type %s does not exist") % type_name

View File

@ -15,7 +15,6 @@
from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging
from oslo_messaging import serializer
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@ -31,19 +30,8 @@ def get_transport():
return oslo_messaging.get_notification_transport(CONF)
class RequestSerializer(serializer.Serializer):
def serialize_entity(self, context, entity):
return entity.to_notification()
def deserialize_entity(self, context, entity):
return entity
def serialize_context(self, context):
return context.to_dict()
def deserialize_context(self, context):
return context.from_dict(context)
def set_defaults(control_exchange='glare'):
oslo_messaging.set_transport_defaults(control_exchange)
class Notifier(object):
@ -59,8 +47,7 @@ class Notifier(object):
if cls.GLARE_NOTIFIER is None:
cls.GLARE_NOTIFIER = oslo_messaging.Notifier(
get_transport(),
publisher_id=CONF.glare_publisher_id,
serializer=RequestSerializer())
publisher_id=CONF.glare_publisher_id)
return cls.GLARE_NOTIFIER
@classmethod
@ -74,7 +61,8 @@ class Notifier(object):
"""
af_notifier = cls._get_notifier()
method = getattr(af_notifier, level.lower())
method(context, "%s.%s" % (cls.SERVICE_NAME, event_type), body)
method({}, "%s.%s" % (cls.SERVICE_NAME, event_type),
body.to_notification())
LOG.debug('Notification event %(event)s send successfully for '
'request %(request)s', {'event': event_type,
'request': context.request_id})

34
glare/objects/all.py Normal file
View File

@ -0,0 +1,34 @@
# Copyright (c) 2016 Mirantis, Inc.
#
# 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 oslo_versionedobjects import fields
from glare.objects import base
from glare.objects.meta import attribute
Field = attribute.Attribute.init
class All(base.ReadOnlyMixin, base.BaseArtifact):
"""Artifact type that allows to get artifacts regardless of their type"""
fields = {
'type_name': Field(fields.StringField,
description="Name of artifact type."),
}
@classmethod
def get_type_name(cls):
return "all"

View File

@ -434,24 +434,24 @@ class BaseArtifact(base.VersionedObject):
else:
action = cls.activate
# check updates for dependencies and validate them
# check updates for links and validate them
try:
for key, value in six.iteritems(updates):
if cls.fields.get(key) is glare_fields.Dependency \
if cls.fields.get(key) is glare_fields.Link \
and value is not None:
# check format
glare_fields.DependencyFieldType.coerce(None, key, value)
glare_fields.LinkFieldType.coerce(None, key, value)
# check containment
if glare_fields.DependencyFieldType.is_external(value):
# validate external dependency
cls._validate_external_dependency(value)
if glare_fields.LinkFieldType.is_external(value):
# validate external link
cls._validate_external_link(value)
else:
type_name = (glare_fields.DependencyFieldType.
type_name = (glare_fields.LinkFieldType.
get_type_name(value))
af_type = registry.get_artifact_type(type_name)
cls._validate_soft_dependency(context, value, af_type)
cls._validate_soft_link(context, value, af_type)
except Exception as e:
msg = (_("Bad dependency in artifact %(af)s: %(msg)s")
msg = (_("Bad link in artifact %(af)s: %(msg)s")
% {"af": artifact.id, "msg": str(e)})
raise exception.BadRequest(msg)
@ -461,12 +461,12 @@ class BaseArtifact(base.VersionedObject):
return action
@classmethod
def _validate_external_dependency(cls, link):
def _validate_external_link(cls, link):
with urlrequest.urlopen(link) as data:
data.read(1)
@classmethod
def _validate_soft_dependency(cls, context, link, af_type):
def _validate_soft_link(cls, context, link, af_type):
af_id = link.split('/')[3]
af_type.get(context, af_id)
@ -1087,23 +1087,9 @@ class BaseArtifact(base.VersionedObject):
res[key] = val
return res
@staticmethod
def schema_type(attr):
if isinstance(attr, fields.IntegerField):
return 'integer'
elif isinstance(attr, fields.FloatField):
return 'number'
elif isinstance(attr, fields.BooleanField):
return 'boolean'
elif isinstance(attr, glare_fields.List):
return 'array'
elif isinstance(attr, (glare_fields.Dict, glare_fields.BlobField)):
return 'object'
return 'string'
@classmethod
def schema_attr(cls, attr, attr_name=''):
attr_type = cls.schema_type(attr)
attr_type = utils.get_schema_type(attr)
schema = {}
# generate schema for validators
@ -1112,6 +1098,7 @@ class BaseArtifact(base.VersionedObject):
schema['type'] = (attr_type
if not attr.nullable else [attr_type, 'null'])
schema['glareType'] = utils.get_glare_type(attr)
output_blob_schema = {
'type': ['object', 'null'],
'properties': {
@ -1134,7 +1121,7 @@ class BaseArtifact(base.VersionedObject):
schema['readOnly'] = True
if isinstance(attr, glare_fields.Dict):
element_type = (cls.schema_type(attr.element_type)
element_type = (utils.get_schema_type(attr.element_type)
if hasattr(attr, 'element_type')
else 'string')
@ -1156,7 +1143,7 @@ class BaseArtifact(base.VersionedObject):
if attr_type == 'array':
schema['items'] = {
'type': (cls.schema_type(attr.element_type)
'type': (utils.get_schema_type(attr.element_type)
if hasattr(attr, 'element_type')
else 'string')}
@ -1193,9 +1180,64 @@ class BaseArtifact(base.VersionedObject):
attr_name=attr_name)
schemas = {'properties': schemas_prop,
'name': cls.get_type_name(),
'version': cls.VERSION,
'title': 'Artifact type %s of version %s' %
(cls.get_type_name(), cls.VERSION),
'type': 'object',
'required': ['name']}
return schemas
class ReadOnlyMixin(object):
"""Mixin that disables all modifying actions on artifacts."""
@classmethod
def create(cls, context, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def update(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def get_action_for_updates(cls, context, artifact, updates, registry):
raise exception.Forbidden("This type is read only.")
@classmethod
def delete(cls, context, af):
raise exception.Forbidden("This type is read only.")
@classmethod
def activate(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def reactivate(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def deactivate(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def publish(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def upload_blob(cls, context, af, field_name, fd, content_type):
raise exception.Forbidden("This type is read only.")
@classmethod
def upload_blob_dict(cls, context, af, field_name, blob_key, fd,
content_type):
raise exception.Forbidden("This type is read only.")
@classmethod
def add_blob_location(cls, context, af, field_name, location, blob_meta):
raise exception.Forbidden("This type is read only.")
@classmethod
def add_blob_dict_location(cls, context, af, field_name,
blob_key, location, blob_meta):
raise exception.Forbidden("This type is read only.")

View File

@ -29,7 +29,7 @@ BlobDict = attribute.BlobDictAttribute.init
class HeatTemplate(base.BaseArtifact):
fields = {
'environments': Dict(glare_fields.Dependency,
'environments': Dict(glare_fields.LinkFieldType,
mutable=True,
description="References to Heat Environments "
"that can be used with current "

View File

@ -112,8 +112,8 @@ class BlobField(fields.AutoTypedField):
AUTO_TYPE = BlobFieldType()
class DependencyFieldType(fields.FieldType):
"""Dependency field specifies Artifact dependency on other artifact or some
class LinkFieldType(fields.FieldType):
"""Link field specifies Artifact dependency on other artifact or some
external resource. From technical perspective it is just soft link to Glare
Artifact or https/http resource. So Artifact users can download the
referenced file by that link.
@ -134,7 +134,7 @@ class DependencyFieldType(fields.FieldType):
@staticmethod
def coerce(obj, attr, value):
# to remove the existing dependency user sets its value to None,
# to remove the existing link user sets its value to None,
# we have to consider this case.
if value is None:
return value
@ -144,7 +144,7 @@ class DependencyFieldType(fields.FieldType):
'not a %(type)s') %
{'attr': attr, 'type': type(value).__name__})
# determine if link is external or internal
external = DependencyFieldType.is_external(value)
external = LinkFieldType.is_external(value)
# validate link itself
if external:
link = urlparse.urlparse(value)
@ -155,7 +155,7 @@ class DependencyFieldType(fields.FieldType):
result = value.split('/')
if len(result) != 4 or result[1] != 'artifacts':
raise ValueError(
_('Dependency link %(link)s is not valid in field '
_('Link %(link)s is not valid in field '
'%(attr)s. The link must be either valid url or '
'reference to artifact. Example: '
'/artifacts/<artifact_type>/<artifact_id>'
@ -163,8 +163,8 @@ class DependencyFieldType(fields.FieldType):
return value
class Dependency(fields.AutoTypedField):
AUTO_TYPE = DependencyFieldType()
class Link(fields.AutoTypedField):
AUTO_TYPE = LinkFieldType()
class List(fields.AutoTypedField):

View File

@ -103,7 +103,7 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
supported_types = []
for module in modules:
supported_types.extend(get_subclasses(module, base.BaseArtifact))
for type_name in CONF.glare.enabled_artifact_types:
for type_name in set(CONF.glare.enabled_artifact_types + ['all']):
for af_type in supported_types:
if type_name == af_type.get_type_name():
cls._validate_artifact_type(af_type)

View File

@ -211,7 +211,7 @@ class MinNumberSize(SizeValidator):
return fields.IntegerField, fields.FloatField
def to_jsonschema(self):
return {'minumum': self.size}
return {'minimum': self.size}
class Unique(Validator):

View File

@ -50,7 +50,7 @@ class MuranoPackage(base.BaseArtifact):
"the package."),
'inherits': Dict(fields.String),
'keywords': List(fields.String, mutable=True),
'dependencies': List(glare_fields.Dependency,
'dependencies': List(glare_fields.LinkFieldType,
required_on_activate=False,
description="List of package dependencies for "
"this package."),

View File

@ -0,0 +1,183 @@
# Copyright (c) 2016 Mirantis, Inc.
#
# 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 uuid
from oslo_serialization import jsonutils
import requests
from glare.tests import functional
def sort_results(lst, target='name'):
return sorted(lst, key=lambda x: x[target])
class TestArtifact(functional.FunctionalTest):
enabled_types = (u'sample_artifact', u'images', u'heat_templates',
u'heat_environments', u'tosca_templates',
u'murano_packages', u'all')
users = {
'user1': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'user2': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'admin': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'admin'
},
'anonymous': {
'id': None,
'tenant_id': None,
'token': None,
'role': None
}
}
def setUp(self):
super(TestArtifact, self).setUp()
self.set_user('user1')
self.glare_server.deployment_flavor = 'noauth'
self.glare_server.enabled_artifact_types = ','.join(
self.enabled_types)
self.glare_server.custom_artifact_types_modules = (
'glare.tests.functional.sample_artifact')
self.start_servers(**self.__dict__.copy())
def tearDown(self):
self.stop_servers()
self._reset_database(self.glare_server.sql_connection)
super(TestArtifact, self).tearDown()
def _url(self, path):
if 'schemas' in path:
return 'http://127.0.0.1:%d%s' % (self.glare_port, path)
else:
return 'http://127.0.0.1:%d/artifacts%s' % (self.glare_port, path)
def set_user(self, username):
if username not in self.users:
raise KeyError
self.current_user = username
def _headers(self, custom_headers=None):
base_headers = {
'X-Identity-Status': 'Confirmed',
'X-Auth-Token': self.users[self.current_user]['token'],
'X-User-Id': self.users[self.current_user]['id'],
'X-Tenant-Id': self.users[self.current_user]['tenant_id'],
'X-Project-Id': self.users[self.current_user]['tenant_id'],
'X-Roles': self.users[self.current_user]['role'],
}
base_headers.update(custom_headers or {})
return base_headers
def create_artifact(self, data=None, status=201,
type_name='sample_artifact'):
return self.post('/' + type_name, data or {}, status=status)
def _check_artifact_method(self, method, url, data=None, status=200,
headers=None):
if not headers:
headers = self._headers()
else:
headers = self._headers(headers)
headers.setdefault("Content-Type", "application/json")
if 'application/json' in headers['Content-Type'] and data is not None:
data = jsonutils.dumps(data)
response = getattr(requests, method)(self._url(url), headers=headers,
data=data)
self.assertEqual(status, response.status_code, response.text)
if status >= 400:
return response.text
if ("application/json" in response.headers["content-type"] or
"application/schema+json" in response.headers["content-type"]):
return jsonutils.loads(response.text)
return response.text
def post(self, url, data=None, status=201, headers=None):
return self._check_artifact_method("post", url, data, status=status,
headers=headers)
def get(self, url, status=200, headers=None):
return self._check_artifact_method("get", url, status=status,
headers=headers)
def delete(self, url, status=204):
response = requests.delete(self._url(url), headers=self._headers())
self.assertEqual(status, response.status_code, response.text)
return response.text
def patch(self, url, data, status=200, headers=None):
if headers is None:
headers = {}
if 'Content-Type' not in headers:
headers.update({'Content-Type': 'application/json-patch+json'})
return self._check_artifact_method("patch", url, data, status=status,
headers=headers)
def put(self, url, data=None, status=200, headers=None):
return self._check_artifact_method("put", url, data, status=status,
headers=headers)
# the test cases below are written in accordance with use cases
# each test tries to cover separate use case in Glare
# all code inside each test tries to cover all operators and data
# involved in use case execution
# each tests represents part of artifact lifecycle
# so we can easily define where is the failed code
make_active = [{"op": "replace", "path": "/status", "value": "active"}]
def activate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_active, status=status)
self.set_user(cur_user)
return af
make_deactivated = [{"op": "replace", "path": "/status",
"value": "deactivated"}]
def deactivate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_deactivated, status=status)
self.set_user(cur_user)
return af
make_public = [{"op": "replace", "path": "/visibility", "value": "public"}]
def publish_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_public, status=status)
self.set_user(cur_user)
return af

View File

@ -36,12 +36,12 @@ class SampleArtifact(base_artifact.BaseArtifact):
description="I am Blob"),
'small_blob': Blob(max_blob_size=10, required_on_activate=False,
mutable=True, filter_ops=[]),
'dependency1': Field(glare_fields.Dependency,
required_on_activate=False,
filter_ops=[]),
'dependency2': Field(glare_fields.Dependency,
required_on_activate=False,
filter_ops=[]),
'link1': Field(glare_fields.Link,
required_on_activate=False,
filter_ops=[]),
'link2': Field(glare_fields.Link,
required_on_activate=False,
filter_ops=[]),
'bool1': Field(fields.FlexibleBooleanField,
required_on_activate=False,
filter_ops=(attribute.FILTER_EQ,),

View File

@ -0,0 +1,96 @@
# Copyright (c) 2016 Mirantis, Inc.
#
# 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 glare.tests.functional import base
class TestAll(base.TestArtifact):
def test_all(self):
for type_name in self.enabled_types:
if type_name == 'all':
continue
for i in range(3):
for j in range(3):
self.create_artifact(
data={'name': '%s_%d' % (type_name, i),
'version': '%d' % j,
'tags': ['tag%s' % i]},
type_name=type_name)
# get all possible artifacts
url = '/all?sort=name:asc&limit=100'
res = self.get(url=url, status=200)['all']
from pprint import pformat
self.assertEqual(54, len(res), pformat(res))
# get artifacts with latest versions
url = '/all?version=latest&sort=name:asc'
res = self.get(url=url, status=200)['all']
self.assertEqual(18, len(res))
for art in res:
self.assertEqual('2.0.0', art['version'])
# get images only
url = '/all?type_name=images&sort=name:asc'
res = self.get(url=url, status=200)['all']
self.assertEqual(9, len(res))
for art in res:
self.assertEqual('images', art['type_name'])
# get images and heat_templates
url = '/all?type_name=in:images,heat_templates&sort=name:asc'
res = self.get(url=url, status=200)['all']
self.assertEqual(18, len(res))
for art in res:
self.assertIn(art['type_name'], ('images', 'heat_templates'))
def test_all_readonlyness(self):
self.create_artifact(data={'name': 'all'}, type_name='all', status=403)
art = self.create_artifact(data={'name': 'image'}, type_name='images')
url = '/all/%s' % art['id']
headers = {'Content-Type': 'application/octet-stream'}
# upload to 'all' is forbidden
self.put(url=url + '/icon', data='data', status=403,
headers=headers)
# update 'all' is forbidden
data = [{
"op": "replace",
"path": "/description",
"value": "text"
}]
self.patch(url=url, data=data, status=403)
# activation is forbidden
data = [{
"op": "replace",
"path": "/status",
"value": "active"
}]
self.patch(url=url, data=data, status=403)
# publishing is forbidden
data = [{
"op": "replace",
"path": "/visibility",
"value": "public"
}]
self.patch(url=url, data=data, status=403)
# get is okay
new_art = self.get(url=url)
self.assertEqual(new_art['id'], art['id'])

View File

@ -17,168 +17,15 @@ import hashlib
import uuid
from oslo_serialization import jsonutils
import requests
from glare.tests import functional
from glare.tests.functional import base
def sort_results(lst, target='name'):
return sorted(lst, key=lambda x: x[target])
class TestArtifact(functional.FunctionalTest):
users = {
'user1': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'user2': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'admin': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'admin'
},
'anonymous': {
'id': None,
'tenant_id': None,
'token': None,
'role': None
}
}
def setUp(self):
super(TestArtifact, self).setUp()
self.set_user('user1')
self.glare_server.deployment_flavor = 'noauth'
self.glare_server.enabled_artifact_types = 'sample_artifact'
self.glare_server.custom_artifact_types_modules = (
'glare.tests.functional.sample_artifact')
self.start_servers(**self.__dict__.copy())
def tearDown(self):
self.stop_servers()
self._reset_database(self.glare_server.sql_connection)
super(TestArtifact, self).tearDown()
def _url(self, path):
if 'schemas' in path:
return 'http://127.0.0.1:%d%s' % (self.glare_port, path)
else:
return 'http://127.0.0.1:%d/artifacts%s' % (self.glare_port, path)
def set_user(self, username):
if username not in self.users:
raise KeyError
self.current_user = username
def _headers(self, custom_headers=None):
base_headers = {
'X-Identity-Status': 'Confirmed',
'X-Auth-Token': self.users[self.current_user]['token'],
'X-User-Id': self.users[self.current_user]['id'],
'X-Tenant-Id': self.users[self.current_user]['tenant_id'],
'X-Project-Id': self.users[self.current_user]['tenant_id'],
'X-Roles': self.users[self.current_user]['role'],
}
base_headers.update(custom_headers or {})
return base_headers
def create_artifact(self, data=None, status=201):
return self.post('/sample_artifact', data or {}, status=status)
def _check_artifact_method(self, method, url, data=None, status=200,
headers=None):
if not headers:
headers = self._headers()
else:
headers = self._headers(headers)
headers.setdefault("Content-Type", "application/json")
if 'application/json' in headers['Content-Type'] and data is not None:
data = jsonutils.dumps(data)
response = getattr(requests, method)(self._url(url), headers=headers,
data=data)
self.assertEqual(status, response.status_code, response.text)
if status >= 400:
return response.text
if ("application/json" in response.headers["content-type"] or
"application/schema+json" in response.headers["content-type"]):
return jsonutils.loads(response.text)
return response.text
def post(self, url, data=None, status=201, headers=None):
return self._check_artifact_method("post", url, data, status=status,
headers=headers)
def get(self, url, status=200, headers=None):
return self._check_artifact_method("get", url, status=status,
headers=headers)
def delete(self, url, status=204):
response = requests.delete(self._url(url), headers=self._headers())
self.assertEqual(status, response.status_code, response.text)
return response.text
def patch(self, url, data, status=200, headers=None):
if headers is None:
headers = {}
if 'Content-Type' not in headers:
headers.update({'Content-Type': 'application/json-patch+json'})
return self._check_artifact_method("patch", url, data, status=status,
headers=headers)
def put(self, url, data=None, status=200, headers=None):
return self._check_artifact_method("put", url, data, status=status,
headers=headers)
# the test cases below are written in accordance with use cases
# each test tries to cover separate use case in Glare
# all code inside each test tries to cover all operators and data
# involved in use case execution
# each tests represents part of artifact lifecycle
# so we can easily define where is the failed code
make_active = [{"op": "replace", "path": "/status", "value": "active"}]
def activate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_active, status=status)
self.set_user(cur_user)
return af
make_deactivated = [{"op": "replace", "path": "/status",
"value": "deactivated"}]
def deactivate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_deactivated, status=status)
self.set_user(cur_user)
return af
make_public = [{"op": "replace", "path": "/visibility", "value": "public"}]
def publish_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_public, status=status)
self.set_user(cur_user)
return af
class TestList(TestArtifact):
class TestList(base.TestArtifact):
def test_list_marker_and_limit(self):
# Create artifacts
art_list = [self.create_artifact({'name': 'name%s' % i,
@ -806,7 +653,7 @@ class TestList(TestArtifact):
self.assertEqual(response_url, result['first'])
class TestBlobs(TestArtifact):
class TestBlobs(base.TestArtifact):
def test_blob_dicts(self):
# Getting empty artifact list
url = '/sample_artifact'
@ -1005,7 +852,7 @@ class TestBlobs(TestArtifact):
status=400, headers=headers)
class TestTags(TestArtifact):
class TestTags(base.TestArtifact):
def test_tags(self):
# Create artifact
art = self.create_artifact({'name': 'name5',
@ -1064,7 +911,7 @@ class TestTags(TestArtifact):
self.patch(url=url, data=patch, status=400)
class TestArtifactOps(TestArtifact):
class TestArtifactOps(base.TestArtifact):
def test_create(self):
"""All tests related to artifact creation"""
# check that cannot create artifact for non-existent artifact type
@ -1136,7 +983,7 @@ class TestArtifactOps(TestArtifact):
# (except blobs and system)
expected = {
"name": "test_big_create",
"dependency1": "/artifacts/sample_artifact/%s" % some_af['id'],
"link1": "/artifacts/sample_artifact/%s" % some_af['id'],
"bool1": True,
"int1": 2323,
"float1": 0.1,
@ -1252,14 +1099,14 @@ class TestArtifactOps(TestArtifact):
url = '/sample_artifact/111111'
self.delete(url=url, status=404)
# check that we can delete artifact with soft dependency
# check that we can delete artifact with soft link
art = self.create_artifact(
data={"name": "test_af", "string_required": "test_str",
"version": "0.0.1"})
artd = self.create_artifact(
data={"name": "test_afd", "string_required": "test_str",
"version": "0.0.1",
"dependency1": '/artifacts/sample_artifact/%s' % art['id']})
"link1": '/artifacts/sample_artifact/%s' % art['id']})
url = '/sample_artifact/%s' % artd['id']
self.delete(url=url, status=204)
@ -1346,7 +1193,7 @@ class TestArtifactOps(TestArtifact):
self.assertEqual("active", deactive_art["status"])
class TestUpdate(TestArtifact):
class TestUpdate(base.TestArtifact):
def test_update_artifact_before_activate(self):
"""Test updates for artifact before activation"""
# create artifact to update
@ -2164,24 +2011,121 @@ class TestUpdate(TestArtifact):
url = '/sample_artifact/%s' % art1['id']
self.patch(url=url, data=data, status=400)
def test_update_remove_properties(self):
data = {
"name": "test_big_create",
"version": "1.0.0",
"bool1": True,
"int1": 2323,
"float1": 0.1,
"str1": "test",
"list_of_str": ["test1", "test2"],
"list_of_int": [0, 1, 2],
"dict_of_str": {"test": "test"},
"dict_of_int": {"test": 0},
"string_mutable": "test",
"string_required": "test",
}
art1 = self.create_artifact(data=data)
class TestDependencies(TestArtifact):
def test_manage_dependencies(self):
# remove the whole list of strings
data = [{'op': 'replace',
'path': '/list_of_str',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
result = self.patch(url=url, data=data)
self.assertEqual([], result['list_of_str'])
# remove the whole list of ints
data = [{'op': 'replace',
'path': '/list_of_int',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
result = self.patch(url=url, data=data)
self.assertEqual([], result['list_of_int'])
# remove the whole dict of strings
data = [{'op': 'replace',
'path': '/dict_of_str',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
result = self.patch(url=url, data=data)
self.assertEqual({}, result['dict_of_str'])
# remove the whole dict of ints
data = [{'op': 'replace',
'path': '/dict_of_int',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
result = self.patch(url=url, data=data)
self.assertEqual({}, result['dict_of_int'])
# remove bool1
data = [{'op': 'replace',
'path': '/bool1',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
result = self.patch(url=url, data=data)
self.assertEqual(False, result['bool1'])
# remove int1
data = [{'op': 'replace',
'path': '/int1',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
result = self.patch(url=url, data=data)
self.assertIsNone(result['int1'])
# remove float1
data = [{'op': 'replace',
'path': '/float1',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
result = self.patch(url=url, data=data)
self.assertIsNone(result['float1'])
# cannot remove id
data = [{'op': 'replace',
'path': '/id',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
self.patch(url=url, data=data, status=403)
# cannot remove name
data = [{'op': 'replace',
'path': '/name',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
self.patch(url=url, data=data, status=409)
headers = {'Content-Type': 'application/octet-stream'}
self.put(url=url + '/blob', data="d" * 1000, headers=headers)
# cannot remove id
data = [{'op': 'replace',
'path': '/blob',
'value': None}]
url = '/sample_artifact/%s' % art1['id']
self.patch(url=url, data=data, status=400)
class TestLinks(base.TestArtifact):
def test_manage_links(self):
some_af = self.create_artifact(data={"name": "test_af"})
dep_af = self.create_artifact(data={"name": "test_dep_af"})
dep_url = "/artifacts/sample_artifact/%s" % some_af['id']
# set valid dependency
patch = [{"op": "replace", "path": "/dependency1", "value": dep_url}]
# set valid link
patch = [{"op": "replace", "path": "/link1", "value": dep_url}]
url = '/sample_artifact/%s' % dep_af['id']
af = self.patch(url=url, data=patch)
self.assertEqual(af['dependency1'], dep_url)
self.assertEqual(af['link1'], dep_url)
# remove dependency from artifact
patch = [{"op": "replace", "path": "/dependency1", "value": None}]
# remove link from artifact
patch = [{"op": "replace", "path": "/link1", "value": None}]
af = self.patch(url=url, data=patch)
self.assertIsNone(af['dependency1'])
self.assertIsNone(af['link1'])
# try to set invalid dependency
patch = [{"op": "replace", "path": "/dependency1", "value": "Invalid"}]
# try to set invalid link
patch = [{"op": "replace", "path": "/link1", "value": "Invalid"}]
self.patch(url=url, data=patch, status=400)

View File

@ -15,11 +15,8 @@
import jsonschema
from oslo_serialization import jsonutils
import requests
from glare.common import utils
from glare.tests import functional
from glare.tests.functional import base
fixture_base_props = {
u'activated_at': {
@ -32,6 +29,7 @@ fixture_base_props = {
u'lt',
u'lte'],
u'format': u'date-time',
u'glareType': u'DateTime',
u'readOnly': True,
u'required_on_activate': False,
u'sortable': True,
@ -47,6 +45,7 @@ fixture_base_props = {
u'lt',
u'lte'],
u'format': u'date-time',
u'glareType': u'DateTime',
u'readOnly': True,
u'sortable': True,
u'type': u'string'},
@ -55,6 +54,7 @@ fixture_base_props = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 4096,
u'mutable': True,
u'required_on_activate': False,
@ -63,6 +63,7 @@ fixture_base_props = {
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']},
@ -86,6 +87,7 @@ fixture_base_props = {
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-fA-F]){4}'
u'-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$',
@ -96,6 +98,7 @@ fixture_base_props = {
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',
@ -104,6 +107,7 @@ fixture_base_props = {
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',
@ -114,6 +118,7 @@ fixture_base_props = {
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',
@ -122,6 +127,7 @@ fixture_base_props = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'required_on_activate': False,
u'sortable': True,
@ -130,6 +136,7 @@ fixture_base_props = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'readOnly': True,
u'required_on_activate': False,
@ -140,6 +147,7 @@ fixture_base_props = {
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'},
@ -154,6 +162,7 @@ fixture_base_props = {
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,
@ -169,6 +178,7 @@ fixture_base_props = {
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'},
@ -177,6 +187,7 @@ fixture_base_props = {
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,
@ -187,6 +198,7 @@ fixture_base_props = {
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,
@ -203,6 +215,7 @@ fixture_base_props = {
u'lt',
u'lte'],
u'format': u'date-time',
u'glareType': u'DateTime',
u'readOnly': True,
u'sortable': True,
u'type': u'string'},
@ -215,6 +228,7 @@ fixture_base_props = {
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-]+)?$/',
@ -226,15 +240,12 @@ fixture_base_props = {
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'}
}
enabled_artifact_types = (
u'sample_artifact', u'images', u'heat_templates',
u'heat_environments', u'tosca_templates', u'murano_packages')
def generate_type_props(props):
props.update(fixture_base_props)
@ -248,6 +259,7 @@ fixtures = {
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']},
@ -276,26 +288,30 @@ fixtures = {
u'null']},
u'bool1': {u'default': False,
u'filter_ops': [u'eq'],
u'glareType': u'Boolean',
u'required_on_activate': False,
u'type': [u'string',
u'null']},
u'bool2': {u'default': False,
u'filter_ops': [u'eq'],
u'glareType': u'Boolean',
u'required_on_activate': False,
u'type': [u'string',
u'null']},
u'link1': {u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'Link',
u'required_on_activate': False,
u'type': [u'string',
u'null']},
u'link2': {u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'Link',
u'required_on_activate': False,
u'type': [u'string',
u'null']},
u'dependency1': {u'filter_ops': [u'eq',
u'neq',
u'in'],
u'required_on_activate': False,
u'type': [u'string',
u'null']},
u'dependency2': {u'filter_ops': [u'eq',
u'neq',
u'in'],
u'required_on_activate': False,
u'type': [u'string',
u'null']},
u'dict_of_blobs': {
u'additionalProperties': {
u'additionalProperties': False,
@ -326,6 +342,7 @@ fixtures = {
u'null']},
u'default': {},
u'filter_ops': [],
u'glareType': u'BlobDict',
u'maxProperties': 255,
u'required_on_activate': False,
u'type': [u'object',
@ -335,6 +352,7 @@ fixtures = {
u'type': u'string'},
u'default': {},
u'filter_ops': [u'eq'],
u'glareType': u'IntegerDict',
u'maxProperties': 255,
u'required_on_activate': False,
u'type': [u'object',
@ -344,6 +362,7 @@ fixtures = {
u'type': u'string'},
u'default': {},
u'filter_ops': [u'eq'],
u'glareType': u'StringDict',
u'maxProperties': 255,
u'required_on_activate': False,
u'type': [u'object',
@ -353,6 +372,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'StringDict',
u'maxProperties': 3,
u'properties': {
u'abc': {u'type': [u'string',
@ -373,6 +393,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'Float',
u'required_on_activate': False,
u'sortable': True,
u'type': [u'number',
@ -384,6 +405,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'Float',
u'required_on_activate': False,
u'sortable': True,
u'type': [u'number',
@ -395,6 +417,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'Integer',
u'required_on_activate': False,
u'sortable': True,
u'type': [u'integer',
@ -406,6 +429,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'Integer',
u'required_on_activate': False,
u'sortable': True,
u'type': [u'integer',
@ -417,13 +441,15 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'Integer',
u'maximum': 20,
u'minumum': 10,
u'minimum': 10,
u'required_on_activate': False,
u'type': [u'integer',
u'null']},
u'list_of_int': {u'default': [],
u'filter_ops': [u'eq'],
u'glareType': u'IntegerList',
u'items': {
u'type': u'string'},
u'maxItems': 255,
@ -432,6 +458,7 @@ fixtures = {
u'null']},
u'list_of_str': {u'default': [],
u'filter_ops': [u'eq'],
u'glareType': u'StringList',
u'items': {
u'type': u'string'},
u'maxItems': 255,
@ -443,6 +470,7 @@ fixtures = {
u'eq',
u'neq',
u'in'],
u'glareType': u'StringList',
u'items': {
u'type': u'string'},
u'maxItems': 3,
@ -452,6 +480,7 @@ fixtures = {
u'unique': True},
u'small_blob': {u'additionalProperties': False,
u'filter_ops': [],
u'glareType': u'Blob',
u'mutable': True,
u'properties': {
u'md5': {u'type': [u'string', u'null']},
@ -486,6 +515,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'String',
u'maxLength': 255,
u'required_on_activate': False,
u'sortable': True,
@ -498,6 +528,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'String',
u'maxLength': 255,
u'mutable': True,
u'required_on_activate': False,
@ -511,6 +542,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'String',
u'maxLength': 255,
u'type': [u'string',
u'null']},
@ -526,6 +558,7 @@ fixtures = {
u'gte',
u'lt',
u'lte'],
u'glareType': u'String',
u'maxLength': 10,
u'required_on_activate': False,
u'type': [u'string',
@ -534,6 +567,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'readOnly': True,
u'sortable': True,
@ -542,6 +576,7 @@ fixtures = {
}),
u'required': [u'name'],
u'title': u'Artifact type sample_artifact of version 1.0',
u'version': u'1.0',
u'type': u'object'},
u'tosca_templates': {
u'name': u'tosca_templates',
@ -550,6 +585,7 @@ fixtures = {
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']},
@ -574,11 +610,13 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'type': [u'string',
u'null']},
}),
u'required': [u'name'],
u'version': u'1.0',
u'title': u'Artifact type tosca_templates of version 1.0',
u'type': u'object'},
u'murano_packages': {
@ -591,6 +629,7 @@ fixtures = {
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,
@ -603,6 +642,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'StringList',
u'items': {u'type': u'string'},
u'maxItems': 255,
u'type': [u'array',
@ -615,6 +655,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'LinkList',
u'items': {u'type': u'string'},
u'maxItems': 255,
u'required_on_activate': False,
@ -625,6 +666,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'mutable': True,
u'type': [u'string',
@ -635,6 +677,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'StringDict',
u'maxProperties': 255,
u'type': [u'object',
u'null']},
@ -642,6 +685,7 @@ fixtures = {
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,
@ -651,6 +695,7 @@ fixtures = {
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']},
@ -679,11 +724,13 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'type': [u'string',
u'null']}
}),
u'required': [u'name'],
u'version': u'1.0',
u'title': u'Artifact type murano_packages of version 1.0',
u'type': u'object'},
u'images': {
@ -697,6 +744,7 @@ fixtures = {
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',
@ -705,6 +753,7 @@ fixtures = {
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']},
@ -720,6 +769,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'type': [u'string',
u'null']},
@ -739,11 +789,13 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'type': [u'string', u'null']},
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']},
@ -773,6 +825,7 @@ fixtures = {
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']},
@ -784,6 +837,7 @@ fixtures = {
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',
@ -795,6 +849,7 @@ fixtures = {
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}-'
u'([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-'
@ -806,7 +861,8 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'minumum': 0,
u'glareType': u'Integer',
u'minimum': 0,
u'required_on_activate': False,
u'type': [u'integer', u'null']},
u'min_ram': {
@ -814,7 +870,8 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'minumum': 0,
u'glareType': u'Integer',
u'minimum': 0,
u'required_on_activate': False,
u'type': [u'integer', u'null']},
u'os_distro': {
@ -825,6 +882,7 @@ fixtures = {
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']},
@ -834,6 +892,7 @@ fixtures = {
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']},
@ -844,12 +903,14 @@ fixtures = {
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-fA-F])'
u'{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$',
u'required_on_activate': False,
u'type': [u'string', u'null']}}),
u'required': [u'name'],
u'version': u'1.0',
u'title': u'Artifact type images of version 1.0',
u'type': u'object'},
u'heat_templates': {
@ -864,6 +925,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'StringDict',
u'maxProperties': 255,
u'mutable': True,
u'type': [u'object',
@ -877,6 +939,7 @@ fixtures = {
u'filter_ops': [u'eq',
u'neq',
u'in'],
u'glareType': u'LinkDict',
u'maxProperties': 255,
u'mutable': True,
u'type': [u'object',
@ -909,6 +972,7 @@ fixtures = {
u'name of template and value is nested '
u'template body.',
u'filter_ops': [],
u'glareType': u'BlobDict',
u'maxProperties': 255,
u'type': [u'object',
u'null']},
@ -916,6 +980,7 @@ fixtures = {
u'additionalProperties': False,
u'description': u'Heat 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']},
@ -938,6 +1003,7 @@ fixtures = {
u'null']},
}),
u'version': u'1.0',
u'required': [u'name'],
u'title': u'Artifact type heat_templates of version 1.0',
u'type': u'object'},
@ -948,6 +1014,7 @@ fixtures = {
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']},
@ -969,70 +1036,40 @@ fixtures = {
}),
u'required': [u'name'],
u'version': u'1.0',
u'title': u'Artifact type heat_environments of version 1.0',
u'type': u'object'},
u'all': {
u'name': u'all',
u'properties': generate_type_props({
u'type_name': {u'description': u'Name of artifact type.',
u'filter_ops': [u'eq', u'neq', u'in'],
u'glareType': u'String',
u'maxLength': 255,
u'type': [u'string', u'null']},
}),
u'required': [u'name'],
u'version': u'1.0',
u'title': u'Artifact type all of version 1.0',
u'type': u'object'}
}
class TestSchemas(functional.FunctionalTest):
def setUp(self):
super(TestSchemas, self).setUp()
self.glare_server.deployment_flavor = 'noauth'
self.glare_server.enabled_artifact_types = ','.join(
enabled_artifact_types)
self.glare_server.custom_artifact_types_modules = (
'glare.tests.functional.sample_artifact')
self.start_servers(**self.__dict__.copy())
def tearDown(self):
self.stop_servers()
self._reset_database(self.glare_server.sql_connection)
super(TestSchemas, self).tearDown()
def _url(self, path):
return 'http://127.0.0.1:%d%s' % (self.glare_port, path)
def _check_artifact_method(self, url, status=200):
headers = {
'X-Identity-Status': 'Confirmed',
}
response = requests.get(self._url(url), headers=headers)
self.assertEqual(status, response.status_code, response.text)
if status >= 400:
return response.text
if ("application/json" in response.headers["content-type"] or
"application/schema+json" in response.headers["content-type"]):
return jsonutils.loads(response.text)
return response.text
def get(self, url, status=200, headers=None):
return self._check_artifact_method(url, status=status)
class TestSchemas(base.TestArtifact):
def test_schemas(self):
# Get schemas for specific artifact type
for at in self.enabled_types:
result = self.get(url='/schemas/%s' % at)
self.assertEqual(fixtures[at], result['schemas'][at],
utils.DictDiffer(
fixtures[at]['properties'],
result['schemas'][at]['properties']))
# Get list schemas of artifacts
result = self.get(url='/schemas')
self.assertEqual(fixtures, result['schemas'], utils.DictDiffer(
result['schemas'], fixtures))
# Get schemas for specific artifact type
for at in enabled_artifact_types:
result = self.get(url='/schemas/%s' % at)
self.assertEqual(fixtures[at], result['schemas'][at],
utils.DictDiffer(
result['schemas'][at]['properties'],
fixtures[at]['properties']))
# Get schema of sample_artifact
result = self.get(url='/schemas/sample_artifact')
self.assertEqual(fixtures['sample_artifact'],
result['schemas']['sample_artifact'],
utils.DictDiffer(
result['schemas']['sample_artifact'][
'properties'],
fixtures['sample_artifact']['properties']))
fixtures, result['schemas']))
# Validation of schemas
result = self.get(url='/schemas')['schemas']

View File

@ -0,0 +1,27 @@
# 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.
import oslo_i18n as i18n
def fake_translate_msgid(msgid, domain, desired_locale=None):
return msgid
i18n.enable_lazy()
# To ensure messages don't really get translated while running tests.
# As there are lots of places where matching is expected when comparing
# exception message(translated) with raw message.
i18n._translate_msgid = fake_translate_msgid

134
glare/tests/unit/base.py Normal file
View File

@ -0,0 +1,134 @@
# Copyright 2012 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.
import os
import shutil
import fixtures
import glance_store as store
from glance_store import location
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_config import fixture as cfg_fixture
from oslo_db import options
from oslo_serialization import jsonutils
import testtools
from glare.common import config
from glare.common import utils
CONF = cfg.CONF
class BaseTestCase(testtools.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()
self._config_fixture = self.useFixture(cfg_fixture.Config())
config.parse_args(args=[])
self.addCleanup(CONF.reset)
self.test_dir = self.useFixture(fixtures.TempDir()).path
self.conf_dir = os.path.join(self.test_dir, 'etc')
utils.safe_mkdirs(self.conf_dir)
self.set_policy()
def set_policy(self):
conf_file = "policy.json"
self.policy_file = self._copy_data_file(conf_file, self.conf_dir)
self.config(policy_file=self.policy_file, group='oslo_policy')
def _copy_data_file(self, file_name, dst_dir):
src_file_name = os.path.join('glare/tests/etc', file_name)
shutil.copy(src_file_name, dst_dir)
dst_file_name = os.path.join(dst_dir, file_name)
return dst_file_name
def set_property_protection_rules(self, rules):
with open(self.property_file, 'w') as f:
for rule_key in rules.keys():
f.write('[%s]\n' % rule_key)
for operation in rules[rule_key].keys():
roles_str = ','.join(rules[rule_key][operation])
f.write('%s = %s\n' % (operation, roles_str))
def config(self, **kw):
"""
Override some configuration values.
The keyword arguments are the names of configuration options to
override and their values.
If a group argument is supplied, the overrides are applied to
the specified configuration option group.
All overrides are automatically cleared at the end of the current
test by the fixtures cleanup process.
"""
self._config_fixture.config(**kw)
class StoreClearingUnitTest(BaseTestCase):
def setUp(self):
super(StoreClearingUnitTest, self).setUp()
# Ensure stores + locations cleared
location.SCHEME_TO_CLS_MAP = {}
self._create_stores()
self.addCleanup(setattr, location, 'SCHEME_TO_CLS_MAP', dict())
def _create_stores(self, passing_config=True):
"""Create known stores. Mock out sheepdog's subprocess dependency
on collie.
:param passing_config: making store driver passes basic configurations.
:returns: the number of how many store drivers been loaded.
"""
store.register_opts(CONF)
self.config(default_store='filesystem',
filesystem_store_datadir=self.test_dir,
group="glance_store")
store.create_stores(CONF)
class IsolatedUnitTest(StoreClearingUnitTest):
"""
Unit test case that establishes a mock environment within
a testing directory (in isolation)
"""
registry = None
def setUp(self):
super(IsolatedUnitTest, self).setUp()
options.set_defaults(CONF, connection='sqlite:////%s/tests.sqlite' %
self.test_dir)
lockutils.set_defaults(os.path.join(self.test_dir))
self.config(debug=False)
self.config(default_store='filesystem',
filesystem_store_datadir=self.test_dir,
group="glance_store")
store.create_stores()
def set_policy_rules(self, rules):
fap = open(CONF.oslo_policy.policy_file, 'w')
fap.write(jsonutils.dumps(rules))
fap.close()

View File

@ -0,0 +1,65 @@
# 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.
from oslo_serialization import jsonutils
import webob
from glare.api import versions
from glare.tests.unit import base
class VersionsTest(base.IsolatedUnitTest):
"""Test the version information returned from the API service."""
def test_get_version_list(self):
req = webob.Request.blank('/', base_url='http://127.0.0.1:9494/')
req.accept = 'application/json'
res = versions.Controller().index(req, is_multi=True)
self.assertEqual(300, res.status_int)
self.assertEqual('application/json', res.content_type)
results = jsonutils.loads(res.body)['versions']
expected = [
{
'id': 'v1.0',
'status': 'EXPERIMENTAL',
'links': [{'rel': 'self',
'href': 'http://127.0.0.1:9494/'}],
'min_version': '1.0',
'version': '1.0'
}
]
self.assertEqual(expected, results)
def test_get_version_list_public_endpoint(self):
req = webob.Request.blank('/', base_url='http://127.0.0.1:9494/')
req.accept = 'application/json'
self.config(bind_host='127.0.0.1', bind_port=9494,
public_endpoint='https://example.com:9494')
res = versions.Controller().index(req, is_multi=True)
self.assertEqual(300, res.status_int)
self.assertEqual('application/json', res.content_type)
results = jsonutils.loads(res.body)['versions']
expected = [
{
'id': 'v1.0',
'status': 'EXPERIMENTAL',
'links': [{'rel': 'self',
'href': 'https://example.com:9494/'}],
'min_version': '1.0',
'version': '1.0'
}
]
self.assertEqual(expected, results)

View File

@ -36,7 +36,7 @@ oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
oslo.messaging>=5.2.0 # Apache-2.0
oslo.middleware>=3.0.0 # Apache-2.0
oslo.policy>=1.9.0 # Apache-2.0
oslo.policy>=1.14.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.versionedobjects>=1.13.0 # Apache-2.0

View File

@ -14,7 +14,7 @@ coverage>=3.6 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
mox3>=0.7.0 # Apache-2.0
mock>=2.0 # BSD
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
requests>=2.10.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testresources>=0.2.4 # Apache-2.0/BSD
@ -22,7 +22,7 @@ testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
psutil<2.0.0,>=1.1.1 # BSD
oslotest>=1.10.0 # Apache-2.0
os-testr>=0.7.0 # Apache-2.0
os-testr>=0.8.0 # Apache-2.0
# Optional packages that should be installed when testing
PyMySQL!=0.7.7,>=0.6.2 # MIT License
@ -34,5 +34,5 @@ python-swiftclient>=2.2.0 # Apache-2.0
# Documentation
os-api-ref>=1.0.0 # Apache-2.0
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
oslosphinx>=4.7.0 # Apache-2.0
reno>=1.8.0 # Apache2