diff --git a/rest-service/manager_rest/rest/endpoint_mapper.py b/rest-service/manager_rest/rest/endpoint_mapper.py index 28a6821320..ac898f44a7 100644 --- a/rest-service/manager_rest/rest/endpoint_mapper.py +++ b/rest-service/manager_rest/rest/endpoint_mapper.py @@ -160,7 +160,8 @@ def setup_resources(api): } # Set version endpoint as a non versioned endpoint - api.add_resource(resources_v1.Version, '/api/version', endpoint='version') + # api.add_resource( + # resources_v1.Version, '/api/version', endpoint='version') for resource, endpoint_suffix in resources_endpoints.items(): if isinstance(endpoint_suffix, str): _set_versioned_urls(api, resource, endpoint_suffix) @@ -174,10 +175,11 @@ def _set_versioned_urls(api, resource_name, endpoint_suffix): for version in SUPPORTED_API_VERSIONS: version_name, resources_impl = version if hasattr(resources_impl, resource_name): - resource = getattr(resources_impl, resource_name) + resource = getattr(resources_impl, resource_name).as_view( + f'{version_name}/{endpoint_suffix}') # 'resource' will persist throughout iterations, holding a reference # to the latest impl. if resource: endpoint = '{0}/{1}'.format(version_name, endpoint_suffix) url = '/api/{0}'.format(endpoint) - api.add_resource(resource, url, endpoint=endpoint) + api.add_url_rule(url, view_func=resource) diff --git a/rest-service/manager_rest/rest/marshal.py b/rest-service/manager_rest/rest/marshal.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rest-service/manager_rest/rest/resources_v1/blueprints.py b/rest-service/manager_rest/rest/resources_v1/blueprints.py index b63de6cc93..4b99895f7b 100644 --- a/rest-service/manager_rest/rest/resources_v1/blueprints.py +++ b/rest-service/manager_rest/rest/resources_v1/blueprints.py @@ -206,4 +206,4 @@ def delete(self, blueprint_id, **kwargs): # However, there is no handling of possible concurrency issue with # regard to that matter at the moment. get_resource_manager().delete_blueprint(blueprint_id, force=False) - return None, 204 + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v1/deployments.py b/rest-service/manager_rest/rest/resources_v1/deployments.py index 5d2715f936..dc925b8172 100644 --- a/rest-service/manager_rest/rest/resources_v1/deployments.py +++ b/rest-service/manager_rest/rest/resources_v1/deployments.py @@ -14,8 +14,10 @@ # * limitations under the License. # -from flask_restful.reqparse import Argument -from flask_restful.inputs import boolean +import pydantic +from typing import Optional, Dict, Any + +from flask import request from cloudify.models_states import DeploymentState @@ -30,13 +32,10 @@ from manager_rest.resource_manager import (ResourceManager, get_resource_manager) from manager_rest.rest.rest_decorators import marshal_with -from manager_rest.rest.rest_utils import (get_args_and_verify_arguments, - get_json_and_verify_params, - validate_inputs) +from manager_rest.rest.rest_utils import validate_inputs class Deployments(SecuredResource): - @swagger.operation( responseClass='List[{0}]'.format(models.Deployment.__name__), nickname="list", @@ -52,6 +51,20 @@ def get(self, _include=None, **kwargs): models.Deployment, include=_include).items +class _DeploymentCreateArgs(pydantic.BaseModel): + blueprint_id: str + inputs: Optional[Dict[str, Any]] = {} + + +class _DeploymentDeleteQuery(pydantic.BaseModel): + force: Optional[bool] = False + delete_logs: Optional[bool] = False + + +class _PrivateResourceArgs(pydantic.BaseModel): + private_resource: Optional[bool] = False + + class DeploymentsId(SecuredResource): @swagger.operation( responseClass=models.Deployment, @@ -86,14 +99,10 @@ def put(self, deployment_id, **kwargs): Create a deployment """ validate_inputs({'deployment_id': deployment_id}) - request_schema = self.create_request_schema() - request_dict = get_json_and_verify_params(request_schema) + request_dict = _DeploymentCreateArgs.parse_obj(request.json).dict() blueprint_id = request_dict['blueprint_id'] bypass_maintenance = is_bypass_maintenance_mode() - args = get_args_and_verify_arguments( - [Argument('private_resource', type=boolean, - default=False)] - ) + args = _PrivateResourceArgs.parse_obj(request.args) skip_plugins_validation = self.get_skip_plugin_validation_flag( request_dict) rm = get_resource_manager() @@ -119,13 +128,6 @@ def put(self, deployment_id, **kwargs): raise return deployment, 201 - def create_request_schema(self): - request_schema = { - 'blueprint_id': {}, - 'inputs': {'optional': True, 'type': dict} - } - return request_schema - def get_skip_plugin_validation_flag(self, request_dict): return True @@ -146,11 +148,7 @@ def get_skip_plugin_validation_flag(self, request_dict): @authorize('deployment_delete') def delete(self, deployment_id, **kwargs): """Delete deployment by id""" - args = get_args_and_verify_arguments([ - Argument('force', type=boolean, default=False), - Argument('delete_logs', type=boolean, default=False) - ]) - + args = _DeploymentDeleteQuery.parse_obj(request.args) bypass_maintenance = is_bypass_maintenance_mode() sm = get_storage_manager() dep = sm.get(models.Deployment, deployment_id) @@ -166,11 +164,20 @@ def delete(self, deployment_id, **kwargs): [delete_execution], bypass_maintenance=bypass_maintenance) workflow_executor.execute_workflow(messages) workflow_executor.delete_source_plugins(dep.id) - return None, 204 + return "", 204 -class DeploymentModifications(SecuredResource): +class _DeploymentModificationArgs(pydantic.BaseModel): + deployment_id: str + context: Optional[Dict[str, Any]] = {} + nodes: Optional[Dict[str, Any]] = {} + + +class _DeploymentIDArgs(pydantic.BaseModel): + deployment_id: Optional[str] = None + +class DeploymentModifications(SecuredResource): @swagger.operation( responseClass=models.DeploymentModification, nickname="modifyDeployment", @@ -189,16 +196,12 @@ class DeploymentModifications(SecuredResource): @authorize('deployment_modify') @marshal_with(models.DeploymentModification) def post(self, **kwargs): - request_dict = get_json_and_verify_params({ - 'deployment_id': {}, - 'context': {'optional': True, 'type': dict}, - 'nodes': {'optional': True, 'type': dict} - }) - deployment_id = request_dict['deployment_id'] - context = request_dict.get('context', {}) - nodes = request_dict.get('nodes', {}) - modification = get_resource_manager(). \ - start_deployment_modification(deployment_id, nodes, context) + args = _DeploymentModificationArgs.parse_obj(request.json) + modification = get_resource_manager().start_deployment_modification( + args.deployment_id, + args.nodes, + args.context, + ) return modification, 201 @swagger.operation( @@ -216,9 +219,7 @@ def post(self, **kwargs): @authorize('deployment_modification_list') @marshal_with(models.DeploymentModification) def get(self, _include=None, **kwargs): - args = get_args_and_verify_arguments( - [Argument('deployment_id', required=False)] - ) + args = _DeploymentIDArgs.parse_obj(request.args) deployment_id_filter = ResourceManager.create_filters_dict( deployment_id=args.deployment_id) return get_storage_manager().list( diff --git a/rest-service/manager_rest/rest/resources_v1/evaluate_functions.py b/rest-service/manager_rest/rest/resources_v1/evaluate_functions.py index 08a67778b5..b1005e1182 100644 --- a/rest-service/manager_rest/rest/resources_v1/evaluate_functions.py +++ b/rest-service/manager_rest/rest/resources_v1/evaluate_functions.py @@ -13,16 +13,24 @@ # * See the License for the specific language governing permissions and # * limitations under the License. +import pydantic +from flask import request +from typing import Any, Dict, Optional + from manager_rest.rest import requests_schema, responses, swagger from manager_rest.rest.rest_decorators import marshal_with from manager_rest.security import SecuredResource from manager_rest.security.authorization import authorize from manager_rest.dsl_functions import evaluate_intrinsic_functions -from manager_rest.rest.rest_utils import get_json_and_verify_params -class EvaluateFunctions(SecuredResource): +class _EvaluateFunctionsArgs(pydantic.BaseModel): + deployment_id: str + context: Optional[Dict[str, Any]] = {} + payload: Dict[str, Any] + +class EvaluateFunctions(SecuredResource): @swagger.operation( responseClass=responses.EvaluatedFunctions, nickname='evaluateFunctions', @@ -41,20 +49,14 @@ class EvaluateFunctions(SecuredResource): @authorize('functions_evaluate') @marshal_with(responses.EvaluatedFunctions) def post(self, **kwargs): - """ - Evaluate intrinsic in payload - """ - request_dict = get_json_and_verify_params({ - 'deployment_id': {}, - 'context': {'optional': True, 'type': dict}, - 'payload': {'type': dict} - }) - - deployment_id = request_dict['deployment_id'] - context = request_dict.get('context', {}) - payload = request_dict.get('payload') + """Evaluate intrinsic in payload""" + args = _EvaluateFunctionsArgs.parse_obj(request.json) processed_payload = evaluate_intrinsic_functions( - deployment_id=deployment_id, - context=context, - payload=payload) - return dict(deployment_id=deployment_id, payload=processed_payload) + deployment_id=args.deployment_id, + context=args.context, + payload=args.payload, + ) + return dict( + deployment_id=args.deployment_id, + payload=processed_payload, + ) diff --git a/rest-service/manager_rest/rest/resources_v1/executions.py b/rest-service/manager_rest/rest/resources_v1/executions.py index 0a7a62a577..1e667afaea 100644 --- a/rest-service/manager_rest/rest/resources_v1/executions.py +++ b/rest-service/manager_rest/rest/resources_v1/executions.py @@ -1,8 +1,10 @@ import uuid from datetime import datetime -from flask_restful.reqparse import Argument -from flask_restful.inputs import boolean +import pydantic + +from flask import request +from typing import Any, Dict, Optional from cloudify.models_states import ExecutionState from manager_rest import manager_exceptions, workflow_executor @@ -17,9 +19,6 @@ not_while_cancelling ) from manager_rest.rest.rest_utils import ( - get_args_and_verify_arguments, - get_json_and_verify_params, - verify_and_convert_bool, parse_datetime_string, valid_user, ) @@ -32,8 +31,32 @@ ) -class Executions(SecuredResource): +class _ExecuteWorkflowArgs(pydantic.BaseModel): + deployment_id: str + workflow_id: str + created_by: Optional[str] = None + created_at: Optional[str] = None + started_at: Optional[str] = None + ended_at: Optional[str] = None + status: Optional[ExecutionState] = None + force_status: Optional[ExecutionState] = None + id: Optional[str] = None + error: Optional[Any] = '' + allow_custom_parameters: Optional[bool] = False + force: Optional[bool] = False + queue: Optional[bool] = False + dry_run: Optional[bool] = False + parameters: Optional[Dict[str, Any]] = {} + wait_after_fail: Optional[int] = 600 + scheduled_time: Optional[str] = None + + +class _ExecutionsListQuery(pydantic.BaseModel): + deployment_id: Optional[str] = None + include_system_workflows: Optional[bool] = False + +class Executions(SecuredResource): @swagger.operation( responseClass='List[{0}]'.format(models.Execution.__name__), nickname="list", @@ -58,11 +81,7 @@ class Executions(SecuredResource): @marshal_with(models.Execution) def get(self, _include=None, **kwargs): """List executions""" - args = get_args_and_verify_arguments( - [Argument('deployment_id', required=False), - Argument('include_system_workflows', type=boolean, - default=False)] - ) + args = _ExecutionsListQuery.parse_obj(request.args) deployment_id_filter = ResourceManager.create_filters_dict( deployment_id=args.deployment_id) return get_resource_manager().list_executions( @@ -75,43 +94,24 @@ def get(self, _include=None, **kwargs): @marshal_with(models.Execution) def post(self, **kwargs): """Execute a workflow""" - request_dict = get_json_and_verify_params({ - 'deployment_id': {'type': str}, - 'workflow_id': {'type': str}, - 'created_by': {'optional': True}, - 'created_at': {'optional': True}, - 'started_at': {'optional': True}, - 'ended_at': {'optional': True}, - 'status': {'optional': True}, - 'id': {'optional': True}, - 'error': {'optional': True}, - }) - - allow_custom_parameters = verify_and_convert_bool( - 'allow_custom_parameters', - request_dict.get('allow_custom_parameters', False)) - force = verify_and_convert_bool( - 'force', - request_dict.get('force', False)) - dry_run = verify_and_convert_bool( - 'dry_run', - request_dict.get('dry_run', False)) - queue = verify_and_convert_bool( - 'queue', - request_dict.get('queue', False)) - - deployment_id = request_dict['deployment_id'] - workflow_id = request_dict['workflow_id'] - parameters = request_dict.get('parameters', None) - wait_after_fail = request_dict.get('wait_after_fail', 600) - scheduled_time = request_dict.get('scheduled_time', None) - force_status = request_dict.get('force_status', None) - creator = request_dict.get('created_by') - created_at = request_dict.get('created_at') - started_at = request_dict.get('started_at') - ended_at = request_dict.get('ended_at') - execution_id = request_dict.get('id') - error = request_dict.get('error') + args = _ExecuteWorkflowArgs.parse_obj(request.json) + allow_custom_parameters = args.allow_custom_parameters + force = args.force + dry_run = args.dry_run + queue = args.queue + + deployment_id = args.deployment_id + workflow_id = args.workflow_id + parameters = args.parameters + wait_after_fail = args.wait_after_fail + scheduled_time = args.scheduled_time + force_status = args.force_status + creator = args.created_by + created_at = args.created_at + started_at = args.started_at + ended_at = args.ended_at + execution_id = args.id + error = args.error if creator: check_user_action_allowed('set_owner', None, True) @@ -264,8 +264,16 @@ def _parse_scheduled_time(self, scheduled_time): return scheduled_utc -class ExecutionsId(SecuredResource): +class _ExecutionAction(pydantic.BaseModel): + action: str + + +class _ExecutionStatusUpdate(pydantic.BaseModel): + status: ExecutionState + error: Optional[str] = '' + +class ExecutionsId(SecuredResource): @swagger.operation( responseClass=models.Execution, nickname="getById", @@ -306,8 +314,8 @@ def post(self, execution_id, **kwargs): """ Apply execution action (cancel, force-cancel) by id """ - request_dict = get_json_and_verify_params({'action'}) - action = request_dict['action'] + args = _ExecutionAction.parse_obj(request.json) + action = args.action valid_actions = ['cancel', 'force-cancel', 'kill', 'resume', 'force-resume', 'requeue'] @@ -348,13 +356,10 @@ def post(self, execution_id, **kwargs): @authorize('execution_status_update') @marshal_with(models.Execution) def patch(self, execution_id, **kwargs): - """ - Update execution status by id - """ - request_dict = get_json_and_verify_params({'status'}) - + """Update execution status by id""" + args = _ExecutionStatusUpdate.parse_obj(request.json) return get_resource_manager().update_execution_status( execution_id, - request_dict['status'], - request_dict.get('error', '') + args.status, + args.error, ) diff --git a/rest-service/manager_rest/rest/resources_v1/nodes.py b/rest-service/manager_rest/rest/resources_v1/nodes.py index f1ac5ce934..e139df4dd6 100644 --- a/rest-service/manager_rest/rest/resources_v1/nodes.py +++ b/rest-service/manager_rest/rest/resources_v1/nodes.py @@ -14,21 +14,16 @@ # * limitations under the License. # -import collections +from typing import Any, Dict, Optional +import pydantic from flask import request -from flask_restful.reqparse import Argument from manager_rest import manager_exceptions from manager_rest.resource_manager import ResourceManager from manager_rest.rest import swagger from manager_rest.rest.rest_decorators import marshal_with -from manager_rest.rest.rest_utils import ( - get_args_and_verify_arguments, - get_json_and_verify_params, - verify_and_convert_bool, - is_deployment_update -) +from manager_rest.rest.rest_utils import is_deployment_update from manager_rest.security import SecuredResource from manager_rest.security.authorization import authorize from manager_rest.storage import ( @@ -38,8 +33,12 @@ ) -class Nodes(SecuredResource): +class _NodesListQuery(pydantic.BaseModel): + deployment_id: Optional[str] = None + node_id: Optional[str] = None + +class Nodes(SecuredResource): @swagger.operation( responseClass='List[{0}]'.format(models.Node.__name__), nickname="listNodes", @@ -54,13 +53,8 @@ class Nodes(SecuredResource): @authorize('node_list') @marshal_with(models.Node) def get(self, _include=None, **kwargs): - """ - List nodes - """ - args = get_args_and_verify_arguments( - [Argument('deployment_id', required=False), - Argument('node_id', required=False)] - ) + """List nodes""" + args = _NodesListQuery.parse_obj(request.args) deployment_id = args.get('deployment_id') node_id = args.get('node_id') @@ -80,8 +74,12 @@ def get(self, _include=None, **kwargs): return nodes -class NodeInstances(SecuredResource): +class _InstancesListQuery(pydantic.BaseModel): + deployment_id: Optional[str] = None + node_name: Optional[str] = None + +class NodeInstances(SecuredResource): @swagger.operation( responseClass='List[{0}]'.format(models.NodeInstance.__name__), nickname="listNodeInstances", @@ -103,17 +101,10 @@ class NodeInstances(SecuredResource): @authorize('node_instance_list') @marshal_with(models.NodeInstance) def get(self, _include=None, **kwargs): - """ - List node instances - """ - args = get_args_and_verify_arguments( - [Argument('deployment_id', required=False), - Argument('node_name', required=False)] - ) - deployment_id = args.get('deployment_id') - node_id = args.get('node_name') + """List node instances""" + args = _InstancesListQuery.parse_obj(request.args) params_filter = ResourceManager.create_filters_dict( - deployment_id=deployment_id, node_id=node_id) + deployment_id=args.deployment_id, node_id=args.node_id) return get_storage_manager().list( models.NodeInstance, filters=params_filter, @@ -121,6 +112,18 @@ def get(self, _include=None, **kwargs): ).items +class _NodeInstanceUpdateArgs(pydantic.BaseModel): + version: int + runtime_properties: Optional[Dict[str, Any]] = None + system_properties: Optional[Dict[str, Any]] = None + relationships: Optional[Any] = None + state: Optional[str] = None + + +class _NodeInstanceUpdateQuery(pydantic.BaseModel): + force: Optional[bool] = False + + class NodeInstancesId(SecuredResource): @swagger.operation( @@ -197,18 +200,9 @@ def get(self, node_instance_id, _include=None, **kwargs): @marshal_with(models.NodeInstance) def patch(self, node_instance_id, **kwargs): """Update node instance by id.""" - request_dict = get_json_and_verify_params( - {'version': {'type': int}} - ) - - if not isinstance(request.json, collections.abc.Mapping): - raise manager_exceptions.BadParametersError( - 'Request body needs to be a mapping') + request_dict = _NodeInstanceUpdateArgs.parse_obj(request.json).dict() version = request_dict['version'] or 1 - force = verify_and_convert_bool( - 'force', - request.args.get('force', False) - ) + force = _NodeInstanceUpdateQuery.parse_obj(request.args).force sm = get_storage_manager() with sm.transaction(): @@ -227,17 +221,17 @@ def patch(self, node_instance_id, **kwargs): 'Node instance update conflict [current version=' f'{instance.version}, update version={version}]' ) - if 'runtime_properties' in request_dict: + if request_dict['runtime_properties'] is not None: instance.runtime_properties = \ request_dict['runtime_properties'] - if 'system_properties' in request_dict: + if request_dict['system_properties'] is not None: old_properties = instance.system_properties instance.system_properties = \ request_dict['system_properties'] self._process_system_properties(instance, old_properties) - if 'state' in request_dict: + if request_dict['state'] is not None: instance.state = request_dict['state'] - if 'relationships' in request_dict: + if request_dict['relationships'] is not None: if not is_deployment_update(): raise manager_exceptions.OnlyDeploymentUpdate() instance.relationships = request_dict['relationships'] diff --git a/rest-service/manager_rest/rest/resources_v1/provider_context.py b/rest-service/manager_rest/rest/resources_v1/provider_context.py index fe27abadc2..a923c0dad5 100644 --- a/rest-service/manager_rest/rest/resources_v1/provider_context.py +++ b/rest-service/manager_rest/rest/resources_v1/provider_context.py @@ -14,8 +14,10 @@ # * limitations under the License. # -from flask_restful.reqparse import Argument -from flask_restful.inputs import boolean +import pydantic +from typing import Any, Optional + +from flask import request from dsl_parser import utils as dsl_parser_utils from manager_rest import manager_exceptions @@ -23,10 +25,6 @@ from manager_rest.resource_manager import get_resource_manager from manager_rest.rest import requests_schema, responses, swagger from manager_rest.rest.rest_decorators import marshal_with -from manager_rest.rest.rest_utils import ( - get_args_and_verify_arguments, - get_json_and_verify_params, -) from manager_rest.security import SecuredResource from manager_rest.security.authorization import authorize from manager_rest.storage import ( @@ -35,6 +33,15 @@ ) +class _ProviderContextCreateArgs(pydantic.BaseModel): + name: str + context: Any + + +class _IsUpdateQuery(pydantic.BaseModel): + update: Optional[bool] = False + + class ProviderContext(SecuredResource): @swagger.operation( @@ -73,15 +80,13 @@ def post(self, **kwargs): """ Create provider context """ - request_dict = get_json_and_verify_params({'context', 'name'}) - args = get_args_and_verify_arguments( - [Argument('update', type=boolean, default=False)] - ) - update = args['update'] + params = _ProviderContextCreateArgs.parse_obj(request.json) + args = _IsUpdateQuery.parse_obj(request.args) + update = args.update context = dict( id=PROVIDER_CONTEXT_ID, - name=request_dict['name'], - context=request_dict['context'] + name=params.name, + context=params.context, ) status_code = 200 if update else 201 diff --git a/rest-service/manager_rest/rest/resources_v2/blueprints.py b/rest-service/manager_rest/rest/resources_v2/blueprints.py index c22cfb4bc7..16854cd612 100644 --- a/rest-service/manager_rest/rest/resources_v2/blueprints.py +++ b/rest-service/manager_rest/rest/resources_v2/blueprints.py @@ -45,18 +45,12 @@ class Blueprints(resources_v1.Blueprints): @rest_decorators.create_filters(models.Blueprint) @rest_decorators.paginate @rest_decorators.sortable(models.Blueprint) - @rest_decorators.all_tenants @rest_decorators.search('id') @rest_decorators.filter_id def get(self, _include=None, filters=None, pagination=None, sort=None, - all_tenants=None, search=None, filter_id=None, **kwargs): - """ - List uploaded blueprints - """ - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + search=None, filter_id=None, **kwargs): + """List uploaded blueprints""" + args = rest_utils.ListQuery.parse_obj(request.args) filters = filters or {} filters.setdefault('is_hidden', False) sm = get_storage_manager() @@ -69,8 +63,8 @@ def get(self, _include=None, filters=None, pagination=None, sort=None, substr_filters=search, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results, + all_tenants=args.all_tenants, + get_all_results=args.get_all_results, filter_rules=filter_rules ) diff --git a/rest-service/manager_rest/rest/resources_v2/deployments.py b/rest-service/manager_rest/rest/resources_v2/deployments.py index 5a55e4465c..20b214a766 100644 --- a/rest-service/manager_rest/rest/resources_v2/deployments.py +++ b/rest-service/manager_rest/rest/resources_v2/deployments.py @@ -48,19 +48,13 @@ class Deployments(resources_v1.Deployments): @rest_decorators.create_filters(models.Deployment) @rest_decorators.paginate @rest_decorators.sortable(models.Deployment) - @rest_decorators.all_tenants @rest_decorators.search_multiple_parameters( {'_search': 'id', '_search_name': 'display_name'}) @rest_decorators.filter_id def get(self, _include=None, filters=None, pagination=None, sort=None, - all_tenants=None, search=None, filter_id=None, **kwargs): - """ - List deployments - """ - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + search=None, filter_id=None, **kwargs): + """List deployments""" + args = rest_utils.ListQuery.parse_obj(request.args) sm = get_storage_manager() filters = filters or {} filters.update(rest_utils.deployment_group_id_filter()) @@ -81,8 +75,8 @@ def get(self, _include=None, filters=None, pagination=None, sort=None, pagination=pagination, sort=sort, sort_labels=sort_labels, - all_tenants=all_tenants, - get_all_results=get_all_results, + all_tenants=args.all_tenants, + get_all_results=args.get_all_results, filter_rules=filter_rules, load_relationships=True, ) diff --git a/rest-service/manager_rest/rest/resources_v2/executions.py b/rest-service/manager_rest/rest/resources_v2/executions.py index 2df9f5c6a1..edc0ca17f1 100644 --- a/rest-service/manager_rest/rest/resources_v2/executions.py +++ b/rest-service/manager_rest/rest/resources_v2/executions.py @@ -14,6 +14,8 @@ # * limitations under the License. # +from typing import Optional + from flask import request from manager_rest.resource_manager import get_resource_manager @@ -30,6 +32,10 @@ from manager_rest.utils import create_filter_params_list_description +class _ExecutionsListQuery(rest_utils.ListQuery): + _include_system_workflows: Optional[bool] = False + + class Executions(resources_v1.Executions): @swagger.operation( responseClass='List[{0}]'.format(models.Execution.__name__), @@ -52,9 +58,8 @@ class Executions(resources_v1.Executions): @rest_decorators.create_filters(models.Execution) @rest_decorators.paginate @rest_decorators.sortable(models.Execution) - @rest_decorators.all_tenants def get(self, _include=None, filters=None, pagination=None, - sort=None, all_tenants=None, **kwargs): + sort=None, **kwargs): """ List executions """ @@ -62,20 +67,14 @@ def get(self, _include=None, filters=None, pagination=None, filters['execution_groups'] = lambda col: col.any( models.ExecutionGroup.id == request.args['_group_id'] ) - is_include_system_workflows = rest_utils.verify_and_convert_bool( - '_include_system_workflows', - request.args.get('_include_system_workflows', False)) - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + args = _ExecutionsListQuery.parse_obj(request.args) return get_resource_manager().list_executions( filters=filters, pagination=pagination, sort=sort, - is_include_system_workflows=is_include_system_workflows, + is_include_system_workflows=args._include_system_workflows, include=_include, - all_tenants=all_tenants, - get_all_results=get_all_results, + all_tenants=args.all_tenants, + get_all_results=args.get_all_results, load_relationships=True, ) diff --git a/rest-service/manager_rest/rest/resources_v2/nodes.py b/rest-service/manager_rest/rest/resources_v2/nodes.py index 4759ad1b50..7decea3a5c 100644 --- a/rest-service/manager_rest/rest/resources_v2/nodes.py +++ b/rest-service/manager_rest/rest/resources_v2/nodes.py @@ -46,17 +46,11 @@ class Nodes(resources_v1.Nodes): @rest_decorators.create_filters(models.Node) @rest_decorators.paginate @rest_decorators.sortable(models.Node) - @rest_decorators.all_tenants @rest_decorators.search('id') def get(self, _include=None, filters=None, pagination=None, - sort=None, all_tenants=None, search=None, **kwargs): - """ - List nodes - """ - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + sort=None, search=None, **kwargs): + """List nodes""" + args = rest_utils.ListQuery.parse_obj(request.args) nodes_list = get_storage_manager().list( models.Node, include=_include, @@ -64,8 +58,8 @@ def get(self, _include=None, filters=None, pagination=None, filters=filters, substr_filters=search, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results + all_tenants=args.all_tenants, + get_all_results=args.get_all_results ) # Update the node instance count to account for group scaling policy for node in nodes_list: @@ -100,17 +94,11 @@ class NodeInstances(resources_v1.NodeInstances): @rest_decorators.create_filters(models.NodeInstance) @rest_decorators.paginate @rest_decorators.sortable(models.NodeInstance) - @rest_decorators.all_tenants @rest_decorators.search('id') def get(self, _include=None, filters=None, pagination=None, - sort=None, all_tenants=None, search=None, **kwargs): - """ - List node instances - """ - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + sort=None, search=None, **kwargs): + """List node instances""" + args = rest_utils.ListQuery.parse_obj(request.args) return get_storage_manager().list( models.NodeInstance, include=_include, @@ -118,6 +106,6 @@ def get(self, _include=None, filters=None, pagination=None, substr_filters=search, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results + all_tenants=args.all_tenants, + get_all_results=args.get_all_results ) diff --git a/rest-service/manager_rest/rest/resources_v2/plugins.py b/rest-service/manager_rest/rest/resources_v2/plugins.py index bb37d93a56..ca7e06b525 100644 --- a/rest-service/manager_rest/rest/resources_v2/plugins.py +++ b/rest-service/manager_rest/rest/resources_v2/plugins.py @@ -191,8 +191,7 @@ def get(self, plugin_id, _include=None, **kwargs): notes="deletes a plugin according to its ID." ) @authorize('plugin_delete') - @rest_decorators.marshal_with(models.Plugin) def delete(self, plugin_id, **kwargs): """Delete plugin by ID""" get_resource_manager().remove_plugin(plugin_id=plugin_id, force=False) - return None, 204 + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v2/snapshots.py b/rest-service/manager_rest/rest/resources_v2/snapshots.py index 14902459e3..5d8c65025d 100644 --- a/rest-service/manager_rest/rest/resources_v2/snapshots.py +++ b/rest-service/manager_rest/rest/resources_v2/snapshots.py @@ -17,6 +17,10 @@ import os import shutil +import pydantic +from flask import request +from typing import Optional + from cloudify.models_states import SnapshotState, ExecutionState from manager_rest import config, manager_exceptions, workflow_executor @@ -65,6 +69,19 @@ def get(self, _include=None, filters=None, pagination=None, ) +class _SnapshotCreateArgs(pydantic.BaseModel): + include_credentials: Optional[bool] = True + include_logs: Optional[bool] = True + include_events: Optional[bool] = True + queue: Optional[bool] = False + tempdir_path: Optional[str] = None + + +class _SnapshotStatusUpdateArgs(pydantic.BaseModel): + status: str + error: Optional[str] = '' + + class SnapshotsId(SecuredResource): @swagger.operation( @@ -93,24 +110,8 @@ def get(self, snapshot_id, _include=None, **kwargs): @rest_decorators.marshal_with(models.Execution) def put(self, snapshot_id): rest_utils.validate_inputs({'snapshot_id': snapshot_id}) - request_dict = rest_utils.get_json_and_verify_params() - include_credentials = rest_utils.verify_and_convert_bool( - 'include_credentials', - request_dict.get('include_credentials', True) - ) - include_logs = rest_utils.verify_and_convert_bool( - 'include_logs', - request_dict.get('include_logs', True) - ) - include_events = rest_utils.verify_and_convert_bool( - 'include_events', - request_dict.get('include_events', True) - ) - queue = rest_utils.verify_and_convert_bool( - 'queue', - request_dict.get('queue', False) - ) - tempdir_path = request_dict.get('tempdir_path') + args = _SnapshotCreateArgs.parse_obj(request.json) + tempdir_path = args.tempdir_path if tempdir_path and not os.access(tempdir_path, os.W_OK): raise manager_exceptions.ForbiddenError( f'Temp dir cannot be created inside unwriteable location ' @@ -119,11 +120,11 @@ def put(self, snapshot_id): execution, messages = get_resource_manager().create_snapshot( snapshot_id, - include_credentials, - include_logs, - include_events, + args.include_credentials, + args.include_logs, + args.include_events, True, - queue, + args.queue, tempdir_path, ) workflow_executor.execute_workflow(messages) @@ -161,11 +162,11 @@ def delete(self, snapshot_id): def patch(self, snapshot_id): """Update snapshot status by id """ - request_dict = rest_utils.get_json_and_verify_params({'status'}) + args = _SnapshotStatusUpdateArgs.parse_obj(request.json) snapshot = get_storage_manager().get(models.Snapshot, snapshot_id) - snapshot.status = request_dict['status'] - snapshot.error = request_dict.get('error', '') + snapshot.status = args.status + snapshot.error = args.error get_storage_manager().update(snapshot) @@ -223,6 +224,13 @@ def get(self, snapshot_id): return get_storage_handler().proxy(snapshot_uri) +class _SnapshotRestoreArgs(pydantic.BaseModel): + force: Optional[bool] = False + restore_certificates: Optional[bool] = False + no_reboot: Optional[bool] = False + timeout: Optional[int] = 300 + + class SnapshotsIdRestore(SecuredResource): @swagger.operation( @@ -233,33 +241,20 @@ class SnapshotsIdRestore(SecuredResource): @authorize('snapshot_restore') @rest_decorators.marshal_with(models.Snapshot) def post(self, snapshot_id): - request_dict = rest_utils.get_json_and_verify_params() - force = rest_utils.verify_and_convert_bool( - 'force', - request_dict['force'] - ) - restore_certificates = rest_utils.verify_and_convert_bool( - 'restore_certificates', - request_dict.get('restore_certificates', False) - ) - no_reboot = rest_utils.verify_and_convert_bool( - 'no_reboot', - request_dict.get('no_reboot', False) - ) - if no_reboot and not restore_certificates: + args = _SnapshotCreateArgs.parse_obj(request.json) + if args.no_reboot and not args.restore_certificates: raise manager_exceptions.BadParametersError( '`no_reboot` is only relevant when `restore_certificates` is ' 'activated') - default_timeout_sec = 300 - request_timeout = request_dict.get('timeout', default_timeout_sec) + request_timeout = args.timeout timeout = rest_utils.convert_to_int(request_timeout) execution, messages = get_resource_manager().restore_snapshot( snapshot_id, - force, + args.force, True, timeout, - restore_certificates, - no_reboot + args.restore_certificates, + args.no_reboot, ) workflow_executor.execute_workflow(messages) return execution, 200 diff --git a/rest-service/manager_rest/rest/resources_v2_1/deployment_update.py b/rest-service/manager_rest/rest/resources_v2_1/deployment_update.py index 8f44bfd703..86b1a8551c 100644 --- a/rest-service/manager_rest/rest/resources_v2_1/deployment_update.py +++ b/rest-service/manager_rest/rest/resources_v2_1/deployment_update.py @@ -13,9 +13,10 @@ # * See the License for the specific language governing permissions and # * limitations under the License. +import pydantic import uuid from datetime import datetime -from typing import Dict +from typing import Any, Dict, List, Optional from flask import request @@ -33,16 +34,36 @@ from manager_rest.resource_manager import get_resource_manager from .. import rest_decorators from ..rest_utils import ( - get_json_and_verify_params, lookup_and_validate_user, parse_datetime_string, remove_invalid_keys, valid_user, - verify_and_convert_bool, wait_for_execution, ) +class _DeploymentUpdateStartArgs(pydantic.BaseModel): + blueprint_id: Optional[str] = None + preview: Optional[bool] = False + runtime_only_evaluation: Optional[bool] = None + force: Optional[bool] = False + inputs: Optional[Dict[str, Any]] = {} + reinstall_list: Optional[List[str]] = [] + blueprint: Optional[str] = None + reevaluate_active_statuses: Optional[bool] = False + auto_correct_types: Optional[bool] = False + update_plugins: Optional[bool] = True + install_first: Optional[bool] = False + ignore_failure: Optional[bool] = False + skip_reinstall: Optional[bool] = False + skip_install: Optional[bool] = False + skip_uninstall: Optional[bool] = False + skip_drift_check: Optional[bool] = False + force_reinstall: Optional[bool] = False + skip_heal: Optional[bool] = False + workflow_id: Optional[str] = None + + class DeploymentUpdate(SecuredResource): @authorize('deployment_update_create') @rest_decorators.marshal_with(models.DeploymentUpdate) @@ -72,17 +93,17 @@ def put(self, id, phase): def _initiate(self, deployment_id): sm = get_storage_manager() rm = get_resource_manager() - preview = verify_and_convert_bool( - 'preview', request.json.get('preview', False)) - runtime_eval = request.json.get('runtime_only_evaluation') - force = verify_and_convert_bool( - 'force', request.json.get('force', False)) + + args = _DeploymentUpdateStartArgs.parse_obj(request.json) + runtime_eval = args.runtime_only_evaluation with sm.transaction() as tx: - blueprint, inputs, reinstall_list = \ - self._get_and_validate_blueprint_and_inputs(deployment_id, - request.json) deployment = sm.get(models.Deployment, deployment_id) + if args.blueprint_id: + blueprint = sm.get(models.Blueprint, args.blueprint_id) + else: + blueprint = deployment.blueprint + inputs = args.inputs if runtime_eval is None: runtime_eval = deployment.runtime_only_evaluation new_inputs = deployment.inputs.copy() @@ -93,7 +114,7 @@ def _initiate(self, deployment_id): new_blueprint=blueprint or deployment.blueprint, old_inputs=deployment.inputs, new_inputs=new_inputs, - preview=preview, + preview=args.preview, runtime_only_evaluation=runtime_eval, state=STATES.UPDATING, ) @@ -101,30 +122,23 @@ def _initiate(self, deployment_id): 'update_id': dep_up.id, 'blueprint_id': blueprint.id, 'inputs': inputs, - 'preview': preview, + 'preview': args.preview, 'runtime_only_evaluation': runtime_eval, - 'force': force, - 'workflow_id': request.json.get('workflow_id', None), - 'reinstall_list': reinstall_list, + 'force': args.force, + 'workflow_id': args.workflow_id, + 'reevaluate_active_statuses': args.reevaluate_active_statuses, + 'auto_correct_types': args.auto_correct_types, + 'update_plugins': args.update_plugins, + 'install_first': args.install_first, + 'ignore_failure': args.ignore_failure, + 'skip_reinstall': args.skip_reinstall, + 'skip_uninstall': args.skip_uninstall, + 'reinstall_list': args.reinstall_list, + 'skip_install': args.skip_install, + 'skip_drift_check': args.skip_drift_check, + 'force_reinstall': args.force_reinstall, + 'skip_heal': args.skip_heal, } - # boolean params - for name, default in [ - ('reevaluate_active_statuses', False), - ('auto_correct_types', False), - ('update_plugins', True), - ('install_first', False), - ('ignore_failure', False), - ('skip_reinstall', False), - ('skip_uninstall', False), - ('skip_install', False), - ('skip_drift_check', False), - ('force_reinstall', False), - ('skip_heal', False), - ]: - execution_args[name] = verify_and_convert_bool( - name, - request.json.get(name, default), - ) update_exec = models.Execution( deployment=deployment, @@ -133,6 +147,7 @@ def _initiate(self, deployment_id): allow_custom_parameters=True, ) sm.put(update_exec) + if current_execution and \ current_execution.workflow_id == 'csys_update_deployment': # if we're created from a update_deployment workflow, join its @@ -146,7 +161,7 @@ def _initiate(self, deployment_id): messages = rm.prepare_executions( [update_exec], allow_overlapping_running_wf=True, - force=force, + force=args.force, ) except manager_exceptions.DependentExistsError: dep_up.state = STATES.FAILED @@ -155,30 +170,28 @@ def _initiate(self, deployment_id): raise workflow_executor.execute_workflow(messages) - if preview: + if args.preview: wait_for_execution(sm, dep_up.execution.id) sm.refresh(dep_up) return dep_up - @staticmethod - def _get_and_validate_blueprint_and_inputs(deployment_id, request_json): - inputs = request_json.get('inputs', {}) - reinstall_list = request_json.get('reinstall_list', []) - blueprint_id = request_json.get('blueprint_id') - if not isinstance(inputs, dict): - raise manager_exceptions.BadParametersError( - 'parameter `inputs` must be of type `dict`') - if not isinstance(reinstall_list, list): - raise manager_exceptions.BadParametersError( - 'parameter `reinstall_list` must be of type `list`') - if blueprint_id is None: - deployment = get_storage_manager().get(models.Deployment, - deployment_id) - blueprint = deployment.blueprint - else: - blueprint = get_storage_manager().get(models.Blueprint, - blueprint_id) - return blueprint, inputs, reinstall_list + +class _CreateDeploymentUpdateArgs(_DeploymentUpdateStartArgs): + deployment_id: str + state: Optional[str] = None + inputs: Optional[Dict[str, Any]] = None + old_blueprint_id: Optional[str] = None + execution_id: Optional[str] = None + created_at: Optional[str] = None + created_by: Optional[str] = None + + +class _UpdateDeploymentUpdateArgs(pydantic.BaseModel): + state: Optional[str] = None + plan: Optional[Dict[str, Any]] = None + steps: Optional[List[Any]] = None + nodes: Optional[Any] = None + node_instances: Optional[Any] = None class DeploymentUpdateId(SecuredResource): @@ -202,17 +215,9 @@ def get(self, update_id, _include=None): @authorize('deployment_update_create') @rest_decorators.marshal_with(models.DeploymentUpdate) def put(self, update_id): - params = get_json_and_verify_params({ - 'deployment_id': {'type': str, 'required': True}, - 'state': {'optional': True}, - 'inputs': {'optional': True}, - 'blueprint_id': {'optional': True}, - 'created_at': {'optional': True}, - 'created_by': {'optional': True}, - 'execution_id': {'optional': True}, - }) + params = _CreateDeploymentUpdateArgs.parse_obj(request.json) sm = get_storage_manager() - if params.get('execution_id') is None and not current_execution: + if params.execution_id is None and not current_execution: # Only allow non-execution creation of dep updates for restores raise manager_exceptions.ForbiddenError( 'Deployment update objects can only be created by executions') @@ -233,7 +238,7 @@ def put(self, update_id): add_steps = False with sm.transaction(): - dep = sm.get(models.Deployment, params['deployment_id']) + dep = sm.get(models.Deployment, params.deployment_id) dep_upd = sm.get(models.DeploymentUpdate, update_id, fail_silently=True) if dep_upd is None: @@ -241,29 +246,29 @@ def put(self, update_id): id=update_id, _execution_fk=execution._storage_id, ) - dep_upd.state = params.get('state') or STATES.UPDATING - dep_upd.new_inputs = params.get('inputs') - if params.get('blueprint_id'): + dep_upd.state = params.state or STATES.UPDATING + dep_upd.new_inputs = params.inputs + if params.blueprint_id: dep_upd.new_blueprint = sm.get( - models.Blueprint, params['blueprint_id']) + models.Blueprint, params.blueprint_id) if created_at: dep_upd.created_at = created_at if created_by: dep_upd.creator = created_by - if params.get('old_blueprint_id'): + if params.old_blueprint_id: dep_upd.old_blueprint = sm.get( - models.Blueprint, params['old_blueprint_id']) + models.Blueprint, params.old_blueprint_id) for attr in [ 'runtime_only_evaluation', 'deployment_plan', 'steps', 'deployment_update_node_instances', 'modified_entity_ids', 'central_plugins_to_install', 'central_plugins_to_uninstall', 'old_inputs', 'deployment_update_nodes', 'visibility', ]: - if params.get(attr) is not None: + if getattr(params, attr, None) is not None: if attr == 'steps': add_steps = True else: - setattr(dep_upd, attr, params[attr]) + setattr(dep_upd, attr, getattr(params, attr, None)) dep_upd.set_deployment(dep) if add_steps: @@ -304,37 +309,35 @@ def _prepare_raw_steps(self, dep_update, raw_steps): @authorize('deployment_update_update') @rest_decorators.marshal_with(models.DeploymentUpdate) def patch(self, update_id): - params = get_json_and_verify_params({ - 'state': {'optional': True}, - 'plan': {'optional': True}, - 'steps': {'optional': True}, - 'nodes': {'optional': True}, - 'node_instances': {'optional': True}, - }) + params = _UpdateDeploymentUpdateArgs.parse_obj(request.json) sm = get_storage_manager() with sm.transaction(): dep_upd = sm.get(models.DeploymentUpdate, update_id) - if params.get('state'): - dep_upd.state = params['state'] - if params.get('plan'): - dep_upd.deployment_plan = params['plan'] - if params.get('steps'): - for step_spec in params['steps']: + if params.state: + dep_upd.state = params.state + if params.plan: + dep_upd.deployment_plan = params.plan + if params.steps: + for step_spec in params.steps: step = models.DeploymentUpdateStep( id=str(uuid.uuid4()), **step_spec ) step.set_deployment_update(dep_upd) - if params.get('nodes'): - dep_upd.deployment_update_nodes = params['nodes'] - if params.get('node_instances'): + if params.nodes: + dep_upd.deployment_update_nodes = params.nodes + if params.node_instances: dep_upd.deployment_update_node_instances = \ - params['node_instances'] + params.node_instances if dep_upd.state == STATES.SUCCESSFUL and not dep_upd.preview: dep_upd.deployment.updated_at = datetime.utcnow() return dep_upd +class _BulkCreateDepUpdatesArgs(pydantic.BaseModel): + deployment_updates: List[Any] = [] + + class DeploymentUpdates(SecuredResource): @swagger.operation( responseClass='List[{0}]'.format(models.DeploymentUpdate.__name__), @@ -374,16 +377,14 @@ def get(self, ) @authorize('deployment_update_create') def post(self): - request_dict = get_json_and_verify_params({ - 'deployment_updates': {'type': list}, - }) + params = _BulkCreateDepUpdatesArgs.parse_obj(request.json) sm = get_storage_manager() - raw_updates = request_dict['deployment_updates'] + raw_updates = params.deployment_updates raw_steps = [] for raw_update in raw_updates: raw_steps.extend(raw_update.pop('steps', [])) if not raw_updates: - return None, 204 + return "", 204 user_cache: Dict[str, models.User] = {} tenant_cache: Dict[str, models.Tenant] = {} with sm.transaction(): @@ -398,7 +399,7 @@ def post(self): models.DeploymentUpdateStep.__table__.insert(), raw_steps, ) - return None, 201 + return "[]", 201 def _prepare_raw_updates(self, sm, raw_updates, user_cache, tenant_cache): if any(item.get('creator') for item in raw_updates): diff --git a/rest-service/manager_rest/rest/resources_v2_1/plugins.py b/rest-service/manager_rest/rest/resources_v2_1/plugins.py index c178dfa5cd..c51f5227af 100644 --- a/rest-service/manager_rest/rest/resources_v2_1/plugins.py +++ b/rest-service/manager_rest/rest/resources_v2_1/plugins.py @@ -13,13 +13,20 @@ # * See the License for the specific language governing permissions and # * limitations under the License. +import pydantic +from flask import request +from typing import Optional + from manager_rest.rest import swagger from manager_rest.storage import models from manager_rest.security.authorization import authorize from manager_rest.resource_manager import get_resource_manager from .. import resources_v2 -from ..rest_utils import verify_and_convert_bool, get_json_and_verify_params + + +class _PluginDeleteArgs(pydantic.BaseModel): + force: Optional[bool] = False class PluginsId(resources_v2.PluginsId): @@ -31,12 +38,10 @@ class PluginsId(resources_v2.PluginsId): ) @authorize('plugin_delete') def delete(self, plugin_id, **kwargs): - """ - Delete plugin by ID - """ - request_dict = get_json_and_verify_params() - force = verify_and_convert_bool( - 'force', request_dict.get('force', False) + """Delete plugin by ID""" + params = _PluginDeleteArgs.parse_obj(request.json) + get_resource_manager().remove_plugin( + plugin_id=plugin_id, + force=params.force, ) - get_resource_manager().remove_plugin(plugin_id=plugin_id, force=force) - return None, 204 + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v3/events.py b/rest-service/manager_rest/rest/resources_v3/events.py index 1dc0339365..4418d2dc89 100644 --- a/rest-service/manager_rest/rest/resources_v3/events.py +++ b/rest-service/manager_rest/rest/resources_v3/events.py @@ -1,6 +1,10 @@ -from datetime import datetime import itertools import json +import pydantic + +from datetime import datetime +from flask import request +from typing import Any, List, Optional from ..resources_v2 import Events as v2_Events @@ -8,7 +12,6 @@ from manager_rest.storage import get_storage_manager, models, db from manager_rest.security.authorization import (authorize, check_user_action_allowed) -from manager_rest.rest import rest_utils from manager_rest.rest.rest_decorators import detach_globals from manager_rest.execution_token import current_execution @@ -21,6 +24,15 @@ def _strip_nul(text): return text.replace('\x00', '') +class _EventsCreateArgs(pydantic.BaseModel): + events: Optional[List[Any]] = [] + logs: Optional[List[Any]] = [] + execution_id: Optional[str] = None + execution_group_id: Optional[str] = None + manager_name: Optional[str] = None + agent_name: Optional[str] = None + + class Events(v2_Events): """Events resource. @@ -33,15 +45,7 @@ class Events(v2_Events): @authorize('event_create', allow_if_execution=True) @detach_globals def post(self): - request_dict = rest_utils.get_json_and_verify_params({ - 'events': {'optional': True}, - 'logs': {'optional': True}, - 'execution_id': {'optional': True}, - 'execution_group_id': {'optional': True}, - 'manager_name': {'optional': True}, - 'agent_name': {'optional': True}, - }) - + request_dict = _EventsCreateArgs.parse_obj(request.json).dict() raw_events = request_dict.get('events') or [] raw_logs = request_dict.get('logs') or [] if any( @@ -83,7 +87,7 @@ def post(self): 'agent_name': request_dict.get('agent_name'), }) if not raw_events and not raw_logs: - return None, 204 + return "", 204 with sm.transaction(): for ev in raw_events: @@ -92,7 +96,7 @@ def post(self): for log in raw_logs: db.session.execute( self._log_from_raw_log(sm, log, exc_params)) - return None, 201 + return "[]", 201 def _event_from_raw_event(self, sm, raw_event, exc_params): task_error_causes = raw_event['context'].get('task_error_causes') diff --git a/rest-service/manager_rest/rest/resources_v3/manager.py b/rest-service/manager_rest/rest/resources_v3/manager.py index a287978cda..31791d9fc1 100644 --- a/rest-service/manager_rest/rest/resources_v3/manager.py +++ b/rest-service/manager_rest/rest/resources_v3/manager.py @@ -1,3 +1,7 @@ +import pydantic +import string +from typing import Optional + from flask import request from flask import current_app @@ -123,6 +127,78 @@ def get(self, **_): return {} +def _validate_allowed_substitutions(param_name, param_value, allowed): + if param_value is None: + return + f = string.Formatter() + invalid = [] + for _, field, _, _ in f.parse(param_value): + if field is None: + # This will occur at the end of a string unless the string ends at + # the end of a field + continue + if field not in allowed: + invalid.append(field) + if invalid: + raise pydantic.ValidationError( + '{candidate_name} has invalid parameters.\n' + 'Invalid parameters found: {invalid}.\n' + 'Allowed: {allowed}'.format( + candidate_name=param_name, + invalid=', '.join(invalid), + allowed=', '.join(allowed), + ) + ) + + +class _LDAPConfigArgs(pydantic.BaseModel): + ldap_server: str + ldap_domain: str + ldap_username: Optional[str] = None + ldap_password: Optional[str] = None + ldap_is_active_directory: Optional[bool] = False + ldap_dn_extra: Optional[str] = None + ldap_ca_cert: Optional[str] = None + ldap_nested_levels: Optional[int] = None + ldap_bind_format: Optional[str] = None + ldap_base_dn: Optional[str] = None + ldap_group_dn: Optional[str] = None + ldap_group_member_filter: Optional[str] = None + ldap_user_filter: Optional[str] = None + ldap_attribute_email: Optional[str] = None + ldap_attribute_first_name: Optional[str] = None + ldap_attribute_last_name: Optional[str] = None + ldap_attribute_uid: Optional[str] = None + ldap_attribute_group_membership: Optional[str] = None + + @pydantic.validator('ldap_bind_format') + def ldap_bind_format_substitutions(cls, v): + _validate_allowed_substitutions('ldap_bind_format', v, allowed=[ + 'username', 'domain', 'base_dn', 'domain_dn', 'group_dn', + ]) + + @pydantic.validator('ldap_group_dn') + def ldap_group_dn_substitutions(cls, v): + _validate_allowed_substitutions( + 'ldap_group_dn', v, + allowed=['base_dn', 'domain_dn'], + ) + + @pydantic.validator('ldap_group_member_filter') + def ldap_group_member_filter_substitutions(cls, v): + _validate_allowed_substitutions( + 'ldap_group_member_filter', v, + allowed=['object_dn'], + ) + + @pydantic.validator('ldap_user_filter') + def ldap_user_filter_substitutions(cls, v): + _validate_allowed_substitutions( + 'ldap_user_filter', v, + allowed=['username', 'base_dn', 'domain_dn', 'group_dn'], + ) + + class LdapAuthentication(SecuredResource): @authorize('ldap_set') @rest_decorators.marshal_with(LdapResponse) @@ -168,41 +244,7 @@ def _validate_set_ldap_request(self): if not premium_enabled: raise MethodNotAllowedError('LDAP is only supported in the ' 'Cloudify premium edition.') - base_substitutions = ['base_dn', 'domain_dn', 'group_dn'] - ldap_config = rest_utils.get_json_and_verify_params({ - 'ldap_server': {}, - 'ldap_domain': {}, - 'ldap_username': {'optional': True}, - 'ldap_password': {'optional': True}, - 'ldap_is_active_directory': {'optional': True}, - 'ldap_dn_extra': {'optional': True}, - 'ldap_ca_cert': {'optional': True}, - 'ldap_nested_levels': {'optional': True}, - 'ldap_bind_format': { - 'optional': True, - 'allowed_substitutions': [ - 'username', 'domain'] + base_substitutions, - }, - 'ldap_group_dn': { - 'optional': True, - 'allowed_substitutions': ['base_dn', 'domain_dn'], - }, - 'ldap_base_dn': {'optional': True}, - 'ldap_group_member_filter': { - 'optional': True, - 'allowed_substitutions': ['object_dn'] - }, - 'ldap_user_filter': { - 'optional': True, - 'allowed_substitutions': ['username'] + base_substitutions, - }, - 'ldap_attribute_email': {'optional': True}, - 'ldap_attribute_first_name': {'optional': True}, - 'ldap_attribute_last_name': {'optional': True}, - 'ldap_attribute_uid': {'optional': True}, - 'ldap_attribute_group_membership': {'optional': True}, - }) - + ldap_config = _LDAPConfigArgs.parse_obj(request.json).dict() if ldap_config.get('ldap_nested_levels') is None: ldap_config['ldap_nested_levels'] = 1 else: @@ -215,12 +257,6 @@ def _validate_set_ldap_request(self): # not a string. ldap_config[attr] = '' - ldap_config['ldap_is_active_directory'] = \ - rest_utils.verify_and_convert_bool( - 'ldap_is_active_directory', - ldap_config.get('ldap_is_active_directory') or False - ) - if ldap_config['ldap_server'].startswith('ldaps://'): if 'ldap_ca_cert' not in ldap_config: raise BadParametersError( diff --git a/rest-service/manager_rest/rest/resources_v3/secrets.py b/rest-service/manager_rest/rest/resources_v3/secrets.py index 023366c6e1..7a8a65afd7 100644 --- a/rest-service/manager_rest/rest/resources_v3/secrets.py +++ b/rest-service/manager_rest/rest/resources_v3/secrets.py @@ -1,7 +1,8 @@ import json import jsonschema -from typing import Dict, List, Any, Iterable +import pydantic +from typing import Dict, List, Any, Iterable, Optional from flask import request from flask_security import current_user @@ -37,6 +38,18 @@ update_imported_secret) +class _SecretArgs(pydantic.BaseModel): + value: Optional[str] = None + provider: Optional[str] = None + provider_options: Optional[dict] = None + secret_schema: Optional[dict] = pydantic.Field( + alias='schema', default=None) + update_if_exists: Optional[bool] = False + is_hidden_value: Optional[bool] = False + visibility: Optional[VisibilityState] = None + creator: Optional[str] = None + + class SecretsKey(SecuredResource): @authorize('secret_get') @rest_decorators.marshal_with(models.Secret) @@ -117,12 +130,7 @@ def patch(self, key): 'least one parameter to update') secret = get_storage_manager().get(models.Secret, key) self._validate_secret_modification_permitted(secret) - self._update_is_hidden_value(secret) - self._update_visibility(secret) - self._update_value(secret) - self._update_owner(secret) - self._update_provider(secret) - self._update_provider_options(secret) + self._update_secret(secret) secret.updated_at = utils.get_formatted_timestamp() return get_storage_manager().update(secret, validate_global=True) @@ -136,63 +144,32 @@ def delete(self, key): secret = storage_manager.get(models.Secret, key) self._validate_secret_modification_permitted(secret) storage_manager.delete(secret, validate_global=True) - return None, 204 + return "", 204 @staticmethod def _get_secret_params(key): rest_utils.validate_inputs({'key': key}) - request_dict = rest_utils.get_json_and_verify_params( - { - 'value': {}, - 'schema': { - 'type': dict, - 'optional': True, - }, - 'provider': { - 'type': str, - 'optional': True, - }, - 'provider_options': { - 'type': dict, - 'optional': True, - }, - }, - ) - value = request_dict['value'] - schema = request_dict.get('schema') or None + params = _SecretArgs.parse_obj(request.json) - if schema: + value = params.value + if params.secret_schema: try: - jsonschema.validate(value, schema) + jsonschema.validate(value, params.secret_schema) except jsonschema.ValidationError as e: raise manager_exceptions.ConflictError( f'Error validating secret value: {e}') except jsonschema.SchemaError as e: raise manager_exceptions.BadParametersError( - f'Invalid secret JSON schema {schema}: {e}') + f'Invalid secret JSON schema {params.secret_schema}: {e}') value = json.dumps(value) - update_if_exists = rest_utils.verify_and_convert_bool( - 'update_if_exists', - request_dict.get('update_if_exists', False), - ) - is_hidden_value = rest_utils.verify_and_convert_bool( - 'is_hidden_value', - request_dict.get('is_hidden_value', False), - ) - visibility_param = rest_utils.get_visibility_parameter( - optional=True, - valid_values=VisibilityState.STATES, - ) visibility = get_resource_manager().get_resource_visibility( models.Secret, key, - visibility_param + params.visibility, ) - provider = None - - if provider_name := request_dict.get('provider'): + if provider_name := params.provider: storage_manager = get_storage_manager() provider = storage_manager.get( @@ -200,16 +177,14 @@ def _get_secret_params(key): provider_name, ) - provider_options = request_dict.get('provider_options') or None - secret_params = { - 'value': value, - 'schema': schema, - 'update_if_exists': update_if_exists, + 'value': params.value, + 'update_if_exists': params.update_if_exists, 'visibility': visibility, - 'is_hidden_value': is_hidden_value, + 'is_hidden_value': params.is_hidden_value, 'provider': provider, - 'provider_options': provider_options, + 'provider_options': params.provider_options, + 'schema': params.secret_schema, } return secret_params @@ -221,79 +196,52 @@ def _validate_secret_modification_permitted(self, secret): 'secret `{1}`'.format(current_user.username, secret.key) ) - def _update_is_hidden_value(self, secret): - is_hidden_value = request.json.get('is_hidden_value') - if is_hidden_value is None: - return - is_hidden_value = rest_utils.verify_and_convert_bool( - 'is_hidden_value', - is_hidden_value - ) - # Only the creator of the secret and the admins can change a secret - # to be hidden value - if not rest_utils.is_hidden_value_permitted(secret): - raise manager_exceptions.ForbiddenError( - 'User `{0}` is not permitted to modify the secret `{1}` ' - 'to be hidden value'.format(current_user.username, secret.key) - ) - secret.is_hidden_value = is_hidden_value + def _update_secret(self, secret): + args = _SecretArgs.parse_obj(request.json) + if args.is_hidden_value is not None: + if not rest_utils.is_hidden_value_permitted(secret): + raise manager_exceptions.ForbiddenError( + f'User `{current_user.username}` is not permitted to ' + f'modify the secret `{secret.key}` to be hidden value' + ) + secret.is_hidden_value = args.is_hidden_value - def _update_visibility(self, secret): - visibility = rest_utils.get_visibility_parameter( - optional=True, - valid_values=VisibilityState.STATES, - ) - if visibility: + if args.visibility: get_resource_manager().validate_visibility_value( secret, - visibility + args.visibility ) - secret.visibility = visibility - - def _update_value(self, secret): - request_dict = rest_utils.get_json_and_verify_params({ - 'value': {'optional': True} - }) - value = request_dict.get('value') - if not value: - return - if secret.schema: - try: - jsonschema.validate(value, secret.schema) - except jsonschema.ValidationError as e: - raise manager_exceptions.ConflictError( - f'Error validating secret value: {e}') - secret.value = encrypt(value) - - def _update_owner(self, secret): - request_dict = rest_utils.get_json_and_verify_params({ - 'creator': {'type': str, 'optional': True} - }) - creator_username = request_dict.get('creator') - if not creator_username: - return - check_user_action_allowed('set_owner', None, True) - creator = rest_utils.valid_user(request_dict.get('creator')) - if creator: - secret.creator = creator - - @staticmethod - def _update_provider(secret): - request_dict = rest_utils.get_json_and_verify_params({ - 'provider': {'type': str, 'optional': True} - }) - provider_name = request_dict.get('provider') - if not provider_name: - return - - storage_manager = get_storage_manager() + secret.visibility = args.visibility + + if args.value: + if secret.schema: + try: + jsonschema.validate(args.value, secret.schema) + except jsonschema.ValidationError as e: + raise manager_exceptions.ConflictError( + f'Error validating secret value: {e}') + secret.value = encrypt(args.value) + + if args.creator: + check_user_action_allowed('set_owner', None, True) + creator = rest_utils.valid_user(args.creator) + if creator: + secret.creator = creator + + if args.provider: + storage_manager = get_storage_manager() - provider = storage_manager.get( - models.SecretsProvider, - provider_name, - ) + secret.provider = storage_manager.get( + models.SecretsProvider, + args.provider, + ) - secret.provider = provider + if args.provider_options: + secret.provider_options = encrypt( + json.dumps( + args.provider_options, + ), + ) @staticmethod def _update_provider_options(secret): @@ -325,17 +273,11 @@ class Secrets(SecuredResource): @rest_decorators.create_filters(models.Secret) @rest_decorators.paginate @rest_decorators.sortable(models.Secret) - @rest_decorators.all_tenants @rest_decorators.search('id') def get(self, _include=None, filters=None, pagination=None, sort=None, - all_tenants=None, search=None, **kwargs): - """ - List secrets - """ - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + search=None, **kwargs): + """List secrets""" + args = rest_utils.ListQuery.parse_obj(request.args) return get_storage_manager().list( models.Secret, include=_include, @@ -343,22 +285,21 @@ def get(self, _include=None, filters=None, pagination=None, sort=None, substr_filters=search, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results, + all_tenants=args.all_tenants, + get_all_results=args.get_all_results ) class SecretsSetVisibility(SecuredResource): - @authorize('secret_update') @rest_decorators.marshal_with(models.Secret) def patch(self, key): """ Set the secret's visibility """ - visibility = rest_utils.get_visibility_parameter() + args = rest_utils.SetVisibilityArgs.parse_obj(request.json) secret = get_storage_manager().get(models.Secret, key) - return get_resource_manager().set_visibility(secret, visibility) + return get_resource_manager().set_visibility(secret, args.visibility) class SecretsExport(SecuredResource): @@ -420,6 +361,13 @@ def _encrypt_values(secrets_list, password): secret['encrypted'] = True +class _SecretsImportArgs(pydantic.BaseModel): + secrets_list: List[Any] + tenant_map: Optional[Dict[str, Any]] = None + passphrase: Optional[str] = None + override_collisions: bool = False + + class SecretsImport(SecuredResource): @authorize('secret_import') def post(self): @@ -443,13 +391,7 @@ def post(self): @staticmethod def _validate_import_secrets_params(): - request_dict = rest_utils.get_json_and_verify_params({ - 'secrets_list': {'type': list, 'optional': False}, - 'tenant_map': {'type': dict, 'optional': True}, - 'passphrase': {'type': str, 'optional': True}, - 'override_collisions': {'type': bool, 'optional': False} - }) - return request_dict + return _SecretsImportArgs.parse_obj(request.json).dict() def _import_secrets(self, secrets_list, tenant_map, passphrase, existing_tenants, colliding_secrets, override): @@ -499,7 +441,8 @@ def _check_timestamp_and_owner(self, secret, secret_errors): def _validate_is_hidden_field(self, secret, missing_fields, secret_errors): if self._is_missing_field(secret, 'is_hidden_value', missing_fields): return - is_hidden_value = self._validate_boolean(secret, 'is_hidden_value') + is_hidden_value = rest_utils.verify_and_convert_bool( + secret['is_hidden_value']) if is_hidden_value is None: secret_errors['is_hidden_value'] = 'Not boolean' else: @@ -556,7 +499,7 @@ def _handle_encryption(self, secret, encryption_key, invalid_value = self._is_missing_field(secret, 'value', missing_fields) if self._is_missing_field(secret, 'encrypted', missing_fields): return - is_encrypted = self._validate_boolean(secret, 'encrypted') + is_encrypted = rest_utils.verify_and_convert_bool(secret['encrypted']) if is_encrypted is None: secret_errors['encrypted'] = 'Not boolean' elif is_encrypted and not invalid_value: @@ -599,14 +542,6 @@ def _is_missing_field(secret, field, missing_fields): return True return False - @staticmethod - def _validate_boolean(secret, field): - try: - field = rest_utils.verify_and_convert_bool('', secret[field]) - except manager_exceptions.BadParametersError: - return None - return field - @staticmethod def _validate_tenant_map(tenant_map, existing_tenants): if not tenant_map: diff --git a/rest-service/manager_rest/rest/resources_v3/tenants.py b/rest-service/manager_rest/rest/resources_v3/tenants.py index 9924029a3c..3d38fb08d4 100644 --- a/rest-service/manager_rest/rest/resources_v3/tenants.py +++ b/rest-service/manager_rest/rest/resources_v3/tenants.py @@ -1,4 +1,5 @@ -from typing import Any +import pydantic +from typing import Any, Optional from flask import request @@ -30,6 +31,10 @@ TenantsListResource = SecuredResource +class _TenantsListQuery(rest_utils.ListQuery): + _get_data: Optional[bool] = False + + class Tenants(TenantsListResource): @authorize('tenant_list') @rest_decorators.marshal_with(TenantResponse) @@ -46,21 +51,17 @@ def get(self, multi_tenancy=None, _include=None, filters=None, def _authorize_with_get_data(): pass - if rest_utils.verify_and_convert_bool( - 'get_data', request.args.get('_get_data', False)): + args = _TenantsListQuery.parse_obj(request.args) + if args._get_data: _authorize_with_get_data() - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) if multi_tenancy: tenants = multi_tenancy.list_tenants(_include, filters, pagination, sort, search, - get_all_results) + args.get_all_results) else: # In community edition we have only the `default_tenant`, so it # should be safe to return it like this @@ -91,6 +92,10 @@ def _clear_tenant_rabbit_creds(tenant): tenant.rabbitmq_vhost = None +class _TenantCreateArgs(pydantic.BaseModel): + rabbitmq_password: Optional[str] = None + + class TenantsId(SecuredMultiTenancyResource): @authorize('tenant_create') @rest_decorators.marshal_with(TenantResponse) @@ -100,11 +105,10 @@ def post(self, tenant_name, multi_tenancy): """ rest_utils.validate_inputs({'tenant_name': tenant_name}) if request.content_length: - request_dict = rest_utils.get_json_and_verify_params({ - 'rabbitmq_password': {'type': str, 'optional': True}, - }) + args = _TenantCreateArgs.parse_obj(request.json) + rabbitmq_password = args.rabbitmq_password else: - request_dict = {} + rabbitmq_password = None if tenant_name in ('users', 'user-groups'): raise BadParametersError( '{0!r} is not allowed as a tenant name ' @@ -114,7 +118,8 @@ def post(self, tenant_name, multi_tenancy): ) return multi_tenancy.create_tenant( tenant_name, - request_dict.get('rabbitmq_password')) + rabbitmq_password, + ) @authorize('tenant_get', get_tenant_from='param') @rest_decorators.marshal_with(TenantResponse) @@ -145,7 +150,13 @@ def delete(self, tenant_name, multi_tenancy): """ rest_utils.validate_inputs({'tenant_name': tenant_name}) multi_tenancy.delete_tenant(tenant_name) - return None, 204 + return "", 204 + + +class _UserTenantArgs(pydantic.BaseModel): + tenant_name: str + username: str + role: Optional[str] = constants.DEFAULT_TENANT_ROLE class TenantUsers(SecuredMultiTenancyResource): @@ -156,25 +167,10 @@ def put(self, multi_tenancy): """ Add a user to a tenant """ - request_dict = rest_utils.get_json_and_verify_params( - { - 'tenant_name': { - 'type': str - }, - 'username': { - 'type': str - }, - 'role': { - 'type': str - }, - }, - ) + request_dict = _UserTenantArgs.parse_obj(request.json).dict() rest_utils.validate_inputs(request_dict) role_name = request_dict.get('role') - if role_name: - rest_utils.verify_role(role_name) - else: - role_name = constants.DEFAULT_TENANT_ROLE + rest_utils.verify_role(role_name) return multi_tenancy.add_user_to_tenant( request_dict['username'], @@ -187,19 +183,7 @@ def put(self, multi_tenancy): @rest_decorators.check_external_authenticator('update user in tenant') def patch(self, multi_tenancy): """Update role in user tenant association.""" - request_dict = rest_utils.get_json_and_verify_params( - { - 'tenant_name': { - 'type': str, - }, - 'username': { - 'type': str, - }, - 'role': { - 'type': str, - }, - }, - ) + request_dict = _UserTenantArgs.parse_obj(request.json).dict() rest_utils.validate_inputs(request_dict) role_name = request_dict['role'] rest_utils.verify_role(role_name) @@ -215,14 +199,19 @@ def delete(self, multi_tenancy): """ Remove a user from a tenant """ - request_dict = rest_utils.get_json_and_verify_params({'tenant_name', - 'username'}) + request_dict = _UserTenantArgs.parse_obj(request.json).dict() rest_utils.validate_inputs(request_dict) multi_tenancy.remove_user_from_tenant( request_dict['username'], request_dict['tenant_name'] ) - return None, 204 + return "", 204 + + +class _GroupTenantArgs(pydantic.BaseModel): + tenant_name: str + group_name: str + role: Optional[str] = constants.DEFAULT_TENANT_ROLE class TenantGroups(SecuredMultiTenancyResource): @@ -232,24 +221,10 @@ def put(self, multi_tenancy): """ Add a group to a tenant """ - request_dict = rest_utils.get_json_and_verify_params( - { - 'tenant_name': { - 'type': str - }, - 'group_name': { - 'type': str - }, - 'role': { - 'type': str - }, - }) + request_dict = _GroupTenantArgs.parse_obj(request.json).dict() rest_utils.validate_inputs(request_dict) role_name = request_dict.get('role') - if role_name: - rest_utils.verify_role(role_name) - else: - role_name = constants.DEFAULT_TENANT_ROLE + rest_utils.verify_role(role_name) return multi_tenancy.add_group_to_tenant( request_dict['group_name'], @@ -261,19 +236,7 @@ def put(self, multi_tenancy): @rest_decorators.marshal_with(TenantResponse) def patch(self, multi_tenancy): """Update role in group tenant association.""" - request_dict = rest_utils.get_json_and_verify_params( - { - 'tenant_name': { - 'type': str, - }, - 'group_name': { - 'type': str, - }, - 'role': { - 'type': str, - }, - }, - ) + request_dict = _GroupTenantArgs.parse_obj(request.json).dict() rest_utils.validate_inputs(request_dict) role_name = request_dict['role'] rest_utils.verify_role(role_name) @@ -288,11 +251,10 @@ def delete(self, multi_tenancy): """ Remove a group from a tenant """ - request_dict = rest_utils.get_json_and_verify_params({'tenant_name', - 'group_name'}) + request_dict = _GroupTenantArgs.parse_obj(request.json).dict() rest_utils.validate_inputs(request_dict) multi_tenancy.remove_group_from_tenant( request_dict['group_name'], request_dict['tenant_name'] ) - return None, 204 + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v3/user_groups.py b/rest-service/manager_rest/rest/resources_v3/user_groups.py index fee65bbf70..f6ea72d2ed 100644 --- a/rest-service/manager_rest/rest/resources_v3/user_groups.py +++ b/rest-service/manager_rest/rest/resources_v3/user_groups.py @@ -13,7 +13,10 @@ # * See the License for the specific language governing permissions and # * limitations under the License. -from flask import current_app +import pydantic +from typing import Optional + +from flask import current_app, request from manager_rest import constants from manager_rest.storage import models @@ -36,6 +39,12 @@ SecuredMultiTenancyResource = MissingPremiumFeatureResource +class _UserGroupCreateArgs(pydantic.BaseModel): + group_name: str + ldap_group_dn: Optional[str] = None + role: Optional[str] = constants.DEFAULT_SYSTEM_ROLE + + class UserGroups(SecuredMultiTenancyResource): @authorize('user_group_list') @rest_decorators.marshal_with(GroupResponse) @@ -62,10 +71,10 @@ def post(self, multi_tenancy): """ Create a group """ - request_dict = rest_utils.get_json_and_verify_params() - group_name = request_dict['group_name'] - ldap_group_dn = request_dict.get('ldap_group_dn') - role = request_dict.get('role', constants.DEFAULT_SYSTEM_ROLE) + params = _UserGroupCreateArgs.parse_obj(request.json) + group_name = params.group_name + ldap_group_dn = params.ldap_group_dn + role = params.role rest_utils.verify_role(role, is_system_role=True) rest_utils.validate_inputs({'group_name': group_name}) if group_name == 'users': @@ -78,18 +87,19 @@ def post(self, multi_tenancy): return multi_tenancy.create_group(group_name, ldap_group_dn, role) -class UserGroupsId(SecuredMultiTenancyResource): +class _UpdateUserGroupArgs(pydantic.BaseModel): + role: str + +class UserGroupsId(SecuredMultiTenancyResource): @authorize('user_group_update') @rest_decorators.marshal_with(GroupResponse) def post(self, group_name, multi_tenancy): """ Set role for a certain group """ - request_dict = rest_utils.get_json_and_verify_params() - role_name = request_dict.get('role') - if not role_name: - raise BadParametersError('`role` not provided') + args = _UpdateUserGroupArgs.parse_obj(request.json) + role_name = args.role rest_utils.verify_role(role_name, is_system_role=True) return multi_tenancy.set_group_role(group_name, role_name) @@ -109,7 +119,12 @@ def delete(self, group_name, multi_tenancy): """ rest_utils.validate_inputs({'group_name': group_name}) multi_tenancy.delete_group(group_name) - return None, 204 + return "", 204 + + +class _UserGroupsUsersArgs(pydantic.BaseModel): + username: str + group_name: str class UserGroupsUsers(SecuredMultiTenancyResource): @@ -126,8 +141,7 @@ def put(self, multi_tenancy): 'Explicit group to user association is not permitted when ' 'using LDAP. Group association to users is done automatically' ' according to the groups associated with the user in LDAP.') - request_dict = rest_utils.get_json_and_verify_params({'username', - 'group_name'}) + request_dict = _UserGroupCreateArgs.parse_obj(request.json).dict() rest_utils.validate_inputs(request_dict) return multi_tenancy.add_user_to_group( request_dict['username'], @@ -139,10 +153,9 @@ def delete(self, multi_tenancy): """ Remove a user from a group """ - request_dict = rest_utils.get_json_and_verify_params({'username', - 'group_name'}) + request_dict = _UserGroupCreateArgs.parse_obj(request.json).dict() multi_tenancy.remove_user_from_group( request_dict['username'], request_dict['group_name'] ) - return None, 204 + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v3/users.py b/rest-service/manager_rest/rest/resources_v3/users.py index 0aa36e4f0d..441e4f6333 100644 --- a/rest-service/manager_rest/rest/resources_v3/users.py +++ b/rest-service/manager_rest/rest/resources_v3/users.py @@ -1,3 +1,7 @@ +import pydantic +from typing import Optional + +from flask import request from flask_security import current_user from flask_security.utils import hash_password @@ -31,6 +35,16 @@ def get(self): return user_datastore.get_user(current_user.username) +class _UserCreateArgs(pydantic.BaseModel): + username: str + password: Optional[str] = None + role: Optional[str] = constants.DEFAULT_SYSTEM_ROLE + created_at: Optional[str] = None + first_login_at: Optional[str] = None + last_login_at: Optional[str] = None + is_prehashed: Optional[bool] = False + + class Users(SecuredMultiTenancyResource): @authorize('user_list') @rest_decorators.marshal_with(UserResponse) @@ -55,28 +69,7 @@ def get(self, multi_tenancy, _include=None, filters=None, pagination=None, @rest_decorators.marshal_with(UserResponse) @rest_decorators.check_external_authenticator('create user') def put(self, multi_tenancy): - """ - Create a user - """ - request_dict = rest_utils.get_json_and_verify_params( - { - 'username': { - 'type': str, - }, - 'password': { - 'type': str, - }, - 'role': { - 'type': str, - 'optional': True, - }, - 'created_at': {'type': str, 'optional': True}, - 'last_login_at': {'type': str, 'optional': True}, - 'first_login_at': {'type': str, 'optional': True}, - } - ) - is_prehashed = rest_utils.verify_and_convert_bool( - 'is_prehashed', request_dict.pop('is_prehashed', False)) + request_dict = _UserCreateArgs.parse_obj(request.json).dict() timestamps = {} set_timestamp_checked = False @@ -88,34 +81,44 @@ def put(self, multi_tenancy): timestamps[timestamp] = rest_utils.parse_datetime_string( request_dict.pop(timestamp)) - # The password shouldn't be validated here password = request_dict.pop('password') password = rest_utils.validate_and_decode_password(password) rest_utils.validate_inputs(request_dict) - role = request_dict.get('role', constants.DEFAULT_SYSTEM_ROLE) + role = request_dict['role'] rest_utils.verify_role(role, is_system_role=True) return multi_tenancy.create_user( request_dict['username'], password, role, - is_prehashed=is_prehashed, created_at=timestamps.get('created_at'), first_login_at=timestamps.get('first_login_at'), last_login_at=timestamps.get('last_login_at'), + is_prehashed=request_dict['is_prehashed'], ) +class _UpdateUserArgs(pydantic.BaseModel): + password: Optional[str] = None + role: Optional[str] = None + show_getting_started: Optional[bool] = None + + +class _HasShowGettingStarted(pydantic.BaseModel): + show_getting_started: Optional[bool] = None + + class UsersIdPremium(SecuredMultiTenancyResource): @rest_decorators.marshal_with(UserResponse) def post(self, username, multi_tenancy): """ Alter settings (e.g. password/role) for a certain user """ - request_dict = rest_utils.get_json_and_verify_params() + request_dict = _UpdateUserArgs.parse_obj(request.json).dict() + query = _HasShowGettingStarted.parse_obj(request.args) password = request_dict.get('password') role_name = request_dict.get('role') - show_getting_started = request_dict.get('show_getting_started') + show_getting_started = query.show_getting_started if password: if role_name: @@ -130,9 +133,6 @@ def post(self, username, multi_tenancy): rest_utils.verify_role(role_name, is_system_role=True) return multi_tenancy.set_user_role(username, role_name) if show_getting_started is not None: - show_getting_started = \ - rest_utils.verify_and_convert_bool('show_getting_started', - show_getting_started) return multi_tenancy.set_show_getting_started( username, show_getting_started) raise BadParametersError( @@ -153,7 +153,7 @@ def delete(self, username, multi_tenancy): Delete a user """ multi_tenancy.delete_user(username) - return None, 204 + return "", 204 def authorize_update(self): # when running unittests, there is no authorization @@ -180,9 +180,10 @@ def post(self, username): """ Change user's password or getting started flag """ - request_dict = rest_utils.get_json_and_verify_params() + request_dict = _UpdateUserArgs.parse_obj(request.json).dict() + query = _HasShowGettingStarted.parse_obj(request.args) password = request_dict.get('password') - show_getting_started = request_dict.get('show_getting_started') + show_getting_started = query.show_getting_started if username != current_user.username: raise BadParametersError('Cannot change settings for ' @@ -195,10 +196,7 @@ def post(self, username): user_datastore.commit() return user if show_getting_started is not None: - set_show_getting_started = \ - rest_utils.verify_and_convert_bool('show_getting_started', - show_getting_started) - user.show_getting_started = set_show_getting_started + user.show_getting_started = show_getting_started user_datastore.commit() return user raise BadParametersError( @@ -209,15 +207,17 @@ def post(self, username): UsersId = UsersIdPremium if _PREMIUM else UsersIdCommunity +class _UserActivateArgs(pydantic.BaseModel): + action: str + + class UsersActive(SecuredMultiTenancyResource): @authorize('user_set_activated') @rest_decorators.marshal_with(UserResponse) def post(self, username, multi_tenancy): - """ - Activate a user - """ - request_dict = rest_utils.get_json_and_verify_params({'action'}) - if request_dict['action'] == 'activate': + """Activate a user""" + args = _UserActivateArgs.parse_obj(request.json) + if args.action == 'activate': return multi_tenancy.activate_user(username) else: return multi_tenancy.deactivate_user(username) diff --git a/rest-service/manager_rest/rest/resources_v3_1/agents.py b/rest-service/manager_rest/rest/resources_v3_1/agents.py index c619f4464c..8ad36b8e30 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/agents.py +++ b/rest-service/manager_rest/rest/resources_v3_1/agents.py @@ -1,6 +1,10 @@ +import pydantic +from typing import Any, Optional + + from flask import current_app, request -from cloudify.models_states import AgentState +from cloudify.models_states import AgentState, VisibilityState from cloudify.cryptography_utils import encrypt, decrypt from cloudify.amqp_client import SendHandler @@ -14,29 +18,28 @@ check_user_action_allowed) from manager_rest.storage import models, get_storage_manager from manager_rest.rest.rest_utils import (validate_inputs, - verify_and_convert_bool, - get_json_and_verify_params, parse_datetime_string, - valid_user) + valid_user, + ListQuery) from manager_rest.workflow_executor import get_amqp_client -class Agents(SecuredResource): +class _AgentsReplaceCertsArgs(pydantic.BaseModel): + broker_ca_cert: Optional[Any] = None + manager_ca_cert: Optional[Any] = None + bundle: Optional[bool] = None + +class Agents(SecuredResource): @rest_decorators.marshal_with(AgentResponse) @rest_decorators.create_filters(models.Agent) @rest_decorators.paginate @rest_decorators.sortable(models.Agent) - @rest_decorators.all_tenants @rest_decorators.search('name') @authorize('agent_list') def get(self, _include=None, filters=None, pagination=None, sort=None, - all_tenants=None, search=None): - - get_all_results = verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + search=None): + args = ListQuery.parse_obj(request.args) return get_storage_manager().list( models.Agent, include=_include, @@ -44,19 +47,17 @@ def get(self, _include=None, filters=None, pagination=None, sort=None, substr_filters=search, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results + all_tenants=args.all_tenants, + get_all_results=args.get_all_results ) @authorize('agent_replace_certs') def patch(self): """Replace CA certificates on running agents.""" - request_dict = get_json_and_verify_params({'bundle': {'type': bool}}) - # broker_ca_cert or manager_ca_cert can be None so no need to - # specify their type - broker_ca_cert = request_dict.get('broker_ca_cert') - manager_ca_cert = request_dict.get('manager_ca_cert') - bundle = request_dict.get('bundle') + args = _AgentsReplaceCertsArgs.parse_obj(request.json) + broker_ca_cert = args.broker_ca_cert + manager_ca_cert = args.manager_ca_cert + bundle = args.bundle sm = get_storage_manager() num_of_updated_agents = 0 @@ -122,6 +123,26 @@ def _get_new_ca_certs(sm, bundle, broker_ca_cert, manager_ca_cert): return new_broker_ca_cert, new_manager_ca_cert +class _AgentCreateArgs(pydantic.BaseModel): + node_instance_id: Optional[str] = None + state: Optional[str] = None + create_rabbitmq_user: Optional[bool] = False + created_at: Optional[str] = None + created_by: Optional[str] = None + rabbitmq_username: Optional[str] = None + rabbitmq_password: Optional[str] = None + rabbitmq_exchange: Optional[str] = None + ip: Optional[str] = None + install_method: Optional[str] = None + system: Optional[str] = None + version: Optional[str] = None + visibility: Optional[VisibilityState] = None + + +class _AgentUpdateArgs(pydantic.BaseModel): + state: str + + class AgentsName(SecuredResource): @rest_decorators.marshal_with(models.Agent) @authorize('agent_get') @@ -139,14 +160,8 @@ def get(self, name): @rest_decorators.marshal_with(models.Agent) @authorize('agent_create') def put(self, name): - """ - Create a new agent - """ - request_dict = get_json_and_verify_params({ - 'node_instance_id': {'type': str}, - 'state': {'type': str}, - 'create_rabbitmq_user': {'type': bool} - }) + """Create a new agent""" + request_dict = _AgentCreateArgs.parse_obj(request.json).dict() validate_inputs({'name': name}) state = request_dict.get('state') self._validate_state(state) @@ -173,11 +188,9 @@ def patch(self, name): """ Update an existing agent """ - request_dict = get_json_and_verify_params({ - 'state': {'type': str} - }) + args = _AgentUpdateArgs.parse_obj(request.json) validate_inputs({'name': name}) - state = request_dict['state'] + state = args.state self._validate_state(state) return self._update_agent(name, state) diff --git a/rest-service/manager_rest/rest/resources_v3_1/blueprints.py b/rest-service/manager_rest/rest/resources_v3_1/blueprints.py index 02143ba5dd..0ddcb6efec 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/blueprints.py +++ b/rest-service/manager_rest/rest/resources_v3_1/blueprints.py @@ -3,12 +3,11 @@ from datetime import datetime from os.path import join from urllib.parse import quote as unquote +import pydantic +from typing import Any, Dict, List, Optional from flask import request -from flask_restful.inputs import boolean -from flask_restful.reqparse import Argument - from cloudify.models_states import VisibilityState, BlueprintUploadState from dsl_parser import constants @@ -28,11 +27,10 @@ swagger, ) from manager_rest.storage import models, get_storage_manager -from manager_rest.utils import get_formatted_timestamp, remove, current_tenant from manager_rest.rest.rest_utils import (get_labels_from_plan, - get_labels_list, - get_args_and_verify_arguments) + get_labels_list) from manager_rest.rest.responses import Label +from manager_rest.utils import get_formatted_timestamp, remove, current_tenant from manager_rest.manager_exceptions import (ConflictError, IllegalActionError, BadParametersError, @@ -49,13 +47,13 @@ def patch(self, blueprint_id): """ Set the blueprint's visibility """ - visibility = rest_utils.get_visibility_parameter() + args = rest_utils.SetVisibilityArgs.parse_obj(request.json) blueprint = get_storage_manager().get(models.Blueprint, blueprint_id) - return get_resource_manager().set_visibility(blueprint, visibility) + return get_resource_manager().set_visibility( + blueprint, args.visibility) class BlueprintsIcon(SecuredResource): - @authorize('blueprint_upload') @rest_decorators.marshal_with(models.Blueprint) def patch(self, blueprint_id): @@ -73,6 +71,45 @@ def patch(self, blueprint_id): return blueprint +class _BlueprintUpdateArgs(pydantic.BaseModel): + class Config: + extra = 'forbid' + + plan: Optional[Dict[str, Any]] = None + description: Optional[str] = None + main_file_name: Optional[str] = None + state: Optional[str] = None + error: Optional[str] = None + error_traceback: Optional[str] = None + labels: Optional[List[Any]] = None + creator: Optional[str] = None + created_at: Optional[str] = None + upload_execution: Optional[str] = None + visibility: Optional[VisibilityState] = None + requirements: Optional[dict] = None + + +class _BlueprintUpdateQuery(pydantic.BaseModel): + visibility: Optional[VisibilityState] = None + + +class _BlueprintUploadQuery(pydantic.BaseModel): + async_upload: Optional[bool] = False + created_by: Optional[str] = None + created_at: Optional[str] = None + labels: Optional[List[Any]] = [] + visibility: Optional[VisibilityState] = None + blueprint_archive_url: Optional[str] = None + state: Optional[str] = None + skip_execution: Optional[bool] = False + application_file_name: Optional[str] = '' + private_resource: Optional[bool] = None + + +class _BlueprintDeleteQuery(pydantic.BaseModel): + force: Optional[bool] = False + + class BlueprintsId(resources_v2.BlueprintsId): @authorize('blueprint_upload') @rest_decorators.marshal_with(models.Blueprint) @@ -83,24 +120,22 @@ def put(self, blueprint_id, **kwargs): rm = get_resource_manager() sm = get_storage_manager() rest_utils.validate_inputs({'blueprint_id': blueprint_id}) - - args = None + args_source = None form_params = request.form.get('params') if form_params: - args = json.loads(form_params) - if not args: - args = request.args.to_dict(flat=False) - - async_upload = args.get('async_upload', False) - created_at = args.get('created_at') - created_by = args.get('created_by') - labels = args.get('labels', []) - visibility = args.get('visibility') - private_resource = args.get('private_resource') - application_file_name = args.get('application_file_name', '') - skip_execution = args.get('skip_execution', False) - state = args.get('state') - blueprint_url = args.get('blueprint_archive_url') + args_source = json.loads(form_params) + if not args_source: + args_source = request.args + args = _BlueprintUploadQuery.parse_obj(args_source) + + labels = args.labels + visibility = args.visibility + private_resource = args.private_resource + application_file_name = args.application_file_name + skip_execution = args.skip_execution + state = args.state + blueprint_url = args.blueprint_archive_url + async_upload = args.async_upload if blueprint_url: if ( @@ -111,18 +146,16 @@ def put(self, blueprint_id, **kwargs): "Can pass blueprint as only one of: URL via query " "parameters, multi-form or chunked.") + created_at = args.created_at if created_at: check_user_action_allowed('set_timestamp', None, True) created_at = rest_utils.parse_datetime_string(created_at) + created_by = args.created_by if created_by: check_user_action_allowed('set_owner', None, True) created_by = rest_utils.valid_user(created_by) - if visibility is not None: - rest_utils.validate_visibility( - visibility, valid_values=VisibilityState.STATES) - unique_labels_check = [] for label in labels: parsed_key, parsed_value = rest_utils.parse_label(label['key'], @@ -226,15 +259,12 @@ def _failed_blueprint(self, sm, blueprint_id, visibility): ) @authorize('blueprint_delete') def delete(self, blueprint_id, **kwargs): - """ - Delete blueprint by id - """ - query_args = get_args_and_verify_arguments( - [Argument('force', type=boolean, default=False)]) + """Delete blueprint by id""" + args = _BlueprintDeleteQuery.parse_obj(request.args) get_resource_manager().delete_blueprint( blueprint_id, - force=query_args.force) - return None, 204 + force=args.force) + return "", 204 @authorize('blueprint_upload') @rest_decorators.marshal_with(models.Blueprint) @@ -250,36 +280,18 @@ def patch(self, blueprint_id, **kwargs): raise IllegalActionError('Update a blueprint request must include ' 'at least one parameter to update') - request_schema = { - 'plan': {'type': dict, 'optional': True}, - 'requirements': {'type': dict, 'optional': True}, - 'description': {'type': str, 'optional': True}, - 'main_file_name': {'type': str, 'optional': True}, - 'visibility': {'type': str, 'optional': True}, - 'state': {'type': str, 'optional': True}, - 'error': {'type': str, 'optional': True}, - 'error_traceback': {'type': str, 'optional': True}, - 'labels': {'type': list, 'optional': True}, - 'creator': {'type': str, 'optional': True}, - 'created_at': {'type': str, 'optional': True}, - 'upload_execution': {'type': str, 'optional': True}, - } - request_dict = rest_utils.get_json_and_verify_params(request_schema) + args = _BlueprintUpdateArgs.parse_obj(request.json) + query = _BlueprintUpdateQuery.parse_obj(request.args) created_at = creator = None - if request_dict.get('created_at'): + if args.created_at is not None: check_user_action_allowed('set_timestamp', None, True) created_at = rest_utils.parse_datetime_string( - request_dict['created_at']) + args.created_at) - if request_dict.get('creator'): + if args.creator is not None: check_user_action_allowed('set_owner', None, True) - creator = rest_utils.valid_user(request_dict['creator']) + creator = rest_utils.valid_user(args.creator) - invalid_params = set(request_dict.keys()) - set(request_schema.keys()) - if invalid_params: - raise BadParametersError( - "Unknown parameters: {}".format(','.join(invalid_params)) - ) sm = get_storage_manager() blueprint = sm.get(models.Blueprint, blueprint_id) # if finished blueprint validation - cleanup DB entry @@ -302,7 +314,7 @@ def patch(self, blueprint_id, **kwargs): return blueprint # set blueprint visibility - visibility = request_dict.get('visibility') + visibility = query.visibility if visibility: if visibility not in VisibilityState.STATES: raise BadParametersError( @@ -312,26 +324,28 @@ def patch(self, blueprint_id, **kwargs): blueprint.visibility = visibility # set other blueprint attributes. - if 'plan' in request_dict: - blueprint.plan = request_dict['plan'] - if 'requirements' in request_dict: - blueprint.requirements = request_dict['requirements'] - if 'description' in request_dict: - blueprint.description = request_dict['description'] - if 'main_file_name' in request_dict: - blueprint.main_file_name = request_dict['main_file_name'] - if 'creator' in request_dict: + if args.plan is not None: + blueprint.plan = args.plan + if args.description is not None: + blueprint.description = args.description + if args.requirements: + blueprint.requirements = args.requirements + if args.main_file_name is not None: + blueprint.main_file_name = args.main_file_name + if creator is not None: blueprint.creator = creator - if 'upload_execution' in request_dict: + if created_at is not None: + blueprint.created_at = created_at + if args.upload_execution is not None: blueprint.upload_execution = sm.get( - models.Execution, request_dict['upload_execution']) + models.Execution, args.upload_execution) - if request_dict.get('plan'): - imported_blueprints = request_dict['plan']\ + if args.plan: + imported_blueprints = args.plan\ .get(constants.IMPORTED_BLUEPRINTS, {}) _validate_imported_blueprints(sm, blueprint, imported_blueprints) # set blueprint state - state = request_dict.get('state') + state = args.state if state: if state not in BlueprintUploadState.STATES: raise BadParametersError( @@ -340,8 +354,8 @@ def patch(self, blueprint_id, **kwargs): ) blueprint.state = state - blueprint.error = request_dict.get('error') - blueprint.error_traceback = request_dict.get('error_traceback') + blueprint.error = args.error + blueprint.error_traceback = args.error_traceback # On finalizing the blueprint upload, extract archive to file # server @@ -360,8 +374,8 @@ def patch(self, blueprint_id, **kwargs): tenant=blueprint.tenant.name) labels_list = None - if request_dict.get('labels') is not None: - raw_list = request_dict['labels'] + if args.labels is not None: + raw_list = args.labels_list if all( 'key' in label and 'value' in label for label in raw_list @@ -400,6 +414,11 @@ def _validate_imported_blueprints(sm, blueprint, imported_blueprints): f'is unavailable: {imported_blueprint}') +class _BlueprintValidateArgs(rest_utils.SetVisibilityArgs): + application_file_name: Optional[str] = None + blueprint_archive_url: Optional[str] = None + + class BlueprintsIdValidate(BlueprintsId): @authorize('blueprint_upload') @rest_decorators.marshal_with(models.Blueprint) @@ -407,22 +426,20 @@ def put(self, blueprint_id, **kwargs): """ Validate a blueprint (id specified) """ - rm = get_resource_manager() sm = get_storage_manager() - args = None + rm = get_resource_manager() + args_source = {} form_params = request.form.get('params') if form_params: - args = json.loads(form_params) - if not args: - args = request.args.to_dict(flat=False) + args_source = json.loads(form_params) + if not args_source: + args_source = request.args.to_dict(flat=False) + args = _BlueprintValidateArgs.parse_obj(args_source) - rest_utils.validate_inputs({'blueprint_id': blueprint_id}) - visibility = args.pop('visibility') + visibility = args.visibility if visibility is not None: rest_utils.validate_visibility( visibility, valid_values=VisibilityState.STATES) - application_file_name = args.pop('application_file_name', '') - blueprint_url = args.pop('blueprint_archive_url', None) with sm.transaction(): blueprint = models.Blueprint( @@ -430,27 +447,27 @@ def put(self, blueprint_id, **kwargs): id=blueprint_id, description=None, main_file_name=None, - visibility=None, + visibility=visibility, state=BlueprintUploadState.VALIDATING, ) sm.put(blueprint) blueprint.upload_execution, messages = rm.upload_blueprint( blueprint_id, - application_file_name, - blueprint_url, + args.application_file_name, + args.blueprint_archive_url, config.instance.file_server_root, # for the import resolver config.instance.marketplace_api_url, # for the import resolver validate_only=True, ) try: - if not blueprint_url: + if not args.blueprint_archive_url: upload_manager.upload_blueprint_archive_to_file_server( blueprint_id) workflow_executor.execute_workflow(messages) except Exception: - sm.sm.delete(blueprint) + sm.delete(blueprint) upload_manager.cleanup_blueprint_archive_from_file_server( blueprint_id, current_tenant.name) raise diff --git a/rest-service/manager_rest/rest/resources_v3_1/cluster_status.py b/rest-service/manager_rest/rest/resources_v3_1/cluster_status.py index ee3c2680f5..29509a9b11 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/cluster_status.py +++ b/rest-service/manager_rest/rest/resources_v3_1/cluster_status.py @@ -13,6 +13,8 @@ # * See the License for the specific language governing permissions and # * limitations under the License. +import pydantic +from typing import Optional from flask import request @@ -24,20 +26,13 @@ from manager_rest.security import SecuredResource from manager_rest.cluster_status_manager import (STATUS, get_cluster_status) -from manager_rest.rest.rest_utils import (verify_and_convert_bool, - get_json_and_verify_params) -class ClusterStatus(SecuredResource): - @staticmethod - def _get_request_dict(): - request_dict = get_json_and_verify_params({ - 'reporting_freq': {'type': int}, - 'report': {'type': dict}, - 'timestamp': {'type': str} - }) - return request_dict +class _ClusterStatusQuery(pydantic.BaseModel): + summary: Optional[bool] = False + +class ClusterStatus(SecuredResource): @swagger.operation( responseClass=responses.Status, nickname="cluster-status", @@ -47,10 +42,8 @@ def _get_request_dict(): @marshal_with(responses.Status) def get(self): """Get the status of the entire cloudify cluster""" - summary_response = verify_and_convert_bool( - 'summary', - request.args.get('summary', False) - ) + args = _ClusterStatusQuery.parse_obj(request.args) + summary_response = args.summary cluster_status = get_cluster_status(detailed=not summary_response) # If the response should be only the summary diff --git a/rest-service/manager_rest/rest/resources_v3_1/community_contacts.py b/rest-service/manager_rest/rest/resources_v3_1/community_contacts.py index 820542e6e3..ae8baec844 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/community_contacts.py +++ b/rest-service/manager_rest/rest/resources_v3_1/community_contacts.py @@ -1,27 +1,32 @@ +import pydantic +from typing import Optional + from requests import post -from flask import current_app +from flask import current_app, request from json import JSONDecodeError from manager_rest.security import SecuredResource from manager_rest.security.authorization import authorize from manager_rest import premium_enabled, manager_exceptions -from manager_rest.rest.rest_utils import get_json_and_verify_params + CREATE_CONTACT_URL = "https://api.cloudify.co/cloudifyCommunityCreateContact" +class _CreateContactArgs(pydantic.BaseModel): + first_name: str + last_name: str + email: str + phone: Optional[str] + is_eula: bool + + class CommunityContacts(SecuredResource): @authorize('community_contact_create') def post(self, **kwargs): if premium_enabled: raise manager_exceptions.CommunityOnly() - request_dict = get_json_and_verify_params({ - 'first_name': {'type': str}, - 'last_name': {'type': str}, - 'email': {'type': str}, - 'phone': {'type': str, 'optional': True}, - 'is_eula': {'type': bool}, - }) + request_dict = _CreateContactArgs.parse_obj(request.json).dict() if not request_dict['is_eula']: raise manager_exceptions.BadParametersError( "EULA must be confirmed by user") diff --git a/rest-service/manager_rest/rest/resources_v3_1/deployments.py b/rest-service/manager_rest/rest/resources_v3_1/deployments.py index e1cf0962eb..12036d1b68 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/deployments.py +++ b/rest-service/manager_rest/rest/resources_v3_1/deployments.py @@ -1,15 +1,16 @@ from base64 import b64decode, b64encode -from builtins import staticmethod +from datetime import datetime import os from shutil import rmtree from tempfile import mkdtemp -from typing import Optional import uuid import zipfile +from typing import Optional, Any, List, Dict +import pydantic + + from flask import request -from flask_restful.inputs import boolean -from flask_restful.reqparse import Argument from sqlalchemy import and_ as sql_and @@ -50,6 +51,9 @@ swagger, ) from manager_rest.rest.responses import Label +from manager_rest.rest.resources_v1.deployments import ( + _DeploymentCreateArgs as v1_DeploymentCreateArgs +) from ..responses_v2 import ListResponse @@ -58,22 +62,40 @@ EXTERNAL_TARGET = 'external_target' -class DeploymentsId(resources_v1.DeploymentsId): +class _DeploymentCreateArgs(v1_DeploymentCreateArgs): + description: Optional[str] = None + deployment_status: Optional[str] = None + installation_status: Optional[str] = None + skip_plugins_validation: Optional[bool] = False + site_name: Optional[str] = None + runtime_only_evaluation: Optional[bool] = False + display_name: Optional[str] = None + labels: Optional[List[Dict[str, str]]] = [] + created_at: Optional[str] = None + created_by: Optional[str] = None + workdir_zip: Optional[Any] = None + workflows: Optional[Dict[str, Any]] = None + groups: Optional[Dict[str, Any]] = None + scaling_groups: Optional[Dict[str, Any]] = None + policy_triggers: Optional[Dict[str, Any]] = None + policy_types: Optional[Dict[str, Any]] = None + outputs: Optional[Dict[str, Any]] = None + resource_tags: Optional[Dict[str, Any]] = None + capabilities: Optional[Dict[str, Any]] = None + visibility: Optional[VisibilityState] = VisibilityState.TENANT + + +class _DeploymentCreateQuery(pydantic.BaseModel): + private_resource: Optional[bool] = None + async_create: Optional[bool] = False - def create_request_schema(self): - request_schema = super(DeploymentsId, self).create_request_schema() - request_schema['skip_plugins_validation'] = { - 'optional': True, 'type': bool} - request_schema['site_name'] = {'optional': True, 'type': str} - request_schema['runtime_only_evaluation'] = { - 'optional': True, 'type': bool - } - request_schema['display_name'] = {'optional': True, 'type': str} - return request_schema - def get_skip_plugin_validation_flag(self, request_dict): - return request_dict.get('skip_plugins_validation', False) +class _DeploymentGetQuery(pydantic.BaseModel): + all_sub_deployments: Optional[bool] = True + include_workdir: Optional[bool] = False + +class DeploymentsId(resources_v1.DeploymentsId): def _error_from_create(self, execution): """Map a failed create-dep-env execution to a REST error response""" if execution.status != ExecutionState.FAILED or not execution.error: @@ -180,40 +202,28 @@ def _add_existing_labels(self, deployment, new_labels): @rest_decorators.marshal_with(models.Deployment) @rest_decorators.not_while_cancelling def put(self, deployment_id, **kwargs): - """ - Create a deployment - """ + """Create a deployment""" rest_utils.validate_inputs({'deployment_id': deployment_id}, validate_value_begins_with_letter=False) - request_schema = self.create_request_schema() - request_dict = rest_utils.get_json_and_verify_params(request_schema) + request_dict = _DeploymentCreateArgs.parse_obj(request.json).dict() blueprint_id = request_dict['blueprint_id'] bypass_maintenance = is_bypass_maintenance_mode() - args = rest_utils.get_args_and_verify_arguments([ - Argument('private_resource', type=boolean), - Argument('async_create', type=boolean, default=False) - ]) - if args.async_create and request_dict.get('workdir_zip'): + args = _DeploymentCreateQuery.parse_obj(request.args) + if args.async_create and request_dict['workdir_zip']: raise DeploymentCreationError( 'Unable to create deployment asynchronously with provided ' 'workdir zip.' ) created_at = owner = None - if request_dict.get('created_at'): + if request_dict['created_at']: check_user_action_allowed('set_timestamp', None, True) created_at = rest_utils.parse_datetime_string( request_dict['created_at']) - if request_dict.get('created_by'): + if request_dict['created_by']: check_user_action_allowed('set_owner', None, True) owner = rest_utils.valid_user(request_dict['created_by']) - visibility = rest_utils.get_visibility_parameter( - optional=True, - valid_values=VisibilityState.STATES - ) - inputs = request_dict.get('inputs', {}) - skip_plugins_validation = self.get_skip_plugin_validation_flag( - request_dict) + inputs = request_dict['inputs'] rm = get_resource_manager() sm = get_storage_manager() blueprint = sm.get(models.Blueprint, blueprint_id) @@ -230,7 +240,7 @@ def put(self, deployment_id, **kwargs): site_name = _get_site_name(request_dict) site = sm.get(models.Site, site_name) if site_name else None - skip_create_dep_env = bool(request_dict.get('workdir_zip')) + skip_create_dep_env = bool(request_dict['workdir_zip']) if not skip_create_dep_env: # create_dep_env will use and populate some attrs if it is running # so don't provide them beforehand or we will try (and fail) to @@ -245,33 +255,32 @@ def put(self, deployment_id, **kwargs): rm.cleanup_failed_deployment(deployment_id) try: with sm.transaction(): - if not skip_plugins_validation: + if not request_dict['skip_plugins_validation']: rm.check_blueprint_plugins_installed(blueprint.plan) deployment = rm.create_deployment( blueprint, deployment_id, private_resource=args.private_resource, - visibility=visibility, + visibility=request_dict['visibility'], site=site, - runtime_only_evaluation=request_dict.get( - 'runtime_only_evaluation', False), + runtime_only_evaluation=request_dict[ + 'runtime_only_evaluation'], created_at=created_at, created_by=owner, - workflows=request_dict.get('workflows'), - groups=request_dict.get('groups'), - scaling_groups=request_dict.get('scaling_groups'), - policy_triggers=request_dict.get('policy_triggers'), - policy_types=request_dict.get('policy_types'), + workflows=request_dict['workflows'], + groups=request_dict['groups'], + scaling_groups=request_dict['scaling_groups'], + policy_triggers=request_dict['policy_triggers'], + policy_types=request_dict['policy_types'], inputs=request_dict.get('inputs'), - outputs=request_dict.get('outputs'), - resource_tags=request_dict.get('resource_tags'), - capabilities=request_dict.get('capabilities'), - description=request_dict.get('description'), - deployment_status=request_dict.get('deployment_status'), - installation_status=request_dict.get( - 'installation_status'), + outputs=request_dict['outputs'], + resource_tags=request_dict['resource_tags'], + capabilities=request_dict['capabilities'], + description=request_dict['description'], + deployment_status=request_dict['deployment_status'], + installation_status=request_dict['installation_status'], + labels=request_dict.get('labels'), display_name=request_dict.get('display_name'), - labels=request_dict.get('labels') ) if skip_create_dep_env: tmpdir_path = mkdtemp() @@ -296,11 +305,12 @@ def put(self, deployment_id, **kwargs): # We don't execute the create_dep_env when a workdir is # provided- this is part of a restore or similar return deployment, 201 + create_execution = \ deployment.make_create_environment_execution( inputs=inputs, labels=labels, - display_name=request_dict.get('display_name'), + display_name=request_dict['display_name'], ) try: messages = rm.prepare_executions( @@ -312,7 +322,8 @@ def put(self, deployment_id, **kwargs): rm.delete_deployment(deployment) raise except ValueError as e: - raise manager_exceptions.BadParametersError(e) + raise manager_exceptions.BadParametersError(str(e)) + workflow_executor.execute_workflow(messages) if not args.async_create: rest_utils.wait_for_execution(sm, deployment.create_execution.id) @@ -396,10 +407,7 @@ def patch(self, deployment_id): @authorize('deployment_get') @rest_decorators.marshal_with(models.Deployment) def get(self, deployment_id, _include=None, **kwargs): - args = rest_utils.get_args_and_verify_arguments([ - Argument('all_sub_deployments', type=boolean, default=True), - Argument('include_workdir', type=boolean, default=False), - ]) + args = _DeploymentGetQuery.parse_obj(request.args) if _include: if not args.all_sub_deployments and 'id' not in _include: # we will need to use id in the _populate_direct method, so it @@ -433,13 +441,11 @@ class DeploymentsSetVisibility(SecuredResource): @authorize('deployment_set_visibility') @rest_decorators.marshal_with(models.Deployment) def patch(self, deployment_id): - """ - Set the deployment's visibility - """ - visibility = rest_utils.get_visibility_parameter() + """Set the deployment's visibility""" + args = rest_utils.SetVisibilityArgs.parse_obj(request.json) return get_resource_manager().set_deployment_visibility( deployment_id, - visibility + args.visibility, ) @@ -458,6 +464,10 @@ def get(self, deployment_id, **kwargs): return dict(deployment_id=deployment_id, capabilities=capabilities) +class _DeploymentsSetSiteArgs(pydantic.BaseModel): + site_name: Optional[str] = None + + class DeploymentsSetSite(SecuredResource): @authorize('deployment_set_site') @@ -466,7 +476,8 @@ def post(self, deployment_id): """ Set the deployment's site """ - site_name = _get_site_name(request.json) + request_dict = _DeploymentsSetSiteArgs.parse_obj(request.json).dict() + site_name = _get_site_name(request_dict) storage_manager = get_storage_manager() deployment = storage_manager.get(models.Deployment, deployment_id) site = None @@ -488,7 +499,7 @@ def _validate_detach_site(self, site_name): def _get_site_name(request_dict): - if 'site_name' not in request_dict: + if not request_dict['site_name']: return None site_name = request_dict['site_name'] @@ -496,6 +507,21 @@ def _get_site_name(request_dict): return site_name +class _CreateIDDArgs(pydantic.BaseModel): + source_deployment_id: str + inter_deployment_dependencies: List[Any] + + +class _IDDParams(pydantic.BaseModel): + dependency_creator: str + source_deployment: str + target_deployment: Optional[str] = None + target_deployment_func: Optional[Dict[str, Any]] = None + external_source: Optional[Dict[str, Any]] = None + external_target: Optional[Dict[str, Any]] = None + is_component_deletion: Optional[bool] = False + + class InterDeploymentDependencies(SecuredResource): @swagger.operation( responseClass=models.InterDeploymentDependencies, @@ -579,11 +605,7 @@ def post(self): :return: a list of InterDeploymentDependency IDs. """ sm = get_storage_manager() - - params = rest_utils.get_json_and_verify_params({ - 'source_deployment_id': {'type': str}, - 'inter_deployment_dependencies': {'type': list} - }) + params = _CreateIDDArgs.parse_obj(request.json).dict() dependencies = params.get('inter_deployment_dependencies') @@ -653,14 +675,7 @@ def _verify_and_get_source_and_target_deployments( @staticmethod def _verify_dependency_params(): - return rest_utils.get_json_and_verify_params({ - DEPENDENCY_CREATOR: {'type': str}, - SOURCE_DEPLOYMENT: {'type': str}, - TARGET_DEPLOYMENT: {'optional': True, 'type': str}, - TARGET_DEPLOYMENT_FUNC: {'optional': True, 'type': dict}, - EXTERNAL_SOURCE: {'optional': True, 'type': dict}, - EXTERNAL_TARGET: {'optional': True, 'type': dict}, - }) + return _IDDParams.parse_obj(request.json).dict() @staticmethod def _get_put_dependency_params(sm): @@ -742,7 +757,7 @@ def delete(self): sm.delete(label) break - return None, 204 + return "", 204 @staticmethod def _get_delete_dependency_params(sm): @@ -802,6 +817,10 @@ def get(self, return inter_deployment_dependencies +class _UpdateIDDArgs(pydantic.BaseModel): + inter_deployment_dependencies: Optional[List[Any]] = [] + + class InterDeploymentDependenciesId(SecuredResource): @swagger.operation( responseClass=models.InterDeploymentDependencies, @@ -823,12 +842,8 @@ def put(self, deployment_id): :return: a list of InterDeploymentDependency IDs. """ sm = get_storage_manager() - - params = rest_utils.get_json_and_verify_params({ - 'inter_deployment_dependencies': {'type': list} - }) - - dependencies = params.get('inter_deployment_dependencies') + params = _UpdateIDDArgs.parse_obj(request.json) + dependencies = params.inter_deployment_dependencies if len(dependencies) > 0 and EXTERNAL_SOURCE in dependencies[0]: source_deployment = None else: @@ -862,28 +877,70 @@ class DeploymentGroups(SecuredResource): @rest_decorators.sortable(models.DeploymentGroup) @rest_decorators.create_filters(models.DeploymentGroup) @rest_decorators.paginate - @rest_decorators.all_tenants - def get(self, _include=None, filters=None, pagination=None, sort=None, - all_tenants=None): + def get(self, _include=None, filters=None, pagination=None, sort=None): if _include and 'deployment_ids' in _include: # If we don't do this, this include will result in lots of queries _include.remove('deployment_ids') _include.append('deployments') - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + args = rest_utils.ListQuery.parse_obj(request.args) return get_storage_manager().list( models.DeploymentGroup, include=_include, filters=filters, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results + all_tenants=args.all_tenants, + get_all_results=args.get_all_results ) +class _NewGroupDeploymentSpec(pydantic.BaseModel): + id: Optional[str] = None + display_name: Optional[str] = None + skip_plugins_validation: Optional[bool] = False + labels: Optional[List[Dict[str, str]]] = [] + runtime_only_evaluation: Optional[bool] = False + inputs: Optional[Dict[str, Any]] = {} + site_name: Optional[str] = None + + +class _DepGroupAddDeploymentsArgs(pydantic.BaseModel): + deployment_ids: Optional[List[str]] = None + filter_id: Optional[str] = None + filter_rules: Optional[List[Dict[str, Any]]] = None + deployments_from_group: Optional[str] = None + new_deployments: Optional[List[_NewGroupDeploymentSpec]] = None + + +class _DepGroupRemoveDeploymentsArgs(pydantic.BaseModel): + deployment_ids: Optional[List[str]] = None + filter_id: Optional[str] = None + filter_rules: Optional[List[Dict[str, Any]]] = None + deployments_from_group: Optional[str] = None + + +class _DepGroupChangeArgs(pydantic.BaseModel): + add: Optional[_DepGroupAddDeploymentsArgs] = {} + remove: Optional[_DepGroupRemoveDeploymentsArgs] = {} + + +class _DepGroupCreateArgs(_DepGroupAddDeploymentsArgs, pydantic.BaseModel): + description: Optional[str] = None + visibility: Optional[VisibilityState] = VisibilityState.TENANT + labels: Optional[List[Dict[str, str]]] = [] + blueprint_id: Optional[str] = None + default_inputs: Optional[Dict[str, Any]] = None + created_by: Optional[str] = None + created_at: Optional[datetime] = None + creation_counter: Optional[int] = None + + +class _DepGroupDeleteArgs(pydantic.BaseModel): + delete_deployments: Optional[bool] = False + force: Optional[bool] = False + delete_logs: Optional[bool] = False + + class DeploymentGroupsId(SecuredResource): @authorize('deployment_group_get') @rest_decorators.marshal_with(models.DeploymentGroup) @@ -894,28 +951,11 @@ def get(self, group_id): @rest_decorators.marshal_with(models.DeploymentGroup, force_get_data=True) @rest_decorators.not_while_cancelling def put(self, group_id): - request_dict = rest_utils.get_json_and_verify_params({ - 'description': {'optional': True}, - 'visibility': {'optional': True}, - 'labels': {'optional': True}, - 'blueprint_id': {'optional': True}, - 'default_inputs': {'optional': True}, - 'filter_id': {'optional': True}, - 'filter_rules': {'optional': True}, - 'deployment_ids': {'optional': True}, - 'new_deployments': {'optional': True}, - 'deployments_from_group': {'optional': True}, - 'created_by': {'optional': True}, - 'created_at': {'optional': True}, - 'creation_counter': {'optional': True} - }) - - created_at = creator = None + request_dict = _DepGroupCreateArgs.parse_obj(request.json).dict() if request_dict.get('created_at'): check_user_action_allowed('set_timestamp', None, True) - created_at = rest_utils.parse_datetime_string( - request_dict['created_at']) + creator = None if request_dict.get('created_by'): check_user_action_allowed('set_owner', None, True) creator = rest_utils.valid_user(request_dict['created_by']) @@ -930,12 +970,12 @@ def put(self, group_id): # flush so the newly-created group gets an ID, so that its # ._storage_id can be used as a FK target db.session.flush() - if 'creation_counter' in request_dict: + if request_dict.get('creation_counter') is not None: group.creation_counter = request_dict['creation_counter'] if creator: group.creator = creator - if created_at: - group.created_at = created_at + if request_dict.get('created_at'): + group.created_at = request_dict['created_at'] self._set_group_attributes(sm, group, request_dict) changed_deps = set() if request_dict.get('labels') is not None: @@ -965,10 +1005,7 @@ def _is_overriding_deployments(self, request_dict): @rest_decorators.marshal_with(models.DeploymentGroup, force_get_data=True) @rest_decorators.not_while_cancelling def patch(self, group_id): - request_dict = rest_utils.get_json_and_verify_params({ - 'add': {'optional': True}, - 'remove': {'optional': True}, - }) + request_dict = _DepGroupChangeArgs.parse_obj(request.json).dict() sm = get_storage_manager() with sm.transaction(): group = sm.get(models.DeploymentGroup, group_id) @@ -1237,7 +1274,7 @@ def _prepare_sites(self, sm, new_deployments): if not site_name: continue try: - new_dep_spec['site'] = sites[site_name] + new_dep_spec['site_name'] = sites[site_name] except KeyError: raise manager_exceptions.NotFoundError( f'Site {site_name} does not exist' @@ -1266,7 +1303,7 @@ def _make_new_group_deployment(self, rm, group, new_dep_spec, count, visibility=group.visibility, runtime_only_evaluation=new_dep_spec.get( 'runtime_only_evaluation', False), - site=new_dep_spec.get('site'), + site=new_dep_spec.get('site_name'), ) group.creation_counter += 1 dep.guaranteed_unique = is_id_unique @@ -1378,11 +1415,7 @@ def _remove_group_deployments(self, sm, group, request_dict): @authorize('deployment_group_delete') def delete(self, group_id): - args = rest_utils.get_args_and_verify_arguments([ - Argument('delete_deployments', type=boolean, default=False), - Argument('force', type=boolean, default=False), - Argument('delete_logs', type=boolean, default=False), - ]) + args = _DepGroupDeleteArgs.parse_obj(request.args) sm = get_storage_manager() rm = get_resource_manager() @@ -1406,7 +1439,7 @@ def delete(self, group_id): workflow_executor.execute_workflow(messages) sm.delete(group) - return None, 204 + return "", 204 def _create_inter_deployment_dependency( diff --git a/rest-service/manager_rest/rest/resources_v3_1/execution_schedules.py b/rest-service/manager_rest/rest/resources_v3_1/execution_schedules.py index 84ea192f98..a60bd1e02e 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/execution_schedules.py +++ b/rest-service/manager_rest/rest/resources_v3_1/execution_schedules.py @@ -1,13 +1,13 @@ +import pydantic +from typing import Any, Dict, List, Optional + + from flask import request -from flask_restful.reqparse import Argument from manager_rest import manager_exceptions from manager_rest.rest import rest_decorators, swagger from manager_rest.rest.rest_utils import ( - get_json_and_verify_params, - verify_and_convert_bool, validate_inputs, - get_args_and_verify_arguments, parse_datetime_multiple_formats, parse_datetime_string, compute_rule_from_scheduling_params, @@ -47,6 +47,47 @@ def get(self, _include=None, filters=None, pagination=None, ) +class _ExecutionArguments(pydantic.BaseModel): + allow_custom_parameters: Optional[bool] = False + force: Optional[bool] = False + dry_run: Optional[bool] = False + wait_after_fail: Optional[int] = 600 + + +class _CreateScheduleArgs(pydantic.BaseModel): + workflow_id: str + since: str + until: Optional[str] = None + created_at: Optional[str] = None + created_by: Optional[str] = None + parameters: Optional[Dict[str, Any]] = None + slip: Optional[int] = 0 + stop_on_fail: Optional[bool] = False + enabled: Optional[bool] = True + rrule: Optional[str] = None + recurrence: Optional[str] = None + weekdays: Optional[List[str]] = None + count: Optional[int] = None + execution_arguments: Optional[_ExecutionArguments] = None + + +class _HasDeploymentID(pydantic.BaseModel): + deployment_id: str + + +class _UpdateScheduleArgs(pydantic.BaseModel): + enabled: Optional[bool] = None + stop_on_fail: Optional[bool] = None + slip: Optional[int] = None + since: Optional[str] = None + until: Optional[str] = None + workflow_id: Optional[str] = None + rrule: Optional[str] = None + recurrence: Optional[str] = None + weekdays: Optional[List[str]] = None + count: Optional[int] = None + + class ExecutionSchedulesId(SecuredResource): @rest_decorators.marshal_with(models.ExecutionSchedule) @authorize('execution_schedule_create') @@ -54,44 +95,45 @@ def put(self, schedule_id, **kwargs): """Schedule a workflow execution""" validate_inputs({'schedule_id': schedule_id}) - deployment_id = get_args_and_verify_arguments([ - Argument('deployment_id', type=str, required=True), - ])['deployment_id'] - request_dict = get_json_and_verify_params({'workflow_id', - 'since'}) - + deployment_id = _HasDeploymentID.parse_obj(request.args).deployment_id + args = _CreateScheduleArgs.parse_obj(request.json) created_at = creator = None - if request_dict.get('created_at'): + if args.created_at: check_user_action_allowed('set_timestamp', None, True) - created_at = parse_datetime_string(request_dict['created_at']) + created_at = parse_datetime_string(args.created_at) - if request_dict.get('created_by'): + if args.created_by: check_user_action_allowed('set_owner', None, True) - creator = valid_user(request_dict['created_by']) + creator = valid_user(args.created_by) + + if args.execution_arguments: + execution_arguments = args.execution_arguments.dict() + else: + execution_arguments = {} - workflow_id = request_dict['workflow_id'] - execution_arguments = self._get_execution_arguments(request_dict) - parameters = request_dict.get('parameters', None) + # rename dry_run -> is_dry_run + execution_arguments['is_dry_run'] = \ + execution_arguments.pop('dry_run', False) + + parameters = args.parameters if parameters is not None and not isinstance(parameters, dict): raise manager_exceptions.BadParametersError( "parameters: expected a dict, but got: {0}".format(parameters)) rm = get_resource_manager() deployment = rm.sm.get(models.Deployment, deployment_id) - rm._verify_workflow_in_deployment(workflow_id, + rm._verify_workflow_in_deployment(args.workflow_id, deployment, deployment_id) - since = request_dict['since'] - until = request_dict.get('until') + since = args.since + until = args.until if since: since = parse_datetime_multiple_formats(since) if until: until = parse_datetime_multiple_formats(until) - rule = compute_rule_from_scheduling_params(request_dict) - slip = request_dict.get('slip', 0) - stop_on_fail = verify_and_convert_bool( - 'stop_on_fail', request_dict.get('stop_on_fail', False)) + rule = compute_rule_from_scheduling_params(args) + slip = args.slip now = get_formatted_timestamp() schedule = models.ExecutionSchedule( id=schedule_id, @@ -101,11 +143,11 @@ def put(self, schedule_id, **kwargs): until=until, rule=rule, slip=slip, - workflow_id=workflow_id, + workflow_id=args.workflow_id, parameters=parameters, execution_arguments=execution_arguments, - stop_on_fail=stop_on_fail, - enabled=request_dict.get('enabled', True), + stop_on_fail=args.stop_on_fail, + enabled=args.enabled, ) if creator: schedule.creator = creator @@ -116,38 +158,29 @@ def put(self, schedule_id, **kwargs): @authorize('execution_schedule_create') def patch(self, schedule_id, **kwargs): """Updates scheduling parameters of an existing execution schedule""" - - deployment_id = get_args_and_verify_arguments([ - Argument('deployment_id', type=str, required=True) - ])['deployment_id'] + deployment_id = _HasDeploymentID.parse_obj(request.args).deployment_id sm = get_storage_manager() schedule = sm.get( models.ExecutionSchedule, None, filters={'id': schedule_id, 'deployment_id': deployment_id} ) - slip = request.json.get('slip') - stop_on_fail = request.json.get('stop_on_fail') - enabled = request.json.get('enabled') - - since = request.json.get('since') - until = request.json.get('until') - workflow_id = request.json.get('workflow_id') - if since: - schedule.since = parse_datetime_multiple_formats(since) - if until: - schedule.until = parse_datetime_multiple_formats(until) - if workflow_id: - schedule.workflow_id = workflow_id - if slip is not None: - schedule.slip = slip - if stop_on_fail is not None: - schedule.stop_on_fail = verify_and_convert_bool('stop_on_fail', - stop_on_fail) - if enabled is not None: - schedule.enabled = verify_and_convert_bool('enabled', enabled) + args = _UpdateScheduleArgs.parse_obj(request.json) + + if args.since: + schedule.since = parse_datetime_multiple_formats(args.since) + if args.until: + schedule.until = parse_datetime_multiple_formats(args.until) + if args.workflow_id: + schedule.workflow_id = args.workflow_id + if args.slip is not None: + schedule.slip = args.slip + if args.stop_on_fail is not None: + schedule.stop_on_fail = args.stop_on_fail + if args.enabled is not None: + schedule.enabled = args.enabled schedule.rule = compute_rule_from_scheduling_params( - request.json, existing_rule=schedule.rule) + args, existing_rule=schedule.rule) schedule.next_occurrence = schedule.compute_next_occurrence() sm.update(schedule) return schedule, 201 @@ -163,9 +196,7 @@ def get(self, schedule_id, _include=None, **kwargs): """ Get execution schedule by id """ - deployment_id = get_args_and_verify_arguments([ - Argument('deployment_id', type=str, required=True) - ])['deployment_id'] + deployment_id = _HasDeploymentID.parse_obj(request.args).deployment_id return get_storage_manager().get( models.ExecutionSchedule, None, @@ -175,35 +206,11 @@ def get(self, schedule_id, _include=None, **kwargs): @authorize('execution_schedule_create') def delete(self, schedule_id): - deployment_id = get_args_and_verify_arguments([ - Argument('deployment_id', type=str, required=True) - ])['deployment_id'] + deployment_id = _HasDeploymentID.parse_obj(request.args).deployment_id sm = get_storage_manager() schedule = sm.get( models.ExecutionSchedule, None, filters={'id': schedule_id, 'deployment_id': deployment_id}) sm.delete(schedule) - return None, 204 - - @staticmethod - def _get_execution_arguments(request_dict): - arguments = request_dict.get('execution_arguments') - if not arguments: - return {} - if not isinstance(arguments, dict): - raise manager_exceptions.BadParametersError( - "execution_arguments: expected a dict, but got: {}" - .format(arguments)) - return { - 'allow_custom_parameters': verify_and_convert_bool( - 'allow_custom_parameters', - arguments.get('allow_custom_parameters', False)), - 'force': verify_and_convert_bool( - 'force', - arguments.get('force', False)), - 'is_dry_run': verify_and_convert_bool( - 'dry_run', - arguments.get('dry_run', False)), - 'wait_after_fail': arguments.get('wait_after_fail', 600) - } + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v3_1/executions.py b/rest-service/manager_rest/rest/resources_v3_1/executions.py index 979bc880dd..ebe5b045ce 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/executions.py +++ b/rest-service/manager_rest/rest/resources_v3_1/executions.py @@ -1,6 +1,7 @@ +import pydantic import uuid from datetime import datetime -from typing import Dict +from typing import Any, Dict, List, Optional from flask import request from sqlalchemy.dialects.postgresql import insert @@ -17,15 +18,19 @@ BadParametersError, NonexistentWorkflowError) from manager_rest.resource_manager import get_resource_manager from manager_rest.rest.rest_utils import ( - get_json_and_verify_params, - verify_and_convert_bool, parse_datetime_multiple_formats, valid_user, parse_datetime_string, + ListQuery, ) from manager_rest.storage import models, db, get_storage_manager, ListResult +class _ExecutionDeleteArgs(pydantic.BaseModel): + keep_last: Optional[int] = None + to_datetime: Optional[str] = None + + class Executions(resources_v2.Executions): @authorize('execution_delete') @rest_decorators.marshal_with(ItemsCount) @@ -33,25 +38,22 @@ class Executions(resources_v2.Executions): @rest_decorators.paginate @rest_decorators.all_tenants def delete(self, filters=None, pagination=None, all_tenants=None): - request_dict = get_json_and_verify_params({ - 'keep_last': {'optional': True, 'type': int}, - 'to_datetime': {'optional': True} - }) - if 'keep_last' in request_dict: - if 'to_datetime' in request_dict: + args = _ExecutionDeleteArgs.parse_obj(request.json) + if args.keep_last is not None: + if args.to_datetime is not None: raise BadParametersError( "Must provide either a `to_datetime` timestamp or a " "`keep_last` number of executions to keep" ) - if request_dict['keep_last'] <= 0: + if args.keep_last <= 0: raise BadParametersError( "`keep_last` must be an integer greater than 0. got {} " - "instead.".format(request_dict['keep_last']) + "instead.".format(args.keep_last) ) requested_time = None - if 'to_datetime' in request_dict: + if args.to_datetime is not None: requested_time = parse_datetime_multiple_formats( - request_dict['to_datetime']) + args.to_datetime) if 'status' in filters: if filters['status'] not in ExecutionState.END_STATES: raise BadParametersError( @@ -61,7 +63,6 @@ def delete(self, filters=None, pagination=None, all_tenants=None): ) else: filters['status'] = ExecutionState.END_STATES - sm = get_storage_manager() executions = sm.list(models.Execution, filters=filters, @@ -86,14 +87,13 @@ def delete(self, filters=None, pagination=None, all_tenants=None): sm.delete(execution) deleted_count += 1 else: - if request_dict.get('keep_last'): - max_to_delete = len(executions) - request_dict['keep_last'] + if args.keep_last: + max_to_delete = len(executions) - args.keep_last for execution in executions: if self._can_delete_execution(execution, dep_creation_execs): sm.delete(execution) deleted_count += 1 - if request_dict.get('keep_last') and deleted_count >= \ - max_to_delete: + if args.keep_last and deleted_count >= max_to_delete: break return ListResult([{'count': deleted_count}], {'pagination': pagination}) @@ -126,49 +126,46 @@ def get(self, execution_id): queue=True, execution=execution)) +class _CreateExecGroupArgs(pydantic.BaseModel): + deployment_group_id: str + workflow_id: str + default_parameters: Optional[Dict[str, Any]] = None + parameters: Optional[Dict[str, Any]] = None + force: Optional[bool] = False + concurrency: Optional[int] = None + created_by: Optional[str] = None + created_at: Optional[str] = None + associated_executions: Optional[List[str]] = None + id: Optional[str] = None + + class ExecutionGroups(SecuredResource): @authorize('execution_group_list', allow_all_tenants=True) @rest_decorators.marshal_with(models.ExecutionGroup) @rest_decorators.sortable(models.ExecutionGroup) @rest_decorators.create_filters(models.ExecutionGroup) @rest_decorators.paginate - @rest_decorators.all_tenants - def get(self, _include=None, filters=None, pagination=None, sort=None, - all_tenants=None): + def get(self, _include=None, filters=None, pagination=None, sort=None): if _include and 'execution_ids' in _include: # If we don't do this, this include will result in lots of queries _include.remove('execution_ids') _include.append('executions') - get_all_results = verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + args = ListQuery.parse_obj(request.args) return get_storage_manager().list( models.ExecutionGroup, include=_include, filters=filters, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results + all_tenants=args.all_tenants, + get_all_results=args.get_all_results ) @authorize('execution_group_create') @rest_decorators.marshal_with(models.ExecutionGroup, force_get_data=True) @rest_decorators.not_while_cancelling def post(self): - request_dict = get_json_and_verify_params({ - 'deployment_group_id': {'type': str}, - 'workflow_id': {'type': str}, - 'default_parameters': {'optional': True}, - 'parameters': {'optional': True}, - 'force': {'optional': True}, - 'concurrency': {'optional': True}, - 'created_by': {'optional': True}, - 'created_at': {'optional': True}, - 'associated_executions': {'optional': True}, - 'id': {'optional': True}, - }) + request_dict = _CreateExecGroupArgs.parse_obj(request.json).dict() default_parameters = request_dict.get('default_parameters') or {} parameters = request_dict.get('parameters') or {} workflow_id = request_dict['workflow_id'] @@ -246,6 +243,15 @@ def post(self): return group +class _ExecutionGroupActionArgs(pydantic.BaseModel): + action: str + + +class _ExecutionGroupUpdateArgs(pydantic.BaseModel): + success_group_id: Optional[str] = None + failure_group_id: Optional[str] = None + + class ExecutionGroupsId(SecuredResource): @authorize('execution_group_get', allow_all_tenants=True) @rest_decorators.marshal_with(models.ExecutionGroup, force_get_data=True) @@ -261,8 +267,8 @@ def get(self, group_id, _include=None, all_tenants=None): @authorize('execution_group_cancel') @rest_decorators.marshal_with(models.ExecutionGroup) def post(self, group_id, **kwargs): - request_dict = get_json_and_verify_params({'action'}) - action = request_dict['action'] + args = _ExecutionGroupActionArgs.parse_obj(request.json) + action = args.action valid_actions = ['cancel', 'force-cancel', 'kill', 'resume', 'force-resume'] @@ -315,20 +321,17 @@ def _resume_group(self, sm, group, action): @authorize('execution_group_update') @rest_decorators.marshal_with(models.ExecutionGroup) def patch(self, group_id, **kwargs): - request_dict = get_json_and_verify_params({ - 'success_group_id': {'optional': True}, - 'failure_group_id': {'optional': True}, - }) + args = _ExecutionGroupUpdateArgs.parse_obj(request.json) sm = get_storage_manager() with sm.transaction(): group = sm.get(models.ExecutionGroup, group_id) - success_group_id = request_dict.get('success_group_id') + success_group_id = args.success_group_id if success_group_id: group.success_group = sm.get( models.DeploymentGroup, success_group_id) self._add_deps_to_group(group) - failure_group_id = request_dict.get('failure_group_id') + failure_group_id = args.failure_group_id if failure_group_id: group.failed_group = sm.get( models.DeploymentGroup, failure_group_id) diff --git a/rest-service/manager_rest/rest/resources_v3_1/filters.py b/rest-service/manager_rest/rest/resources_v3_1/filters.py index aaa0cfaf9f..cf58d52044 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/filters.py +++ b/rest-service/manager_rest/rest/resources_v3_1/filters.py @@ -1,3 +1,6 @@ +import pydantic +from typing import Any, List, Optional + from flask import request from cloudify.models_states import VisibilityState @@ -19,14 +22,12 @@ class BlueprintsFilters(SecuredResource): @rest_decorators.marshal_with(models.BlueprintsFilter) @rest_decorators.paginate @rest_decorators.sortable(models.BlueprintsFilter) - @rest_decorators.all_tenants @rest_decorators.search('id') - def get(self, _include=None, pagination=None, sort=None, - all_tenants=None, search=None): + def get(self, _include=None, pagination=None, sort=None, search=None): """List blueprints filters""" return list_resource_filters(models.BlueprintsFilter, _include, - pagination, sort, all_tenants, search) + pagination, sort, search) class DeploymentsFilters(SecuredResource): @@ -34,35 +35,42 @@ class DeploymentsFilters(SecuredResource): @rest_decorators.marshal_with(models.DeploymentsFilter) @rest_decorators.paginate @rest_decorators.sortable(models.DeploymentsFilter) - @rest_decorators.all_tenants @rest_decorators.search('id') - def get(self, _include=None, pagination=None, sort=None, - all_tenants=None, search=None): + def get(self, _include=None, pagination=None, sort=None, search=None): """List deployments filters""" return list_resource_filters(models.DeploymentsFilter, _include, - pagination, sort, all_tenants, search) + pagination, sort, search) def list_resource_filters(filters_model, _include=None, pagination=None, - sort=None, all_tenants=None, search=None): - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + sort=None, search=None): + args = rest_utils.ListQuery.parse_obj(request.args) result = get_storage_manager().list( filters_model, include=_include, substr_filters=search, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results, + all_tenants=args.all_tenants, + get_all_results=args.get_all_results, ) return result +class _CreateFilterArgs(pydantic.BaseModel): + filter_rules: List[Any] + created_at: Optional[str] = None + created_by: Optional[str] = None + visibility: Optional[VisibilityState] = None + + +class _UpdateFilterArgs(pydantic.BaseModel): + filter_rules: Optional[List[Any]] = None + visibility: Optional[VisibilityState] = None + + class FiltersId(SecuredResource): def put(self, filters_model, filter_id, filtered_resource): """Create a filter""" @@ -71,21 +79,16 @@ def put(self, filters_model, filter_id, filtered_resource): raise manager_exceptions.BadParametersError( f'All filters with a `{RESERVED_PREFIX}` prefix are reserved ' f'for internal use.') - request_dict = rest_utils.get_json_and_verify_params( - {'filter_rules': {'type': list}}) - filter_rules = create_filter_rules_list(request_dict['filter_rules'], + args = _CreateFilterArgs.parse_obj(request.json) + filter_rules = create_filter_rules_list(args.filter_rules, filtered_resource) - visibility = rest_utils.get_visibility_parameter( - optional=True, valid_values=VisibilityState.STATES) - created_at = creator = None - if 'created_at' in request_dict: + if args.created_at is not None: check_user_action_allowed('set_timestamp', None, True) - created_at = rest_utils.parse_datetime_string( - request_dict['created_at']) - if 'created_by' in request_dict: + created_at = rest_utils.parse_datetime_string(args.created_at) + if args.created_by is not None: check_user_action_allowed('set_owner', None, True) - creator = rest_utils.valid_user(request_dict['created_by']) + creator = rest_utils.valid_user(args.created_by) now = get_formatted_timestamp() new_filter = filters_model( @@ -93,7 +96,7 @@ def put(self, filters_model, filter_id, filtered_resource): value=filter_rules, created_at=created_at or now, updated_at=now, - visibility=visibility, + visibility=args.visibility, creator=creator, ) @@ -116,7 +119,7 @@ def delete(self, filters_model, filter_id): filter_elem = storage_manager.get(filters_model, filter_id) _verify_not_a_system_filter(filter_elem, 'delete') storage_manager.delete(filter_elem, validate_global=True) - return None, 204 + return "", 204 def patch(self, filters_model, filter_id, filtered_resource): """Update a filter by its ID @@ -129,20 +132,17 @@ def patch(self, filters_model, filter_id, filtered_resource): 'Update a filter request must include at least one parameter ' 'to update') - request_dict = rest_utils.get_json_and_verify_params( - {'filter_rules': {'type': list, 'optional': True}}) + args = _UpdateFilterArgs.parse_obj(request.json) - filter_rules = request_dict.get('filter_rules') - visibility = rest_utils.get_visibility_parameter( - optional=True, valid_values=VisibilityState.STATES) + filter_rules = args.filter_rules storage_manager = get_storage_manager() filter_elem = storage_manager.get(filters_model, filter_id) _verify_not_a_system_filter(filter_elem, 'update') - if visibility: + if args.visibility: get_resource_manager().validate_visibility_value( - filter_elem, visibility) - filter_elem.visibility = visibility + filter_elem, args.visibility) + filter_elem.visibility = args.visibility if filter_rules: new_filter_rules = create_filter_rules_list(filter_rules, filtered_resource) diff --git a/rest-service/manager_rest/rest/resources_v3_1/labels.py b/rest-service/manager_rest/rest/resources_v3_1/labels.py index 3bc44b24f3..f30a127166 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/labels.py +++ b/rest-service/manager_rest/rest/resources_v3_1/labels.py @@ -1,3 +1,6 @@ +import pydantic +from typing import Optional + from flask import request from manager_rest.constants import RESERVED_LABELS @@ -9,6 +12,13 @@ from .. import rest_decorators, rest_utils +class _HasReserved(pydantic.BaseModel): + reserved: Optional[bool] = pydantic.Field( + default=False, + alias='_reserved', + ) + + class DeploymentsLabels(SecuredResource): @authorize('labels_list') @rest_decorators.marshal_list_response @@ -16,7 +26,7 @@ class DeploymentsLabels(SecuredResource): @rest_decorators.search('key') def get(self, pagination=None, search=None): """Get all deployments' labels' keys""" - if _is_reserved_labels_keys_in_request(): + if _HasReserved.parse_obj(request.args).reserved: return ListResult.from_list(items=RESERVED_LABELS) return get_labels_keys(models.Deployment, models.DeploymentLabel, @@ -45,7 +55,7 @@ class BlueprintsLabels(SecuredResource): @rest_decorators.search('key') def get(self, pagination=None, search=None): """Get all blueprints' labels' keys""" - if _is_reserved_labels_keys_in_request(): + if _HasReserved.parse_obj(request.args).reserved: return ListResult.from_list(items=RESERVED_LABELS) return get_labels_keys(models.Blueprint, models.BlueprintLabel, @@ -69,16 +79,13 @@ def get(self, key, pagination=None, search=None): def get_labels_keys(resource_model, resource_labels_model, pagination, search): """Get all the resource's labels' keys""" - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + args = rest_utils.ListQuery.parse_obj(request.args) results = get_storage_manager().list( resource_labels_model, include=['key'], pagination=pagination, filters={'_labeled_model_fk': resource_model._storage_id}, - get_all_results=get_all_results, + get_all_results=args.get_all_results, distinct=['key'], substr_filters=search, sort={'key': 'asc'} @@ -92,17 +99,14 @@ def get_labels_key_values(key, resource_model, resource_labels_model, pagination, search): """Get all resource's labels' values for the specified key.""" rest_utils.validate_inputs({'label_key': key}) - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + args = rest_utils.ListQuery.parse_obj(request.args) results = get_storage_manager().list( resource_labels_model, include=['value'], pagination=pagination, filters={'key': key, '_labeled_model_fk': resource_model._storage_id}, - get_all_results=get_all_results, + get_all_results=args.get_all_results, distinct=['value'], substr_filters=search, sort={'value': 'asc'} @@ -110,8 +114,3 @@ def get_labels_key_values(key, resource_model, resource_labels_model, results.items = [label.value for label in results] return results - - -def _is_reserved_labels_keys_in_request(): - return rest_utils.verify_and_convert_bool( - 'reserved', request.args.get('_reserved', False)) diff --git a/rest-service/manager_rest/rest/resources_v3_1/license.py b/rest-service/manager_rest/rest/resources_v3_1/license.py index 6f778c0925..7a88e4db30 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/license.py +++ b/rest-service/manager_rest/rest/resources_v3_1/license.py @@ -1,4 +1,6 @@ -from flask import request +import pydantic + +from flask import request, jsonify from manager_rest.rest import responses_v3 from manager_rest.security.authorization import authorize @@ -9,7 +11,6 @@ ) from manager_rest.manager_exceptions import ConflictError from manager_rest.storage import models, get_storage_manager -from manager_rest.rest.rest_utils import get_json_and_verify_params from manager_rest.rest.rest_decorators import ( marshal_with, paginate @@ -52,6 +53,10 @@ def delete(self, license_handler, pagination=None): return license_handler.remove_license() +class _CustomerIDArgs(pydantic.BaseModel): + customer_id: str + + class LicenseCommunity(SecuredResource): @marshal_with(responses_v3.License) @authorize('license_upload') @@ -59,9 +64,7 @@ def post(self): """ Store the Customer ID received from Hubspot in the Manager. """ - request_dict = get_json_and_verify_params({ - 'customer_id': {'type': str}, - }) + args = _CustomerIDArgs.parse_obj(request.json) sm = get_storage_manager() licenses = sm.list(models.License, get_all_results=True) customer_id = str(licenses[0].customer_id) if licenses else None @@ -69,7 +72,7 @@ def post(self): raise ConflictError( 'A Customer ID already exists for this manager: ' '{}'.format(customer_id)) - return sm.put(models.License(customer_id=request_dict['customer_id'])) + return sm.put(models.License(customer_id=args.customer_id)) @premium_only def put(self): @@ -89,7 +92,7 @@ def delete(self): class LicenseCheckPremium(SecuredResource): def get(self): - return "OK", 200 + return '"OK"', 200 class LicenseCheckCommunity(SecuredResource): @@ -101,8 +104,10 @@ def get(self): customer_id = str(licenses[0].customer_id) if licenses else None if customer_id: return customer_id, 200 - return {"message": "No Customer ID found on the manager", - "error_code": "missing_cloudify_license"}, 400 + return jsonify({ + "message": "No Customer ID found on the manager", + "error_code": "missing_cloudify_license" + }), 400 LicenseCheck = LicenseCheckPremium if _PREMIUM else LicenseCheckCommunity diff --git a/rest-service/manager_rest/rest/resources_v3_1/log_bundles.py b/rest-service/manager_rest/rest/resources_v3_1/log_bundles.py index e5881fa388..5233060a23 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/log_bundles.py +++ b/rest-service/manager_rest/rest/resources_v3_1/log_bundles.py @@ -1,4 +1,8 @@ +import pydantic import os +from typing import Optional + +from flask import request from cloudify.models_states import LogBundleState, ExecutionState @@ -45,8 +49,16 @@ def get(self, _include=None, filters=None, pagination=None, ) -class LogBundlesId(SecuredResource): +class _CreateLogBundleArgs(pydantic.BaseModel): + queue: Optional[bool] = False + + +class _UpdateLogBundleArgs(pydantic.BaseModel): + status: str + error: Optional[str] = '' + +class LogBundlesId(SecuredResource): @swagger.operation( responseClass=models.LogBundle, nickname='getById', @@ -73,14 +85,10 @@ def get(self, log_bundle_id, _include=None, **kwargs): @rest_decorators.marshal_with(models.Execution) def put(self, log_bundle_id): rest_utils.validate_inputs({'log_bundle_id': log_bundle_id}) - request_dict = rest_utils.get_json_and_verify_params() - queue = rest_utils.verify_and_convert_bool( - 'queue', - request_dict.get('queue', False) - ) + args = _CreateLogBundleArgs.parse_obj(request.json) execution, messages = get_resource_manager().create_log_bundle( log_bundle_id, - queue, + args.queue, ) workflow_executor.execute_workflow(messages) return execution, 201 @@ -118,12 +126,11 @@ def delete(self, log_bundle_id): def patch(self, log_bundle_id): """Update log bundle status by id """ - request_dict = rest_utils.get_json_and_verify_params({'status'}) + args = _UpdateLogBundleArgs.parse_obj(request.json) log_bundle = get_storage_manager().get(models.LogBundle, log_bundle_id) - - log_bundle.status = request_dict['status'] - log_bundle.error = request_dict.get('error', '') + log_bundle.status = args.status + log_bundle.error = args.error get_storage_manager().update(log_bundle) diff --git a/rest-service/manager_rest/rest/resources_v3_1/manager.py b/rest-service/manager_rest/rest/resources_v3_1/manager.py index 714addf5ee..91683d35de 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/manager.py +++ b/rest-service/manager_rest/rest/resources_v3_1/manager.py @@ -18,10 +18,10 @@ import tempfile import tarfile import zipfile -from typing import Any from flask import request, send_file -from flask_restful.reqparse import Argument +import pydantic +from typing import Any, Optional from cloudify.constants import MANAGER_RESOURCES_PATH from manager_rest.manager_exceptions import ( @@ -29,7 +29,6 @@ UploadFileMissing, ) from manager_rest.security import SecuredResource, premium_only -from manager_rest.rest import rest_utils from manager_rest.storage import get_storage_manager, models from manager_rest.security.authorization import ( authorize, @@ -127,6 +126,10 @@ def delete(self): RabbitMQBrokersId = _CommunityBrokersId +class _ListManagersQuery(pydantic.BaseModel): + hostname: Optional[str] = None + + class Managers(managers_base): @marshal_with(models.Manager) @paginate @@ -137,10 +140,8 @@ def get(self, pagination=None, _include=None): :param hostname: optional hostname to return only a specific manager :param _include: optional, what columns to include in the response """ - args = rest_utils.get_args_and_verify_arguments([ - Argument('hostname', type=str, required=False) - ]) - hostname = args.get('hostname') + args = _ListManagersQuery.parse_obj(request.args) + hostname = args.hostname if hostname: return get_storage_manager().list( models.Manager, @@ -197,9 +198,18 @@ def get(self, **_): return {'files': files_list}, 200 +class _FSProxyGetArgs(pydantic.BaseModel): + archive: Optional[bool] = False + + +class _FSProxyPutArgs(pydantic.BaseModel): + extract: Optional[bool] = False + + class FileServerProxy(SecuredResource): - def __init__(self): - self.storage_handler = get_storage_handler() + @property + def storage_handler(self): + return get_storage_handler() def get(self, path=None, **_): rel_path = _resource_relative_path(path) @@ -207,14 +217,11 @@ def get(self, path=None, **_): if not path: return {}, 404 - args = rest_utils.get_args_and_verify_arguments([ - Argument('archive', type=bool, required=False) - ]) - as_archive = args.get('archive', False) + args = _FSProxyGetArgs.parse_obj(request.args) if not _is_resource_path_directory(rel_path): return self.storage_handler.proxy(rel_path) - elif not as_archive: + elif not args.archive: files_list = [os.path.relpath(file_name, rel_path) for file_name in self.storage_handler.list(rel_path)] return {'files': files_list}, 200 @@ -253,17 +260,14 @@ def get(self, path=None, **_): return result def put(self, path=None): - args = rest_utils.get_args_and_verify_arguments([ - Argument('extract', type=bool, required=False) - ]) - extract = args.get('extract', False) + args = _FSProxyPutArgs.parse_obj(request.args) _, tmp_file_name = tempfile.mkstemp() with open(tmp_file_name, 'wb') as tmp_file: tmp_file.write(request.data) tmp_file.close() - if not extract: + if not args.extract: return self.storage_handler.move(tmp_file_name, path) try: diff --git a/rest-service/manager_rest/rest/resources_v3_1/manager_config.py b/rest-service/manager_rest/rest/resources_v3_1/manager_config.py index 48b5f4941a..ae443fde8f 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/manager_config.py +++ b/rest-service/manager_rest/rest/resources_v3_1/manager_config.py @@ -13,11 +13,11 @@ # * See the License for the specific language governing permissions and # * limitations under the License. -from typing import Dict +import pydantic +from typing import Any, Dict, Optional -from flask_restful.reqparse import Argument +from flask import request -from manager_rest.rest import rest_utils from manager_rest.rest.rest_decorators import ( marshal_with ) @@ -28,6 +28,10 @@ from manager_rest.storage import get_storage_manager, models +class _ConfigListQuery(pydantic.BaseModel): + scope: Optional[str] = None + + class ManagerConfig(SecuredResource): @authorize('manager_config_get') def get(self): @@ -36,10 +40,7 @@ def get(self): Scope can be eg. "rest" or "mgmtworker", for filtering out the settings only for a single Manager component. """ - args = rest_utils.get_args_and_verify_arguments([ - Argument('scope', type=str, required=False) - ]) - scope = args.get('scope') + scope = _ConfigListQuery.parse_obj(request.args).scope result: Dict = {'metadata': {}} if scope: result['items'] = self._get_items(scope) @@ -74,6 +75,11 @@ def _authorization_config(self): } +class _UpdateConfigArgs(pydantic.BaseModel): + value: Any + force: Optional[bool] = False + + class ManagerConfigId(SecuredResource): @marshal_with(models.Config) @authorize('manager_config_get') @@ -97,12 +103,9 @@ def put(self, name): must validate. Names can be prefixed with scope, using the same semantics as GET. """ - data = rest_utils.get_json_and_verify_params({ - 'value': {}, - 'force': {'type': bool, 'optional': True} - }) - value = data['value'] - force = data.get('force', False) + data = _UpdateConfigArgs.parse_obj(request.json) + value = data.value + force = data.force config.instance.update_db({name: value}, force) return self._get_config(name) diff --git a/rest-service/manager_rest/rest/resources_v3_1/nodes.py b/rest-service/manager_rest/rest/resources_v3_1/nodes.py index fcfa397eaa..2da122b8f7 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/nodes.py +++ b/rest-service/manager_rest/rest/resources_v3_1/nodes.py @@ -1,5 +1,8 @@ +import pydantic from collections import defaultdict -from typing import Dict +from typing import Any, List, Dict, Optional + +from flask import request from ..resources_v3 import ( Nodes as v3_Nodes, @@ -16,27 +19,29 @@ from manager_rest.security import SecuredResource +class _CreateNodesArgs(pydantic.BaseModel): + deployment_id: str + nodes: List[Dict[str, Any]] + + class Nodes(v3_Nodes): @authorize('node_list') def post(self): - request_dict = rest_utils.get_json_and_verify_params({ - 'deployment_id': {'type': str}, - 'nodes': {'type': list}, - }) + args = _CreateNodesArgs.parse_obj(request.json) sm = get_storage_manager() - raw_nodes = request_dict['nodes'] + raw_nodes = args.nodes if not raw_nodes: - return None, 204 + return "", 204 with sm.transaction(): - deployment_id = request_dict['deployment_id'] + deployment_id = args.deployment_id deployment = sm.get(models.Deployment, deployment_id) self._prepare_raw_nodes(deployment, raw_nodes) db.session.execute( models.Node.__table__.insert(), raw_nodes, ) - return None, 201 + return "[]", 201 def _prepare_raw_nodes(self, deployment, raw_nodes): if any(item.get('creator') for item in raw_nodes): @@ -106,17 +111,19 @@ def _prepare_node_relationships(self, raw_relationships): return prepared_relationships +class _UpdateNodeArgs(pydantic.BaseModel): + plugins: Optional[Dict[str, Any]] = None + operations: Optional[Dict[str, Any]] = None + properties: Optional[Dict[str, Any]] = None + capabilities: Optional[Dict[str, Any]] = None + relationships: Optional[Any] = None + + class NodesId(SecuredResource): @authorize('node_update') @only_deployment_update def patch(self, deployment_id, node_id): - request_dict = rest_utils.get_json_and_verify_params({ - 'plugins': {'optional': True}, - 'operations': {'optional': True}, - 'relationships': {'optional': True}, - 'properties': {'optional': True}, - 'capabilities': {'optional': True}, - }) + request_dict = _UpdateNodeArgs.parse_obj(request.json).dict() sm = get_storage_manager() with sm.transaction(): deployment = sm.get(models.Deployment, deployment_id) @@ -146,7 +153,7 @@ def patch(self, deployment_id, node_id): node.planned_number_of_instances = \ scalable['planned_instances'] sm.update(node) - return None, 204 + return "", 204 @authorize('node_delete') @only_deployment_update @@ -157,29 +164,31 @@ def delete(self, deployment_id, node_id): node = sm.get(models.Node, None, filters={'id': node_id, 'deployment': deployment}) sm.delete(node) - return None, 204 + return "", 204 + + +class _CreateNodeInstancesArgs(pydantic.BaseModel): + deployment_id: str + node_instances: List[Dict[str, Any]] class NodeInstances(v2_NodeInstances): @authorize('node_list') def post(self): - request_dict = rest_utils.get_json_and_verify_params({ - 'deployment_id': {'type': str}, - 'node_instances': {'type': list}, - }) + args = _CreateNodeInstancesArgs.parse_obj(request.json) sm = get_storage_manager() - raw_instances = request_dict['node_instances'] + raw_instances = args.node_instances if not raw_instances: - return None, 204 + return "", 204 with sm.transaction(): - deployment_id = request_dict['deployment_id'] + deployment_id = args.deployment_id deployment = sm.get(models.Deployment, deployment_id) self._prepare_raw_instances(sm, deployment, raw_instances) db.session.execute( models.NodeInstance.__table__.insert(), raw_instances, ) - return None, 201 + return "[]", 201 def _prepare_raw_instances(self, sm, deployment, raw_instances): if any(item.get('creator') for item in raw_instances): @@ -236,4 +245,4 @@ def delete(self, node_instance_id): with sm.transaction(): instance = sm.get(models.NodeInstance, node_instance_id) sm.delete(instance) - return None, 204 + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v3_1/operations.py b/rest-service/manager_rest/rest/resources_v3_1/operations.py index d4c8fbe49c..fcc84971ef 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/operations.py +++ b/rest-service/manager_rest/rest/resources_v3_1/operations.py @@ -1,6 +1,8 @@ +import pydantic from datetime import datetime +from typing import Any, Dict, List, Optional + from flask import request -from flask_restful.reqparse import Argument from cloudify import constants as common_constants from cloudify.workflows import events as common_events, tasks @@ -8,11 +10,7 @@ from sqlalchemy.dialects.postgresql import JSON from manager_rest import manager_exceptions -from manager_rest.rest.rest_utils import ( - get_args_and_verify_arguments, - get_json_and_verify_params, - parse_datetime_string, -) +from manager_rest.rest.rest_utils import parse_datetime_string from manager_rest.rest.rest_decorators import ( marshal_with, paginate, @@ -30,22 +28,29 @@ from manager_rest.execution_token import current_execution +class _OperationsActionArgs(pydantic.BaseModel): + action: str + + +class _OperationListQuery(pydantic.BaseModel): + graph_id: Optional[str] = None + execution: Optional[str] = None + state: Optional[str] = None + skip_internal: Optional[bool] = None + execution_id: Optional[str] = None + + class Operations(SecuredResource): @authorize('operations') @marshal_with(models.Operation) @paginate def get(self, _include=None, pagination=None, **kwargs): - args = get_args_and_verify_arguments([ - Argument('graph_id', type=str, required=False), - Argument('execution_id', type=str, required=False), - Argument('state', type=str, required=False), - Argument('skip_internal', type=bool, required=False), - ]) + args = _OperationListQuery.parse_obj(request.args) sm = get_storage_manager() - graph_id = args.get('graph_id') - exc_id = args.get('execution_id') - state = args.get('state') - skip_internal = args.get('skip_internal') + graph_id = args.graph_id + exc_id = args.execution_id + state = args.state + skip_internal = args.skip_internal filters = {} if graph_id and exc_id: @@ -74,11 +79,11 @@ def get(self, _include=None, pagination=None, **kwargs): @authorize('operations') def post(self, **kwargs): - request_dict = get_json_and_verify_params({'action'}) - action = request_dict['action'] + args = _OperationsActionArgs.parse_obj(request.json) + action = args.action if action == 'update-stored': self._update_stored_operations() - return None, 204 + return "", 204 def _update_stored_operations(self): """Recompute operation inputs, for resumable ops of the given node @@ -92,7 +97,7 @@ def _update_stored_operations(self): """ deployment_id = request.json['deployment_id'] if not deployment_id: - return None, 204 + return "", 204 node_id = request.json['node_id'] op_name = request.json['operation'] @@ -179,6 +184,23 @@ def _update_operation_inputs(self, sm, operation, new_inputs): sm.update(operation, modified_attrs=['parameters']) +class _CreateOperationArgs(pydantic.BaseModel): + name: str + graph_id: str + dependencies: List[str] + parameters: Optional[Dict[str, Any]] = None + type: str + + +class _UpdateOperationArgs(pydantic.BaseModel): + state: Optional[str] = None + result: Optional[Any] = None + exception: Optional[str] = None + exception_causes: Optional[Any] = None + manager_name: Optional[str] = None + agent_name: Optional[str] = None + + class OperationsId(SecuredResource): @authorize('operations') @marshal_with(models.Operation) @@ -188,48 +210,35 @@ def get(self, operation_id, **kwargs): @authorize('operations') @marshal_with(models.Operation) def put(self, operation_id, **kwargs): - params = get_json_and_verify_params({ - 'name': {'type': str, 'required': True}, - 'graph_id': {'type': str, 'required': True}, - 'dependencies': {'type': list, 'required': True}, - 'parameters': {'type': dict}, - 'type': {'type': str} - }) + params = _CreateOperationArgs.parse_obj(request.json) operation = get_resource_manager().create_operation( operation_id, - name=params['name'], - graph_id=params['graph_id'], - dependencies=params['dependencies'], - type=params['type'], - parameters=params['parameters'] + name=params.name, + graph_id=params.graph_id, + dependencies=params.dependencies, + type=params.type, + parameters=params.parameters ) return operation, 201 @authorize('operations', allow_if_execution=True) @detach_globals def patch(self, operation_id, **kwargs): - request_dict = get_json_and_verify_params({ - 'state': {'type': str}, - 'result': {'optional': True}, - 'exception': {'optional': True}, - 'exception_causes': {'optional': True}, - 'manager_name': {'optional': True}, - 'agent_name': {'optional': True}, - }) + args = _UpdateOperationArgs.parse_obj(request.json) sm = get_storage_manager() with sm.transaction(): instance = sm.get(models.Operation, operation_id, locking=True) old_state = instance.state - instance.manager_name = request_dict.get('manager_name') - instance.agent_name = request_dict.get('agent_name') - instance.state = request_dict.get('state', instance.state) + instance.manager_name = args.manager_name + instance.agent_name = args.agent_name + instance.state = args.state or instance.state if instance.state == common_constants.TASK_SUCCEEDED: self._on_task_success(sm, instance) self._insert_event( instance, - request_dict.get('result'), - request_dict.get('exception'), - request_dict.get('exception_causes') + args.result, + args.exception, + args.exception_causes, ) if not instance.is_nop and \ old_state not in common_constants.TERMINATED_STATES and \ @@ -397,18 +406,20 @@ def delete(self, operation_id, **kwargs): return instance, 200 +class _GraphsListQuery(pydantic.BaseModel): + execution_id: str + name: Optional[str] = None + + class TasksGraphs(SecuredResource): @authorize('operations') @marshal_with(models.TasksGraph) @paginate def get(self, _include=None, pagination=None, **kwargs): - args = get_args_and_verify_arguments([ - Argument('execution_id', type=str, required=True), - Argument('name', type=str, required=False) - ]) + args = _GraphsListQuery.parse_obj(request.args) sm = get_storage_manager() - execution_id = args.get('execution_id') - name = args.get('name') + execution_id = args.execution_id + name = args.name execution = sm.get(models.Execution, execution_id) filters = {'execution': execution} if name: @@ -421,45 +432,49 @@ def get(self, _include=None, pagination=None, **kwargs): ) +class _CreateGraphArgs(pydantic.BaseModel): + name: str + execution_id: str + operations: Optional[List[Any]] = None + created_at: Optional[str] = None + graph_id: Optional[str] = None + + +class _UpdateGraphArgs(pydantic.BaseModel): + state: str + + class TasksGraphsId(SecuredResource): @authorize('operations') @marshal_with(models.TasksGraph) def post(self, **kwargs): - params = get_json_and_verify_params({ - 'name': {'type': str}, - 'execution_id': {'type': str}, - 'operations': {'optional': True}, - 'created_at': {'optional': True}, - 'graph_id': {'optional': True}, - }) - created_at = params.get('created_at') - operations = params.get('operations') or [] - if params.get('graph_id'): + params = _CreateGraphArgs.parse_obj(request.json) + created_at = params.created_at + operations = params.operations + if params.graph_id is not None: check_user_action_allowed('set_execution_details') if created_at or any(op.get('created_at') for op in operations): check_user_action_allowed('set_timestamp') - created_at = parse_datetime_string(params.get('created_at')) + created_at = parse_datetime_string(params.created_at) for op in operations: if op.get('created_at'): op['created_at'] = parse_datetime_string(op['created_at']) sm = get_storage_manager() with sm.transaction(): tasks_graph = get_resource_manager().create_tasks_graph( - name=params['name'], - execution_id=params['execution_id'], - operations=params.get('operations', []), + name=params.name, + execution_id=params.execution_id, + operations=params.operations, created_at=created_at, - graph_id=params.get('graph_id') + graph_id=params.graph_id, ) return tasks_graph, 201 @authorize('operations') @marshal_with(models.TasksGraph) def patch(self, tasks_graph_id, **kwargs): - request_dict = get_json_and_verify_params( - {'state': {'type': str}} - ) + params = _UpdateGraphArgs.parse_obj(request.json) sm = get_storage_manager() instance = sm.get(models.TasksGraph, tasks_graph_id, locking=True) - instance.state = request_dict.get('state', instance.state) + instance.state = params.state or instance.state return sm.update(instance) diff --git a/rest-service/manager_rest/rest/resources_v3_1/permissions.py b/rest-service/manager_rest/rest/resources_v3_1/permissions.py index 8f8b396b4d..37218dc10d 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/permissions.py +++ b/rest-service/manager_rest/rest/resources_v3_1/permissions.py @@ -88,4 +88,4 @@ def delete(self, role_name, permission_name): sm.delete(perm) role.updated_at = datetime.utcnow() sm.put(role) - return None, 204 + return "", 204 diff --git a/rest-service/manager_rest/rest/resources_v3_1/plugins.py b/rest-service/manager_rest/rest/resources_v3_1/plugins.py index 2e8d509535..ceb77a789d 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/plugins.py +++ b/rest-service/manager_rest/rest/resources_v3_1/plugins.py @@ -1,5 +1,7 @@ -from flask import request as flask_request -from werkzeug.exceptions import BadRequest +import pydantic +from typing import Any, Dict, List, Optional + +from flask import request from cloudify.models_states import VisibilityState @@ -30,37 +32,53 @@ class PluginsSetVisibility(SecuredResource): - @authorize('resource_set_visibility') @rest_decorators.marshal_with(models.Plugin) def patch(self, plugin_id): """ Set the plugin's visibility """ - visibility = rest_utils.get_visibility_parameter() + args = rest_utils.SetVisibilityArgs.parse_obj(request.json) plugin = get_storage_manager().get(models.Plugin, plugin_id) - return get_resource_manager().set_visibility(plugin, visibility) + return get_resource_manager().set_visibility(plugin, args.visibility) + + +class _PluginUploadArgs(pydantic.BaseModel): + visibility: Optional[VisibilityState] = None class Plugins(resources_v2.Plugins): @authorize('plugin_upload') @rest_decorators.marshal_with(models.Plugin) def post(self, **kwargs): - """ - Upload a plugin - """ - - visibility = rest_utils.get_visibility_parameter( - optional=True, - is_argument=True, - valid_values=VisibilityState.STATES - ) + """Upload a plugin""" + args = _PluginUploadArgs.parse_obj(request.args) with rest_utils.skip_nested_marshalling(): - return super(Plugins, self).post(visibility=visibility) + return super(Plugins, self).post(visibility=args.visibility) + + +class _CreatePluginUpdateArgs(pydantic.BaseModel): + plugin_names: Optional[List[str]] = [] + all_to_latest: Optional[bool] = None + to_latest: Optional[List[str]] = [] + all_to_minor: Optional[bool] = None + to_minor: Optional[List[str]] = [] + mapping: Optional[Dict[str, Any]] = {} + force: Optional[bool] = False + auto_correct_types: Optional[bool] = False + reevaluate_active_statuses: Optional[bool] = False + all_tenants: Optional[bool] = None + created_by: Optional[str] = None + created_at: Optional[str] = None + update_id: Optional[str] = None + execution_id: Optional[str] = None + state: Optional[str] = None + affected_deployments: Optional[List[str]] = [] + temp_blueprint_id: Optional[str] = None + deployments_per_tenant: Optional[dict] = None class PluginsUpdate(SecuredResource): - @authorize('plugins_update_create') @rest_decorators.marshal_with(models.PluginsUpdate) def post(self, id, phase, **kwargs): @@ -79,27 +97,8 @@ def post(self, id, phase, **kwargs): :param phase: either PHASES.INITIAL or PHASES.FINAL (internal). """ try: - args = rest_utils.get_json_and_verify_params({ - PLUGIN_NAMES: {'type': list, 'optional': True}, - ALL_TO_LATEST: {'type': bool, 'optional': True}, - TO_LATEST: {'type': list, 'optional': True}, - ALL_TO_MINOR: {'type': bool, 'optional': True}, - TO_MINOR: {'type': list, 'optional': True}, - MAPPING: {'type': dict, 'optional': True}, - FORCE: {'type': bool, 'optional': True}, - AUTO_CORRECT_TYPES: {'type': bool, 'optional': True}, - REEVALUATE_ACTIVE_STATUSES: {'type': bool, 'optional': True}, - 'all_tenants': {'type': bool, 'optional': True}, - 'created_by': {'type': str, 'optional': True}, - 'created_at': {'type': str, 'optional': True}, - 'update_id': {'type': str, 'optional': True}, - 'execution_id': {'type': str, 'optional': True}, - 'state': {'type': str, 'optional': True}, - 'affected_deployments': {'type': list, 'optional': True}, - 'deployments_per_tenant': {'type': dict, 'optional': True}, - 'temp_blueprint_id': {'type': str, 'optional': True}, - }) - except BadRequest: + args = _CreatePluginUpdateArgs.parse_obj(request.json).dict() + except Exception: # continue on any parsing error, even invalid json args = {} filter_args = [ @@ -116,12 +115,11 @@ def post(self, id, phase, **kwargs): update_manager = get_plugins_updates_manager() - if any(arg in args for arg in ['created_by', 'created_at', - 'update_id', - 'execution_id', 'state', - 'affected_deployments', - 'deployments_per_tenant', - 'temp_blueprint_id']): + if any(arg is not None in args for arg in [ + 'created_by', 'created_at', 'update_id', 'execution_id', 'state', + 'affected_deployments', 'temp_blueprint_id', + 'deployments_per_tenant', + ]): check_user_action_allowed('set_plugin_update_details') if not args.get('state'): raise manager_exceptions.BadParametersError( @@ -225,6 +223,26 @@ def get(self, return plugins_update +class _PluginForceInstallArgs(pydantic.BaseModel): + action: str + managers: Optional[List[str]] = None + agents: Optional[List[str]] = None + + +class _PluginUpdateArgs(pydantic.BaseModel): + state: str + agent: Optional[str] = None + manager: Optional[str] = None + error: Optional[str] = None + + +class _PluginSetOwnerArgs(pydantic.BaseModel): + creator: Optional[str] = None + blueprint_labels: Optional[Dict[str, Any]] = None + labels: Optional[Dict[str, Any]] = None + resource_tags: Optional[Dict[str, Any]] = None + + class PluginsId(resources_v2_1.PluginsId): @authorize('plugin_upload') @rest_decorators.marshal_with(models.Plugin) @@ -234,23 +252,17 @@ def post(self, plugin_id, **kwargs): This method is for internal use only. """ sm = get_storage_manager() - action_dict = rest_utils.get_json_and_verify_params({ - 'action': {'type': str}, - }) + params = _PluginForceInstallArgs.parse_obj(request.json) plugin = sm.get(models.Plugin, plugin_id) - if action_dict.get('action') == 'install': - install_dict = rest_utils.get_json_and_verify_params({ - 'managers': {'type': list, 'optional': True}, - 'agents': {'type': list, 'optional': True}, - }) + if params.action == 'install': get_resource_manager().install_plugin( plugin, - manager_names=install_dict.get('managers'), - agent_names=install_dict.get('agents'), + manager_names=params.managers, + agent_names=params.agents, ) return get_resource_manager().install_plugin(plugin) else: - raise manager_exceptions.UnknownAction(action_dict.get('action')) + raise manager_exceptions.UnknownAction(params.action) @authorize('plugin_upload', allow_if_execution=True) def put(self, plugin_id, **kwargs): @@ -259,14 +271,9 @@ def put(self, plugin_id, **kwargs): Only updating the state is supported right now. This method is for internal use only. """ - request_dict = rest_utils.get_json_and_verify_params({ - 'agent': {'type': str, 'optional': True}, - 'manager': {'type': str, 'optional': True}, - 'state': {'type': str}, - 'error': {'type': str, 'optional': True}, - }) - agent_name = request_dict.get('agent') - manager_name = request_dict.get('manager') + params = _PluginUpdateArgs.parse_obj(request.json) + agent_name = params.agent + manager_name = params.manager if agent_name and manager_name: raise manager_exceptions.ConflictError( 'Expected agent or manager, got both') @@ -292,7 +299,7 @@ def put(self, plugin_id, **kwargs): # response = plugin.to_response() get_resource_manager().set_plugin_state( plugin=plugin, manager=manager, agent=agent, - state=request_dict['state'], error=request_dict.get('error')) + state=params.state, error=params.error) except manager_exceptions.SQLStorageException as e: # plugin was most likely deleted concurrently - refetch it # to confirm: the .get() will throw a 404 @@ -308,21 +315,19 @@ def patch(self, plugin_id, **kwargs): Only updating the ownership is supported right now. """ - request_dict = rest_utils.get_json_and_verify_params({ - 'creator': {'type': str, 'optional': True}, - 'blueprint_labels': {'type': dict, 'optional': True}, - 'labels': {'type': dict, 'optional': True}, - }) + params = _PluginSetOwnerArgs.parse_obj(request.json) sm = get_storage_manager() plugin = sm.get(models.Plugin, plugin_id) - if 'creator' in request_dict: + if params.creator: check_user_action_allowed('set_owner', None, True) - creator = rest_utils.valid_user(request_dict['creator']) + creator = rest_utils.valid_user(params.creator) plugin.creator = creator - for key in ['blueprint_labels', 'labels', 'resource_tags']: - if key not in request_dict: - continue - setattr(plugin, key, request_dict[key]) + if params.blueprint_labels is not None: + plugin.blueprint_labels = params.blueprint_labels + if params.labels is not None: + plugin.labels = params.labels + if params.resource_tags is not None: + plugin.resource_tags = params.resource_tags sm.update(plugin) return plugin.to_response() @@ -341,7 +346,7 @@ def get(self, plugin_id, **kwargs): """ Download plugin yaml """ - dsl_version = flask_request.args.get('dsl_version') + dsl_version = request.args.get('dsl_version') plugin = get_storage_manager().get(models.Plugin, plugin_id) yaml_file_path = plugin.yaml_file_path(dsl_version) return get_storage_handler().proxy(yaml_file_path) diff --git a/rest-service/manager_rest/rest/resources_v3_1/searches.py b/rest-service/manager_rest/rest/resources_v3_1/searches.py index 3e2d67d216..e9219de96d 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/searches.py +++ b/rest-service/manager_rest/rest/resources_v3_1/searches.py @@ -1,7 +1,8 @@ +import pydantic from collections import defaultdict +from typing import Any, Dict, List, Optional from flask import request -from flask_restful.reqparse import Argument from dsl_parser.utils import get_function @@ -85,19 +86,19 @@ def _swagger_searches_docs(resource_model, resource_name): } +class _SearchArgs(pydantic.BaseModel): + filter_rules: Optional[List[Any]] = None + constraints: Optional[Dict[str, Any]] = None + + class ResourceSearches(SecuredResource): def post(self, resource_model, filters_model, _include, filters, - pagination, sort, all_tenants, search, filter_id, **kwargs): + pagination, sort, search, filter_id, + constraints=None, **kwargs): """List resource items""" - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) - request_schema = {'filter_rules': {'optional': True, 'type': list}, - 'constraints': {'optional': True, 'type': dict}} - request_dict = rest_utils.get_json_and_verify_params(request_schema) - constraints = kwargs.get('constraints') if 'constraints' in kwargs \ - else request_dict.get('constraints') + query = rest_utils.ListQuery.parse_obj(request.args) + args = _SearchArgs.parse_obj(request.json) + constraints = constraints or args.constraints resource_field = kwargs.get('resource_field', 'id') sm = get_storage_manager() filter_rules = get_filter_rules(sm, @@ -105,9 +106,8 @@ def post(self, resource_model, filters_model, _include, filters, resource_field, filters_model, filter_id, - request_dict.get('filter_rules'), + args.filter_rules, constraints) - sort_labels = None if sort: sort_labels = {k.split(":", 1)[1]: v @@ -123,8 +123,8 @@ def post(self, resource_model, filters_model, _include, filters, pagination=pagination, sort=sort, sort_labels=sort_labels, - all_tenants=all_tenants, - get_all_results=get_all_results, + all_tenants=query.all_tenants, + get_all_results=query.get_all_results, filter_rules=filter_rules, ) @@ -138,15 +138,14 @@ class DeploymentsSearches(ResourceSearches): @rest_decorators.marshal_with(models.Deployment) @rest_decorators.paginate @rest_decorators.sortable(models.Deployment) - @rest_decorators.all_tenants @rest_decorators.search_multiple_parameters(DEPLOYMENT_SEARCHES_MAP) @rest_decorators.filter_id def post(self, _include=None, pagination=None, sort=None, - all_tenants=None, search=None, filter_id=None, **kwargs): + search=None, filter_id=None, **kwargs): """List deployments using filter rules or DSL constraints""" filters = rest_utils.deployment_group_id_filter() return super().post(models.Deployment, models.DeploymentsFilter, - _include, filters, pagination, sort, all_tenants, + _include, filters, pagination, sort, search, filter_id, resource_field='display_name', **kwargs) @@ -158,15 +157,14 @@ class BlueprintsSearches(ResourceSearches): @rest_decorators.marshal_with(models.Blueprint) @rest_decorators.paginate @rest_decorators.sortable(models.Blueprint) - @rest_decorators.all_tenants @rest_decorators.search('id') @rest_decorators.filter_id def post(self, _include=None, pagination=None, sort=None, - all_tenants=None, search=None, filter_id=None, **kwargs): + search=None, filter_id=None, **kwargs): """List blueprints using filter rules or DSL constraints""" filters = {'is_hidden': False} return super().post(models.Blueprint, models.BlueprintsFilter, - _include, filters, pagination, sort, all_tenants, + _include, filters, pagination, sort, search, filter_id, **kwargs) @@ -183,15 +181,14 @@ class WorkflowsSearches(ResourceSearches): filter_rules_types=FILTER_RULE_TYPES) @authorize('deployment_list', allow_all_tenants=True) @rest_decorators.marshal_list_response - @rest_decorators.all_tenants @rest_decorators.search('id') @rest_decorators.filter_id - def post(self, all_tenants=None, search=None, filter_id=None, **kwargs): + def post(self, search=None, filter_id=None, **kwargs): """List workflows using filter rules""" _include = ['id', 'workflows'] filters = rest_utils.deployment_group_id_filter() result = super().post(models.Deployment, models.DeploymentsFilter, - _include, filters, None, None, all_tenants, + _include, filters, None, None, search, filter_id, **kwargs) return workflows_list_response(result) @@ -203,14 +200,13 @@ class NodesSearches(ResourceSearches): @rest_decorators.marshal_with(models.Node) @rest_decorators.paginate @rest_decorators.sortable(models.Node) - @rest_decorators.all_tenants @rest_decorators.search('id') def post(self, _include=None, pagination=None, sort=None, - all_tenants=None, search=None, **kwargs): + search=None, **kwargs): """List Nodes using filter rules or DSL constraints""" blueprint_id, deployment_id, constraints = \ retrieve_constraints(id_required=True) - + all_tenants = False # TODO if blueprint_id: sm = get_storage_manager() blueprint = sm.get(models.Blueprint, blueprint_id, @@ -251,16 +247,16 @@ class NodeTypesSearches(ResourceSearches): @rest_decorators.marshal_with(models.Node) @rest_decorators.paginate @rest_decorators.sortable(models.Node) - @rest_decorators.all_tenants @rest_decorators.search('type') def post(self, _include=None, pagination=None, sort=None, - all_tenants=None, search=None, **kwargs): + search=None, **kwargs): """List Nodes using filter rules or DSL constraints""" blueprint_id, deployment_id, constraints = \ retrieve_constraints(id_required=True) if 'name_pattern' in constraints: constraints['type_specs'] = constraints.pop('name_pattern') + all_tenants = False # TODO if blueprint_id: sm = get_storage_manager() blueprint = sm.get(models.Blueprint, blueprint_id, @@ -274,7 +270,7 @@ def post(self, _include=None, pagination=None, sort=None, deployment_id, constraints['valid_values']) filters = {'deployment_id': deployment_id} return super().post(models.Node, None, _include, filters, pagination, - sort, all_tenants, search, None, + sort, search, None, constraints=constraints, resource_field='type', **kwargs) @@ -299,6 +295,10 @@ def node_types_from_plan(blueprint, search_value, constraints): ) +class _InstanceSearchQuery(pydantic.BaseModel): + node_id: Optional[str] = None + + class NodeInstancesSearches(ResourceSearches): @swagger.operation(**_swagger_searches_docs(models.NodeInstance, 'node_instances')) @@ -306,25 +306,21 @@ class NodeInstancesSearches(ResourceSearches): @rest_decorators.marshal_with(models.NodeInstance) @rest_decorators.paginate @rest_decorators.sortable(models.NodeInstance) - @rest_decorators.all_tenants @rest_decorators.search('id') def post(self, _include=None, pagination=None, sort=None, - all_tenants=None, search=None, **kwargs): + search=None, **kwargs): """List NodeInstances using filter rules""" _, deployment_id, constraints = retrieve_constraints() if 'name_pattern' in constraints: constraints['id_specs'] = constraints.pop('name_pattern') - args = rest_utils.get_args_and_verify_arguments([ - Argument('node_id', required=False), - ]) - node_id = args.get('node_id') + node_id = _InstanceSearchQuery.parse_obj(request.args).node_id filters = {} if deployment_id: filters['deployment_id'] = deployment_id if node_id: filters['node_id'] = node_id return super().post(models.NodeInstance, None, _include, filters, - pagination, sort, all_tenants, search, None, + pagination, sort, search, None, constraints=constraints, resource_field='id', **kwargs) @@ -341,8 +337,18 @@ def post(self, _include=None, pagination=None, sort=None, search=None, **kwargs): """List secrets using filter rules or DSL constraints""" return super().post(models.Secret, None, _include, {}, pagination, - sort, False, search, None, - resource_field='key', **kwargs) + sort, search, None, resource_field='key', **kwargs) + + +class _SearchParamQuery(pydantic.BaseModel): + search: Optional[str] = pydantic.Field( + alias='_search', + default=None, + ) + get_all_results: Optional[bool] = pydantic.Field( + alias='_get_all_results', + default=False, + ) class CapabilitiesSearches(ResourceSearches): @@ -401,15 +407,10 @@ def post(self, search=None, _include=None, if 'name_pattern' in constraints: constraints['capability_key_specs'] = \ constraints.pop('name_pattern') - args = rest_utils.get_args_and_verify_arguments([ - Argument('_search', required=False), - ]) - search = args._search - - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + + args = _SearchParamQuery.parse_obj(request.args) + search = args.search + get_all_results = args.get_all_results deployments = get_storage_manager().list( models.Deployment, @@ -506,9 +507,10 @@ def post(self, _include=None, pagination=None, all_tenants=None, **kwargs): if 'name_pattern' in constraints: constraints['scaling_group_name_specs'] = \ constraints.pop('name_pattern') - search_value = rest_utils.get_args_and_verify_arguments([ - Argument('_search', required=False), - ]).get('_search') + + args = _SearchParamQuery.parse_obj(request.args) + search_value = args.search + get_all_results = args.get_all_results if blueprint_id: sm = get_storage_manager() @@ -518,11 +520,6 @@ def post(self, _include=None, pagination=None, all_tenants=None, **kwargs): search_value, constraints) - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) - deployments = get_storage_manager().list( models.Deployment, include=_include, @@ -561,14 +558,15 @@ def build_response(scaling_groups, search_value, constraints): ) +class _ConstraintsArgs(pydantic.BaseModel): + deployment_id: Optional[str] = None + blueprint_id: Optional[str] = None + constraints: Optional[dict] = None + + def retrieve_constraints(id_required=False): - args = rest_utils.get_args_and_verify_arguments([ - Argument('deployment_id', required=False), - Argument('blueprint_id', required=False), - ]) - request_dict = rest_utils.get_json_and_verify_params( - {'constraints': {'optional': True, 'type': dict}}) - constraints = request_dict.get('constraints', {}) + args = _ConstraintsArgs.parse_obj(request.json).dict() + constraints = args.constraints or {} if args.get('deployment_id') and 'deployment_id' in constraints: raise manager_exceptions.BadParametersError( "You should provide either a valid 'deployment_id' parameter " diff --git a/rest-service/manager_rest/rest/resources_v3_1/sites.py b/rest-service/manager_rest/rest/resources_v3_1/sites.py index ee9d92dcb4..84a9977daa 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/sites.py +++ b/rest-service/manager_rest/rest/resources_v3_1/sites.py @@ -1,3 +1,6 @@ +import pydantic +from typing import Optional + from flask import request from cloudify.models_states import VisibilityState @@ -11,10 +14,15 @@ ) from manager_rest.storage import models, get_storage_manager from manager_rest.resource_manager import get_resource_manager -from manager_rest.rest.rest_utils import (validate_inputs, - verify_and_convert_bool, - get_visibility_parameter, - get_json_and_verify_params) +from manager_rest.rest.rest_utils import validate_inputs, ListQuery + + +class _SiteCreateArgs(pydantic.BaseModel): + location: Optional[str] = None + new_name: Optional[str] = None + created_at: Optional[str] = None + created_by: Optional[str] = None + visibility: Optional[VisibilityState] = None class SitesName(SecuredResource): @@ -42,13 +50,13 @@ def put(self, name): new_site.visibility = (request_dict['visibility'] or VisibilityState.TENANT) - if 'created_at' in request_dict: + if request_dict['created_at']: check_user_action_allowed('set_timestamp') new_site.created_at = request_dict['created_at'] else: new_site.created_at = utils.get_formatted_timestamp() - if 'created_by' in request_dict: + if request_dict['created_by']: check_user_action_allowed('set_owner') new_site.creator = rest_utils.valid_user( request_dict['created_by']) @@ -64,10 +72,12 @@ def post(self, name): storage_manager = get_storage_manager() self._validate_new_name(request_dict, storage_manager, name) site = storage_manager.get(models.Site, name) - site.name = request_dict.get('new_name', site.name) - site.id = request_dict.get('new_name', site.id) - site.latitude = request_dict.get('latitude', site.latitude) - site.longitude = request_dict.get('longitude', site.longitude) + site.name = request_dict['new_name'] or site.name + site.id = request_dict['new_name'] or site.id + if 'latitude' in request_dict: + site.latitude = request_dict['latitude'] + if 'longitude' in request_dict: + site.longitude = request_dict['longitude'] visibility = request_dict['visibility'] if visibility: get_resource_manager().validate_visibility_value(site, visibility) @@ -82,19 +92,11 @@ def delete(self, name): storage_manager = get_storage_manager() site = storage_manager.get(models.Site, name) storage_manager.delete(site, validate_global=True) - return None, 204 + return "", 204 def _validate_site_params(self, name): validate_inputs({'name': name}) - visibility = get_visibility_parameter( - optional=True, - valid_values=VisibilityState.STATES, - ) - request_dict = get_json_and_verify_params({ - 'location': {'type': str, 'optional': True}, - 'new_name': {'type': str, 'optional': True} - }) - request_dict['visibility'] = visibility + request_dict = _SiteCreateArgs.parse_obj(request.json).dict() self._validate_location(request_dict) return request_dict @@ -155,17 +157,11 @@ class Sites(SecuredResource): @rest_decorators.create_filters(models.Site) @rest_decorators.paginate @rest_decorators.sortable(models.Site) - @rest_decorators.all_tenants @rest_decorators.search('name') def get(self, _include=None, filters=None, pagination=None, sort=None, - all_tenants=None, search=None): - """ - List sites - """ - get_all_results = verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + search=None): + """List sites""" + args = ListQuery.parse_obj(request.args) return get_storage_manager().list( models.Site, include=_include, @@ -173,6 +169,6 @@ def get(self, _include=None, filters=None, pagination=None, sort=None, substr_filters=search, pagination=pagination, sort=sort, - all_tenants=all_tenants, - get_all_results=get_all_results + all_tenants=args.all_tenants, + get_all_results=args.get_all_results ) diff --git a/rest-service/manager_rest/rest/resources_v3_1/status.py b/rest-service/manager_rest/rest/resources_v3_1/status.py index fd4d029295..f323965959 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/status.py +++ b/rest-service/manager_rest/rest/resources_v3_1/status.py @@ -1,7 +1,8 @@ +import pydantic import socket import http.client import xmlrpc.client -from typing import Dict +from typing import Dict, Optional from flask import request from flask import current_app @@ -15,7 +16,6 @@ from manager_rest.security import SecuredResource from manager_rest.security.authorization import authorize from manager_rest.rest.rest_decorators import marshal_with -from manager_rest.rest.rest_utils import verify_and_convert_bool from manager_rest.syncthing_status_manager import get_syncthing_status try: @@ -78,8 +78,11 @@ def get(self): return "FAIL", 500 -class Status(SecuredResource): +class _IsSummaryQuery(pydantic.BaseModel): + summary: Optional[bool] = False + +class Status(SecuredResource): @swagger.operation( responseClass=responses.Status, nickname="status", @@ -89,13 +92,10 @@ class Status(SecuredResource): @marshal_with(responses.Status) def get(self): """Get the status of the manager services""" - summary_response = verify_and_convert_bool( - 'summary', - request.args.get('summary', False) - ) + args = _IsSummaryQuery.parse_obj(request.args) status, services = _get_status_and_services() - if summary_response: + if args.summary: return {'status': status, 'services': {}} return {'status': status, 'services': services} diff --git a/rest-service/manager_rest/rest/resources_v3_1/summary.py b/rest-service/manager_rest/rest/resources_v3_1/summary.py index 975db0460b..f0e19c1f9d 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/summary.py +++ b/rest-service/manager_rest/rest/resources_v3_1/summary.py @@ -30,19 +30,20 @@ from functools import wraps +class _SummaryQuery(rest_utils.ListQuery): + _target_field: Optional[str] = None + _sub_field: Optional[str] = None + + class BaseSummary(SecuredResource): summary_fields: List[str] = [] auth_req: Optional[str] = None model: Optional[Type[SQLModelBase]] = None - def get(self, pagination=None, all_tenants=None, filters=None): - target_field = request.args.get('_target_field') - subfield = request.args.get('_sub_field') - - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + def get(self, pagination=None, filters=None): + args = _SummaryQuery.parse_obj(request.args) + target_field = args._target_field + subfield = args._sub_field if target_field not in self.summary_fields: raise manager_exceptions.BadParametersError( @@ -67,8 +68,8 @@ def get(self, pagination=None, all_tenants=None, filters=None): sub_field=subfield, model_class=self.model, pagination=pagination, - all_tenants=all_tenants, - get_all_results=get_all_results, + all_tenants=args.all_tenants, + get_all_results=args.get_all_results, filters=filters, ) @@ -118,7 +119,6 @@ class SummarizeDeployments(BaseSummary): @authorize(auth_req, allow_all_tenants=True) @marshal_summary('deployments') - @rest_decorators.all_tenants @rest_decorators.create_filters(models.Deployment) @rest_decorators.paginate def get(self, *args, **kwargs): @@ -138,7 +138,6 @@ class SummarizeNodes(BaseSummary): @marshal_summary('nodes') @rest_decorators.create_filters(models.Node) @rest_decorators.paginate - @rest_decorators.all_tenants def get(self, *args, **kwargs): return super(SummarizeNodes, self).get(*args, **kwargs) @@ -160,7 +159,6 @@ class SummarizeNodeInstances(BaseSummary): @marshal_summary('node_instances') @rest_decorators.create_filters(models.NodeInstance) @rest_decorators.paginate - @rest_decorators.all_tenants def get(self, *args, **kwargs): return super(SummarizeNodeInstances, self).get(*args, **kwargs) @@ -182,7 +180,6 @@ class SummarizeExecutions(BaseSummary): @marshal_summary('executions') @rest_decorators.create_filters(models.Execution) @rest_decorators.paginate - @rest_decorators.all_tenants def get(self, *args, **kwargs): return super(SummarizeExecutions, self).get(*args, **kwargs) @@ -197,7 +194,6 @@ class SummarizeBlueprints(BaseSummary): @authorize(auth_req, allow_all_tenants=True) @marshal_summary('blueprints') - @rest_decorators.all_tenants @rest_decorators.create_filters(models.Blueprint) @rest_decorators.paginate def get(self, *args, **kwargs): @@ -216,15 +212,12 @@ class SummarizeExecutionSchedules(BaseSummary): @authorize(auth_req, allow_all_tenants=True) @marshal_summary('execution_schedules') - @rest_decorators.all_tenants @rest_decorators.create_filters(models.ExecutionSchedule) @rest_decorators.paginate def get(self, *args, **kwargs): - target_field = request.args.get('_target_field') - get_all_results = rest_utils.verify_and_convert_bool( - '_get_all_results', - request.args.get('_get_all_results', False) - ) + args = _SummaryQuery.parse_obj(request.args) + target_field = args._target_field + if target_field not in self.summary_fields: raise manager_exceptions.BadParametersError( 'Field {target} is not available for summary. Valid fields ' @@ -236,8 +229,8 @@ def get(self, *args, **kwargs): schedules_list = get_storage_manager().list( models.ExecutionSchedule, pagination=kwargs.get('pagination'), - all_tenants=kwargs.get('all_tenants'), - get_all_results=get_all_results, + all_tenants=args.all_tenants, + get_all_results=args.get_all_results, filters=kwargs.get('filters'), ) summary_dict: Dict[Tuple, int] = {} diff --git a/rest-service/manager_rest/rest/resources_v3_1/tokens.py b/rest-service/manager_rest/rest/resources_v3_1/tokens.py index da164ede94..d097bf34b5 100644 --- a/rest-service/manager_rest/rest/resources_v3_1/tokens.py +++ b/rest-service/manager_rest/rest/resources_v3_1/tokens.py @@ -1,7 +1,10 @@ +import pydantic from datetime import datetime +from typing import Optional from cloudify.utils import parse_utc_datetime +from flask import request from flask_security import current_user from manager_rest.manager_exceptions import NotFoundError @@ -13,11 +16,14 @@ from manager_rest.storage.models_base import db from manager_rest.rest.rest_decorators import ( create_filters, marshal_with, paginate, sortable) -from manager_rest.rest.rest_utils import get_json_and_verify_params -class Tokens(SecuredResource): +class _TokenCreateArgs(pydantic.BaseModel): + description: Optional[str] = None + expiration_date: Optional[str] = None + +class Tokens(SecuredResource): @authorize('token_get') @marshal_with(responses.Tokens) @create_filters(models.Token) @@ -44,17 +50,13 @@ def post(self): """Create a new token.""" _purge_expired_user_tokens() - request_dict = get_json_and_verify_params({ - 'description': {'type': str, 'optional': True}, - 'expiration_date': {'optional': True}, - }) - - expiration_date = request_dict.get('expiration_date') + params = _TokenCreateArgs.parse_obj(request.json) + expiration_date = params.expiration_date if expiration_date: expiration_date = parse_utc_datetime( expiration_date, timezone="UTC") - return current_user.create_auth_token(request_dict.get('description'), + return current_user.create_auth_token(params.description, expiration_date) @@ -66,7 +68,7 @@ def delete(self, token_id): token = sm.get(models.Token, token_id, fail_silently=True) if token and _can_manage_token(token): sm.delete(token) - return None, 204 + return "", 204 else: raise NotFoundError(f'Could not find token {token_id}') diff --git a/rest-service/manager_rest/rest/rest_decorators.py b/rest-service/manager_rest/rest/rest_decorators.py index ccbc1eb7a2..ed9e228cda 100644 --- a/rest-service/manager_rest/rest/rest_decorators.py +++ b/rest-service/manager_rest/rest/rest_decorators.py @@ -3,16 +3,13 @@ from typing import Dict from datetime import datetime -from flask_restful import fields, marshal -from flask_restful.utils import unpack +from flask_restful import marshal from flask import request, current_app from cloudify.models_states import ExecutionState from manager_rest import config, manager_exceptions from manager_rest.utils import current_tenant -from manager_rest.security.authorization import is_user_action_allowed -from manager_rest.storage.models_base import SQLModelBase, db -from manager_rest.storage.management_models import User +from manager_rest.storage.models_base import db from manager_rest.execution_token import current_execution from manager_rest.rest.rest_utils import ( normalize_value, @@ -20,7 +17,6 @@ request_use_all_tenants, is_deployment_update, ) - from .responses_v2 import ListResponse from .validation_models import ( Pagination, @@ -28,6 +24,15 @@ Sort, ) +from flask import request +from flask_restful import marshal, fields +from flask_restful.utils import unpack +from manager_rest.storage.management_models import User +from manager_rest.storage.models_base import SQLModelBase + +from manager_rest.rest.responses_v2 import ListResponse +from manager_rest.security.authorization import is_user_action_allowed + INCLUDE = 'Include' SORT = 'Sort' FILTER = 'Filter' @@ -35,41 +40,6 @@ SPECIAL_CHARS = ['\\', '_', '%'] -def _validate_fields(valid_fields, fields_to_check, action): - """Assert that `fields_to_check` is a subset of `valid_fields` - - :param valid_fields: A list/dict of valid fields - :param fields_to_check: A list/dict of fields to check - :param action: The action being performed (Sort/Include/Filter) - """ - error_type = {INCLUDE: manager_exceptions.NoSuchIncludeFieldError, - SORT: manager_exceptions.BadParametersError, - FILTER: manager_exceptions.BadParametersError} - unknowns = [k for k in fields_to_check if k not in valid_fields] - if unknowns: - raise error_type[action]( - '{action} keys \'{key_names}\' do not exist. Allowed ' - 'keys are: {fields}' - .format( - action=action, - key_names=unknowns, - fields=list(valid_fields)) - ) - - -# region V1 decorators - -def insecure_rest_method(func): - """block an insecure REST method if manager disabled insecure endpoints - """ - @wraps(func) - def wrapper(*args, **kwargs): - if config.instance.insecure_endpoints_disabled: - raise manager_exceptions.MethodNotAllowedError() - return func(*args, **kwargs) - return wrapper - - class marshal_with(object): def __init__(self, response_class, force_get_data=False): """ @@ -161,14 +131,14 @@ def _is_include_parameter_in_request(): @staticmethod def _get_data(): get_data = request.args.get('_get_data', False) - return verify_and_convert_bool('get_data', get_data) + return verify_and_convert_bool(get_data) @staticmethod def _include_hash(): include_hash = request.args.get('_include_hash', False) if include_hash and is_user_action_allowed( 'get_password_hash', None, True): - return verify_and_convert_bool('include_hash', include_hash) + return verify_and_convert_bool(include_hash) return False def _get_fields_to_include(self): @@ -198,7 +168,40 @@ def _get_skipped_fields(self): return self.response_class.skipped_fields.get(api_version, []) return [] -# endregion + +def _validate_fields(valid_fields, fields_to_check, action): + """Assert that `fields_to_check` is a subset of `valid_fields` + + :param valid_fields: A list/dict of valid fields + :param fields_to_check: A list/dict of fields to check + :param action: The action being performed (Sort/Include/Filter) + """ + error_type = {INCLUDE: manager_exceptions.NoSuchIncludeFieldError, + SORT: manager_exceptions.BadParametersError, + FILTER: manager_exceptions.BadParametersError} + unknowns = [k for k in fields_to_check if k not in valid_fields] + if unknowns: + raise error_type[action]( + '{action} keys \'{key_names}\' do not exist. Allowed ' + 'keys are: {fields}' + .format( + action=action, + key_names=unknowns, + fields=list(valid_fields)) + ) + + +# region V1 decorators + +def insecure_rest_method(func): + """block an insecure REST method if manager disabled insecure endpoints + """ + @wraps(func) + def wrapper(*args, **kwargs): + if config.instance.insecure_endpoints_disabled: + raise manager_exceptions.MethodNotAllowedError() + return func(*args, **kwargs) + return wrapper # region V2 decorators @@ -427,7 +430,7 @@ def evaluate_functions(func): @wraps(func) def wrapper(*args, **kwargs): val = request.args.get('_evaluate_functions', False) - val = verify_and_convert_bool('_evaluate_functions', val) + val = verify_and_convert_bool(val) kwargs['evaluate_functions'] = val return func(*args, **kwargs) return wrapper diff --git a/rest-service/manager_rest/rest/rest_utils.py b/rest-service/manager_rest/rest/rest_utils.py index bca4bf923a..18a634caa5 100644 --- a/rest-service/manager_rest/rest/rest_utils.py +++ b/rest-service/manager_rest/rest/rest_utils.py @@ -1,5 +1,7 @@ +import pydantic import re import unicodedata +from typing import Optional from contextlib import contextmanager from dateutil import rrule @@ -9,13 +11,11 @@ import dateutil.parser import os import pytz -import string import uuid from retrying import retry from flask_security import current_user -from flask import request, make_response, current_app -from flask_restful.reqparse import Argument, RequestParser +from flask import request, make_response from urllib.parse import quote as urlquote from dsl_parser import tasks @@ -61,95 +61,14 @@ def skip_nested_marshalling(): delattr(request, '__skip_marshalling') -def get_json_and_verify_params(params=None): - params = params or [] - if request.content_type != 'application/json': - raise manager_exceptions.UnsupportedContentTypeError( - 'Content type must be application/json') - - request_dict = request.json - is_params_dict = isinstance(params, dict) - - def is_optional(param_name): - return is_params_dict and params[param_name].get('optional', False) - - def check_type(param_name): - return is_params_dict and params[param_name].get('type', None) - - for param in params: - if param not in request_dict: - if is_optional(param): - continue - raise manager_exceptions.BadParametersError( - 'Missing {0} in json request body'.format(param)) - - param_type = check_type(param) - if param_type and not isinstance(request_dict[param], param_type): - raise manager_exceptions.BadParametersError( - '{0} parameter is expected to be of type {1} but is of type ' - '{2}'.format(param, - param_type.__name__, - type(request_dict[param]).__name__)) - - if is_params_dict: - _validate_allowed_substitutions( - param_name=param, - param_value=request_dict[param], - allowed=params[param].get('allowed_substitutions', None), - ) - return request_dict - - -def _validate_allowed_substitutions(param_name, param_value, allowed): - if allowed is None or param_value is None: - current_app.logger.debug( - 'Empty value or no allowed substitutions ' - 'defined for %s, skipping.', param_name) - return - f = string.Formatter() - invalid = [] - current_app.logger.debug('Checking allowed substitutions for %s (%s)', - param_name, ','.join(allowed)) - current_app.logger.debug('Value is: %s', param_value) - for _, field, _, _ in f.parse(param_value): - if field is None: - # This will occur at the end of a string unless the string ends at - # the end of a field - continue - current_app.logger.debug('Found %s', field) - if field not in allowed: - current_app.logger.debug('Field not valid.') - invalid.append(field) - if invalid: - raise manager_exceptions.BadParametersError( - '{candidate_name} has invalid parameters.\n' - 'Invalid parameters found: {invalid}.\n' - 'Allowed: {allowed}'.format( - candidate_name=param_name, - invalid=', '.join(invalid), - allowed=', '.join(allowed), - ) - ) - - -def get_args_and_verify_arguments(arguments): - request_parser = RequestParser() - for argument in arguments: - argument.location = 'args' - request_parser.args.append(argument) - return request_parser.parse_args() - - -def verify_and_convert_bool(attribute_name, str_bool): - if isinstance(str_bool, bool): - return str_bool - if isinstance(str_bool, str): - if str_bool.lower() == 'true': - return True - if str_bool.lower() == 'false': - return False - raise manager_exceptions.BadParametersError( - '{0} must be , got {1}'.format(attribute_name, str_bool)) +def verify_and_convert_bool(value): + if isinstance(value, str): + value = value.lower() + if value in {0, '0', 'off', 'f', 'false', 'n', 'no'}: + return False + if value in {1, '1', 'on', 't', 'true', 'y', 'yes'}: + return True + raise manager_exceptions.BadParametersError(f'invalid boolean: {value}') def convert_to_int(value): @@ -264,26 +183,7 @@ def verify_role(role_name, is_system_role=False): def request_use_all_tenants(): - return verify_and_convert_bool('all_tenants', - request.args.get('_all_tenants', False)) - - -def get_visibility_parameter(optional=False, - is_argument=False, - valid_values=VISIBILITY_EXCEPT_PRIVATE): - if is_argument: - args = get_args_and_verify_arguments( - [Argument('visibility', default=None)] - ) - visibility = args.visibility - else: - request_dict = get_json_and_verify_params({ - 'visibility': {'optional': optional, 'type': str} - }) - visibility = request_dict.get('visibility', None) - - validate_visibility(visibility, valid_values) - return visibility + return verify_and_convert_bool(request.args.get('_all_tenants', False)) def validate_visibility(visibility, valid_values): @@ -704,11 +604,11 @@ def test_unique_labels(labels_list): 'You cannot define the same label twice') -def compute_rule_from_scheduling_params(request_dict, existing_rule=None): - rrule_string = request_dict.get('rrule') - recurrence = request_dict.get('recurrence') - weekdays = request_dict.get('weekdays') - count = request_dict.get('count') +def compute_rule_from_scheduling_params(args, existing_rule=None): + rrule_string = args.rrule + recurrence = args.recurrence + weekdays = args.weekdays + count = args.count # we need to have at least: rrule; or count=1; or recurrence if rrule_string: @@ -723,11 +623,8 @@ def compute_rule_from_scheduling_params(request_dict, existing_rule=None): "invalid RRULE string provided: {}".format(e)) return {'rrule': rrule_string} else: - if count: - count = convert_to_int(request_dict.get('count')) - recurrence = _verify_schedule_recurrence( - request_dict.get('recurrence')) - weekdays = _verify_weekdays(request_dict.get('weekdays'), recurrence) + recurrence = _verify_schedule_recurrence(recurrence) + weekdays = _verify_weekdays(weekdays, recurrence) if existing_rule: count = count or existing_rule.get('count') recurrence = recurrence or existing_rule.get('recurrence') @@ -867,3 +764,15 @@ def remove_invalid_keys(input_dict, valid_params): clear = input_dict.keys() - valid_params for param in clear: input_dict.pop(param) + + +class SetVisibilityArgs(pydantic.BaseModel): + visibility: VisibilityState + + +class ListQuery(pydantic.BaseModel): + all_tenants: Optional[bool] = False + get_all_results: Optional[bool] = pydantic.Field( + default=False, + alias='_get_all_results', + ) diff --git a/rest-service/manager_rest/security/authorization.py b/rest-service/manager_rest/security/authorization.py index 68060f3d79..17178896b1 100644 --- a/rest-service/manager_rest/security/authorization.py +++ b/rest-service/manager_rest/security/authorization.py @@ -1,3 +1,4 @@ +import pydantic from functools import wraps from flask import request @@ -10,8 +11,11 @@ from manager_rest.storage import get_storage_manager from manager_rest.constants import CLOUDIFY_TENANT_HEADER from manager_rest.manager_exceptions import NotFoundError, ForbiddenError -from manager_rest.rest.rest_utils import (get_json_and_verify_params, - request_use_all_tenants) +from manager_rest.rest.rest_utils import request_use_all_tenants + + +class _WithTenantArgs(pydantic.BaseModel): + tenant_name: str def authorize(action, @@ -27,8 +31,8 @@ def wrapper(*args, **kwargs): elif get_tenant_from == 'param': tenant_name = kwargs['tenant_name'] elif get_tenant_from == 'data': - tenant_name = get_json_and_verify_params( - {'tenant_name': {'type': str}}).get('tenant_name') + data = _WithTenantArgs.parse_obj(request.json) + tenant_name = data.tenant_name if allow_if_execution: if current_execution and ( diff --git a/rest-service/manager_rest/security/secured_resource.py b/rest-service/manager_rest/security/secured_resource.py index 52bd6869a7..ba213d7c34 100644 --- a/rest-service/manager_rest/security/secured_resource.py +++ b/rest-service/manager_rest/security/secured_resource.py @@ -15,6 +15,7 @@ from functools import wraps +from flask.views import MethodView from flask_restful import Resource from flask_restful.utils import unpack from werkzeug.exceptions import HTTPException @@ -108,8 +109,9 @@ def allow_on_community(func): return func -class SecuredResource(Resource): - method_decorators = [authenticate] +class SecuredResource(MethodView): + init_every_request = False + decorators = [authenticate] class MissingPremiumFeatureResource(Resource): diff --git a/rest-service/manager_rest/server.py b/rest-service/manager_rest/server.py index ce42c52e18..39670ac7c8 100644 --- a/rest-service/manager_rest/server.py +++ b/rest-service/manager_rest/server.py @@ -2,12 +2,11 @@ import os import time import yaml -from contextlib import contextmanager from io import StringIO -from flask_restful import Api from flask import Flask, jsonify, Blueprint, current_app from flask_security import Security +import pydantic from sqlalchemy import event from sqlalchemy.exc import OperationalError from sqlalchemy.orm.session import close_all_sessions @@ -31,6 +30,7 @@ log_request, log_response) + if premium_enabled: from cloudify_premium.authentication.extended_auth_handler \ import configure_auth @@ -56,6 +56,14 @@ def manager_exception(error): return error.to_response(), error.status_code, error.additional_headers +@app_errors.app_errorhandler(pydantic.ValidationError) +def validation_error(e): + return ( + manager_exceptions.BadParametersError(str(e)).to_response(), + manager_exceptions.BadParametersError.status_code, + ) + + @app_errors.app_errorhandler(InternalServerError) def internal_error(e): s_traceback = StringIO() @@ -181,8 +189,7 @@ def __init__(self, load_config=True): user_datastore.find_or_create_role(name=role['name']) user_datastore.commit() - with self._prevent_flask_restful_error_handling(): - setup_resources(Api(self)) + setup_resources(self) self.register_blueprint(app_errors) def _set_flask_security(self): @@ -223,22 +230,6 @@ def _set_sql_alchemy(self): self.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(self) # Prepare the app for use with flask-sqlalchemy - @contextmanager - def _prevent_flask_restful_error_handling(self): - """Add flask-restful under this, to avoid installing its errorhandlers - - Flask-restful's errorhandlers are both not flexible enough, and too - complex. We want to simply use flask's error handling mechanism, - so this will make sure that flask-restful's are overridden with the - default ones. - """ - orig_handle_exc = self.handle_exception - orig_handle_user_exc = self.handle_user_exception - yield - # mypy says you cannot assign to method, but yes you can! - self.handle_exception = orig_handle_exc # type: ignore - self.handle_user_exception = orig_handle_user_exc # type: ignore - app: CloudifyFlaskApp diff --git a/rest-service/manager_rest/storage/models_base.py b/rest-service/manager_rest/storage/models_base.py index 05416fa0f1..758c18008d 100644 --- a/rest-service/manager_rest/storage/models_base.py +++ b/rest-service/manager_rest/storage/models_base.py @@ -119,6 +119,7 @@ class CIColumn(Column): """A column for case insensitive string fields """ is_ci = True + inherit_cache = True def _get_extension_type(desc): diff --git a/rest-service/manager_rest/test/endpoints/test_agents.py b/rest-service/manager_rest/test/endpoints/test_agents.py index 71185574f0..49b476be85 100644 --- a/rest-service/manager_rest/test/endpoints/test_agents.py +++ b/rest-service/manager_rest/test/endpoints/test_agents.py @@ -202,8 +202,6 @@ def test_update_without_state(self): self._agent('agent_1') response = self.patch('/agents/agent_1', {}) self.assertEqual(response.status_code, 400) - self.assertEqual(response.json['message'], - 'Missing state in json request body') def test_update_invalid_state(self): self._agent('agent_1') diff --git a/rest-service/manager_rest/test/endpoints/test_blueprints.py b/rest-service/manager_rest/test/endpoints/test_blueprints.py index 0e774ad187..4e91f9da80 100644 --- a/rest-service/manager_rest/test/endpoints/test_blueprints.py +++ b/rest-service/manager_rest/test/endpoints/test_blueprints.py @@ -431,7 +431,7 @@ def test_blueprint_update_invalid_param(self): blueprint_id) self.assertRaisesRegex( CloudifyClientError, - 'Unknown parameters: abc', + 'abc', self.client.blueprints.update, blueprint_id, {'abc': 123} @@ -444,15 +444,14 @@ def test_blueprint_update_invalid_param_type(self): blueprint_id) self.assertRaisesRegex( CloudifyClientError, - 'visibility parameter is expected to be of type {}'.format( - str.__name__), + 'visibility', self.client.blueprints.update, blueprint_id, {'visibility': 123} ) self.assertRaisesRegex( CloudifyClientError, - 'plan parameter is expected to be of type dict', + 'plan', self.client.blueprints.update, blueprint_id, {'plan': 'abcd'} diff --git a/rest-service/manager_rest/test/endpoints/test_community_contacts.py b/rest-service/manager_rest/test/endpoints/test_community_contacts.py index a00a8ac5c5..f01fd88a1e 100644 --- a/rest-service/manager_rest/test/endpoints/test_community_contacts.py +++ b/rest-service/manager_rest/test/endpoints/test_community_contacts.py @@ -45,7 +45,6 @@ def test_create_contact_missing_data(self): with self.assertRaises(CloudifyClientError) as cm: self._mock_post_contact(data, {}) assert cm.exception.status_code == 400 - assert "Missing first_name in json request body" in str(cm.exception) def test_create_contact_no_eula(self): data = self.data.copy() @@ -63,14 +62,12 @@ def test_create_contact_invalid_email(self): with self.assertRaises(CloudifyClientError) as cm: self._mock_post_contact(data, return_value) assert cm.exception.status_code == 400 - assert "problem while submitting the form" in str(cm.exception) def test_create_contact_post_fails(self): data = self.data.copy() with self.assertRaises(CloudifyClientError) as cm: self._mock_post_contact(data, return_value=None, return_ok=False) assert cm.exception.status_code == 400 - assert "problem while submitting the form" in str(cm.exception) def test_create_contact_malformed_return_value(self): data = self.data.copy() @@ -83,4 +80,3 @@ def test_create_contact_malformed_return_value(self): mock_post.return_value = mock_return self.client._client.post('/contacts', data=data) assert cm.exception.status_code == 400 - assert "problem while submitting the form" in str(cm.exception) diff --git a/rest-service/manager_rest/test/endpoints/test_deployment_groups.py b/rest-service/manager_rest/test/endpoints/test_deployment_groups.py index f3a26a8888..02fa751f83 100644 --- a/rest-service/manager_rest/test/endpoints/test_deployment_groups.py +++ b/rest-service/manager_rest/test/endpoints/test_deployment_groups.py @@ -1,9 +1,11 @@ import pytest import unittest +from datetime import datetime from typing import Dict, Any from unittest import mock -from datetime import datetime +import dateutil.parser +import pytz from cloudify.models_states import VisibilityState, ExecutionState from cloudify_rest_client.exceptions import ( @@ -151,13 +153,12 @@ def test_set_visibility(self): ) assert group.visibility == VisibilityState.TENANT - with self.assertRaisesRegex( - CloudifyClientError, 'visibility_states') as cm: + with self.assertRaisesRegex(CloudifyClientError, 'visibility') as cm: self.client.deployment_groups.put( 'group1', visibility='invalid visibility' ) - assert cm.exception.status_code == 409 + assert cm.exception.status_code == 400 def test_create_deployment(self): self.client.deployment_groups.put( @@ -666,7 +667,7 @@ def test_add_labels_to_added_deployments(self): self.client.deployment_groups.put( 'group1', # add a deployment using all 3 ways: by id, by clone, by filter - deployments_from_group=['group2'], # dep1 + deployments_from_group='group2', # dep1 filter_id='filter1', # dep2 deployment_ids=['dep3'], ) @@ -693,7 +694,7 @@ def test_add_labels_deployments_added_twice(self): ) self.client.deployment_groups.put( 'group1', - deployments_from_group=['group2'], # dep1 + deployments_from_group='group2', # dep1 deployment_ids=['dep1'], ) dep1 = self.client.deployments.get('dep1') @@ -993,6 +994,21 @@ def test_invalid_inputs(self): }} ]) + def test_set_create_attrs(self): + other_user = models.User(username='other') + self.sm.put(other_user) + created_at = datetime(2010, 2, 3, 4, 5, 6, tzinfo=pytz.UTC) + self.client.deployment_groups.put( + 'group1', + created_by=other_user.username, + created_at=created_at.isoformat(), + creation_counter=42, + ) + group = models.DeploymentGroup.query.filter_by(id='group1').one() + assert group.creator == other_user + assert dateutil.parser.parse(group.created_at) == created_at + assert group.creation_counter == 42 + class ExecutionGroupsTestCase(base_test.BaseServerTestCase): def setUp(self): diff --git a/rest-service/manager_rest/test/endpoints/test_deployments.py b/rest-service/manager_rest/test/endpoints/test_deployments.py index 1e2b8be00a..0998c526d6 100644 --- a/rest-service/manager_rest/test/endpoints/test_deployments.py +++ b/rest-service/manager_rest/test/endpoints/test_deployments.py @@ -963,7 +963,7 @@ def test_inputs(self): node2 = self.client.nodes.get('dep1', 'http_web_server2') self.assertEqual('9090', node2.properties['port']) with self.assertRaisesRegex( - CloudifyClientError, 'inputs parameter is expected'): + CloudifyClientError, 'dict'): self.put_deployment( deployment_id='dep2', blueprint_id='b1122', @@ -1255,7 +1255,7 @@ def test_creation_success_with_different_site_visibility(self): site_name=self.SITE_NAME) deployment = self.client.deployments.get(resource_id) self.assertEqual(deployment.site_name, self.SITE_NAME) - self.assertEqual(deployment.visibility, VisibilityState.TENANT) + self.assertEqual(deployment.visibility, VisibilityState.TENANT.value) def test_creation_failure_invalid_site_visibility(self): self.client.sites.create(self.SITE_NAME, diff --git a/rest-service/manager_rest/test/endpoints/test_executions.py b/rest-service/manager_rest/test/endpoints/test_executions.py index ce493fe79d..a4f248747c 100644 --- a/rest-service/manager_rest/test/endpoints/test_executions.py +++ b/rest-service/manager_rest/test/endpoints/test_executions.py @@ -317,7 +317,7 @@ def test_update_execution_status_with_error(self): self.assertEqual('', execution.error) def test_update_nonexistent_execution(self): - resp = self.patch('/executions/1234', {'status': 'new-status'}) + resp = self.patch('/executions/1234', {'status': 'terminated'}) self.assertEqual(404, resp.status_code) def test_cancel_execution_by_id(self): diff --git a/rest-service/manager_rest/test/endpoints/test_rest_decorators.py b/rest-service/manager_rest/test/endpoints/test_rest_decorators.py index e4559e5342..d211d91c49 100644 --- a/rest-service/manager_rest/test/endpoints/test_rest_decorators.py +++ b/rest-service/manager_rest/test/endpoints/test_rest_decorators.py @@ -27,6 +27,7 @@ rangeable, sortable, ) +# from manager_rest.rest.marshal import marshal_with class PaginateTest(TestCase): @@ -250,3 +251,19 @@ def test_invalid(self): with self.app.test_request_context('/?_sort=%20abcd'): with self.assertRaises(ValidationError): sortable()(Mock)() + + +# def test_marshal_with(): +# class A: +# response_fields = {} +# app = Flask(__name__) +# with app.test_request_context(): +# r = marshal_with(A)(lambda: [])() +# print(r) + +# class A: +# resource_fields = {} +# app = Flask(__name__) +# with app.test_request_context(): +# r = marshal_with(A)(lambda: [])() +# print(r) diff --git a/rest-service/manager_rest/test/endpoints/test_sites.py b/rest-service/manager_rest/test/endpoints/test_sites.py index 41dee0acd5..08b01bcb6f 100644 --- a/rest-service/manager_rest/test/endpoints/test_sites.py +++ b/rest-service/manager_rest/test/endpoints/test_sites.py @@ -24,13 +24,16 @@ class SitesTestCase(base_test.BaseServerTestCase): def _put_site(self, name='test_site'): - site = models.Site() - site.id = name - site.name = name - site.latitude = 42 - site.longitude = 43 - site.visibility = VisibilityState.TENANT - site.created_at = utils.get_formatted_timestamp() + site = models.Site( + id=name, + name=name, + latitude=42, + longitude=43, + visibility=VisibilityState.TENANT, + created_at=utils.get_formatted_timestamp(), + creator=self.user, + tenant=self.tenant, + ) self.sm.put(site) def _create_sites(self, sites_number): @@ -183,9 +186,8 @@ def test_create_site_invalid_location(self): self._test_invalid_location(self.client.sites.create) def test_create_site_invalid_visibility(self): - error_msg = "400: Invalid visibility: `test`" self.assertRaisesRegex(CloudifyClientError, - error_msg, + 'visibility', self.client.sites.create, 'test_site', visibility='test') @@ -245,9 +247,8 @@ def test_update_site_invalid_name(self): self.assertEqual(site.name, 'test_site') def test_update_site_invalid_visibility(self): - error_msg = "400: Invalid visibility: `test`" self.assertRaisesRegex(CloudifyClientError, - error_msg, + 'visibility', self.client.sites.update, 'test_site', visibility='test') diff --git a/rest-service/requirements.txt b/rest-service/requirements.txt index 12433a55a7..1cb60e43dd 100644 --- a/rest-service/requirements.txt +++ b/rest-service/requirements.txt @@ -40,7 +40,7 @@ charset-normalizer==2.1.1 # requests click==8.1.3 # via flask -cloudify-common @ https://github.com/cloudify-cosmo/cloudify-common/archive/master.zip +cloudify-common @ https://github.com/cloudify-cosmo/cloudify-common/archive/flask-restful-1.zip # via # -r requirements.in # cloudify-rest-service (setup.py)