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:
parent
0002df8710
commit
35e35a17bd
128
glance/artifacts/dependency.py
Normal file
128
glance/artifacts/dependency.py
Normal 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
|
76
glance/artifacts/domain/__init__.py
Normal file
76
glance/artifacts/domain/__init__.py
Normal 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
|
198
glance/artifacts/domain/proxy.py
Normal file
198
glance/artifacts/domain/proxy.py
Normal 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
|
54
glance/artifacts/gateway.py
Normal file
54
glance/artifacts/gateway.py
Normal 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
|
198
glance/artifacts/location.py
Normal file
198
glance/artifacts/location.py
Normal 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
218
glance/artifacts/updater.py
Normal 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)
|
@ -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")
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
71
glance/tests/unit/test_store_artifact.py
Normal file
71
glance/tests/unit/test_store_artifact.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user