Update docstrings
Change-Id: I2be4437a5afb221f56f4d93a24be0356e7abc033
This commit is contained in:
@@ -67,7 +67,7 @@ class ContextMiddleware(base_middleware.ConfigurableMiddleware):
|
||||
|
||||
@staticmethod
|
||||
def process_request(req):
|
||||
"""Convert authentication information into a request context
|
||||
"""Convert authentication information into a request context.
|
||||
|
||||
Generate a RequestContext object from the available
|
||||
authentication headers and store on the 'context' attribute
|
||||
@@ -88,7 +88,7 @@ class ContextMiddleware(base_middleware.ConfigurableMiddleware):
|
||||
|
||||
@staticmethod
|
||||
def _get_anonymous_context():
|
||||
"""Anonymous user has only Read-Only grants"""
|
||||
"""Anonymous user has only Read-Only grants."""
|
||||
return RequestContext(read_only=True, is_admin=False)
|
||||
|
||||
@staticmethod
|
||||
|
@@ -16,7 +16,7 @@
|
||||
"""
|
||||
A filter middleware that inspects the requested URI for a version string
|
||||
and/or Accept headers and attempts to negotiate an API controller to
|
||||
return
|
||||
return.
|
||||
"""
|
||||
|
||||
import microversion_parse
|
||||
@@ -32,7 +32,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_version_from_accept(accept_header, vnd_mime_type):
|
||||
"""Try to parse accept header to extract api version
|
||||
"""Try to parse accept header to extract api version.
|
||||
|
||||
:param accept_header: accept header
|
||||
:return: version string in the request or None if not specified
|
||||
|
@@ -40,7 +40,7 @@ class APIVersionRequest(object):
|
||||
"""Create an API version request object.
|
||||
|
||||
:param version_string: String representation of APIVersionRequest.
|
||||
Correct format is 'X.Y', where 'X' and 'Y' are int values.
|
||||
Correct format is 'X.Y', where 'X' and 'Y' are int values.
|
||||
"""
|
||||
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$", version_string)
|
||||
if match:
|
||||
@@ -95,9 +95,9 @@ class APIVersionRequest(object):
|
||||
greater than or equal to the minimum version and less than
|
||||
or equal to the maximum version.
|
||||
|
||||
@param min_version: Minimum acceptable version.
|
||||
@param max_version: Maximum acceptable version.
|
||||
@returns: boolean
|
||||
:param min_version: Minimum acceptable version.
|
||||
:param max_version: Maximum acceptable version.
|
||||
:returns: boolean
|
||||
"""
|
||||
return min_version <= self <= max_version
|
||||
|
||||
@@ -109,15 +109,15 @@ class APIVersionRequest(object):
|
||||
|
||||
@classmethod
|
||||
def min_version(cls):
|
||||
"""Minimal allowed api version"""
|
||||
"""Minimal allowed api version."""
|
||||
return APIVersionRequest(cls._MIN_API_VERSION)
|
||||
|
||||
@classmethod
|
||||
def max_version(cls):
|
||||
"""Maximal allowed api version"""
|
||||
"""Maximal allowed api version."""
|
||||
return APIVersionRequest(cls._MAX_API_VERSION)
|
||||
|
||||
@classmethod
|
||||
def default_version(cls):
|
||||
"""Default api version if no version in request"""
|
||||
"""Default api version if no version in request."""
|
||||
return APIVersionRequest(cls._DEFAULT_API_VERSION)
|
||||
|
@@ -22,13 +22,14 @@ from glare.i18n import _
|
||||
class VersionedMethod(object):
|
||||
|
||||
def __init__(self, name, start_version, end_version, func):
|
||||
"""Versioning information for a single method
|
||||
"""Versioning information for a single method.
|
||||
|
||||
:param name: Name of the method
|
||||
:param start_version: Minimum acceptable version
|
||||
:param end_version: Maximum acceptable_version
|
||||
:param func: Method to call
|
||||
Minimum and maximums are inclusive
|
||||
"""
|
||||
# NOTE(kairat): minimums and maximums are inclusive
|
||||
self.name = name
|
||||
self.start_version = start_version
|
||||
self.end_version = end_version
|
||||
@@ -41,7 +42,7 @@ class VersionedMethod(object):
|
||||
|
||||
class VersionedResource(object):
|
||||
"""Versioned mixin that provides ability to define versioned methods and
|
||||
return appropriate methods based on user request
|
||||
return appropriate methods based on user request.
|
||||
"""
|
||||
|
||||
# prefix for all versioned methods in class
|
||||
@@ -52,6 +53,7 @@ class VersionedResource(object):
|
||||
"""Determines whether function list contains version intervals
|
||||
intersections or not. General algorithm:
|
||||
https://en.wikipedia.org/wiki/Intersection_algorithm
|
||||
|
||||
:param func_list: list of VersionedMethod objects
|
||||
:return: boolean
|
||||
"""
|
||||
@@ -128,9 +130,10 @@ class VersionedResource(object):
|
||||
def version_select(*args, **kwargs):
|
||||
"""Look for the method which matches the name supplied and version
|
||||
constraints and calls it with the supplied arguments.
|
||||
|
||||
:returns: Returns the result of the method called
|
||||
:raises: VersionNotFoundForAPIMethod if there is no method which
|
||||
matches the name and version constraints
|
||||
matches the name and version constraints
|
||||
"""
|
||||
# versioning is used in 3 classes: request deserializer and
|
||||
# controller have request as first argument
|
||||
|
@@ -50,11 +50,12 @@ supported_versions = api_versioning.VersionedResource.supported_versions
|
||||
|
||||
class RequestDeserializer(api_versioning.VersionedResource,
|
||||
wsgi.JSONRequestDeserializer):
|
||||
"""Glare deserializer for incoming webop Requests.
|
||||
Deserializer converts incoming request into bunch of python primitives.
|
||||
So other components doesn't work with requests at all. Deserializer also
|
||||
executes primary API validation without any knowledge about Artifact
|
||||
structure.
|
||||
"""Glare deserializer for incoming webob requests.
|
||||
|
||||
Deserializer checks and converts incoming request into a bunch of Glare
|
||||
primitives. So other service components don't work with requests at all.
|
||||
Deserializer also performs primary API validation without any knowledge
|
||||
about concrete artifact type structure.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@@ -74,6 +75,7 @@ class RequestDeserializer(api_versioning.VersionedResource,
|
||||
return content_type
|
||||
|
||||
def _get_request_body(self, req):
|
||||
"""Get request json body and convert it to python structures."""
|
||||
return self.from_json(req.body)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@@ -137,7 +139,7 @@ class RequestDeserializer(api_versioning.VersionedResource,
|
||||
patch = jsonpatch.JsonPatch(body)
|
||||
try:
|
||||
# Initially patch object doesn't validate input. It's only checked
|
||||
# we call get operation on each method
|
||||
# when we call get operation on each method
|
||||
tuple(map(patch._get_operation, patch.patch))
|
||||
except (jsonpatch.InvalidJsonPatch, TypeError):
|
||||
msg = _("Json Patch body is malformed")
|
||||
@@ -152,7 +154,7 @@ class RequestDeserializer(api_versioning.VersionedResource,
|
||||
data = self._get_request_body(req)
|
||||
if 'url' not in data:
|
||||
msg = _("url is required when specifying external location. "
|
||||
"Cannot find url in body: %s") % str(data)
|
||||
"Cannot find 'url' in request body: %s") % str(data)
|
||||
raise exc.BadRequest(msg)
|
||||
else:
|
||||
data = req.body_file
|
||||
@@ -176,10 +178,10 @@ def log_request_progress(f):
|
||||
|
||||
class ArtifactsController(api_versioning.VersionedResource):
|
||||
"""API controller for Glare Artifacts.
|
||||
|
||||
Artifact Controller prepares incoming data for Glare Engine and redirects
|
||||
data to appropriate engine method (so only controller is working with
|
||||
Engine. Once the data returned from Engine Controller returns data
|
||||
in appropriate format for Response Serializer.
|
||||
data to the appropriate engine method. Once the response data is returned
|
||||
from the engine Controller passes it next to Response Serializer.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -188,12 +190,21 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def list_type_schemas(self, req):
|
||||
"""List of detailed descriptions of enabled artifact types.
|
||||
|
||||
:return: list of json-schemas of all enabled artifact types.
|
||||
"""
|
||||
type_schemas = self.engine.list_type_schemas(req.context)
|
||||
return type_schemas
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def show_type_schema(self, req, type_name):
|
||||
"""Get detailed artifact type description.
|
||||
|
||||
:param type_name: artifact type name
|
||||
:return: json-schema representation of artifact type
|
||||
"""
|
||||
type_schema = self.engine.show_type_schema(req.context, type_name)
|
||||
return {type_name: type_schema}
|
||||
|
||||
@@ -202,10 +213,10 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
def create(self, req, type_name, values):
|
||||
"""Create artifact record in Glare.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param values: dict with artifact fields {field_name: field_value}
|
||||
:return definition of created artifact
|
||||
:param req: user request
|
||||
:param type_name: artifact type name
|
||||
:param values: dict with artifact fields
|
||||
:return: definition of created artifact
|
||||
"""
|
||||
return self.engine.create(req.context, type_name, values)
|
||||
|
||||
@@ -218,14 +229,14 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of artifact to update
|
||||
:param patch: json patch with artifact changes
|
||||
:return definition of updated artifact
|
||||
:return: definition of updated artifact
|
||||
"""
|
||||
return self.engine.update(req.context, type_name, artifact_id, patch)
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def delete(self, req, type_name, artifact_id):
|
||||
"""Delete artifact from Glare
|
||||
"""Delete artifact from Glare.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
@@ -236,7 +247,7 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def show(self, req, type_name, artifact_id):
|
||||
"""Show detailed artifact info
|
||||
"""Show detailed artifact info.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
@@ -249,7 +260,7 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
@log_request_progress
|
||||
def list(self, req, type_name, filters, marker=None, limit=None,
|
||||
sort=None, latest=False):
|
||||
"""List available artifacts
|
||||
"""List available artifacts.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
@@ -261,7 +272,7 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
:param sort: sorting options
|
||||
:param latest: flag that indicates, that only artifacts with highest
|
||||
versions should be returned in output
|
||||
:return: list of artifacts
|
||||
:return: list of requested artifact definitions
|
||||
"""
|
||||
artifacts = self.engine.list(req.context, type_name, filters, marker,
|
||||
limit, sort, latest)
|
||||
@@ -273,18 +284,19 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def upload_blob(self, req, type_name, artifact_id, blob_name, data,
|
||||
def upload_blob(self, req, type_name, artifact_id, blob_path, data,
|
||||
content_type):
|
||||
"""Upload blob into Glare repo
|
||||
"""Upload blob into Glare repo.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of Artifact to reactivate
|
||||
:param blob_name: name of blob field in artifact
|
||||
:param data: Artifact payload
|
||||
:param artifact_id: id of artifact where to perform upload
|
||||
:param blob_path: path to artifact blob
|
||||
:param data: blob payload
|
||||
:param content_type: data content-type
|
||||
:return: definition of requested artifact with uploaded blob
|
||||
"""
|
||||
field_name, _sep, blob_key = blob_name.partition('/')
|
||||
field_name, _sep, blob_key = blob_path.partition('/')
|
||||
if not blob_key:
|
||||
blob_key = None
|
||||
if content_type == ('application/vnd+openstack.glare-custom-location'
|
||||
@@ -300,16 +312,16 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
|
||||
@supported_versions(min_ver='1.0')
|
||||
@log_request_progress
|
||||
def download_blob(self, req, type_name, artifact_id, blob_name):
|
||||
"""Download blob data from Artifact
|
||||
def download_blob(self, req, type_name, artifact_id, blob_path):
|
||||
"""Download blob data from Artifact.
|
||||
|
||||
:param req: User request
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of Artifact to reactivate
|
||||
:param blob_name: name of blob field in artifact
|
||||
:return: iterator that returns blob data
|
||||
:param type_name: artifact type name
|
||||
:param artifact_id: id of artifact from where to perform download
|
||||
:param blob_path: path to artifact blob
|
||||
:return: requested blob data
|
||||
"""
|
||||
field_name, _sep, blob_key = blob_name.partition('/')
|
||||
field_name, _sep, blob_key = blob_path.partition('/')
|
||||
if not blob_key:
|
||||
blob_key = None
|
||||
data, meta = self.engine.download_blob(
|
||||
@@ -320,10 +332,10 @@ class ArtifactsController(api_versioning.VersionedResource):
|
||||
|
||||
class ResponseSerializer(api_versioning.VersionedResource,
|
||||
wsgi.JSONResponseSerializer):
|
||||
"""Glare Response Serializer converts data received from Glare Engine
|
||||
(it consists from plain data types - dict, int, string, file descriptors,
|
||||
etc) to WSGI Requests. It also specifies proper response status and
|
||||
content type as specified by API design.
|
||||
"""Glare serializer for outgoing responses.
|
||||
|
||||
Converts data received from the engine to WSGI responses. It also
|
||||
specifies proper response status and content type as declared in the API.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@@ -425,7 +437,7 @@ class ResponseSerializer(api_versioning.VersionedResource,
|
||||
|
||||
|
||||
def create_resource():
|
||||
"""Artifact resource factory method"""
|
||||
"""Artifact resource factory method."""
|
||||
deserializer = RequestDeserializer()
|
||||
serializer = ResponseSerializer()
|
||||
controller = ArtifactsController()
|
||||
|
@@ -83,16 +83,16 @@ class API(wsgi.Router):
|
||||
allowed_methods='GET, PATCH, DELETE')
|
||||
|
||||
# ---blobs---
|
||||
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_name:.*?}',
|
||||
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_path:.*?}',
|
||||
controller=glare_resource,
|
||||
action='download_blob',
|
||||
conditions={'method': ['GET']},
|
||||
body_reject=True)
|
||||
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_name:.*?}',
|
||||
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_path:.*?}',
|
||||
controller=glare_resource,
|
||||
action='upload_blob',
|
||||
conditions={'method': ['PUT']})
|
||||
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_name:.*?}',
|
||||
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_path:.*?}',
|
||||
controller=reject_method_resource,
|
||||
action='reject',
|
||||
allowed_methods='GET, PUT')
|
||||
|
@@ -60,6 +60,7 @@ class Controller(object):
|
||||
@staticmethod
|
||||
def index(req, is_multi):
|
||||
"""Respond to a request for all OpenStack API versions.
|
||||
|
||||
:param is_multi: defines if multiple choices should be response status
|
||||
or not
|
||||
:param req: user request object
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
|
||||
"""
|
||||
Glare (Glare Artifact Repository) API service
|
||||
Glare (Glare Artifact Repository) API service.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
@@ -14,7 +14,7 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Routines for configuring Glare
|
||||
Routines for configuring Glare.
|
||||
"""
|
||||
|
||||
import logging.config
|
||||
|
@@ -24,7 +24,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
class GlareException(Exception):
|
||||
"""
|
||||
Base Glare Exception
|
||||
Base Glare Exception class.
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
|
@@ -97,7 +97,7 @@ def reset():
|
||||
|
||||
|
||||
def authorize(policy_name, target, context, do_raise=True):
|
||||
"""Method checks that user action can be executed according to policies
|
||||
"""Method checks that user action can be executed according to policies.
|
||||
|
||||
:param policy_name: policy name
|
||||
:param target:
|
||||
|
@@ -53,7 +53,7 @@ error_map = [{'catch': store_exc.NotFound,
|
||||
@utils.error_handler(error_map)
|
||||
def save_blob_to_store(blob_id, blob, context, max_size,
|
||||
store_type=None, verifier=None):
|
||||
"""Save file to specified store type and return location info to the user
|
||||
"""Save file to specified store type and return location info to the user.
|
||||
|
||||
:param store_type: type of the store, None means save to default store.
|
||||
:param blob_id: id of artifact
|
||||
@@ -84,7 +84,7 @@ def load_from_store(uri, context):
|
||||
|
||||
@utils.error_handler(error_map)
|
||||
def delete_blob(uri, context):
|
||||
"""Delete blob from backend store
|
||||
"""Delete blob from backend store.
|
||||
|
||||
:param uri: blob uri
|
||||
:param context: user context
|
||||
|
@@ -68,7 +68,7 @@ def chunkreadable(iter, chunk_size=65536):
|
||||
|
||||
def chunkiter(fp, chunk_size=65536):
|
||||
"""
|
||||
Return an iterator to a file-like obj which yields fixed size chunks
|
||||
Return an iterator to a file-like obj which yields fixed size chunks.
|
||||
|
||||
:param fp: a file-like object
|
||||
:param chunk_size: maximum size of chunk
|
||||
@@ -361,7 +361,7 @@ except re.error:
|
||||
def no_4byte_params(f):
|
||||
"""
|
||||
Checks that no 4 byte unicode characters are allowed
|
||||
in dicts' keys/values and string's parameters
|
||||
in dicts' keys/values and string's parameters.
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
@@ -423,8 +423,7 @@ def split_filter_op(expression):
|
||||
When no operator is found, default to an equality comparison.
|
||||
|
||||
:param expression: the expression to parse
|
||||
|
||||
:returns: a tuple (operator, threshold) parsed from expression
|
||||
:return: a tuple (operator, threshold) parsed from expression
|
||||
"""
|
||||
left, sep, right = expression.partition(':')
|
||||
if sep:
|
||||
@@ -500,11 +499,8 @@ def evaluate_filter_op(value, operator, threshold):
|
||||
:param value: evaluated against the operator, as left side of expression
|
||||
:param operator: any supported filter operation
|
||||
:param threshold: to compare value against, as right side of expression
|
||||
|
||||
:raises: InvalidFilterOperatorValue if an unknown operator is provided
|
||||
|
||||
:returns: boolean result of applied comparison
|
||||
|
||||
:return: boolean result of applied comparison
|
||||
"""
|
||||
if operator == 'gt':
|
||||
return value > threshold
|
||||
@@ -525,21 +521,25 @@ def evaluate_filter_op(value, operator, threshold):
|
||||
|
||||
class error_handler(object):
|
||||
def __init__(self, error_map, default_exception=None):
|
||||
"""Init method of the class.
|
||||
|
||||
:param error_map: dict of exception that can be raised
|
||||
in func and exceptions that must be raised for these exceptions.
|
||||
For example, if sqlalchemy NotFound might be raised and we need
|
||||
re-raise it as glare NotFound exception then error_map must
|
||||
contain {"catch": SQLAlchemyNotFound,
|
||||
"raise": exceptions.NotFound}
|
||||
:param default_exception: default exception that must be raised if
|
||||
exception that cannot be found in error map was raised
|
||||
:return: func
|
||||
"""
|
||||
self.error_map = error_map
|
||||
self.default_exception = default_exception
|
||||
|
||||
def __call__(self, f):
|
||||
"""Decorator that catches exception that came from func or method
|
||||
:param f: targer func
|
||||
:param error_map: dict of exception that can be raised
|
||||
in func and exceptions that must be raised for these exceptions.
|
||||
For example, if sqlalchemy NotFound might be raised and we need
|
||||
re-raise it as glare NotFound exception then error_map must
|
||||
contain {"catch": SQLAlchemyNotFound,
|
||||
"raise": exceptions.NotFound}
|
||||
:param default_exception: default exception that must be raised if
|
||||
exception that cannot be found in error map was raised
|
||||
:return: func
|
||||
"""Decorator that catches exception that came from func or method.
|
||||
|
||||
:param f: target func
|
||||
"""
|
||||
|
||||
def new_function(*args, **kwargs):
|
||||
|
113
glare/engine.py
113
glare/engine.py
@@ -39,15 +39,16 @@ class Engine(object):
|
||||
"""Engine is responsible for executing different helper operations when
|
||||
processing incoming requests from Glare API.
|
||||
Engine receives incoming data and does the following:
|
||||
- check basic policy permissions
|
||||
- requests artifact definition from registry
|
||||
- check access permission(ro, rw)
|
||||
- lock artifact for update if needed
|
||||
- check basic policy permissions;
|
||||
- requests artifact definition from artifact type registry;
|
||||
- check access permission(ro, rw);
|
||||
- lock artifact for update if needed;
|
||||
- pass data to base artifact to execute all business logic operations
|
||||
with database;
|
||||
- notify other users about finished operation.
|
||||
Engine should not include any business logic and validation related
|
||||
to Artifacts. Engine should not know any internal details of Artifacts
|
||||
because it controls access to Artifacts in common.
|
||||
to Artifacts. Engine should not know any internal details of artifact
|
||||
type, because this part of the work is done by Base artifact type.
|
||||
"""
|
||||
def __init__(self):
|
||||
# register all artifact types
|
||||
@@ -69,28 +70,32 @@ class Engine(object):
|
||||
@classmethod
|
||||
def _get_artifact(cls, context, type_name, artifact_id,
|
||||
read_only=False):
|
||||
"""Return artifact for users
|
||||
"""Return artifact requested by user.
|
||||
Check access permissions and policies.
|
||||
|
||||
Return artifact for reading/modification by users. Check
|
||||
access permissions and policies for artifact.
|
||||
:param context: user context
|
||||
:param type_name: artifact type name
|
||||
:param artifact_id: id of the artifact to be updated
|
||||
:param read_only: flag, if set to True only read access is checked,
|
||||
if False then engine checks if artifact can be modified by the user
|
||||
"""
|
||||
|
||||
def _check_read_write_access(ctx, af):
|
||||
"""Check if artifact can be modified by user
|
||||
"""Check if artifact can be modified by user.
|
||||
|
||||
:param ctx: user context
|
||||
:param af: artifact definition
|
||||
:raise Forbidden if access is not allowed
|
||||
:raise: Forbidden if access is not allowed
|
||||
"""
|
||||
if not ctx.is_admin and ctx.tenant != af.owner or ctx.read_only:
|
||||
raise exception.Forbidden()
|
||||
|
||||
def _check_read_only_access(ctx, af):
|
||||
"""Check if user has read only access to artifact
|
||||
"""Check if user has read only access to artifact.
|
||||
|
||||
:param ctx: user context
|
||||
:param af: artifact definition
|
||||
:raise Forbidden if access is not allowed
|
||||
:raise: Forbidden if access is not allowed
|
||||
"""
|
||||
private = af.visibility != 'public'
|
||||
if (private and
|
||||
@@ -125,13 +130,19 @@ class Engine(object):
|
||||
return schemas[type_name]
|
||||
|
||||
@classmethod
|
||||
def create(cls, context, type_name, field_values):
|
||||
"""Create new artifact in Glare"""
|
||||
def create(cls, context, type_name, values):
|
||||
"""Create artifact record in Glare.
|
||||
|
||||
:param context: user context
|
||||
:param type_name: artifact type name
|
||||
:param values: dict with artifact fields
|
||||
:return: dict representation of created artifact
|
||||
"""
|
||||
action_name = "artifact:create"
|
||||
policy.authorize(action_name, field_values, context)
|
||||
policy.authorize(action_name, values, context)
|
||||
artifact_type = registry.ArtifactRegistry.get_artifact_type(type_name)
|
||||
# acquire version lock and execute artifact create
|
||||
af = artifact_type.create(context, field_values)
|
||||
af = artifact_type.create(context, values)
|
||||
# notify about new artifact
|
||||
Notifier.notify(context, action_name, af)
|
||||
# return artifact to the user
|
||||
@@ -142,24 +153,23 @@ class Engine(object):
|
||||
"""Update artifact with json patch.
|
||||
|
||||
Apply patch to artifact and validate artifact before updating it
|
||||
in database. If there is request for visibility change or custom
|
||||
location change then call specific method for that.
|
||||
in database. If there is request for visibility or status change
|
||||
then call specific method for that.
|
||||
|
||||
:param context: user context
|
||||
:param type_name: name of artifact type
|
||||
:param artifact_id: id of the artifact to be updated
|
||||
:param patch: json patch
|
||||
:return: updated artifact
|
||||
:param patch: json patch object
|
||||
:return: dict representation of updated artifact
|
||||
"""
|
||||
|
||||
def get_updates(af_dict, patch_with_upd):
|
||||
"""Get updated values for artifact and json patch
|
||||
def _get_updates(af_dict, patch_with_upd):
|
||||
"""Get updated values for artifact and json patch.
|
||||
|
||||
:param af_dict: current artifact definition as dict
|
||||
:param patch_with_upd: json-patch
|
||||
:return: dict of updated attributes and their values
|
||||
:param patch_with_upd: json-patch object
|
||||
:return dict of updated attributes and their values
|
||||
"""
|
||||
|
||||
try:
|
||||
af_dict_patched = patch_with_upd.apply(af_dict)
|
||||
diff = utils.DictDiffer(af_dict_patched, af_dict)
|
||||
@@ -187,7 +197,7 @@ class Engine(object):
|
||||
with base.BaseArtifact.lock_engine.acquire(context, lock_key):
|
||||
artifact = cls._get_artifact(context, type_name, artifact_id)
|
||||
af_dict = artifact.to_dict()
|
||||
updates = get_updates(af_dict, patch)
|
||||
updates = _get_updates(af_dict, patch)
|
||||
LOG.debug("Update diff successfully calculated for artifact "
|
||||
"%(af)s %(diff)s", {'af': artifact_id, 'diff': updates})
|
||||
|
||||
@@ -196,6 +206,8 @@ class Engine(object):
|
||||
else:
|
||||
action = artifact.get_action_for_updates(
|
||||
context, artifact, updates, registry.ArtifactRegistry)
|
||||
LOG.debug("Action %(action)s was defined to values %(val)s.",
|
||||
{'action': action.__name__, 'val': updates})
|
||||
action_name = "artifact:%s" % action.__name__
|
||||
policy.authorize(action_name, af_dict, context)
|
||||
modified_af = action(context, artifact, updates)
|
||||
@@ -204,7 +216,13 @@ class Engine(object):
|
||||
|
||||
@classmethod
|
||||
def get(cls, context, type_name, artifact_id):
|
||||
"""Return artifact representation from artifact repo."""
|
||||
"""Show detailed artifact info.
|
||||
|
||||
:param context: user context
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of artifact to show
|
||||
:return: definition of requested artifact
|
||||
"""
|
||||
policy.authorize("artifact:get", {}, context)
|
||||
af = cls._get_artifact(context, type_name, artifact_id,
|
||||
read_only=True)
|
||||
@@ -225,7 +243,7 @@ class Engine(object):
|
||||
:param sort: sorting options
|
||||
:param latest: flag that indicates, that only artifacts with highest
|
||||
versions should be returned in output
|
||||
:return: list of artifacts
|
||||
:return: list of artifact definitions
|
||||
"""
|
||||
policy.authorize("artifact:list", {}, context)
|
||||
artifact_type = registry.ArtifactRegistry.get_artifact_type(type_name)
|
||||
@@ -237,7 +255,12 @@ class Engine(object):
|
||||
|
||||
@classmethod
|
||||
def delete(cls, context, type_name, artifact_id):
|
||||
"""Delete artifact from glare"""
|
||||
"""Delete artifact from Glare.
|
||||
|
||||
:param context: User context
|
||||
:param type_name: Artifact type name
|
||||
:param artifact_id: id of artifact to delete
|
||||
"""
|
||||
af = cls._get_artifact(context, type_name, artifact_id)
|
||||
policy.authorize("artifact:delete", af.to_dict(), context)
|
||||
af.delete(context, af)
|
||||
@@ -254,9 +277,9 @@ class Engine(object):
|
||||
:param field_name: name of blob or blob dict field
|
||||
:param location: external blob url
|
||||
:param blob_meta: dictionary containing blob metadata like md5 checksum
|
||||
:param blob_key: if field_name is blob dict it specifies concrete key
|
||||
:param blob_key: if field_name is blob dict it specifies key
|
||||
in this dict
|
||||
:return updated artifact
|
||||
:return: dict representation of updated artifact
|
||||
"""
|
||||
af = cls._get_artifact(context, type_name, artifact_id)
|
||||
action_name = 'artifact:set_location'
|
||||
@@ -299,11 +322,10 @@ class Engine(object):
|
||||
:param artifact_id: id of the artifact to be updated
|
||||
:param field_name: name of blob or blob dict field
|
||||
:param fd: file descriptor that Glare uses to upload the file
|
||||
:param field_name: name of blob dict field
|
||||
:param content_type: data content-type
|
||||
:param blob_key: if field_name is blob dict it specifies concrete key
|
||||
in this dict
|
||||
:return file iterator for requested file
|
||||
:param blob_key: if field_name is blob dict it specifies key
|
||||
in this dictionary
|
||||
:return: dict representation of updated artifact
|
||||
"""
|
||||
af = cls._get_artifact(context, type_name, artifact_id)
|
||||
action_name = "artifact:upload"
|
||||
@@ -348,12 +370,12 @@ class Engine(object):
|
||||
# if upload failed remove blob from db and storage
|
||||
with excutils.save_and_reraise_exception(logger=LOG):
|
||||
if blob_key is None:
|
||||
af.update_blob(context, af.id, {field_name: None})
|
||||
af.update_blob(context, af.id, field_name, None)
|
||||
else:
|
||||
blob_dict_attr = modified_af[field_name]
|
||||
del blob_dict_attr[blob_key]
|
||||
af.update_blob(context, af.id,
|
||||
{field_name: blob_dict_attr})
|
||||
field_name, blob_dict_attr)
|
||||
blob_name = "%s[%s]" % (field_name, blob_key) \
|
||||
if blob_key else field_name
|
||||
LOG.info(_LI("Successfully finished blob upload for artifact "
|
||||
@@ -378,40 +400,43 @@ class Engine(object):
|
||||
def update_blob(cls, context, type_name, artifact_id, blob,
|
||||
field_name, blob_key=None, validate=False):
|
||||
"""Update blob info.
|
||||
|
||||
:param context: user context
|
||||
:param type_name: name of artifact type
|
||||
:param artifact_id: id of the artifact to be updated
|
||||
:param blob: blob representation in dict format
|
||||
:param field_name: name of blob or blob dict field
|
||||
:param blob_key: if field_name is blob dict it specifies concrete key
|
||||
:param blob_key: if field_name is blob dict it specifies key
|
||||
in this dict
|
||||
:param validate: enable validation of possibility of blob uploading
|
||||
:return updated artifact
|
||||
|
||||
:return: dict representation of updated artifact
|
||||
"""
|
||||
lock_key = "%s:%s" % (type_name, artifact_id)
|
||||
with base.BaseArtifact.lock_engine.acquire(context, lock_key):
|
||||
af = cls._get_artifact(context, type_name, artifact_id)
|
||||
if validate:
|
||||
af.validate_upload_allowed(context, af, field_name, blob_key)
|
||||
af.validate_upload_allowed(af, field_name, blob_key)
|
||||
if blob_key is None:
|
||||
setattr(af, field_name, blob)
|
||||
return af.update_blob(
|
||||
context, af.id, {field_name: getattr(af, field_name)})
|
||||
context, af.id, field_name, getattr(af, field_name))
|
||||
else:
|
||||
blob_dict_attr = getattr(af, field_name)
|
||||
blob_dict_attr[blob_key] = blob
|
||||
return af.update_blob(
|
||||
context, af.id, {field_name: blob_dict_attr})
|
||||
context, af.id, field_name, blob_dict_attr)
|
||||
|
||||
@classmethod
|
||||
def download_blob(cls, context, type_name, artifact_id, field_name,
|
||||
blob_key=None):
|
||||
"""Download binary data from Glare Artifact.
|
||||
|
||||
:param context: user context
|
||||
:param type_name: name of artifact type
|
||||
:param artifact_id: id of the artifact to be updated
|
||||
:param field_name: name of blob or blob dict field
|
||||
:param blob_key: if field_name is blob dict it specifies concrete key
|
||||
:param blob_key: if field_name is blob dict it specifies key
|
||||
in this dict
|
||||
:return: file iterator for requested file
|
||||
"""
|
||||
|
@@ -20,11 +20,10 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LockApiBase(object):
|
||||
"""Lock Api Base class that responsible for acquiring/releasing locks
|
||||
"""
|
||||
"""Lock Api Base class that responsible for acquiring/releasing locks."""
|
||||
|
||||
def create_lock(self, context, lock_key):
|
||||
"""Acquire lock for current user
|
||||
"""Acquire lock for current user.
|
||||
|
||||
:param context user context
|
||||
:param lock_key: unique lock identifier that defines lock scope
|
||||
@@ -33,22 +32,21 @@ class LockApiBase(object):
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_lock(self, context, lock_id):
|
||||
"""Delete acquired user lock
|
||||
"""Delete acquired user lock.
|
||||
|
||||
:param context: user context
|
||||
:param lock_id: lock internal identifier
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Lock(object):
|
||||
"""Object that stores lock context for users. This class is internal
|
||||
and used only for Lock Engine. So users shouldn't use this class directly
|
||||
and used only in lock engine, so users shouldn't use this class directly.
|
||||
"""
|
||||
|
||||
def __init__(self, context, lock_id, lock_key, release_method):
|
||||
"""Initialize lock context"""
|
||||
"""Initialize lock context."""
|
||||
self.context = context
|
||||
self.lock_id = lock_id
|
||||
self.lock_key = lock_key
|
||||
@@ -64,31 +62,33 @@ class Lock(object):
|
||||
|
||||
class LockEngine(object):
|
||||
"""Glare lock engine.
|
||||
|
||||
Defines how artifact updates must be synchronized with each other. When
|
||||
some user obtains lock for the same piece of data then other user cannot
|
||||
request that lock and get Conflict error.
|
||||
This little engine also allows to encapsulate lock logic in one place so
|
||||
we can potentially add tooz functionality in future to Glare. Right now
|
||||
there are troubles with locks in Galera (especially in mysql) and zookeeper
|
||||
requires additional work from IT engineers. So we need support production
|
||||
ready DB locks in our implementation.
|
||||
some user obtains a lock for the same artifact then other user cannot
|
||||
request that lock and gets a Conflict error.
|
||||
"""
|
||||
# NOTE(kairat): Lock Engine also allows to encapsulate lock logic in one
|
||||
# place so we can potentially add tooz functionality in future to Glare.
|
||||
# Right now there are troubles with locks in Galera (especially in mysql)
|
||||
# and zookeeper requires additional work from IT engineers. So we need
|
||||
# support production ready DB locks in our implementation.
|
||||
|
||||
MAX_LOCK_LENGTH = 255
|
||||
|
||||
def __init__(self, lock_api):
|
||||
"""Initialize lock engine with some lock api
|
||||
"""Initialize lock engine with some lock api.
|
||||
|
||||
:param lock_api: api that allows to create/delete locks. It must be
|
||||
db_api but it might be replaced with DLM in near future.
|
||||
:param lock_api: api that allows to create/delete locks
|
||||
"""
|
||||
# NOTE(kairat): lock_api is db_api now but it might be
|
||||
# replaced with DLM in near future.
|
||||
self.lock_api = lock_api
|
||||
|
||||
def acquire(self, context, lock_key):
|
||||
"""Acquire lock to update whole artifact
|
||||
"""Acquire lock for artifact.
|
||||
|
||||
Acquire lock to update artifact. If there is some other
|
||||
lock for the same artifact then raise Conflict Error.
|
||||
If there is some other lock with the same key then
|
||||
raise Conflict Error.
|
||||
|
||||
:param context: user context
|
||||
:param lock_key: lock key
|
||||
@@ -106,6 +106,10 @@ class LockEngine(object):
|
||||
return Lock(context, lock_id, lock_key, self.release)
|
||||
|
||||
def release(self, lock):
|
||||
"""Release lock for artifact.
|
||||
|
||||
:param lock: Lock object
|
||||
"""
|
||||
if lock.lock_id is not None:
|
||||
self.lock_api.delete_lock(lock.context, lock.lock_id)
|
||||
LOG.info(_LI("Lock %(lock_id)s released for lock_key %(key)s"),
|
||||
|
@@ -35,9 +35,7 @@ def set_defaults(control_exchange='glare'):
|
||||
|
||||
|
||||
class Notifier(object):
|
||||
"""Simple interface to receive Glare notifier
|
||||
|
||||
"""
|
||||
"""Simple interface to receive Glare notifier."""
|
||||
|
||||
SERVICE_NAME = 'artifact'
|
||||
GLARE_NOTIFIER = None
|
||||
@@ -52,7 +50,7 @@ class Notifier(object):
|
||||
|
||||
@classmethod
|
||||
def notify(cls, context, event_type, body, level='INFO'):
|
||||
"""Notify Glare listeners with some useful info
|
||||
"""Notify Glare listeners with some useful info.
|
||||
|
||||
:param context: User request context
|
||||
:param event_type: type of event
|
||||
|
@@ -63,7 +63,7 @@ class All(base.BaseArtifact):
|
||||
raise exception.Forbidden("This type is read only.")
|
||||
|
||||
@classmethod
|
||||
def update_blob(cls, context, af_id, values):
|
||||
def update_blob(cls, context, af_id, field_name, values):
|
||||
raise exception.Forbidden("This type is read only.")
|
||||
|
||||
@classmethod
|
||||
|
@@ -170,19 +170,19 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def is_blob(cls, field_name):
|
||||
"""Helper to check that field is blob
|
||||
"""Helper to check that a field is a blob.
|
||||
|
||||
:param field_name: name of field
|
||||
:return: True if field is a blob, False otherwise
|
||||
:param field_name: name of the field
|
||||
:return: True if the field is a blob, False otherwise
|
||||
"""
|
||||
return isinstance(cls.fields.get(field_name), glare_fields.BlobField)
|
||||
|
||||
@classmethod
|
||||
def is_blob_dict(cls, field_name):
|
||||
"""Helper to check that field is blob dict
|
||||
"""Helper to check that field is a blob dict.
|
||||
|
||||
:param field_name: name of field
|
||||
:return: True if field is a blob dict, False otherwise
|
||||
:param field_name: name of the field
|
||||
:return: True if the field is a blob dict, False otherwise
|
||||
"""
|
||||
return (isinstance(cls.fields.get(field_name), glare_fields.Dict) and
|
||||
cls.fields[field_name].element_type ==
|
||||
@@ -190,18 +190,18 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def is_link(cls, field_name):
|
||||
"""Helper to check that field is link
|
||||
"""Helper to check that a field is a link.
|
||||
|
||||
:param field_name: name of field
|
||||
:param field_name: name of the field
|
||||
:return: True if field is a link, False otherwise
|
||||
"""
|
||||
return isinstance(cls.fields.get(field_name), glare_fields.Link)
|
||||
|
||||
@classmethod
|
||||
def is_link_dict(cls, field_name):
|
||||
"""Helper to check that field is link dict
|
||||
"""Helper to check that a field is a link dict.
|
||||
|
||||
:param field_name: name of field
|
||||
:param field_name: name of the field
|
||||
:return: True if field is a link dict, False otherwise
|
||||
"""
|
||||
return (isinstance(cls.fields.get(field_name), glare_fields.Dict) and
|
||||
@@ -210,10 +210,10 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def is_link_list(cls, field_name):
|
||||
"""Helper to check that field is link list
|
||||
"""Helper to check that a field is a link list.
|
||||
|
||||
:param field_name: name of field
|
||||
:return: True if field is a link list, False otherwise
|
||||
:param field_name: name of the field
|
||||
:return: True if the field is a link list, False otherwise
|
||||
"""
|
||||
return (isinstance(cls.fields.get(field_name), glare_fields.List) and
|
||||
cls.fields[field_name].element_type ==
|
||||
@@ -221,11 +221,11 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def _init_artifact(cls, context, values):
|
||||
"""Initialize an empty versioned object with values
|
||||
"""Initialize an empty versioned object with values.
|
||||
|
||||
Initialize vo object with default values and values specified by user.
|
||||
Also reset all changes for initialized object so user of the method
|
||||
can track own changes.
|
||||
Also reset all changes of initialized object so user can track own
|
||||
changes.
|
||||
|
||||
:param context: user context
|
||||
:param values: values needs to be set
|
||||
@@ -239,31 +239,28 @@ class BaseArtifact(base.VersionedObject):
|
||||
default_attrs.append(attr)
|
||||
if default_attrs:
|
||||
af.obj_set_defaults(*default_attrs)
|
||||
|
||||
# apply values specified by user
|
||||
for name, value in six.iteritems(values):
|
||||
setattr(af, name, value)
|
||||
return af
|
||||
|
||||
@classmethod
|
||||
def get_type_name(cls):
|
||||
"""Return type name that allows to find Artifact Type in Glare
|
||||
"""Return type name that allows to find artifact type in Glare
|
||||
|
||||
Type name allows to find Artifact Type definition in Glare registry
|
||||
so Engine can instantiate Artifacts. Artifact also becomes available
|
||||
with artifact type in Glare API.
|
||||
For example, when get_type_name returns 'my_artifact' then
|
||||
users can list artifacts by GET <host_name>/v1/artifacts/my_artifact.
|
||||
This type name is also used in glare configuration when turning on/off
|
||||
specific Artifact Types.
|
||||
:return: string that identifies current Artifact Type.
|
||||
Type name allows to find artifact type definition in Glare registry.
|
||||
|
||||
:return: string that identifies current artifact type
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def _lock_version(cls, context, values):
|
||||
"""Calculate version scope for new artifact
|
||||
"""Create scope lock for artifact creation.
|
||||
|
||||
:param values: af values
|
||||
:return: string that identifies af version or None
|
||||
:param values: artifact values
|
||||
:return: Lock object
|
||||
"""
|
||||
name = values.get('name')
|
||||
version = values.get('version', cls.DEFAULT_ARTIFACT_VERSION)
|
||||
@@ -274,10 +271,15 @@ class BaseArtifact(base.VersionedObject):
|
||||
return cls.lock_engine.acquire(context, scope_id)
|
||||
|
||||
@classmethod
|
||||
def _lock_updated_version(cls, af, updates):
|
||||
name = updates.get('name', af.name)
|
||||
version = updates.get('version', af.version)
|
||||
visibility = updates.get('visibility', af.visibility)
|
||||
def _lock_updated_version(cls, af, values):
|
||||
"""Create scope lock for artifact update.
|
||||
|
||||
:param values: artifact values
|
||||
:return: Lock object
|
||||
"""
|
||||
name = values.get('name', af.name)
|
||||
version = values.get('version', af.version)
|
||||
visibility = values.get('visibility', af.visibility)
|
||||
scope_id = None
|
||||
if (name, version, visibility) != (af.name, af.version, af.visibility):
|
||||
# no version change == no lock for version
|
||||
@@ -289,11 +291,11 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def create(cls, context, values):
|
||||
"""Create new Artifact in Glare repo
|
||||
"""Create new artifact in Glare repo.
|
||||
|
||||
:param context: user context
|
||||
:param values: Dict with specified artifact properties
|
||||
:return: definition of create Artifact
|
||||
:param values: dictionary with specified artifact fields
|
||||
:return: created artifact object
|
||||
"""
|
||||
if context.tenant is None or context.read_only:
|
||||
msg = _("It's forbidden to anonymous users to create artifacts.")
|
||||
@@ -317,11 +319,18 @@ class BaseArtifact(base.VersionedObject):
|
||||
LOG.info(_LI("Parameters validation for artifact creation "
|
||||
"passed for request %s."), context.request_id)
|
||||
af_vals = cls.db_api.create(
|
||||
context, af.obj_changes_to_primitive(), cls.get_type_name())
|
||||
context, af._obj_changes_to_primitive(), cls.get_type_name())
|
||||
return cls._init_artifact(context, af_vals)
|
||||
|
||||
@classmethod
|
||||
def _validate_versioning(cls, context, name, version, is_public=False):
|
||||
"""Validate if artifact with given name and version already exists.
|
||||
|
||||
:param context: user context
|
||||
:param name: name of artifact to be checked
|
||||
:param version: version of artifact
|
||||
:param is_public: flag that indicates to search artifact globally
|
||||
"""
|
||||
if version is not None and name not in (None, ""):
|
||||
filters = [('name', name), ('version', version),
|
||||
('status', 'neq:deleted')]
|
||||
@@ -341,7 +350,7 @@ class BaseArtifact(base.VersionedObject):
|
||||
@classmethod
|
||||
def _validate_change_allowed(cls, field_names, af=None,
|
||||
validate_blob_names=True):
|
||||
"""Validate if fields can be updated in artifact"""
|
||||
"""Validate if fields can be updated in artifact."""
|
||||
af_status = cls.STATUS.DRAFTED if af is None else af.status
|
||||
if af_status not in (cls.STATUS.ACTIVE, cls.STATUS.DRAFTED):
|
||||
msg = _("Forbidden to change attributes "
|
||||
@@ -350,15 +359,15 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
for field_name in field_names:
|
||||
if field_name not in cls.fields:
|
||||
msg = _("%s property does not exist") % field_name
|
||||
msg = _("%s field does not exist") % field_name
|
||||
raise exception.BadRequest(msg)
|
||||
field = cls.fields[field_name]
|
||||
if field.system is True:
|
||||
msg = _("Cannot specify system property %s. It is not "
|
||||
msg = _("Cannot specify system field %s. It is not "
|
||||
"available for modifying by users.") % field_name
|
||||
raise exception.Forbidden(msg)
|
||||
if af_status == cls.STATUS.ACTIVE and not field.mutable:
|
||||
msg = (_("Forbidden to change property '%s' after activation.")
|
||||
msg = (_("Forbidden to change field '%s' after activation.")
|
||||
% field_name)
|
||||
raise exception.Forbidden(message=msg)
|
||||
if validate_blob_names and \
|
||||
@@ -369,12 +378,12 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def update(cls, context, af, values):
|
||||
"""Update Artifact in Glare repo
|
||||
"""Update artifact in Glare repo.
|
||||
|
||||
:param context: user Context
|
||||
:param af: current definition of Artifact in Glare
|
||||
:param context: user context
|
||||
:param af: current definition of artifact
|
||||
:param values: dictionary with changes for artifact
|
||||
:return: definition of updated Artifact
|
||||
:return: updated artifact object
|
||||
"""
|
||||
# reset all changes of artifact to reuse them after update
|
||||
af.obj_reset_changes()
|
||||
@@ -396,35 +405,39 @@ class BaseArtifact(base.VersionedObject):
|
||||
"update passed for request %(request)s."),
|
||||
{'artifact': af.id, 'request': context.request_id})
|
||||
updated_af = cls.db_api.update(
|
||||
context, af.id, af.obj_changes_to_primitive())
|
||||
context, af.id, af._obj_changes_to_primitive())
|
||||
return cls._init_artifact(context, updated_af)
|
||||
|
||||
@classmethod
|
||||
def get_action_for_updates(cls, context, artifact, updates, registry):
|
||||
"""The method defines how to detect appropriate action based on update
|
||||
def get_action_for_updates(cls, context, af, values, registry):
|
||||
"""Define the appropriate method for artifact update.
|
||||
|
||||
Validate request for update and determine if it is request for action.
|
||||
Also do a validation for request for action if it is an action.
|
||||
Based on update params this method defines what action engine should
|
||||
call for artifact update: activate, deactivate, reactivate, publish or
|
||||
just a regular update of artifact fields.
|
||||
|
||||
:return: action reference for updates dict
|
||||
:param context: user context
|
||||
:param af: current definition of artifact
|
||||
:param values: dictionary with changes for artifact
|
||||
:param registry: registry of enabled artifact types
|
||||
:return: method reference for updates dict
|
||||
"""
|
||||
action = cls.update
|
||||
if 'visibility' in updates:
|
||||
if 'visibility' in values:
|
||||
# validate publish action format
|
||||
action = cls.publish
|
||||
elif 'status' in updates:
|
||||
status = updates['status']
|
||||
return cls.publish
|
||||
elif 'status' in values:
|
||||
status = values['status']
|
||||
if status == cls.STATUS.DEACTIVATED:
|
||||
action = cls.deactivate
|
||||
return cls.deactivate
|
||||
elif status == cls.STATUS.ACTIVE:
|
||||
if artifact.status == artifact.STATUS.DEACTIVATED:
|
||||
action = cls.reactivate
|
||||
if af.status == af.STATUS.DEACTIVATED:
|
||||
return cls.reactivate
|
||||
else:
|
||||
action = cls.activate
|
||||
return cls.activate
|
||||
|
||||
# check updates for links and validate them
|
||||
try:
|
||||
for key, value in six.iteritems(updates):
|
||||
for key, value in six.iteritems(values):
|
||||
if cls.is_link(key) and value is not None:
|
||||
cls._validate_link(key, value, context, registry)
|
||||
elif cls.is_link_dict(key) and value:
|
||||
@@ -434,14 +447,11 @@ class BaseArtifact(base.VersionedObject):
|
||||
for l in value:
|
||||
cls._validate_link(key, l, context, registry)
|
||||
except Exception as e:
|
||||
msg = (_("Bad link in artifact %(af)s: %(msg)s")
|
||||
% {"af": artifact.id, "msg": str(e)})
|
||||
msg = (_("Broken link in artifact %(af)s: %(msg)s")
|
||||
% {"af": af.id, "msg": str(e)})
|
||||
raise exception.BadRequest(msg)
|
||||
|
||||
LOG.debug("Action %(action)s defined to updates %(updates)s.",
|
||||
{'action': action.__name__, 'updates': updates})
|
||||
|
||||
return action
|
||||
return cls.update
|
||||
|
||||
@classmethod
|
||||
def _validate_link(cls, key, value, context, registry):
|
||||
@@ -450,36 +460,29 @@ class BaseArtifact(base.VersionedObject):
|
||||
# check containment
|
||||
if glare_fields.LinkFieldType.is_external(value):
|
||||
# validate external link
|
||||
cls._validate_external_link(value)
|
||||
with urlrequest.urlopen(value) as data:
|
||||
data.read(1)
|
||||
else:
|
||||
type_name = (glare_fields.LinkFieldType.
|
||||
get_type_name(value))
|
||||
af_type = registry.get_artifact_type(type_name)
|
||||
cls._validate_soft_link(context, value, af_type)
|
||||
|
||||
@classmethod
|
||||
def _validate_external_link(cls, link):
|
||||
with urlrequest.urlopen(link) as data:
|
||||
data.read(1)
|
||||
|
||||
@classmethod
|
||||
def _validate_soft_link(cls, context, link, af_type):
|
||||
af_id = link.split('/')[3]
|
||||
af_type.get(context, af_id)
|
||||
af_id = value.split('/')[3]
|
||||
af_type.get(context, af_id)
|
||||
|
||||
@classmethod
|
||||
def get(cls, context, artifact_id):
|
||||
"""Return Artifact from Glare repo
|
||||
|
||||
:param context: user context
|
||||
:param artifact_id: id of requested Artifact
|
||||
:return: Artifact definition
|
||||
:param artifact_id: id of requested artifact
|
||||
:return: requested artifact object
|
||||
"""
|
||||
af = cls.db_api.get(context, artifact_id)
|
||||
return cls._init_artifact(context, af)
|
||||
|
||||
@classmethod
|
||||
def _get_field_type(cls, obj):
|
||||
"""Get string representation of field type for filters."""
|
||||
if isinstance(obj, fields.IntegerField) or obj is fields.Integer:
|
||||
return 'int'
|
||||
elif isinstance(obj, fields.FloatField) or obj is fields.Float:
|
||||
@@ -491,6 +494,7 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def _parse_sort_values(cls, sort):
|
||||
"""Prepare sorting parameters for database."""
|
||||
new_sort = []
|
||||
for key, direction in sort:
|
||||
if key not in cls.fields:
|
||||
@@ -504,12 +508,6 @@ class BaseArtifact(base.VersionedObject):
|
||||
cls.fields.get(key))))
|
||||
return new_sort
|
||||
|
||||
@classmethod
|
||||
def _validate_filter_name(cls, filter_name):
|
||||
if cls.fields.get(filter_name) is None:
|
||||
msg = _("Unable filter '%s'") % filter_name
|
||||
raise exception.BadRequest(msg)
|
||||
|
||||
@classmethod
|
||||
def _validate_filter_ops(cls, filter_name, op):
|
||||
field = cls.fields.get(filter_name)
|
||||
@@ -550,7 +548,10 @@ class BaseArtifact(base.VersionedObject):
|
||||
msg = _("Field %s is not Dict") % filter_name
|
||||
raise exception.BadRequest(msg)
|
||||
|
||||
cls._validate_filter_name(filter_name)
|
||||
if cls.fields.get(filter_name) is None:
|
||||
msg = _("Unable filter '%s'") % filter_name
|
||||
raise exception.BadRequest(msg)
|
||||
|
||||
field_type = cls.fields.get(filter_name)
|
||||
|
||||
if isinstance(field_type, glare_fields.List) or isinstance(
|
||||
@@ -581,16 +582,18 @@ class BaseArtifact(base.VersionedObject):
|
||||
@classmethod
|
||||
def list(cls, context, filters=None, marker=None, limit=None,
|
||||
sort=None, latest=False):
|
||||
"""List all available Artifacts in Glare repo
|
||||
"""Return list of artifacts requested by user.
|
||||
|
||||
:param context: user context
|
||||
:param filters: filtering conditions to Artifact list
|
||||
:param marker: id of Artifact that identifies where Glare should
|
||||
start listing Artifacts. So all Artifacts before that Artifact in
|
||||
resulting list must be ignored. It is useful for Artifact pagination.
|
||||
:param limit: maximum number of Artifact items in list.
|
||||
:param sort: sorting preferences when requesting Artifact list.
|
||||
:return: list of Artifacts
|
||||
:param filters: filters that need to be applied to artifact
|
||||
:param marker: the artifact that considered as begin of the list
|
||||
so all artifacts before marker (including marker itself) will not be
|
||||
added to artifact list
|
||||
:param limit: maximum number of items in the list
|
||||
:param sort: sorting options
|
||||
:param latest: flag that indicates, that only artifacts with highest
|
||||
versions should be returned in output
|
||||
:return: list of artifact objects
|
||||
"""
|
||||
if sort is not None:
|
||||
sort = cls._parse_sort_values(sort)
|
||||
@@ -635,14 +638,11 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def delete(cls, context, af):
|
||||
"""Delete Artifact and all blobs from Glare.
|
||||
"""Delete artifact and all its blobs from Glare.
|
||||
|
||||
:param context: user context
|
||||
:param af: definition of artifact targeted to delete
|
||||
:param af: artifact object targeted for deletion
|
||||
"""
|
||||
if af.visibility == 'public' and not context.is_admin:
|
||||
msg = _("Only admins are allowed to delete public artifacts")
|
||||
raise exception.Forbidden(msg)
|
||||
# marking artifact as deleted
|
||||
cls.db_api.update(context, af.id, {'status': cls.STATUS.DELETED})
|
||||
|
||||
@@ -669,17 +669,17 @@ class BaseArtifact(base.VersionedObject):
|
||||
# delete blobs one by one
|
||||
cls._delete_blobs(blobs, context, af)
|
||||
LOG.info(_LI("Blobs successfully deleted for artifact %s"), af.id)
|
||||
|
||||
# delete the artifact itself
|
||||
# delete artifact itself
|
||||
cls.db_api.delete(context, af.id)
|
||||
|
||||
@classmethod
|
||||
def activate(cls, context, af, values):
|
||||
"""Activate Artifact and make it available for users
|
||||
"""Activate artifact and make it available for usage.
|
||||
|
||||
:param context: User Context
|
||||
:param af: current Artifact definition in Glare
|
||||
:return: definition of activated Artifact
|
||||
:param context: user context
|
||||
:param af: current artifact object
|
||||
:param values: dictionary with changes for artifact
|
||||
:return: artifact object with changed status
|
||||
"""
|
||||
# validate that came to artifact as updates
|
||||
if values != {'status': cls.STATUS.ACTIVE}:
|
||||
@@ -700,16 +700,17 @@ class BaseArtifact(base.VersionedObject):
|
||||
LOG.info(_LI("Parameters validation for artifact %(artifact)s "
|
||||
"activate passed for request %(request)s."),
|
||||
{'artifact': af.id, 'request': context.request_id})
|
||||
active_af = cls.db_api.update(context, af.id, values)
|
||||
return cls._init_artifact(context, active_af)
|
||||
af = cls.db_api.update(context, af.id, {'status': cls.STATUS.ACTIVE})
|
||||
return cls._init_artifact(context, af)
|
||||
|
||||
@classmethod
|
||||
def reactivate(cls, context, af, values):
|
||||
"""Make Artifact active after de-activation
|
||||
"""Make Artifact active after deactivation
|
||||
|
||||
:param context: user context
|
||||
:param af: definition of de-activated Artifact
|
||||
:return: definition of active Artifact
|
||||
:param af: current artifact object
|
||||
:param values: dictionary with changes for artifact
|
||||
:return: artifact object with changed status
|
||||
"""
|
||||
# validate that came to artifact as updates
|
||||
if values != {'status': cls.STATUS.ACTIVE}:
|
||||
@@ -723,21 +724,23 @@ class BaseArtifact(base.VersionedObject):
|
||||
LOG.info(_LI("Parameters validation for artifact %(artifact)s "
|
||||
"reactivate passed for request %(request)s."),
|
||||
{'artifact': af.id, 'request': context.request_id})
|
||||
af = cls.db_api.update(context, af.id, values)
|
||||
af = cls.db_api.update(context, af.id, {'status': cls.STATUS.ACTIVE})
|
||||
return cls._init_artifact(context, af)
|
||||
|
||||
@classmethod
|
||||
def deactivate(cls, context, af, values):
|
||||
"""Deny Artifact downloading due to security concerns
|
||||
"""Deny Artifact downloading due to security concerns.
|
||||
|
||||
If user uploaded suspicious Artifact then Cloud Admins(or other users -
|
||||
it depends on policy configurations) can deny Artifact download by
|
||||
users by making Artifact de-activated. After additional investigation
|
||||
Artifact can be re-activated or deleted from Glare.
|
||||
If user uploaded suspicious artifact then administrators(or other
|
||||
users - it depends on policy configurations) can deny artifact data
|
||||
to be downloaded by regular users by making artifact deactivated.
|
||||
After additional investigation artifact can be reactivated or
|
||||
deleted from Glare.
|
||||
|
||||
:param context: user context
|
||||
:param af: Artifact definition in Glare
|
||||
:return: definition of de-activated Artifact
|
||||
:param af: current artifact object
|
||||
:param values: dictionary with changes for artifact
|
||||
:return: artifact object with changed status
|
||||
"""
|
||||
if values != {'status': cls.STATUS.DEACTIVATED}:
|
||||
msg = _("Only {'status': %s} is allowed in a request "
|
||||
@@ -751,16 +754,18 @@ class BaseArtifact(base.VersionedObject):
|
||||
LOG.info(_LI("Parameters validation for artifact %(artifact)s "
|
||||
"deactivate passed for request %(request)s."),
|
||||
{'artifact': af.id, 'request': context.request_id})
|
||||
af = cls.db_api.update(context, af.id, values)
|
||||
af = cls.db_api.update(context, af.id,
|
||||
{'status': cls.STATUS.DEACTIVATED})
|
||||
return cls._init_artifact(context, af)
|
||||
|
||||
@classmethod
|
||||
def publish(cls, context, af, values):
|
||||
"""Make Artifact available for everyone
|
||||
"""Make artifact available for all tenants.
|
||||
|
||||
:param context: user context
|
||||
:param af: definition of published Artifact
|
||||
:return: definition of active Artifact
|
||||
:param af: current artifact object
|
||||
:param values: dictionary with changes for artifact
|
||||
:return: artifact object with changed visibility
|
||||
"""
|
||||
if values != {'visibility': 'public'}:
|
||||
msg = _("Only {'visibility': 'public'} is allowed in a request "
|
||||
@@ -778,17 +783,27 @@ class BaseArtifact(base.VersionedObject):
|
||||
LOG.info(_LI("Parameters validation for artifact %(artifact)s "
|
||||
"publish passed for request %(request)s."),
|
||||
{'artifact': af.id, 'request': context.request_id})
|
||||
af = cls.db_api.update(context, af.id, values)
|
||||
af = cls.db_api.update(context, af.id, {'visibility': 'public'})
|
||||
return cls._init_artifact(context, af)
|
||||
|
||||
@classmethod
|
||||
def get_max_blob_size(cls, field_name):
|
||||
"""Get the maximum allowed blob size in bytes.
|
||||
|
||||
:param field_name: blob or blob dict field name
|
||||
:return: maximum blob size in bytes
|
||||
"""
|
||||
return getattr(cls.fields[field_name], 'max_blob_size',
|
||||
attribute.BlobAttribute.DEFAULT_MAX_BLOB_SIZE)
|
||||
|
||||
@classmethod
|
||||
def validate_upload_allowed(cls, context, af, field_name, blob_key=None):
|
||||
"""Validate if given blob is ready for uploading."""
|
||||
def validate_upload_allowed(cls, af, field_name, blob_key=None):
|
||||
"""Validate if given blob is ready for uploading.
|
||||
|
||||
:param af: current artifact object
|
||||
:param field_name: blob or blob dict field name
|
||||
:param blob_key: indicates key name if field_name is a blob dict
|
||||
"""
|
||||
|
||||
blob_name = "%s[%s]" % (field_name, blob_key)\
|
||||
if blob_key else field_name
|
||||
@@ -818,35 +833,40 @@ class BaseArtifact(base.VersionedObject):
|
||||
{'artifact': af.id, 'blob_name': blob_name})
|
||||
|
||||
@classmethod
|
||||
def update_blob(cls, context, af_id, values):
|
||||
"""Upload binary object as artifact property
|
||||
def update_blob(cls, context, af_id, field_name, values):
|
||||
"""Update blob info in database.
|
||||
|
||||
:param context: user context
|
||||
:param af_id: id of modified artifact
|
||||
:param field_name: blob or blob dict field name
|
||||
:param values: updated blob values
|
||||
:return updated Artifact definition in Glare
|
||||
:return updated artifact definition in Glare
|
||||
"""
|
||||
af_upd = cls.db_api.update_blob(context, af_id, values)
|
||||
af_upd = cls.db_api.update_blob(context, af_id, {field_name: values})
|
||||
return cls._init_artifact(context, af_upd)
|
||||
|
||||
@classmethod
|
||||
def validate_activate(cls, context, af, values=None):
|
||||
"""Validation hook for activation."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def validate_upload(cls, context, af, field_name, fd):
|
||||
"""Validation hook for uploading."""
|
||||
return fd, None
|
||||
|
||||
@classmethod
|
||||
def validate_publish(cls, context, af):
|
||||
"""Validation hook for publishing."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_default_store(cls, context, af, field_name, blob_key):
|
||||
"""Return a default store type for artifact type."""
|
||||
pass
|
||||
|
||||
def to_notification(self):
|
||||
"""Return notification body that can be send to listeners
|
||||
"""Return notification body that can be send to listeners.
|
||||
|
||||
:return: dict with notification information
|
||||
"""
|
||||
@@ -865,13 +885,13 @@ class BaseArtifact(base.VersionedObject):
|
||||
}
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert oslo versioned object to dictionary
|
||||
"""Convert oslo versioned object to dictionary.
|
||||
|
||||
:return: dict with field names and field values
|
||||
"""
|
||||
return self.obj_to_primitive()['versioned_object.data']
|
||||
|
||||
def obj_changes_to_primitive(self):
|
||||
def _obj_changes_to_primitive(self):
|
||||
changes = self.obj_get_changes()
|
||||
res = {}
|
||||
for key, val in six.iteritems(changes):
|
||||
@@ -882,7 +902,7 @@ class BaseArtifact(base.VersionedObject):
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def schema_attr(cls, attr, attr_name=''):
|
||||
def _schema_attr(cls, attr, attr_name=''):
|
||||
attr_type = utils.get_schema_type(attr)
|
||||
schema = {}
|
||||
|
||||
@@ -968,10 +988,11 @@ class BaseArtifact(base.VersionedObject):
|
||||
|
||||
@classmethod
|
||||
def gen_schemas(cls):
|
||||
"""Return json schema representation of the artifact type."""
|
||||
schemas_prop = {}
|
||||
for attr_name, attr in six.iteritems(cls.fields):
|
||||
schemas_prop[attr_name] = cls.schema_attr(attr,
|
||||
attr_name=attr_name)
|
||||
schemas_prop[attr_name] = cls._schema_attr(
|
||||
attr, attr_name=attr_name)
|
||||
schemas = {'properties': schemas_prop,
|
||||
'name': cls.get_type_name(),
|
||||
'version': cls.VERSION,
|
||||
|
@@ -29,7 +29,7 @@ class Attribute(object):
|
||||
def __init__(self, field_class, mutable=False, required_on_activate=True,
|
||||
system=False, validators=None, nullable=True, default=None,
|
||||
sortable=False, filter_ops=None, description=""):
|
||||
"""Init and validate attribute"""
|
||||
"""Init and validate attribute."""
|
||||
if not issubclass(field_class, fields.AutoTypedField):
|
||||
raise exc.IncorrectArtifactType(
|
||||
"Field class %s must be sub-class of AutoTypedField." %
|
||||
@@ -115,7 +115,7 @@ class Attribute(object):
|
||||
|
||||
@classmethod
|
||||
def init(cls, *args, **kwargs):
|
||||
"""Fabric to build attributes"""
|
||||
"""Fabric to build attributes."""
|
||||
return cls(*args, **kwargs).get_field()
|
||||
|
||||
|
||||
|
@@ -49,7 +49,7 @@ CONF.register_opts(registry_options)
|
||||
|
||||
|
||||
def import_submodules(module):
|
||||
"""Import all submodules of a module
|
||||
"""Import all submodules of a module.
|
||||
|
||||
:param module: Package name
|
||||
:return list of imported modules
|
||||
@@ -92,7 +92,7 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
|
||||
|
||||
@classmethod
|
||||
def register_all_artifacts(cls):
|
||||
"""Register all artifacts in glare"""
|
||||
"""Register all artifacts in Glare."""
|
||||
# get all submodules in glare.objects
|
||||
# please note that we registering trusted modules first
|
||||
# and applying custom modules after that to allow custom modules
|
||||
@@ -114,7 +114,7 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
|
||||
|
||||
@classmethod
|
||||
def get_artifact_type(cls, type_name):
|
||||
"""Return artifact type based on artifact type name
|
||||
"""Return artifact type based on artifact type name.
|
||||
|
||||
:param type_name: name of artifact type
|
||||
:return: artifact class
|
||||
@@ -126,5 +126,5 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
|
||||
|
||||
@classmethod
|
||||
def reset_registry(cls):
|
||||
"""Resets all registered artifact type classes"""
|
||||
"""Resets all registered artifact type classes."""
|
||||
cls._registry._obj_classes = collections.defaultdict(list)
|
||||
|
@@ -26,7 +26,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Validator(object):
|
||||
"""Common interface for all validators"""
|
||||
"""Common interface for all validators."""
|
||||
|
||||
def validate(self, value):
|
||||
raise NotImplementedError()
|
||||
|
@@ -12,7 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Glare WSGI module
|
||||
"""Glare WSGI module.
|
||||
|
||||
Use this module to deploy glare as WSGI application.
|
||||
|
||||
|
Reference in New Issue
Block a user