Update docstrings

Change-Id: I2be4437a5afb221f56f4d93a24be0356e7abc033
This commit is contained in:
Mike Fedosin
2017-02-14 23:43:28 +03:00
parent 19a1260a94
commit 3b45faece9
22 changed files with 357 additions and 293 deletions

View File

@@ -67,7 +67,7 @@ class ContextMiddleware(base_middleware.ConfigurableMiddleware):
@staticmethod @staticmethod
def process_request(req): 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 Generate a RequestContext object from the available
authentication headers and store on the 'context' attribute authentication headers and store on the 'context' attribute
@@ -88,7 +88,7 @@ class ContextMiddleware(base_middleware.ConfigurableMiddleware):
@staticmethod @staticmethod
def _get_anonymous_context(): 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) return RequestContext(read_only=True, is_admin=False)
@staticmethod @staticmethod

View File

@@ -16,7 +16,7 @@
""" """
A filter middleware that inspects the requested URI for a version string A filter middleware that inspects the requested URI for a version string
and/or Accept headers and attempts to negotiate an API controller to and/or Accept headers and attempts to negotiate an API controller to
return return.
""" """
import microversion_parse import microversion_parse
@@ -32,7 +32,7 @@ LOG = logging.getLogger(__name__)
def get_version_from_accept(accept_header, vnd_mime_type): 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 :param accept_header: accept header
:return: version string in the request or None if not specified :return: version string in the request or None if not specified

View File

@@ -40,7 +40,7 @@ class APIVersionRequest(object):
"""Create an API version request object. """Create an API version request object.
:param version_string: String representation of APIVersionRequest. :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) match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$", version_string)
if match: if match:
@@ -95,9 +95,9 @@ class APIVersionRequest(object):
greater than or equal to the minimum version and less than greater than or equal to the minimum version and less than
or equal to the maximum version. or equal to the maximum version.
@param min_version: Minimum acceptable version. :param min_version: Minimum acceptable version.
@param max_version: Maximum acceptable version. :param max_version: Maximum acceptable version.
@returns: boolean :returns: boolean
""" """
return min_version <= self <= max_version return min_version <= self <= max_version
@@ -109,15 +109,15 @@ class APIVersionRequest(object):
@classmethod @classmethod
def min_version(cls): def min_version(cls):
"""Minimal allowed api version""" """Minimal allowed api version."""
return APIVersionRequest(cls._MIN_API_VERSION) return APIVersionRequest(cls._MIN_API_VERSION)
@classmethod @classmethod
def max_version(cls): def max_version(cls):
"""Maximal allowed api version""" """Maximal allowed api version."""
return APIVersionRequest(cls._MAX_API_VERSION) return APIVersionRequest(cls._MAX_API_VERSION)
@classmethod @classmethod
def default_version(cls): 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) return APIVersionRequest(cls._DEFAULT_API_VERSION)

View File

@@ -22,13 +22,14 @@ from glare.i18n import _
class VersionedMethod(object): class VersionedMethod(object):
def __init__(self, name, start_version, end_version, func): 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 name: Name of the method
:param start_version: Minimum acceptable version :param start_version: Minimum acceptable version
:param end_version: Maximum acceptable_version :param end_version: Maximum acceptable_version
:param func: Method to call :param func: Method to call
Minimum and maximums are inclusive
""" """
# NOTE(kairat): minimums and maximums are inclusive
self.name = name self.name = name
self.start_version = start_version self.start_version = start_version
self.end_version = end_version self.end_version = end_version
@@ -41,7 +42,7 @@ class VersionedMethod(object):
class VersionedResource(object): class VersionedResource(object):
"""Versioned mixin that provides ability to define versioned methods and """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 # prefix for all versioned methods in class
@@ -52,6 +53,7 @@ class VersionedResource(object):
"""Determines whether function list contains version intervals """Determines whether function list contains version intervals
intersections or not. General algorithm: intersections or not. General algorithm:
https://en.wikipedia.org/wiki/Intersection_algorithm https://en.wikipedia.org/wiki/Intersection_algorithm
:param func_list: list of VersionedMethod objects :param func_list: list of VersionedMethod objects
:return: boolean :return: boolean
""" """
@@ -128,9 +130,10 @@ class VersionedResource(object):
def version_select(*args, **kwargs): def version_select(*args, **kwargs):
"""Look for the method which matches the name supplied and version """Look for the method which matches the name supplied and version
constraints and calls it with the supplied arguments. constraints and calls it with the supplied arguments.
:returns: Returns the result of the method called :returns: Returns the result of the method called
:raises: VersionNotFoundForAPIMethod if there is no method which :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 # versioning is used in 3 classes: request deserializer and
# controller have request as first argument # controller have request as first argument

View File

@@ -50,11 +50,12 @@ supported_versions = api_versioning.VersionedResource.supported_versions
class RequestDeserializer(api_versioning.VersionedResource, class RequestDeserializer(api_versioning.VersionedResource,
wsgi.JSONRequestDeserializer): wsgi.JSONRequestDeserializer):
"""Glare deserializer for incoming webop Requests. """Glare deserializer for incoming webob requests.
Deserializer converts incoming request into bunch of python primitives.
So other components doesn't work with requests at all. Deserializer also Deserializer checks and converts incoming request into a bunch of Glare
executes primary API validation without any knowledge about Artifact primitives. So other service components don't work with requests at all.
structure. Deserializer also performs primary API validation without any knowledge
about concrete artifact type structure.
""" """
@staticmethod @staticmethod
@@ -74,6 +75,7 @@ class RequestDeserializer(api_versioning.VersionedResource,
return content_type return content_type
def _get_request_body(self, req): def _get_request_body(self, req):
"""Get request json body and convert it to python structures."""
return self.from_json(req.body) return self.from_json(req.body)
@supported_versions(min_ver='1.0') @supported_versions(min_ver='1.0')
@@ -137,7 +139,7 @@ class RequestDeserializer(api_versioning.VersionedResource,
patch = jsonpatch.JsonPatch(body) patch = jsonpatch.JsonPatch(body)
try: try:
# Initially patch object doesn't validate input. It's only checked # 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)) tuple(map(patch._get_operation, patch.patch))
except (jsonpatch.InvalidJsonPatch, TypeError): except (jsonpatch.InvalidJsonPatch, TypeError):
msg = _("Json Patch body is malformed") msg = _("Json Patch body is malformed")
@@ -152,7 +154,7 @@ class RequestDeserializer(api_versioning.VersionedResource,
data = self._get_request_body(req) data = self._get_request_body(req)
if 'url' not in data: if 'url' not in data:
msg = _("url is required when specifying external location. " 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) raise exc.BadRequest(msg)
else: else:
data = req.body_file data = req.body_file
@@ -176,10 +178,10 @@ def log_request_progress(f):
class ArtifactsController(api_versioning.VersionedResource): class ArtifactsController(api_versioning.VersionedResource):
"""API controller for Glare Artifacts. """API controller for Glare Artifacts.
Artifact Controller prepares incoming data for Glare Engine and redirects Artifact Controller prepares incoming data for Glare Engine and redirects
data to appropriate engine method (so only controller is working with data to the appropriate engine method. Once the response data is returned
Engine. Once the data returned from Engine Controller returns data from the engine Controller passes it next to Response Serializer.
in appropriate format for Response Serializer.
""" """
def __init__(self): def __init__(self):
@@ -188,12 +190,21 @@ class ArtifactsController(api_versioning.VersionedResource):
@supported_versions(min_ver='1.0') @supported_versions(min_ver='1.0')
@log_request_progress @log_request_progress
def list_type_schemas(self, req): 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) type_schemas = self.engine.list_type_schemas(req.context)
return type_schemas return type_schemas
@supported_versions(min_ver='1.0') @supported_versions(min_ver='1.0')
@log_request_progress @log_request_progress
def show_type_schema(self, req, type_name): 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) type_schema = self.engine.show_type_schema(req.context, type_name)
return {type_name: type_schema} return {type_name: type_schema}
@@ -202,10 +213,10 @@ class ArtifactsController(api_versioning.VersionedResource):
def create(self, req, type_name, values): def create(self, req, type_name, values):
"""Create artifact record in Glare. """Create artifact record in Glare.
:param req: User request :param req: user request
:param type_name: Artifact type name :param type_name: artifact type name
:param values: dict with artifact fields {field_name: field_value} :param values: dict with artifact fields
:return definition of created artifact :return: definition of created artifact
""" """
return self.engine.create(req.context, type_name, values) 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 type_name: Artifact type name
:param artifact_id: id of artifact to update :param artifact_id: id of artifact to update
:param patch: json patch with artifact changes :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) return self.engine.update(req.context, type_name, artifact_id, patch)
@supported_versions(min_ver='1.0') @supported_versions(min_ver='1.0')
@log_request_progress @log_request_progress
def delete(self, req, type_name, artifact_id): def delete(self, req, type_name, artifact_id):
"""Delete artifact from Glare """Delete artifact from Glare.
:param req: User request :param req: User request
:param type_name: Artifact type name :param type_name: Artifact type name
@@ -236,7 +247,7 @@ class ArtifactsController(api_versioning.VersionedResource):
@supported_versions(min_ver='1.0') @supported_versions(min_ver='1.0')
@log_request_progress @log_request_progress
def show(self, req, type_name, artifact_id): def show(self, req, type_name, artifact_id):
"""Show detailed artifact info """Show detailed artifact info.
:param req: User request :param req: User request
:param type_name: Artifact type name :param type_name: Artifact type name
@@ -249,7 +260,7 @@ class ArtifactsController(api_versioning.VersionedResource):
@log_request_progress @log_request_progress
def list(self, req, type_name, filters, marker=None, limit=None, def list(self, req, type_name, filters, marker=None, limit=None,
sort=None, latest=False): sort=None, latest=False):
"""List available artifacts """List available artifacts.
:param req: User request :param req: User request
:param type_name: Artifact type name :param type_name: Artifact type name
@@ -261,7 +272,7 @@ class ArtifactsController(api_versioning.VersionedResource):
:param sort: sorting options :param sort: sorting options
:param latest: flag that indicates, that only artifacts with highest :param latest: flag that indicates, that only artifacts with highest
versions should be returned in output 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, artifacts = self.engine.list(req.context, type_name, filters, marker,
limit, sort, latest) limit, sort, latest)
@@ -273,18 +284,19 @@ class ArtifactsController(api_versioning.VersionedResource):
@supported_versions(min_ver='1.0') @supported_versions(min_ver='1.0')
@log_request_progress @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): content_type):
"""Upload blob into Glare repo """Upload blob into Glare repo.
:param req: User request :param req: User request
:param type_name: Artifact type name :param type_name: Artifact type name
:param artifact_id: id of Artifact to reactivate :param artifact_id: id of artifact where to perform upload
:param blob_name: name of blob field in artifact :param blob_path: path to artifact blob
:param data: Artifact payload :param data: blob payload
:param content_type: data content-type :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: if not blob_key:
blob_key = None blob_key = None
if content_type == ('application/vnd+openstack.glare-custom-location' if content_type == ('application/vnd+openstack.glare-custom-location'
@@ -300,16 +312,16 @@ class ArtifactsController(api_versioning.VersionedResource):
@supported_versions(min_ver='1.0') @supported_versions(min_ver='1.0')
@log_request_progress @log_request_progress
def download_blob(self, req, type_name, artifact_id, blob_name): def download_blob(self, req, type_name, artifact_id, blob_path):
"""Download blob data from Artifact """Download blob data from Artifact.
:param req: User request :param req: User request
:param type_name: Artifact type name :param type_name: artifact type name
:param artifact_id: id of Artifact to reactivate :param artifact_id: id of artifact from where to perform download
:param blob_name: name of blob field in artifact :param blob_path: path to artifact blob
:return: iterator that returns blob data :return: requested blob data
""" """
field_name, _sep, blob_key = blob_name.partition('/') field_name, _sep, blob_key = blob_path.partition('/')
if not blob_key: if not blob_key:
blob_key = None blob_key = None
data, meta = self.engine.download_blob( data, meta = self.engine.download_blob(
@@ -320,10 +332,10 @@ class ArtifactsController(api_versioning.VersionedResource):
class ResponseSerializer(api_versioning.VersionedResource, class ResponseSerializer(api_versioning.VersionedResource,
wsgi.JSONResponseSerializer): wsgi.JSONResponseSerializer):
"""Glare Response Serializer converts data received from Glare Engine """Glare serializer for outgoing responses.
(it consists from plain data types - dict, int, string, file descriptors,
etc) to WSGI Requests. It also specifies proper response status and Converts data received from the engine to WSGI responses. It also
content type as specified by API design. specifies proper response status and content type as declared in the API.
""" """
@staticmethod @staticmethod
@@ -425,7 +437,7 @@ class ResponseSerializer(api_versioning.VersionedResource,
def create_resource(): def create_resource():
"""Artifact resource factory method""" """Artifact resource factory method."""
deserializer = RequestDeserializer() deserializer = RequestDeserializer()
serializer = ResponseSerializer() serializer = ResponseSerializer()
controller = ArtifactsController() controller = ArtifactsController()

View File

@@ -83,16 +83,16 @@ class API(wsgi.Router):
allowed_methods='GET, PATCH, DELETE') allowed_methods='GET, PATCH, DELETE')
# ---blobs--- # ---blobs---
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_name:.*?}', mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_path:.*?}',
controller=glare_resource, controller=glare_resource,
action='download_blob', action='download_blob',
conditions={'method': ['GET']}, conditions={'method': ['GET']},
body_reject=True) body_reject=True)
mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_name:.*?}', mapper.connect('/artifacts/{type_name}/{artifact_id}/{blob_path:.*?}',
controller=glare_resource, controller=glare_resource,
action='upload_blob', action='upload_blob',
conditions={'method': ['PUT']}) 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, controller=reject_method_resource,
action='reject', action='reject',
allowed_methods='GET, PUT') allowed_methods='GET, PUT')

View File

@@ -60,6 +60,7 @@ class Controller(object):
@staticmethod @staticmethod
def index(req, is_multi): def index(req, is_multi):
"""Respond to a request for all OpenStack API versions. """Respond to a request for all OpenStack API versions.
:param is_multi: defines if multiple choices should be response status :param is_multi: defines if multiple choices should be response status
or not or not
:param req: user request object :param req: user request object

View File

@@ -16,7 +16,7 @@
""" """
Glare (Glare Artifact Repository) API service Glare (Glare Artifact Repository) API service.
""" """
import os import os

View File

@@ -14,7 +14,7 @@
# under the License. # under the License.
""" """
Routines for configuring Glare Routines for configuring Glare.
""" """
import logging.config import logging.config

View File

@@ -24,7 +24,7 @@ LOG = logging.getLogger(__name__)
class GlareException(Exception): class GlareException(Exception):
""" """
Base Glare Exception Base Glare Exception class.
To correctly use this class, inherit from it and define To correctly use this class, inherit from it and define
a 'message' property. That message will get printf'd a 'message' property. That message will get printf'd

View File

@@ -97,7 +97,7 @@ def reset():
def authorize(policy_name, target, context, do_raise=True): 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 policy_name: policy name
:param target: :param target:

View File

@@ -53,7 +53,7 @@ error_map = [{'catch': store_exc.NotFound,
@utils.error_handler(error_map) @utils.error_handler(error_map)
def save_blob_to_store(blob_id, blob, context, max_size, def save_blob_to_store(blob_id, blob, context, max_size,
store_type=None, verifier=None): 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 store_type: type of the store, None means save to default store.
:param blob_id: id of artifact :param blob_id: id of artifact
@@ -84,7 +84,7 @@ def load_from_store(uri, context):
@utils.error_handler(error_map) @utils.error_handler(error_map)
def delete_blob(uri, context): def delete_blob(uri, context):
"""Delete blob from backend store """Delete blob from backend store.
:param uri: blob uri :param uri: blob uri
:param context: user context :param context: user context

View File

@@ -68,7 +68,7 @@ def chunkreadable(iter, chunk_size=65536):
def chunkiter(fp, 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 fp: a file-like object
:param chunk_size: maximum size of chunk :param chunk_size: maximum size of chunk
@@ -361,7 +361,7 @@ except re.error:
def no_4byte_params(f): def no_4byte_params(f):
""" """
Checks that no 4 byte unicode characters are allowed 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): def wrapper(*args, **kwargs):
@@ -423,8 +423,7 @@ def split_filter_op(expression):
When no operator is found, default to an equality comparison. When no operator is found, default to an equality comparison.
:param expression: the expression to parse :param expression: the expression to parse
:return: a tuple (operator, threshold) parsed from expression
:returns: a tuple (operator, threshold) parsed from expression
""" """
left, sep, right = expression.partition(':') left, sep, right = expression.partition(':')
if sep: 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 value: evaluated against the operator, as left side of expression
:param operator: any supported filter operation :param operator: any supported filter operation
:param threshold: to compare value against, as right side of expression :param threshold: to compare value against, as right side of expression
:raises: InvalidFilterOperatorValue if an unknown operator is provided :raises: InvalidFilterOperatorValue if an unknown operator is provided
:return: boolean result of applied comparison
:returns: boolean result of applied comparison
""" """
if operator == 'gt': if operator == 'gt':
return value > threshold return value > threshold
@@ -525,21 +521,25 @@ def evaluate_filter_op(value, operator, threshold):
class error_handler(object): class error_handler(object):
def __init__(self, error_map, default_exception=None): 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.error_map = error_map
self.default_exception = default_exception self.default_exception = default_exception
def __call__(self, f): def __call__(self, f):
"""Decorator that catches exception that came from func or method """Decorator that catches exception that came from func or method.
:param f: targer func
:param error_map: dict of exception that can be raised :param f: target func
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
""" """
def new_function(*args, **kwargs): def new_function(*args, **kwargs):

View File

@@ -39,15 +39,16 @@ class Engine(object):
"""Engine is responsible for executing different helper operations when """Engine is responsible for executing different helper operations when
processing incoming requests from Glare API. processing incoming requests from Glare API.
Engine receives incoming data and does the following: Engine receives incoming data and does the following:
- check basic policy permissions - check basic policy permissions;
- requests artifact definition from registry - requests artifact definition from artifact type registry;
- check access permission(ro, rw) - check access permission(ro, rw);
- lock artifact for update if needed - lock artifact for update if needed;
- pass data to base artifact to execute all business logic operations - pass data to base artifact to execute all business logic operations
with database;
- notify other users about finished operation. - notify other users about finished operation.
Engine should not include any business logic and validation related Engine should not include any business logic and validation related
to Artifacts. Engine should not know any internal details of Artifacts to Artifacts. Engine should not know any internal details of artifact
because it controls access to Artifacts in common. type, because this part of the work is done by Base artifact type.
""" """
def __init__(self): def __init__(self):
# register all artifact types # register all artifact types
@@ -69,28 +70,32 @@ class Engine(object):
@classmethod @classmethod
def _get_artifact(cls, context, type_name, artifact_id, def _get_artifact(cls, context, type_name, artifact_id,
read_only=False): 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 :param context: user context
access permissions and policies for artifact. :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): 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 ctx: user context
:param af: artifact definition :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: if not ctx.is_admin and ctx.tenant != af.owner or ctx.read_only:
raise exception.Forbidden() raise exception.Forbidden()
def _check_read_only_access(ctx, af): 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 ctx: user context
:param af: artifact definition :param af: artifact definition
:raise Forbidden if access is not allowed :raise: Forbidden if access is not allowed
""" """
private = af.visibility != 'public' private = af.visibility != 'public'
if (private and if (private and
@@ -125,13 +130,19 @@ class Engine(object):
return schemas[type_name] return schemas[type_name]
@classmethod @classmethod
def create(cls, context, type_name, field_values): def create(cls, context, type_name, values):
"""Create new artifact in Glare""" """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" 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) artifact_type = registry.ArtifactRegistry.get_artifact_type(type_name)
# acquire version lock and execute artifact create # acquire version lock and execute artifact create
af = artifact_type.create(context, field_values) af = artifact_type.create(context, values)
# notify about new artifact # notify about new artifact
Notifier.notify(context, action_name, af) Notifier.notify(context, action_name, af)
# return artifact to the user # return artifact to the user
@@ -142,24 +153,23 @@ class Engine(object):
"""Update artifact with json patch. """Update artifact with json patch.
Apply patch to artifact and validate artifact before updating it Apply patch to artifact and validate artifact before updating it
in database. If there is request for visibility change or custom in database. If there is request for visibility or status change
location change then call specific method for that. then call specific method for that.
:param context: user context :param context: user context
:param type_name: name of artifact type :param type_name: name of artifact type
:param artifact_id: id of the artifact to be updated :param artifact_id: id of the artifact to be updated
:param patch: json patch :param patch: json patch object
:return: updated artifact :return: dict representation of updated artifact
""" """
def get_updates(af_dict, patch_with_upd): def _get_updates(af_dict, patch_with_upd):
"""Get updated values for artifact and json patch """Get updated values for artifact and json patch.
:param af_dict: current artifact definition as dict :param af_dict: current artifact definition as dict
:param patch_with_upd: json-patch :param patch_with_upd: json-patch object
:return: dict of updated attributes and their values :return dict of updated attributes and their values
""" """
try: try:
af_dict_patched = patch_with_upd.apply(af_dict) af_dict_patched = patch_with_upd.apply(af_dict)
diff = utils.DictDiffer(af_dict_patched, 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): with base.BaseArtifact.lock_engine.acquire(context, lock_key):
artifact = cls._get_artifact(context, type_name, artifact_id) artifact = cls._get_artifact(context, type_name, artifact_id)
af_dict = artifact.to_dict() 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 " LOG.debug("Update diff successfully calculated for artifact "
"%(af)s %(diff)s", {'af': artifact_id, 'diff': updates}) "%(af)s %(diff)s", {'af': artifact_id, 'diff': updates})
@@ -196,6 +206,8 @@ class Engine(object):
else: else:
action = artifact.get_action_for_updates( action = artifact.get_action_for_updates(
context, artifact, updates, registry.ArtifactRegistry) 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__ action_name = "artifact:%s" % action.__name__
policy.authorize(action_name, af_dict, context) policy.authorize(action_name, af_dict, context)
modified_af = action(context, artifact, updates) modified_af = action(context, artifact, updates)
@@ -204,7 +216,13 @@ class Engine(object):
@classmethod @classmethod
def get(cls, context, type_name, artifact_id): 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) policy.authorize("artifact:get", {}, context)
af = cls._get_artifact(context, type_name, artifact_id, af = cls._get_artifact(context, type_name, artifact_id,
read_only=True) read_only=True)
@@ -225,7 +243,7 @@ class Engine(object):
:param sort: sorting options :param sort: sorting options
:param latest: flag that indicates, that only artifacts with highest :param latest: flag that indicates, that only artifacts with highest
versions should be returned in output versions should be returned in output
:return: list of artifacts :return: list of artifact definitions
""" """
policy.authorize("artifact:list", {}, context) policy.authorize("artifact:list", {}, context)
artifact_type = registry.ArtifactRegistry.get_artifact_type(type_name) artifact_type = registry.ArtifactRegistry.get_artifact_type(type_name)
@@ -237,7 +255,12 @@ class Engine(object):
@classmethod @classmethod
def delete(cls, context, type_name, artifact_id): 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) af = cls._get_artifact(context, type_name, artifact_id)
policy.authorize("artifact:delete", af.to_dict(), context) policy.authorize("artifact:delete", af.to_dict(), context)
af.delete(context, af) af.delete(context, af)
@@ -254,9 +277,9 @@ class Engine(object):
:param field_name: name of blob or blob dict field :param field_name: name of blob or blob dict field
:param location: external blob url :param location: external blob url
:param blob_meta: dictionary containing blob metadata like md5 checksum :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 in this dict
:return updated artifact :return: dict representation of updated artifact
""" """
af = cls._get_artifact(context, type_name, artifact_id) af = cls._get_artifact(context, type_name, artifact_id)
action_name = 'artifact:set_location' action_name = 'artifact:set_location'
@@ -299,11 +322,10 @@ class Engine(object):
:param artifact_id: id of the artifact to be updated :param artifact_id: id of the artifact to be updated
:param field_name: name of blob or blob dict field :param field_name: name of blob or blob dict field
:param fd: file descriptor that Glare uses to upload the file :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 content_type: data content-type
: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 in this dictionary
:return file iterator for requested file :return: dict representation of updated artifact
""" """
af = cls._get_artifact(context, type_name, artifact_id) af = cls._get_artifact(context, type_name, artifact_id)
action_name = "artifact:upload" action_name = "artifact:upload"
@@ -348,12 +370,12 @@ class Engine(object):
# if upload failed remove blob from db and storage # if upload failed remove blob from db and storage
with excutils.save_and_reraise_exception(logger=LOG): with excutils.save_and_reraise_exception(logger=LOG):
if blob_key is None: if blob_key is None:
af.update_blob(context, af.id, {field_name: None}) af.update_blob(context, af.id, field_name, None)
else: else:
blob_dict_attr = modified_af[field_name] blob_dict_attr = modified_af[field_name]
del blob_dict_attr[blob_key] del blob_dict_attr[blob_key]
af.update_blob(context, af.id, af.update_blob(context, af.id,
{field_name: blob_dict_attr}) field_name, blob_dict_attr)
blob_name = "%s[%s]" % (field_name, blob_key) \ blob_name = "%s[%s]" % (field_name, blob_key) \
if blob_key else field_name if blob_key else field_name
LOG.info(_LI("Successfully finished blob upload for artifact " 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, def update_blob(cls, context, type_name, artifact_id, blob,
field_name, blob_key=None, validate=False): field_name, blob_key=None, validate=False):
"""Update blob info. """Update blob info.
:param context: user context :param context: user context
:param type_name: name of artifact type :param type_name: name of artifact type
:param artifact_id: id of the artifact to be updated :param artifact_id: id of the artifact to be updated
:param blob: blob representation in dict format :param blob: blob representation in dict format
:param field_name: name of blob or blob dict field :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 in this dict
:param validate: enable validation of possibility of blob uploading :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) lock_key = "%s:%s" % (type_name, artifact_id)
with base.BaseArtifact.lock_engine.acquire(context, lock_key): with base.BaseArtifact.lock_engine.acquire(context, lock_key):
af = cls._get_artifact(context, type_name, artifact_id) af = cls._get_artifact(context, type_name, artifact_id)
if validate: 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: if blob_key is None:
setattr(af, field_name, blob) setattr(af, field_name, blob)
return af.update_blob( return af.update_blob(
context, af.id, {field_name: getattr(af, field_name)}) context, af.id, field_name, getattr(af, field_name))
else: else:
blob_dict_attr = getattr(af, field_name) blob_dict_attr = getattr(af, field_name)
blob_dict_attr[blob_key] = blob blob_dict_attr[blob_key] = blob
return af.update_blob( return af.update_blob(
context, af.id, {field_name: blob_dict_attr}) context, af.id, field_name, blob_dict_attr)
@classmethod @classmethod
def download_blob(cls, context, type_name, artifact_id, field_name, def download_blob(cls, context, type_name, artifact_id, field_name,
blob_key=None): blob_key=None):
"""Download binary data from Glare Artifact. """Download binary data from Glare Artifact.
:param context: user context :param context: user context
:param type_name: name of artifact type :param type_name: name of artifact type
:param artifact_id: id of the artifact to be updated :param artifact_id: id of the artifact to be updated
:param field_name: name of blob or blob dict field :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 in this dict
:return: file iterator for requested file :return: file iterator for requested file
""" """

View File

@@ -20,11 +20,10 @@ LOG = logging.getLogger(__name__)
class LockApiBase(object): 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): def create_lock(self, context, lock_key):
"""Acquire lock for current user """Acquire lock for current user.
:param context user context :param context user context
:param lock_key: unique lock identifier that defines lock scope :param lock_key: unique lock identifier that defines lock scope
@@ -33,22 +32,21 @@ class LockApiBase(object):
raise NotImplementedError() raise NotImplementedError()
def delete_lock(self, context, lock_id): def delete_lock(self, context, lock_id):
"""Delete acquired user lock """Delete acquired user lock.
:param context: user context :param context: user context
:param lock_id: lock internal identifier :param lock_id: lock internal identifier
:return:
""" """
raise NotImplementedError() raise NotImplementedError()
class Lock(object): class Lock(object):
"""Object that stores lock context for users. This class is internal """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): def __init__(self, context, lock_id, lock_key, release_method):
"""Initialize lock context""" """Initialize lock context."""
self.context = context self.context = context
self.lock_id = lock_id self.lock_id = lock_id
self.lock_key = lock_key self.lock_key = lock_key
@@ -64,31 +62,33 @@ class Lock(object):
class LockEngine(object): class LockEngine(object):
"""Glare lock engine. """Glare lock engine.
Defines how artifact updates must be synchronized with each other. When 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 some user obtains a lock for the same artifact then other user cannot
request that lock and get Conflict error. request that lock and gets a 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.
""" """
# 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 MAX_LOCK_LENGTH = 255
def __init__(self, lock_api): 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 :param lock_api: api that allows to create/delete locks
db_api but it might be replaced with DLM in near future.
""" """
# NOTE(kairat): lock_api is db_api now but it might be
# replaced with DLM in near future.
self.lock_api = lock_api self.lock_api = lock_api
def acquire(self, context, lock_key): 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 If there is some other lock with the same key then
lock for the same artifact then raise Conflict Error. raise Conflict Error.
:param context: user context :param context: user context
:param lock_key: lock key :param lock_key: lock key
@@ -106,6 +106,10 @@ class LockEngine(object):
return Lock(context, lock_id, lock_key, self.release) return Lock(context, lock_id, lock_key, self.release)
def release(self, lock): def release(self, lock):
"""Release lock for artifact.
:param lock: Lock object
"""
if lock.lock_id is not None: if lock.lock_id is not None:
self.lock_api.delete_lock(lock.context, lock.lock_id) self.lock_api.delete_lock(lock.context, lock.lock_id)
LOG.info(_LI("Lock %(lock_id)s released for lock_key %(key)s"), LOG.info(_LI("Lock %(lock_id)s released for lock_key %(key)s"),

View File

@@ -35,9 +35,7 @@ def set_defaults(control_exchange='glare'):
class Notifier(object): class Notifier(object):
"""Simple interface to receive Glare notifier """Simple interface to receive Glare notifier."""
"""
SERVICE_NAME = 'artifact' SERVICE_NAME = 'artifact'
GLARE_NOTIFIER = None GLARE_NOTIFIER = None
@@ -52,7 +50,7 @@ class Notifier(object):
@classmethod @classmethod
def notify(cls, context, event_type, body, level='INFO'): 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 context: User request context
:param event_type: type of event :param event_type: type of event

View File

@@ -63,7 +63,7 @@ class All(base.BaseArtifact):
raise exception.Forbidden("This type is read only.") raise exception.Forbidden("This type is read only.")
@classmethod @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.") raise exception.Forbidden("This type is read only.")
@classmethod @classmethod

View File

@@ -170,19 +170,19 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def is_blob(cls, field_name): 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 :param field_name: name of the field
:return: True if field is a blob, False otherwise :return: True if the field is a blob, False otherwise
""" """
return isinstance(cls.fields.get(field_name), glare_fields.BlobField) return isinstance(cls.fields.get(field_name), glare_fields.BlobField)
@classmethod @classmethod
def is_blob_dict(cls, field_name): 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 :param field_name: name of the field
:return: True if field is a blob dict, False otherwise :return: True if the field is a blob dict, False otherwise
""" """
return (isinstance(cls.fields.get(field_name), glare_fields.Dict) and return (isinstance(cls.fields.get(field_name), glare_fields.Dict) and
cls.fields[field_name].element_type == cls.fields[field_name].element_type ==
@@ -190,18 +190,18 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def is_link(cls, field_name): 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: True if field is a link, False otherwise
""" """
return isinstance(cls.fields.get(field_name), glare_fields.Link) return isinstance(cls.fields.get(field_name), glare_fields.Link)
@classmethod @classmethod
def is_link_dict(cls, field_name): 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: True if field is a link dict, False otherwise
""" """
return (isinstance(cls.fields.get(field_name), glare_fields.Dict) and return (isinstance(cls.fields.get(field_name), glare_fields.Dict) and
@@ -210,10 +210,10 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def is_link_list(cls, field_name): 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 :param field_name: name of the field
:return: True if field is a link list, False otherwise :return: True if the field is a link list, False otherwise
""" """
return (isinstance(cls.fields.get(field_name), glare_fields.List) and return (isinstance(cls.fields.get(field_name), glare_fields.List) and
cls.fields[field_name].element_type == cls.fields[field_name].element_type ==
@@ -221,11 +221,11 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def _init_artifact(cls, context, values): 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. Initialize vo object with default values and values specified by user.
Also reset all changes for initialized object so user of the method Also reset all changes of initialized object so user can track own
can track own changes. changes.
:param context: user context :param context: user context
:param values: values needs to be set :param values: values needs to be set
@@ -239,31 +239,28 @@ class BaseArtifact(base.VersionedObject):
default_attrs.append(attr) default_attrs.append(attr)
if default_attrs: if default_attrs:
af.obj_set_defaults(*default_attrs) af.obj_set_defaults(*default_attrs)
# apply values specified by user
for name, value in six.iteritems(values): for name, value in six.iteritems(values):
setattr(af, name, value) setattr(af, name, value)
return af return af
@classmethod @classmethod
def get_type_name(cls): 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 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. :return: string that identifies current artifact type
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.
""" """
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
def _lock_version(cls, context, values): def _lock_version(cls, context, values):
"""Calculate version scope for new artifact """Create scope lock for artifact creation.
:param values: af values :param values: artifact values
:return: string that identifies af version or None :return: Lock object
""" """
name = values.get('name') name = values.get('name')
version = values.get('version', cls.DEFAULT_ARTIFACT_VERSION) version = values.get('version', cls.DEFAULT_ARTIFACT_VERSION)
@@ -274,10 +271,15 @@ class BaseArtifact(base.VersionedObject):
return cls.lock_engine.acquire(context, scope_id) return cls.lock_engine.acquire(context, scope_id)
@classmethod @classmethod
def _lock_updated_version(cls, af, updates): def _lock_updated_version(cls, af, values):
name = updates.get('name', af.name) """Create scope lock for artifact update.
version = updates.get('version', af.version)
visibility = updates.get('visibility', af.visibility) :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 scope_id = None
if (name, version, visibility) != (af.name, af.version, af.visibility): if (name, version, visibility) != (af.name, af.version, af.visibility):
# no version change == no lock for version # no version change == no lock for version
@@ -289,11 +291,11 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def create(cls, context, values): def create(cls, context, values):
"""Create new Artifact in Glare repo """Create new artifact in Glare repo.
:param context: user context :param context: user context
:param values: Dict with specified artifact properties :param values: dictionary with specified artifact fields
:return: definition of create Artifact :return: created artifact object
""" """
if context.tenant is None or context.read_only: if context.tenant is None or context.read_only:
msg = _("It's forbidden to anonymous users to create artifacts.") 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 " LOG.info(_LI("Parameters validation for artifact creation "
"passed for request %s."), context.request_id) "passed for request %s."), context.request_id)
af_vals = cls.db_api.create( 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) return cls._init_artifact(context, af_vals)
@classmethod @classmethod
def _validate_versioning(cls, context, name, version, is_public=False): 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, ""): if version is not None and name not in (None, ""):
filters = [('name', name), ('version', version), filters = [('name', name), ('version', version),
('status', 'neq:deleted')] ('status', 'neq:deleted')]
@@ -341,7 +350,7 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def _validate_change_allowed(cls, field_names, af=None, def _validate_change_allowed(cls, field_names, af=None,
validate_blob_names=True): 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 af_status = cls.STATUS.DRAFTED if af is None else af.status
if af_status not in (cls.STATUS.ACTIVE, cls.STATUS.DRAFTED): if af_status not in (cls.STATUS.ACTIVE, cls.STATUS.DRAFTED):
msg = _("Forbidden to change attributes " msg = _("Forbidden to change attributes "
@@ -350,15 +359,15 @@ class BaseArtifact(base.VersionedObject):
for field_name in field_names: for field_name in field_names:
if field_name not in cls.fields: 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) raise exception.BadRequest(msg)
field = cls.fields[field_name] field = cls.fields[field_name]
if field.system is True: 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 "available for modifying by users.") % field_name
raise exception.Forbidden(msg) raise exception.Forbidden(msg)
if af_status == cls.STATUS.ACTIVE and not field.mutable: 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) % field_name)
raise exception.Forbidden(message=msg) raise exception.Forbidden(message=msg)
if validate_blob_names and \ if validate_blob_names and \
@@ -369,12 +378,12 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def update(cls, context, af, values): def update(cls, context, af, values):
"""Update Artifact in Glare repo """Update artifact in Glare repo.
:param context: user Context :param context: user context
:param af: current definition of Artifact in Glare :param af: current definition of artifact
:param values: dictionary with changes for 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 # reset all changes of artifact to reuse them after update
af.obj_reset_changes() af.obj_reset_changes()
@@ -396,35 +405,39 @@ class BaseArtifact(base.VersionedObject):
"update passed for request %(request)s."), "update passed for request %(request)s."),
{'artifact': af.id, 'request': context.request_id}) {'artifact': af.id, 'request': context.request_id})
updated_af = cls.db_api.update( 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) return cls._init_artifact(context, updated_af)
@classmethod @classmethod
def get_action_for_updates(cls, context, artifact, updates, registry): def get_action_for_updates(cls, context, af, values, registry):
"""The method defines how to detect appropriate action based on update """Define the appropriate method for artifact update.
Validate request for update and determine if it is request for action. Based on update params this method defines what action engine should
Also do a validation for request for action if it is an action. 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 values:
if 'visibility' in updates:
# validate publish action format # validate publish action format
action = cls.publish return cls.publish
elif 'status' in updates: elif 'status' in values:
status = updates['status'] status = values['status']
if status == cls.STATUS.DEACTIVATED: if status == cls.STATUS.DEACTIVATED:
action = cls.deactivate return cls.deactivate
elif status == cls.STATUS.ACTIVE: elif status == cls.STATUS.ACTIVE:
if artifact.status == artifact.STATUS.DEACTIVATED: if af.status == af.STATUS.DEACTIVATED:
action = cls.reactivate return cls.reactivate
else: else:
action = cls.activate return cls.activate
# check updates for links and validate them # check updates for links and validate them
try: 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: if cls.is_link(key) and value is not None:
cls._validate_link(key, value, context, registry) cls._validate_link(key, value, context, registry)
elif cls.is_link_dict(key) and value: elif cls.is_link_dict(key) and value:
@@ -434,14 +447,11 @@ class BaseArtifact(base.VersionedObject):
for l in value: for l in value:
cls._validate_link(key, l, context, registry) cls._validate_link(key, l, context, registry)
except Exception as e: except Exception as e:
msg = (_("Bad link in artifact %(af)s: %(msg)s") msg = (_("Broken link in artifact %(af)s: %(msg)s")
% {"af": artifact.id, "msg": str(e)}) % {"af": af.id, "msg": str(e)})
raise exception.BadRequest(msg) raise exception.BadRequest(msg)
LOG.debug("Action %(action)s defined to updates %(updates)s.", return cls.update
{'action': action.__name__, 'updates': updates})
return action
@classmethod @classmethod
def _validate_link(cls, key, value, context, registry): def _validate_link(cls, key, value, context, registry):
@@ -450,36 +460,29 @@ class BaseArtifact(base.VersionedObject):
# check containment # check containment
if glare_fields.LinkFieldType.is_external(value): if glare_fields.LinkFieldType.is_external(value):
# validate external link # validate external link
cls._validate_external_link(value) with urlrequest.urlopen(value) as data:
data.read(1)
else: else:
type_name = (glare_fields.LinkFieldType. type_name = (glare_fields.LinkFieldType.
get_type_name(value)) get_type_name(value))
af_type = registry.get_artifact_type(type_name) af_type = registry.get_artifact_type(type_name)
cls._validate_soft_link(context, value, af_type) af_id = value.split('/')[3]
af_type.get(context, af_id)
@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)
@classmethod @classmethod
def get(cls, context, artifact_id): def get(cls, context, artifact_id):
"""Return Artifact from Glare repo """Return Artifact from Glare repo
:param context: user context :param context: user context
:param artifact_id: id of requested Artifact :param artifact_id: id of requested artifact
:return: Artifact definition :return: requested artifact object
""" """
af = cls.db_api.get(context, artifact_id) af = cls.db_api.get(context, artifact_id)
return cls._init_artifact(context, af) return cls._init_artifact(context, af)
@classmethod @classmethod
def _get_field_type(cls, obj): def _get_field_type(cls, obj):
"""Get string representation of field type for filters."""
if isinstance(obj, fields.IntegerField) or obj is fields.Integer: if isinstance(obj, fields.IntegerField) or obj is fields.Integer:
return 'int' return 'int'
elif isinstance(obj, fields.FloatField) or obj is fields.Float: elif isinstance(obj, fields.FloatField) or obj is fields.Float:
@@ -491,6 +494,7 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def _parse_sort_values(cls, sort): def _parse_sort_values(cls, sort):
"""Prepare sorting parameters for database."""
new_sort = [] new_sort = []
for key, direction in sort: for key, direction in sort:
if key not in cls.fields: if key not in cls.fields:
@@ -504,12 +508,6 @@ class BaseArtifact(base.VersionedObject):
cls.fields.get(key)))) cls.fields.get(key))))
return new_sort 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 @classmethod
def _validate_filter_ops(cls, filter_name, op): def _validate_filter_ops(cls, filter_name, op):
field = cls.fields.get(filter_name) field = cls.fields.get(filter_name)
@@ -550,7 +548,10 @@ class BaseArtifact(base.VersionedObject):
msg = _("Field %s is not Dict") % filter_name msg = _("Field %s is not Dict") % filter_name
raise exception.BadRequest(msg) 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) field_type = cls.fields.get(filter_name)
if isinstance(field_type, glare_fields.List) or isinstance( if isinstance(field_type, glare_fields.List) or isinstance(
@@ -581,16 +582,18 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def list(cls, context, filters=None, marker=None, limit=None, def list(cls, context, filters=None, marker=None, limit=None,
sort=None, latest=False): sort=None, latest=False):
"""List all available Artifacts in Glare repo """Return list of artifacts requested by user.
:param context: user context :param context: user context
:param filters: filtering conditions to Artifact list :param filters: filters that need to be applied to artifact
:param marker: id of Artifact that identifies where Glare should :param marker: the artifact that considered as begin of the list
start listing Artifacts. So all Artifacts before that Artifact in so all artifacts before marker (including marker itself) will not be
resulting list must be ignored. It is useful for Artifact pagination. added to artifact list
:param limit: maximum number of Artifact items in list. :param limit: maximum number of items in the list
:param sort: sorting preferences when requesting Artifact list. :param sort: sorting options
:return: list of Artifacts :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: if sort is not None:
sort = cls._parse_sort_values(sort) sort = cls._parse_sort_values(sort)
@@ -635,14 +638,11 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def delete(cls, context, af): 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 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 # marking artifact as deleted
cls.db_api.update(context, af.id, {'status': cls.STATUS.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 # delete blobs one by one
cls._delete_blobs(blobs, context, af) cls._delete_blobs(blobs, context, af)
LOG.info(_LI("Blobs successfully deleted for artifact %s"), af.id) LOG.info(_LI("Blobs successfully deleted for artifact %s"), af.id)
# delete artifact itself
# delete the artifact itself
cls.db_api.delete(context, af.id) cls.db_api.delete(context, af.id)
@classmethod @classmethod
def activate(cls, context, af, values): 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 context: user context
:param af: current Artifact definition in Glare :param af: current artifact object
:return: definition of activated Artifact :param values: dictionary with changes for artifact
:return: artifact object with changed status
""" """
# validate that came to artifact as updates # validate that came to artifact as updates
if values != {'status': cls.STATUS.ACTIVE}: if values != {'status': cls.STATUS.ACTIVE}:
@@ -700,16 +700,17 @@ class BaseArtifact(base.VersionedObject):
LOG.info(_LI("Parameters validation for artifact %(artifact)s " LOG.info(_LI("Parameters validation for artifact %(artifact)s "
"activate passed for request %(request)s."), "activate passed for request %(request)s."),
{'artifact': af.id, 'request': context.request_id}) {'artifact': af.id, 'request': context.request_id})
active_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, active_af) return cls._init_artifact(context, af)
@classmethod @classmethod
def reactivate(cls, context, af, values): def reactivate(cls, context, af, values):
"""Make Artifact active after de-activation """Make Artifact active after deactivation
:param context: user context :param context: user context
:param af: definition of de-activated Artifact :param af: current artifact object
:return: definition of active Artifact :param values: dictionary with changes for artifact
:return: artifact object with changed status
""" """
# validate that came to artifact as updates # validate that came to artifact as updates
if values != {'status': cls.STATUS.ACTIVE}: if values != {'status': cls.STATUS.ACTIVE}:
@@ -723,21 +724,23 @@ class BaseArtifact(base.VersionedObject):
LOG.info(_LI("Parameters validation for artifact %(artifact)s " LOG.info(_LI("Parameters validation for artifact %(artifact)s "
"reactivate passed for request %(request)s."), "reactivate passed for request %(request)s."),
{'artifact': af.id, 'request': context.request_id}) {'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) return cls._init_artifact(context, af)
@classmethod @classmethod
def deactivate(cls, context, af, values): 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 - If user uploaded suspicious artifact then administrators(or other
it depends on policy configurations) can deny Artifact download by users - it depends on policy configurations) can deny artifact data
users by making Artifact de-activated. After additional investigation to be downloaded by regular users by making artifact deactivated.
Artifact can be re-activated or deleted from Glare. After additional investigation artifact can be reactivated or
deleted from Glare.
:param context: user context :param context: user context
:param af: Artifact definition in Glare :param af: current artifact object
:return: definition of de-activated Artifact :param values: dictionary with changes for artifact
:return: artifact object with changed status
""" """
if values != {'status': cls.STATUS.DEACTIVATED}: if values != {'status': cls.STATUS.DEACTIVATED}:
msg = _("Only {'status': %s} is allowed in a request " 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 " LOG.info(_LI("Parameters validation for artifact %(artifact)s "
"deactivate passed for request %(request)s."), "deactivate passed for request %(request)s."),
{'artifact': af.id, 'request': context.request_id}) {'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) return cls._init_artifact(context, af)
@classmethod @classmethod
def publish(cls, context, af, values): def publish(cls, context, af, values):
"""Make Artifact available for everyone """Make artifact available for all tenants.
:param context: user context :param context: user context
:param af: definition of published Artifact :param af: current artifact object
:return: definition of active Artifact :param values: dictionary with changes for artifact
:return: artifact object with changed visibility
""" """
if values != {'visibility': 'public'}: if values != {'visibility': 'public'}:
msg = _("Only {'visibility': 'public'} is allowed in a request " 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 " LOG.info(_LI("Parameters validation for artifact %(artifact)s "
"publish passed for request %(request)s."), "publish passed for request %(request)s."),
{'artifact': af.id, 'request': context.request_id}) {'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) return cls._init_artifact(context, af)
@classmethod @classmethod
def get_max_blob_size(cls, field_name): 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', return getattr(cls.fields[field_name], 'max_blob_size',
attribute.BlobAttribute.DEFAULT_MAX_BLOB_SIZE) attribute.BlobAttribute.DEFAULT_MAX_BLOB_SIZE)
@classmethod @classmethod
def validate_upload_allowed(cls, context, af, field_name, blob_key=None): def validate_upload_allowed(cls, af, field_name, blob_key=None):
"""Validate if given blob is ready for uploading.""" """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)\ blob_name = "%s[%s]" % (field_name, blob_key)\
if blob_key else field_name if blob_key else field_name
@@ -818,35 +833,40 @@ class BaseArtifact(base.VersionedObject):
{'artifact': af.id, 'blob_name': blob_name}) {'artifact': af.id, 'blob_name': blob_name})
@classmethod @classmethod
def update_blob(cls, context, af_id, values): def update_blob(cls, context, af_id, field_name, values):
"""Upload binary object as artifact property """Update blob info in database.
:param context: user context :param context: user context
:param af_id: id of modified artifact :param af_id: id of modified artifact
:param field_name: blob or blob dict field name
:param values: updated blob values :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) return cls._init_artifact(context, af_upd)
@classmethod @classmethod
def validate_activate(cls, context, af, values=None): def validate_activate(cls, context, af, values=None):
"""Validation hook for activation."""
pass pass
@classmethod @classmethod
def validate_upload(cls, context, af, field_name, fd): def validate_upload(cls, context, af, field_name, fd):
"""Validation hook for uploading."""
return fd, None return fd, None
@classmethod @classmethod
def validate_publish(cls, context, af): def validate_publish(cls, context, af):
"""Validation hook for publishing."""
pass pass
@classmethod @classmethod
def get_default_store(cls, context, af, field_name, blob_key): def get_default_store(cls, context, af, field_name, blob_key):
"""Return a default store type for artifact type."""
pass pass
def to_notification(self): 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 :return: dict with notification information
""" """
@@ -865,13 +885,13 @@ class BaseArtifact(base.VersionedObject):
} }
def to_dict(self): 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: dict with field names and field values
""" """
return self.obj_to_primitive()['versioned_object.data'] 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() changes = self.obj_get_changes()
res = {} res = {}
for key, val in six.iteritems(changes): for key, val in six.iteritems(changes):
@@ -882,7 +902,7 @@ class BaseArtifact(base.VersionedObject):
return res return res
@classmethod @classmethod
def schema_attr(cls, attr, attr_name=''): def _schema_attr(cls, attr, attr_name=''):
attr_type = utils.get_schema_type(attr) attr_type = utils.get_schema_type(attr)
schema = {} schema = {}
@@ -968,10 +988,11 @@ class BaseArtifact(base.VersionedObject):
@classmethod @classmethod
def gen_schemas(cls): def gen_schemas(cls):
"""Return json schema representation of the artifact type."""
schemas_prop = {} schemas_prop = {}
for attr_name, attr in six.iteritems(cls.fields): for attr_name, attr in six.iteritems(cls.fields):
schemas_prop[attr_name] = cls.schema_attr(attr, schemas_prop[attr_name] = cls._schema_attr(
attr_name=attr_name) attr, attr_name=attr_name)
schemas = {'properties': schemas_prop, schemas = {'properties': schemas_prop,
'name': cls.get_type_name(), 'name': cls.get_type_name(),
'version': cls.VERSION, 'version': cls.VERSION,

View File

@@ -29,7 +29,7 @@ class Attribute(object):
def __init__(self, field_class, mutable=False, required_on_activate=True, def __init__(self, field_class, mutable=False, required_on_activate=True,
system=False, validators=None, nullable=True, default=None, system=False, validators=None, nullable=True, default=None,
sortable=False, filter_ops=None, description=""): sortable=False, filter_ops=None, description=""):
"""Init and validate attribute""" """Init and validate attribute."""
if not issubclass(field_class, fields.AutoTypedField): if not issubclass(field_class, fields.AutoTypedField):
raise exc.IncorrectArtifactType( raise exc.IncorrectArtifactType(
"Field class %s must be sub-class of AutoTypedField." % "Field class %s must be sub-class of AutoTypedField." %
@@ -115,7 +115,7 @@ class Attribute(object):
@classmethod @classmethod
def init(cls, *args, **kwargs): def init(cls, *args, **kwargs):
"""Fabric to build attributes""" """Fabric to build attributes."""
return cls(*args, **kwargs).get_field() return cls(*args, **kwargs).get_field()

View File

@@ -49,7 +49,7 @@ CONF.register_opts(registry_options)
def import_submodules(module): def import_submodules(module):
"""Import all submodules of a module """Import all submodules of a module.
:param module: Package name :param module: Package name
:return list of imported modules :return list of imported modules
@@ -92,7 +92,7 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
@classmethod @classmethod
def register_all_artifacts(cls): def register_all_artifacts(cls):
"""Register all artifacts in glare""" """Register all artifacts in Glare."""
# get all submodules in glare.objects # get all submodules in glare.objects
# please note that we registering trusted modules first # please note that we registering trusted modules first
# and applying custom modules after that to allow custom modules # and applying custom modules after that to allow custom modules
@@ -114,7 +114,7 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
@classmethod @classmethod
def get_artifact_type(cls, type_name): 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 :param type_name: name of artifact type
:return: artifact class :return: artifact class
@@ -126,5 +126,5 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
@classmethod @classmethod
def reset_registry(cls): def reset_registry(cls):
"""Resets all registered artifact type classes""" """Resets all registered artifact type classes."""
cls._registry._obj_classes = collections.defaultdict(list) cls._registry._obj_classes = collections.defaultdict(list)

View File

@@ -26,7 +26,7 @@ LOG = logging.getLogger(__name__)
class Validator(object): class Validator(object):
"""Common interface for all validators""" """Common interface for all validators."""
def validate(self, value): def validate(self, value):
raise NotImplementedError() raise NotImplementedError()

View File

@@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""Glare WSGI module """Glare WSGI module.
Use this module to deploy glare as WSGI application. Use this module to deploy glare as WSGI application.