From 649dfbc28e55c25bf742a4315f2d756dc9d26143 Mon Sep 17 00:00:00 2001 From: Winson Chan Date: Mon, 6 Apr 2015 20:38:06 +0000 Subject: [PATCH] Add workbook and workflow validate commands Add workbook-validate and workflow-validate commands to validate workbook and workflow definition respectively. The commands print the error if validation fails else returns command prompt. Change-Id: I25b220a2a9abd720f24ee6b23ce37ee977845793 Implements: blueprint api-validate-dsl --- mistralclient/api/v2/workbooks.py | 15 ++++ mistralclient/api/v2/workflows.py | 15 ++++ mistralclient/commands/v2/workbooks.py | 46 +++++++++--- mistralclient/commands/v2/workflows.py | 48 ++++++++++--- mistralclient/shell.py | 22 +++--- .../tests/unit/v2/test_cli_workbooks.py | 46 ++++++++---- .../tests/unit/v2/test_cli_workflows.py | 46 ++++++++---- mistralclient/tests/unit/v2/test_workbooks.py | 71 ++++++++++++++++++- 8 files changed, 252 insertions(+), 57 deletions(-) diff --git a/mistralclient/api/v2/workbooks.py b/mistralclient/api/v2/workbooks.py index 03f005ed..f0b7b471 100644 --- a/mistralclient/api/v2/workbooks.py +++ b/mistralclient/api/v2/workbooks.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -62,3 +63,17 @@ class WorkbookManager(base.ResourceManager): self._ensure_not_empty(name=name) self._delete('/workbooks/%s' % name) + + def validate(self, definition): + self._ensure_not_empty(definition=definition) + + resp = self.client.http_client.post( + '/workbooks/validate', + definition, + headers={'content-type': 'text/plain'} + ) + + if resp.status_code != 200: + self._raise_api_exception(resp) + + return base.extract_json(resp, None) diff --git a/mistralclient/api/v2/workflows.py b/mistralclient/api/v2/workflows.py index d7f5a863..2ec1dc87 100644 --- a/mistralclient/api/v2/workflows.py +++ b/mistralclient/api/v2/workflows.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -64,3 +65,17 @@ class WorkflowManager(base.ResourceManager): self._ensure_not_empty(name=name) self._delete('/workflows/%s' % name) + + def validate(self, definition): + self._ensure_not_empty(definition=definition) + + resp = self.client.http_client.post( + '/workflows/validate', + definition, + headers={'content-type': 'text/plain'} + ) + + if resp.status_code != 200: + self._raise_api_exception(resp) + + return base.extract_json(resp, None) diff --git a/mistralclient/commands/v2/workbooks.py b/mistralclient/commands/v2/workbooks.py index f1cbe841..635eec65 100644 --- a/mistralclient/commands/v2/workbooks.py +++ b/mistralclient/commands/v2/workbooks.py @@ -1,18 +1,17 @@ # Copyright 2014 - Mirantis, Inc. -# All Rights Reserved +# Copyright 2015 - StackStorm, Inc. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import argparse import logging @@ -22,6 +21,8 @@ from cliff import show from mistralclient.api.v2 import workbooks from mistralclient.commands.v2 import base +from mistralclient import exceptions as exc + LOG = logging.getLogger(__name__) @@ -153,3 +154,28 @@ class GetDefinition(command.Command): parsed_args.name).definition self.app.stdout.write(definition or "\n") + + +class Validate(show.ShowOne): + """Validate workbook.""" + + def get_parser(self, prog_name): + parser = super(Validate, self).get_parser(prog_name) + + parser.add_argument( + 'definition', + type=argparse.FileType('r'), + help='Workbook definition file' + ) + + return parser + + def take_action(self, parsed_args): + result = workbooks.WorkbookManager(self.app.client).validate( + parsed_args.definition.read()) + + if not result.get('valid', None): + raise exc.MistralClientException( + result.get('error', 'Unknown exception.')) + + return tuple(), tuple() diff --git a/mistralclient/commands/v2/workflows.py b/mistralclient/commands/v2/workflows.py index d4b94b1f..6138214e 100644 --- a/mistralclient/commands/v2/workflows.py +++ b/mistralclient/commands/v2/workflows.py @@ -1,18 +1,17 @@ # Copyright 2014 - Mirantis, Inc. -# All Rights Reserved +# Copyright 2015 - StackStorm, Inc. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import argparse import logging @@ -22,6 +21,8 @@ from cliff import show from mistralclient.api.v2 import workflows from mistralclient.commands.v2 import base +from mistralclient import exceptions as exc + LOG = logging.getLogger(__name__) @@ -163,4 +164,29 @@ class GetDefinition(command.Command): definition = workflows.WorkflowManager(self.app.client).get( parsed_args.name).definition - self.app.stdout.write(definition or "\n") \ No newline at end of file + self.app.stdout.write(definition or "\n") + + +class Validate(show.ShowOne): + """Validate workflow.""" + + def get_parser(self, prog_name): + parser = super(Validate, self).get_parser(prog_name) + + parser.add_argument( + 'definition', + type=argparse.FileType('r'), + help='Workflow definition file' + ) + + return parser + + def take_action(self, parsed_args): + result = workflows.WorkflowManager(self.app.client).validate( + parsed_args.definition.read()) + + if not result.get('valid', None): + raise exc.MistralClientException( + result.get('error', 'Unknown exception.')) + + return tuple(), tuple() diff --git a/mistralclient/shell.py b/mistralclient/shell.py index 403dab11..e83d9370 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -1,18 +1,16 @@ -# Copyright 2015 StackStorm, Inc. -# All Rights Reserved +# Copyright 2015 - StackStorm, Inc. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ Command-line interface to the Mistral APIs @@ -247,6 +245,7 @@ class MistralShell(app.App): 'workbook-update': mistralclient.commands.v2.workbooks.Update, 'workbook-get-definition': mistralclient.commands.v2.workbooks.GetDefinition, + 'workbook-validate': mistralclient.commands.v2.workbooks.Validate, 'workflow-list': mistralclient.commands.v2.workflows.List, 'workflow-get': mistralclient.commands.v2.workflows.Get, 'workflow-create': mistralclient.commands.v2.workflows.Create, @@ -254,6 +253,7 @@ class MistralShell(app.App): 'workflow-update': mistralclient.commands.v2.workflows.Update, 'workflow-get-definition': mistralclient.commands.v2.workflows.GetDefinition, + 'workflow-validate': mistralclient.commands.v2.workflows.Validate, 'environment-create': mistralclient.commands.v2.environments.Create, 'environment-delete': diff --git a/mistralclient/tests/unit/v2/test_cli_workbooks.py b/mistralclient/tests/unit/v2/test_cli_workbooks.py index d651b6f3..1530680c 100644 --- a/mistralclient/tests/unit/v2/test_cli_workbooks.py +++ b/mistralclient/tests/unit/v2/test_cli_workbooks.py @@ -1,23 +1,23 @@ -# Copyright 2014 Mirantis, Inc. -# All Rights Reserved +# Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import mock from mistralclient.api.v2 import workbooks from mistralclient.commands.v2 import workbooks as workbook_cmd +from mistralclient import exceptions as exc from mistralclient.tests.unit import base @@ -95,4 +95,26 @@ class TestCLIWorkbooksV2(base.BaseCommandTest): self.call(workbook_cmd.GetDefinition, app_args=['name']) - self.app.stdout.write.assert_called_with(WB_DEF) \ No newline at end of file + self.app.stdout.write.assert_called_with(WB_DEF) + + @mock.patch('argparse.open', create=True) + @mock.patch('mistralclient.api.v2.workbooks.WorkbookManager.validate') + def test_validate(self, mock, mock_open): + mock.return_value = {'valid': True} + mock_open.return_value = mock.MagicMock(spec=file) + + result = self.call(workbook_cmd.Validate, app_args=['wb.yaml']) + + self.assertEqual(result[0], tuple()) + self.assertEqual(result[1], tuple()) + + @mock.patch('argparse.open', create=True) + @mock.patch('mistralclient.api.v2.workbooks.WorkbookManager.validate') + def test_validate_failed(self, mock, mock_open): + mock.return_value = {'valid': False, 'error': 'Invalid DSL...'} + mock_open.return_value = mock.MagicMock(spec=file) + + self.assertRaises(exc.MistralClientException, + self.call, + workbook_cmd.Validate, + app_args=['wb.yaml']) diff --git a/mistralclient/tests/unit/v2/test_cli_workflows.py b/mistralclient/tests/unit/v2/test_cli_workflows.py index 625d0417..c15ae4e7 100644 --- a/mistralclient/tests/unit/v2/test_cli_workflows.py +++ b/mistralclient/tests/unit/v2/test_cli_workflows.py @@ -1,23 +1,23 @@ -# Copyright 2014 Mirantis, Inc. -# All Rights Reserved +# Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import mock from mistralclient.api.v2 import workflows from mistralclient.commands.v2 import workflows as workflow_cmd +from mistralclient import exceptions as exc from mistralclient.tests.unit import base @@ -90,4 +90,26 @@ class TestCLIWorkflowsV2(base.BaseCommandTest): self.call(workflow_cmd.GetDefinition, app_args=['name']) - self.app.stdout.write.assert_called_with(WF_DEF) \ No newline at end of file + self.app.stdout.write.assert_called_with(WF_DEF) + + @mock.patch('argparse.open', create=True) + @mock.patch('mistralclient.api.v2.workflows.WorkflowManager.validate') + def test_validate(self, mock, mock_open): + mock.return_value = {'valid': True} + mock_open.return_value = mock.MagicMock(spec=file) + + result = self.call(workflow_cmd.Validate, app_args=['wf.yaml']) + + self.assertEqual(result[0], tuple()) + self.assertEqual(result[1], tuple()) + + @mock.patch('argparse.open', create=True) + @mock.patch('mistralclient.api.v2.workflows.WorkflowManager.validate') + def test_validate_failed(self, mock, mock_open): + mock.return_value = {'valid': False, 'error': 'Invalid DSL...'} + mock_open.return_value = mock.MagicMock(spec=file) + + self.assertRaises(exc.MistralClientException, + self.call, + workflow_cmd.Validate, + app_args=['wf.yaml']) diff --git a/mistralclient/tests/unit/v2/test_workbooks.py b/mistralclient/tests/unit/v2/test_workbooks.py index 0ca3d114..c0c28981 100644 --- a/mistralclient/tests/unit/v2/test_workbooks.py +++ b/mistralclient/tests/unit/v2/test_workbooks.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from mistralclient.api import base as api_base from mistralclient.api.v2 import workbooks from mistralclient.tests.unit.v2 import base @@ -41,11 +43,25 @@ workflows: action: std.http url="http://some_url" server_id=1 """ -WORKBOOK = {'definition': WB_DEF} +INVALID_WB_DEF = """ +version: 2.0 +name: wb + +workflows: + wf1: + type: direct + tasks: + task1: + action: std.http url="localhost:8989" + workflow: wf2 +""" + +WORKBOOK = {'definition': WB_DEF} URL_TEMPLATE = '/workbooks' URL_TEMPLATE_NAME = '/workbooks/%s' +URL_TEMPLATE_VALIDATE = '/workbooks/validate' class TestWorkbooksV2(base.BaseClientV2Test): @@ -112,3 +128,56 @@ class TestWorkbooksV2(base.BaseClientV2Test): self.workbooks.delete('wb') mock.assert_called_once_with(URL_TEMPLATE_NAME % 'wb') + + def test_validate(self): + mock = self.mock_http_post(status_code=200, + content={'valid': True}) + + result = self.workbooks.validate(WB_DEF) + + self.assertIsNotNone(result) + self.assertIn('valid', result) + self.assertTrue(result['valid']) + + mock.assert_called_once_with( + URL_TEMPLATE_VALIDATE, + WB_DEF, + headers={'content-type': 'text/plain'} + ) + + def test_validate_failed(self): + mock_result = { + "valid": False, + "error": "Task properties 'action' and 'workflow' " + "can't be specified both" + } + + mock = self.mock_http_post(status_code=200, content=mock_result) + + result = self.workbooks.validate(INVALID_WB_DEF) + + self.assertIsNotNone(result) + self.assertIn('valid', result) + self.assertFalse(result['valid']) + self.assertIn('error', result) + self.assertIn("Task properties 'action' and 'workflow' " + "can't be specified both", result['error']) + + mock.assert_called_once_with( + URL_TEMPLATE_VALIDATE, + INVALID_WB_DEF, + headers={'content-type': 'text/plain'} + ) + + def test_validate_api_failed(self): + mock = self.mock_http_post(status_code=500, content={}) + + self.assertRaises(api_base.APIException, + self.workbooks.validate, + WB_DEF) + + mock.assert_called_once_with( + URL_TEMPLATE_VALIDATE, + WB_DEF, + headers={'content-type': 'text/plain'} + )