Domain layer for Artifact Repository

Introduces a layered domain model for Artifact Repository designed
similar to the domain model of v2 Images: a number of proxies for
Artifact Objects, their Repositories and collections split into layers
by appropriate functional aspect.

The following layers are added:
 * Database Repository layer - encapsulates DB APIs;
 * Dependencies Layer - encapsulates dependecy management (artifact ids
   are mapped to the actual Artifact References and back);
 * Location Layer - encapsulates store interaction for Blobs (similar to
   location layer of Images API);
 * Updater layer - wraps the collection-based properties of Artifacts
   for proper updates by JSONPatch calls.

Artifact-specific layers are added into "artifacts" subdirectory of
domain package. A gateway which creates layered proxy is added as well.

Implements-blueprint: artifact-repository

FastTrack

Co-Authored-By: Mike Fedosin <mfedosin@mirantis.com>
Co-Authored-By: Inessa Vasilevskaya <ivasilevskaya@mirantis.com>
Co-Authored-By: Alexander Tivelkov <ativelkov@mirantis.com>

Change-Id: I9b6d0e86c6577929230d58e7403fbefab167f36b
This commit is contained in:
Mike Fedosin 2014-11-05 21:23:51 +03:00
parent 0002df8710
commit 35e35a17bd
11 changed files with 1067 additions and 1 deletions

View File

@ -0,0 +1,128 @@
# Copyright (c) 2015 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 glance.artifacts.domain import proxy
import glance.common.artifacts.definitions as definitions
import glance.common.exception as exc
from glance import i18n
_ = i18n._
class ArtifactProxy(proxy.Artifact):
def __init__(self, artifact, repo):
super(ArtifactProxy, self).__init__(artifact)
self.artifact = artifact
self.repo = repo
def set_type_specific_property(self, prop_name, value):
if prop_name not in self.metadata.attributes.dependencies:
return super(ArtifactProxy, self).set_type_specific_property(
prop_name, value)
# for every dependency have to transfer dep_id into a dependency itself
if value is None:
setattr(self.artifact, prop_name, None)
else:
if not isinstance(value, list):
setattr(self.artifact, prop_name,
self._fetch_dependency(value))
else:
setattr(self.artifact, prop_name,
[self._fetch_dependency(dep_id) for dep_id in value])
def _fetch_dependency(self, dep_id):
# check for circular dependency id -> id
if self.id == dep_id:
raise exc.ArtifactCircularDependency()
art = self.repo.get(artifact_id=dep_id)
# repo returns a proxy of some level.
# Need to find the base declarative artifact
while not isinstance(art, definitions.ArtifactType):
art = art.base
return art
class ArtifactRepo(proxy.ArtifactRepo):
def __init__(self, repo, plugins,
item_proxy_class=None, item_proxy_kwargs=None):
self.plugins = plugins
super(ArtifactRepo, self).__init__(repo,
item_proxy_class=ArtifactProxy,
item_proxy_kwargs={'repo': self})
def _check_dep_state(self, dep, state):
"""Raises an exception if dependency 'dep' is not in state 'state'"""
if dep.state != state:
raise exc.Invalid(_(
"Not all dependencies are in '%s' state") % state)
def publish(self, artifact, *args, **kwargs):
"""
Creates transitive dependencies,
checks that all dependencies are in active state and
transfers artifact from creating to active state
"""
# make sure that all required dependencies exist
artifact.__pre_publish__(*args, **kwargs)
# make sure that all dependencies are active
for param in artifact.metadata.attributes.dependencies:
dependency = getattr(artifact, param)
if isinstance(dependency, list):
for dep in dependency:
self._check_dep_state(dep, 'active')
elif dependency:
self._check_dep_state(dependency, 'active')
# as state is changed on db save, have to retrieve the freshly changed
# artifact (the one passed into the func will have old state value)
artifact = self.base.publish(self.helper.unproxy(artifact))
return self.helper.proxy(artifact)
def remove(self, artifact):
"""
Checks that artifact has no dependencies and removes it.
Otherwise an exception is raised
"""
for param in artifact.metadata.attributes.dependencies:
if getattr(artifact, param):
raise exc.Invalid(_(
"Dependency property '%s' has to be deleted first") %
param)
return self.base.remove(self.helper.unproxy(artifact))
class ArtifactFactory(proxy.ArtifactFactory):
def __init__(self, base, klass, repo):
self.klass = klass
self.repo = repo
super(ArtifactFactory, self).__init__(
base, artifact_proxy_class=ArtifactProxy,
artifact_proxy_kwargs={'repo': self.repo})
def new_artifact(self, *args, **kwargs):
"""
Creates an artifact without dependencies first
and then adds them to the newly created artifact
"""
# filter dependencies
no_deps = {p: kwargs[p] for p in kwargs
if p not in self.klass.metadata.attributes.dependencies}
deps = {p: kwargs[p] for p in kwargs
if p in self.klass.metadata.attributes.dependencies}
artifact = super(ArtifactFactory, self).new_artifact(*args, **no_deps)
# now set dependencies
for dep_param, dep_value in deps.iteritems():
setattr(artifact, dep_param, dep_value)
return artifact

View File

@ -0,0 +1,76 @@
# Copyright (c) 2015 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_utils import timeutils
from glance import i18n
_ = i18n._
class Artifact(object):
def __init__(self, id, name, version, type_name, type_version,
visibility, state, owner, created_at=None,
updated_at=None, **kwargs):
self.id = id
self.name = name
self.type_name = type_name
self.version = version
self.type_version = type_version
self.visibility = visibility
self.state = state
self.owner = owner
self.created_at = created_at
self.updated_at = updated_at
self.description = kwargs.pop('description', None)
self.blobs = kwargs.pop('blobs', {})
self.properties = kwargs.pop('properties', {})
self.dependencies = kwargs.pop('dependencies', {})
self.tags = kwargs.pop('tags', [])
if kwargs:
message = _("__init__() got unexpected keyword argument '%s'")
raise TypeError(message % kwargs.keys()[0])
class ArtifactFactory(object):
def __init__(self, context, klass):
self.klass = klass
self.context = context
def new_artifact(self, name, version, **kwargs):
id = kwargs.pop('id', str(uuid.uuid4()))
tags = kwargs.pop('tags', [])
# pop reserved fields from kwargs dict
for param in ['owner', 'created_at', 'updated_at',
'deleted_at', 'visibility', 'state']:
kwargs.pop(param, '')
curr_timestamp = timeutils.utcnow()
base = self.klass(id=id,
name=name,
version=version,
visibility='private',
state='creating',
# XXX FIXME remove after using authentification
# paste-flavor
# (no or '' as owner will always be there)
owner=self.context.owner or '',
created_at=curr_timestamp,
updated_at=curr_timestamp,
tags=tags,
**kwargs)
return base

View File

@ -0,0 +1,198 @@
# Copyright (c) 2015 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 collections
import six
from glance.domain import proxy as image_proxy
def _proxy_artifact_property(attr):
def getter(self):
return self.get_type_specific_property(attr)
def setter(self, value):
return self.set_type_specific_property(attr, value)
return property(getter, setter)
class ArtifactHelper(image_proxy.Helper):
"""
Artifact-friendly proxy helper: does all the same as regular helper
but also dynamically proxies all the type-specific attributes,
including properties, blobs and dependencies
"""
def proxy(self, obj):
if obj is None or self.proxy_class is None:
return obj
if not hasattr(obj, 'metadata'):
return super(ArtifactHelper, self).proxy(obj)
extra_attrs = {}
for att_name in six.iterkeys(obj.metadata.attributes.all):
extra_attrs[att_name] = _proxy_artifact_property(att_name)
new_proxy_class = type("%s(%s)" % (obj.metadata.type_name,
self.proxy_class.__module__),
(self.proxy_class,),
extra_attrs)
return new_proxy_class(obj, **self.proxy_kwargs)
class ArtifactRepo(object):
def __init__(self, base, proxy_helper=None, item_proxy_class=None,
item_proxy_kwargs=None):
self.base = base
if proxy_helper is None:
proxy_helper = ArtifactHelper(item_proxy_class, item_proxy_kwargs)
self.helper = proxy_helper
def get(self, *args, **kwargs):
return self.helper.proxy(self.base.get(*args, **kwargs))
def list(self, *args, **kwargs):
items = self.base.list(*args, **kwargs)
return [self.helper.proxy(item) for item in items]
def add(self, item):
base_item = self.helper.unproxy(item)
result = self.base.add(base_item)
return self.helper.proxy(result)
def save(self, item):
base_item = self.helper.unproxy(item)
result = self.base.save(base_item)
return self.helper.proxy(result)
def remove(self, item):
base_item = self.helper.unproxy(item)
result = self.base.remove(base_item)
return self.helper.proxy(result)
def publish(self, item, *args, **kwargs):
base_item = self.helper.unproxy(item)
result = self.base.publish(base_item, *args, **kwargs)
return self.helper.proxy(result)
class Artifact(object):
def __init__(self, base, proxy_class=None, proxy_kwargs=None):
self.base = base
self.helper = ArtifactHelper(proxy_class, proxy_kwargs)
# it is enough to proxy metadata only, other properties will be proxied
# automatically by ArtifactHelper
metadata = _proxy_artifact_property('metadata')
def set_type_specific_property(self, prop_name, value):
setattr(self.base, prop_name, value)
def get_type_specific_property(self, prop_name):
return getattr(self.base, prop_name)
def __pre_publish__(self, *args, **kwargs):
self.base.__pre_publish__(*args, **kwargs)
class ArtifactFactory(object):
def __init__(self, base,
artifact_proxy_class=Artifact,
artifact_proxy_kwargs=None):
self.artifact_helper = ArtifactHelper(artifact_proxy_class,
artifact_proxy_kwargs)
self.base = base
def new_artifact(self, *args, **kwargs):
t = self.base.new_artifact(*args, **kwargs)
return self.artifact_helper.proxy(t)
class ArtifactBlob(object):
def __init__(self, base, artifact_blob_proxy_class=None,
artifact_blob_proxy_kwargs=None):
self.base = base
self.helper = image_proxy.Helper(artifact_blob_proxy_class,
artifact_blob_proxy_kwargs)
size = _proxy_artifact_property('size')
locations = _proxy_artifact_property('locations')
checksum = _proxy_artifact_property('checksum')
item_key = _proxy_artifact_property('item_key')
def set_type_specific_property(self, prop_name, value):
setattr(self.base, prop_name, value)
def get_type_specific_property(self, prop_name):
return getattr(self.base, prop_name)
def to_dict(self):
return self.base.to_dict()
class ArtifactProperty(object):
def __init__(self, base, proxy_class=None, proxy_kwargs=None):
self.base = base
self.helper = ArtifactHelper(proxy_class, proxy_kwargs)
def set_type_specific_property(self, prop_name, value):
setattr(self.base, prop_name, value)
def get_type_specific_property(self, prop_name):
return getattr(self.base, prop_name)
class List(collections.MutableSequence):
def __init__(self, base, item_proxy_class=None,
item_proxy_kwargs=None):
self.base = base
self.helper = image_proxy.Helper(item_proxy_class, item_proxy_kwargs)
def __len__(self):
return len(self.base)
def __delitem__(self, index):
del self.base[index]
def __getitem__(self, index):
item = self.base[index]
return self.helper.proxy(item)
def insert(self, index, value):
self.base.insert(index, self.helper.unproxy(value))
def __setitem__(self, index, value):
self.base[index] = self.helper.unproxy(value)
class Dict(collections.MutableMapping):
def __init__(self, base, item_proxy_class=None, item_proxy_kwargs=None):
self.base = base
self.helper = image_proxy.Helper(item_proxy_class, item_proxy_kwargs)
def __setitem__(self, key, value):
self.base[key] = self.helper.unproxy(value)
def __getitem__(self, key):
item = self.base[key]
return self.helper.proxy(item)
def __delitem__(self, key):
del self.base[key]
def __len__(self):
return len(self.base)
def __iter__(self):
for key in self.base.keys():
yield key

View File

@ -0,0 +1,54 @@
# Copyright (c) 2015 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 glance_store
from glance.artifacts import dependency
from glance.artifacts import domain
from glance.artifacts import location
from glance.artifacts import updater
from glance.common import store_utils
import glance.db
class Gateway(object):
def __init__(self, db_api=None, store_api=None, plugins=None):
self.db_api = db_api or glance.db.get_api()
self.store_api = store_api or glance_store
self.store_utils = store_utils
self.plugins = plugins
def get_artifact_type_factory(self, context, klass):
declarative_factory = domain.ArtifactFactory(context, klass)
repo = self.get_artifact_repo(context)
dependencies_factory = dependency.ArtifactFactory(declarative_factory,
klass, repo)
factory = location.ArtifactFactoryProxy(dependencies_factory,
context,
self.store_api,
self.store_utils)
updater_factory = updater.ArtifactFactoryProxy(factory)
return updater_factory
def get_artifact_repo(self, context):
artifact_repo = glance.db.ArtifactRepo(context,
self.db_api,
self.plugins)
dependencies_repo = dependency.ArtifactRepo(artifact_repo,
self.plugins)
repo = location.ArtifactRepoProxy(dependencies_repo,
context,
self.store_api,
self.store_utils)
updater_repo = updater.ArtifactRepoProxy(repo)
return updater_repo

View File

@ -0,0 +1,198 @@
# Copyright (c) 2015 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 sys
import uuid
from oslo_config import cfg
from oslo_log import log as logging
from glance.artifacts.domain import proxy
from glance.common.artifacts import definitions
from glance.common import utils
from glance import i18n
_ = i18n._
_LE = i18n._LE
_LW = i18n._LW
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class ArtifactFactoryProxy(proxy.ArtifactFactory):
def __init__(self, factory, context, store_api, store_utils):
self.context = context
self.store_api = store_api
self.store_utils = store_utils
proxy_kwargs = {'store_api': store_api,
'store_utils': store_utils,
'context': self.context}
super(ArtifactFactoryProxy, self).__init__(
factory,
artifact_proxy_class=ArtifactProxy,
artifact_proxy_kwargs=proxy_kwargs)
class ArtifactProxy(proxy.Artifact):
def __init__(self, artifact, context, store_api, store_utils):
self.artifact = artifact
self.context = context
self.store_api = store_api
self.store_utils = store_utils
super(ArtifactProxy,
self).__init__(artifact,
proxy_class=ArtifactBlobProxy,
proxy_kwargs={"context": self.context,
"store_api": self.store_api})
def set_type_specific_property(self, prop_name, value):
if prop_name not in self.artifact.metadata.attributes.blobs:
super(ArtifactProxy, self).set_type_specific_property(prop_name,
value)
return
item_key = "%s.%s" % (self.artifact.id, prop_name)
# XXX FIXME have to add support for BinaryObjectList properties
blob = definitions.Blob(item_key=item_key)
blob_proxy = self.helper.proxy(blob)
if value is None:
for location in blob_proxy.locations:
blob_proxy.delete_from_store(location)
else:
data = value[0]
size = value[1]
blob_proxy.upload_to_store(data, size)
setattr(self.artifact, prop_name, blob)
def get_type_specific_property(self, prop_name):
base = super(ArtifactProxy, self).get_type_specific_property(prop_name)
if base is None:
return None
if prop_name in self.artifact.metadata.attributes.blobs:
if isinstance(self.artifact.metadata.attributes.blobs[prop_name],
list):
return ArtifactBlobProxyList(self.artifact.id,
prop_name,
base,
self.context,
self.store_api)
else:
return self.helper.proxy(base)
else:
return base
class ArtifactRepoProxy(proxy.ArtifactRepo):
def __init__(self, artifact_repo, context, store_api, store_utils):
self.context = context
self.store_api = store_api
proxy_kwargs = {'context': context, 'store_api': store_api,
'store_utils': store_utils}
super(ArtifactRepoProxy, self).__init__(
artifact_repo,
proxy_helper=proxy.ArtifactHelper(ArtifactProxy, proxy_kwargs))
def get(self, *args, **kwargs):
return self.helper.proxy(self.base.get(*args, **kwargs))
class ArtifactBlobProxy(proxy.ArtifactBlob):
def __init__(self, blob, context, store_api):
self.context = context
self.store_api = store_api
self.blob = blob
super(ArtifactBlobProxy, self).__init__(blob)
def delete_from_store(self, location):
try:
ret = self.store_api.delete_from_backend(location['value'],
context=self.context)
location['status'] = 'deleted'
return ret
except self.store_api.NotFound:
msg = _LW('Failed to delete blob'
' %s in store from URI') % self.blob.id
LOG.warn(msg)
except self.store_api.StoreDeleteNotSupported as e:
LOG.warn(utils.exception_to_str(e))
except self.store_api.UnsupportedBackend:
exc_type = sys.exc_info()[0].__name__
msg = (_LE('Failed to delete blob'
' %(blob_id)s from store: %(exc)s') %
dict(blob_id=self.blob.id, exc=exc_type))
LOG.error(msg)
def upload_to_store(self, data, size):
location, ret_size, checksum, loc_meta = self.store_api.add_to_backend(
CONF,
self.blob.item_key,
utils.LimitingReader(utils.CooperativeReader(data),
CONF.image_size_cap),
size,
context=self.context)
self.blob.size = ret_size
self.blob.locations = [{'status': 'active', 'value': location}]
self.blob.checksum = checksum
@property
def data_stream(self):
if len(self.locations) > 0:
err = None
try:
for location in self.locations:
data, size = self.store_api.get_from_backend(
location['value'],
context=self.context)
return data
except Exception as e:
LOG.warn(_('Get blob %(name)s data failed: '
'%(err)s.') % {'name': self.blob.item_key,
'err': utils.exception_to_str(e)})
err = e
# tried all locations
LOG.error(_LE('Glance tried all active locations to get data '
'for blob %s '
'but all have failed.') % self.blob.item_key)
raise err
class ArtifactBlobProxyList(proxy.List):
def __init__(self, artifact_id, prop_name, bloblist, context, store_api):
self.artifact_id = artifact_id
self.prop_name = prop_name
self.context = context
self.store_api = store_api
super(ArtifactBlobProxyList,
self).__init__(bloblist,
item_proxy_class=ArtifactBlobProxy,
item_proxy_kwargs={'context': context,
'store_api': store_api})
def insert(self, index, value):
data = value[0]
size = value[1]
item_key = "%s.%s.%s" % (self.artifact_id, self.prop_name,
uuid.uuid4())
blob = definitions.Blob(item_key=item_key)
blob_proxy = self.helper.proxy(blob)
blob_proxy.upload_to_store(data, size)
super(ArtifactBlobProxyList, self).insert(index, blob_proxy)
def __setitem__(self, index, value):
blob = self[index]
data = value[0]
size = value[1]
blob.upload_to_store(data, size)

218
glance/artifacts/updater.py Normal file
View File

@ -0,0 +1,218 @@
# Copyright (c) 2015 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 glance.artifacts.domain import proxy
from glance.common import exception as exc
from glance import i18n
_ = i18n._
class JsonPatchUpdateMixin(object):
def _split_path(self, path):
"""Splits path into (prop_name, rest_of_path)"""
parts = path.lstrip('/').split('/', 1)
return parts[0], None if len(parts) == 1 else parts[1]
def _proc_key(self, key_str):
"""A method to retrieve value by some string key"""
raise NotImplemented(
"Function must be overloaded to use mixin correctly")
class ArtifactProxy(proxy.Artifact, JsonPatchUpdateMixin):
"""A proxy that is capable of modifying an artifact via jsonpatch methods.
Currently supported methods are update, remove, replace.
"""
def __init__(self, artifact):
self.artifact = artifact
super(ArtifactProxy, self).__init__(artifact)
def __getattr__(self, name):
if not hasattr(self, name):
raise exc.ArtifactInvalidProperty(prop=name)
return super(ArtifactProxy, self).__getattr__(name)
def _perform_op(self, op, **kwargs):
path = kwargs.get("path")
value = kwargs.get("value")
prop_name, path_left = self._split_path(path)
if not path_left:
return setattr(self, prop_name, value)
try:
prop = self._get_prop_to_update(prop_name, path_left)
# correct path_left and call corresponding update method
kwargs["path"] = path_left
getattr(prop, op)(path=kwargs["path"], value=kwargs.get("value"))
return setattr(self, prop_name, prop)
except exc.InvalidJsonPatchPath:
# NOTE(ivasilevskaya): here exception is reraised with
# 'part of path' substituted with with 'full path' to form a
# more relevant message
raise exc.InvalidJsonPatchPath(
path=path, explanation=_("No property to access"))
def _get_prop_to_update(self, prop_name, path):
"""Proxies properties that can be modified via update request.
All properties can be updated save for 'metadata' and blobs.
Due to the fact that empty lists and dicts are represented with null
values, have to check precise type definition by consulting metadata.
"""
prop = super(ArtifactProxy, self).get_type_specific_property(
prop_name)
if (prop_name == "metadata" or
prop_name in self.artifact.metadata.attributes.blobs):
return prop
if not prop:
# get correct type for empty list/dict
klass = self.artifact.metadata.attributes.all[prop_name]
if isinstance(klass, list):
prop = []
elif isinstance(klass, dict):
prop = {}
return wrap_property(prop, path)
def replace(self, path, value):
self._perform_op("replace", path=path, value=value)
def remove(self, path, value=None):
self._perform_op("remove", path=path)
def add(self, path, value):
self._perform_op("add", path=path, value=value)
class ArtifactFactoryProxy(proxy.ArtifactFactory):
def __init__(self, factory):
super(ArtifactFactoryProxy, self).__init__(factory)
class ArtifactRepoProxy(proxy.ArtifactRepo):
def __init__(self, repo):
super(ArtifactRepoProxy, self).__init__(
repo, item_proxy_class=ArtifactProxy)
def wrap_property(prop_value, full_path):
if isinstance(prop_value, list):
return ArtifactListPropertyProxy(prop_value, full_path)
if isinstance(prop_value, dict):
return ArtifactDictPropertyProxy(prop_value, full_path)
# no other types are supported
raise exc.InvalidJsonPatchPath(path=full_path)
class ArtifactListPropertyProxy(proxy.List, JsonPatchUpdateMixin):
"""A class to wrap a list property.
Makes possible to modify the property value via supported jsonpatch
requests (update/remove/replace).
"""
def __init__(self, prop_value, path):
super(ArtifactListPropertyProxy, self).__init__(
prop_value)
def _proc_key(self, idx_str, should_exist=True):
"""JsonPatchUpdateMixin method overload.
Only integers less than current array length and '-' (last elem)
in path are allowed.
Raises an InvalidJsonPatchPath exception if any of the conditions above
are not met.
"""
if idx_str == '-':
return len(self) - 1
try:
idx = int(idx_str)
if not should_exist and len(self) == 0:
return 0
if len(self) < idx + 1:
msg = _("Array has no element at position %d") % idx
raise exc.InvalidJsonPatchPath(explanation=msg, path=idx)
return idx
except (ValueError, TypeError):
msg = _("Not an array idx '%s'") % idx_str
raise exc.InvalidJsonPatchPath(explanation=msg, path=idx_str)
def add(self, path, value):
# by now arrays can't contain complex structures (due to Declarative
# Framework limitations and DB storage model),
# so will 'path' == idx equality is implied.
idx = self._proc_key(path, False)
if idx == len(self) - 1:
self.append(value)
else:
self.insert(idx, value)
return self.base
def remove(self, path, value=None):
# by now arrays can't contain complex structures, so will imply that
# 'path' == idx [see comment for add()]
del self[self._proc_key(path)]
return self.base
def replace(self, path, value):
# by now arrays can't contain complex structures, so will imply that
# 'path' == idx [see comment for add()]
self[self._proc_key(path)] = value
return self.base
class ArtifactDictPropertyProxy(proxy.Dict, JsonPatchUpdateMixin):
"""A class to wrap a dict property.
Makes possible to modify the property value via supported jsonpatch
requests (update/remove/replace).
"""
def __init__(self, prop_value, path):
super(ArtifactDictPropertyProxy, self).__init__(
prop_value)
def _proc_key(self, key_str, should_exist=True):
"""JsonPatchUpdateMixin method overload"""
if should_exist and key_str not in self.keys():
msg = _("No such key '%s' in a dict") % key_str
raise exc.InvalidJsonPatchPath(path=key_str, explanation=msg)
return key_str
def replace(self, path, value):
start, rest = self._split_path(path)
# the full path MUST exist in replace operation, so let's check
# that such key exists
key = self._proc_key(start)
if not rest:
self[key] = value
else:
prop = wrap_property(self[key], rest)
self[key] = prop.replace(rest, value)
def remove(self, path, value=None):
start, rest = self._split_path(path)
key = self._proc_key(start)
if not rest:
del self[key]
else:
prop = wrap_property(self[key], rest)
prop.remove(rest)
def add(self, path, value):
start, rest = self._split_path(path)
if not rest:
self[start] = value
else:
key = self._proc_key(start)
prop = wrap_property(self[key], rest)
self[key] = prop.add(rest, value)

View File

@ -497,6 +497,10 @@ class ArtifactDuplicateTransitiveDependency(Duplicate):
" already has the transitive dependency=%(dep)s")
class ArtifactCircularDependency(Invalid):
message = _("Artifact with a circular dependency can not be created")
class ArtifactUnsupportedPropertyOperator(Invalid):
message = _("Operator %(op)s is not supported")

View File

@ -24,7 +24,9 @@ import re
import jsonschema
import glance.common.exception as exc
from glance.openstack.common._i18n import _
from glance import i18n
_ = i18n._
class JsonPatchValidatorMixin(object):

View File

@ -2,6 +2,7 @@
# Administrator of the National Aeronautics and Space Administration.
# Copyright 2010-2012 OpenStack Foundation
# Copyright 2013 IBM Corp.
# Copyright 2015 Mirantis, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -21,6 +22,8 @@ from oslo_utils import importutils
from wsme.rest import json
from glance.api.v2.model.metadef_property_type import PropertyType
from glance import artifacts as ga
from glance.common.artifacts import serialization
from glance.common import crypt
from glance.common import exception
from glance.common import location_strategy
@ -58,6 +61,99 @@ IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size', 'virtual_size',
'protected'])
class ArtifactRepo(object):
fields = ['id', 'name', 'version', 'type_name', 'type_version',
'visibility', 'state', 'owner', 'scope', 'created_at',
'updated_at', 'tags', 'dependencies', 'blobs', 'properties']
def __init__(self, context, db_api, plugins):
self.context = context
self.db_api = db_api
self.plugins = plugins
def get(self, artifact_id, type_name=None, type_version=None,
show_level=None, include_deleted=False):
if show_level is None:
show_level = ga.Showlevel.BASIC
try:
db_api_artifact = self.db_api.artifact_get(self.context,
artifact_id,
type_name,
type_version,
show_level)
if db_api_artifact["state"] == 'deleted' and not include_deleted:
raise exception.ArtifactNotFound(artifact_id)
except (exception.ArtifactNotFound, exception.ArtifactForbidden):
msg = _("No artifact found with ID %s") % artifact_id
raise exception.ArtifactNotFound(msg)
return serialization.deserialize_from_db(db_api_artifact, self.plugins)
def list(self, marker=None, limit=None,
sort_keys=None, sort_dirs=None, filters=None,
show_level=None):
sort_keys = ['created_at'] if sort_keys is None else sort_keys
sort_dirs = ['desc'] if sort_dirs is None else sort_dirs
if show_level is None:
show_level = ga.Showlevel.NONE
db_api_artifacts = self.db_api.artifact_get_all(
self.context, filters=filters, marker=marker, limit=limit,
sort_keys=sort_keys, sort_dirs=sort_dirs, show_level=show_level)
artifacts = []
for db_api_artifact in db_api_artifacts:
artifact = serialization.deserialize_from_db(db_api_artifact,
self.plugins)
artifacts.append(artifact)
return artifacts
def _format_artifact_from_db(self, db_artifact):
kwargs = {k: db_artifact.get(k, None) for k in self.fields}
return glance.domain.Artifact(**kwargs)
def add(self, artifact):
artifact_values = serialization.serialize_for_db(artifact)
artifact_values['updated_at'] = artifact.updated_at
self.db_api.artifact_create(self.context, artifact_values,
artifact.type_name, artifact.type_version)
def save(self, artifact):
artifact_values = serialization.serialize_for_db(artifact)
try:
db_api_artifact = self.db_api.artifact_update(
self.context,
artifact_values,
artifact.id,
artifact.type_name,
artifact.type_version)
except (exception.ArtifactNotFound,
exception.ArtifactForbidden):
msg = _("No artifact found with ID %s") % artifact.id
raise exception.ArtifactNotFound(msg)
return serialization.deserialize_from_db(db_api_artifact, self.plugins)
def remove(self, artifact):
try:
self.db_api.artifact_delete(self.context, artifact.id,
artifact.type_name,
artifact.type_version)
except (exception.NotFound, exception.Forbidden):
msg = _("No artifact found with ID %s") % artifact.id
raise exception.ArtifactNotFound(msg)
def publish(self, artifact):
try:
artifact_changed = (
self.db_api.artifact_publish(
self.context,
artifact.id,
artifact.type_name,
artifact.type_version))
return serialization.deserialize_from_db(artifact_changed,
self.plugins)
except (exception.NotFound, exception.Forbidden):
msg = _("No artifact found with ID %s") % artifact.id
raise exception.ArtifactNotFound(msg)
class ImageRepo(object):
def __init__(self, context, db_api):

View File

@ -22,8 +22,10 @@ from oslo_config import cfg
import oslo_utils.importutils
from oslo_utils import timeutils
from glance.artifacts import domain as artifacts_domain
import glance.async
from glance.async import taskflow_executor
from glance.common.artifacts import definitions
from glance.common import exception
from glance import domain
import glance.tests.utils as test_utils
@ -569,3 +571,22 @@ class TestTaskExecutorFactory(test_utils.BaseTestCase):
# NOTE(flaper87): "eventlet" executor. short name to avoid > 79.
te_evnt = task_executor_factory.new_task_executor(context)
self.assertIsInstance(te_evnt, taskflow_executor.TaskExecutor)
class TestArtifact(definitions.ArtifactType):
prop1 = definitions.Dict()
prop2 = definitions.Integer(min_value=10)
class TestArtifactTypeFactory(test_utils.BaseTestCase):
def setUp(self):
super(TestArtifactTypeFactory, self).setUp()
context = mock.Mock(owner='me')
self.factory = artifacts_domain.ArtifactFactory(context, TestArtifact)
def test_new_artifact_min_params(self):
artifact = self.factory.new_artifact("foo", "1.0.0-alpha")
self.assertEqual('creating', artifact.state)
self.assertEqual('me', artifact.owner)
self.assertTrue(artifact.id is not None)

View File

@ -0,0 +1,71 @@
# Copyright (c) 2015 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 datetime import datetime
from glance.artifacts.domain import proxy
from glance.artifacts import location
from glance.common.artifacts import definitions
import glance.context
from glance.tests.unit import utils as unit_test_utils
from glance.tests import utils
BASE_URI = 'http://storeurl.com/container'
UUID1 = 'c80a1a6c-bd1f-41c5-90ee-81afedb1d58d'
UUID2 = '971ec09a-8067-4bc8-a91f-ae3557f1c4c7'
USER1 = '54492ba0-f4df-4e4e-be62-27f4d76b29cf'
TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81'
TENANT3 = '228c6da5-29cd-4d67-9457-ed632e083fc0'
class ArtifactStub(definitions.ArtifactType):
file = definitions.BinaryObject()
file_list = definitions.BinaryObjectList()
class TestStoreArtifact(utils.BaseTestCase):
def setUp(self):
self.store_api = unit_test_utils.FakeStoreAPI()
self.store_utils = unit_test_utils.FakeStoreUtils(self.store_api)
ts = datetime.now()
self.artifact_stub = ArtifactStub(id=UUID2, state='creating',
created_at=ts, updated_at=ts,
version='1.0', owner='me',
name='foo')
super(TestStoreArtifact, self).setUp()
def test_set_blob_data(self):
context = glance.context.RequestContext(user=USER1)
helper = proxy.ArtifactHelper(location.ArtifactProxy,
proxy_kwargs={
'context': context,
'store_api': self.store_api,
'store_utils': self.store_utils
})
artifact = helper.proxy(self.artifact_stub)
artifact.file = ('YYYY', 4)
self.assertEqual(4, artifact.file.size)
def test_set_bloblist_data(self):
context = glance.context.RequestContext(user=USER1)
helper = proxy.ArtifactHelper(location.ArtifactProxy,
proxy_kwargs={
'context': context,
'store_api': self.store_api,
'store_utils': self.store_utils
})
artifact = helper.proxy(self.artifact_stub)
artifact.file_list.append(('YYYY', 4))
self.assertEqual(4, artifact.file_list[0].size)