diff --git a/fuelclient/commands/graph.py b/fuelclient/commands/graph.py index ef0a86f8..32eacace 100644 --- a/fuelclient/commands/graph.py +++ b/fuelclient/commands/graph.py @@ -19,6 +19,7 @@ import os from fuelclient.cli import error from fuelclient.cli.serializers import Serializer from fuelclient.commands import base +from fuelclient.common import data_utils class FileMethodsMixin(object): @@ -148,3 +149,134 @@ class GraphExecute(base.BaseCommand): self.app.stdout.write( "Deployment was executed\n" ) + + +class GraphDownload(base.BaseCommand): + """Download deployment graph configuration.""" + entity_name = 'graph' + + def get_parser(self, prog_name): + parser = super(GraphDownload, self).get_parser(prog_name) + tasks_level = parser.add_mutually_exclusive_group() + parser.add_argument('-e', + '--env', + type=int, + required=True, + help='Id of the environment') + + tasks_level.add_argument('-a', + '--all', + action="store_true", + required=False, + default=False, + help='Download merged graph for the ' + 'environment') + tasks_level.add_argument('-c', + '--cluster', + action="store_true", + required=False, + default=False, + help='Download cluster-specific tasks') + tasks_level.add_argument('-p', + '--plugins', + action="store_true", + required=False, + default=False, + help='Download plugins-specific tasks') + tasks_level.add_argument('-r', + '--release', + action="store_true", + required=False, + default=False, + help='Download release-specific tasks') + + parser.add_argument('-t', + '--type', + type=str, + default=None, + required=False, + help='Graph type string') + parser.add_argument('-f', + '--file', + type=str, + required=False, + default=None, + help='YAML file that contains tasks data.') + return parser + + @classmethod + def get_default_tasks_data_path(cls): + return os.path.join( + os.path.abspath(os.curdir), + "cluster_graph" + ) + + @classmethod + def write_tasks_to_file(cls, tasks_data, serializer=None, file_path=None): + serializer = serializer or Serializer() + if file_path: + return serializer.write_to_full_path( + file_path, + tasks_data + ) + else: + return serializer.write_to_path( + cls.get_default_tasks_data_path(), + tasks_data + ) + + def take_action(self, args): + tasks_data = [] + for tasks_level_name in ('all', 'cluster', 'release', 'plugins'): + if getattr(args, tasks_level_name): + tasks_data = self.client.download( + env_id=args.env, + level=tasks_level_name, + graph_type=args.type + ) + break + + # write to file + graph_data_file_path = self.write_tasks_to_file( + tasks_data=tasks_data, + serializer=Serializer(), + file_path=args.file) + + self.app.stdout.write( + "Tasks were downloaded to {0}\n".format(graph_data_file_path) + ) + + +class GraphList(base.BaseListCommand): + """Upload deployment graph configuration.""" + entity_name = 'graph' + columns = ("id", + "name", + "tasks", + "relations") + + def get_parser(self, prog_name): + parser = super(GraphList, self).get_parser(prog_name) + parser.add_argument('-e', + '--env', + type=int, + required=True, + help='Id of the environment') + return parser + + def take_action(self, parsed_args): + data = self.client.list( + env_id=parsed_args.env + ) + # format fields + for d in data: + d['relations'] = "\n".join( + 'as "{type}" to {model}(ID={model_id})' + .format(**r) for r in d['relations'] + ) + d['tasks'] = ', '.join(sorted(t['id'] for t in d['tasks'])) + data = data_utils.get_display_data_multi(self.columns, data) + scolumn_ids = [self.columns.index(col) + for col in parsed_args.sort_columns] + data.sort(key=lambda x: [x[scolumn_id] for scolumn_id in scolumn_ids]) + return self.columns, data diff --git a/fuelclient/tests/unit/v2/cli/test_deployment_graph.py b/fuelclient/tests/unit/v2/cli/test_deployment_graph.py index 512495ad..1c5e272c 100644 --- a/fuelclient/tests/unit/v2/cli/test_deployment_graph.py +++ b/fuelclient/tests/unit/v2/cli/test_deployment_graph.py @@ -15,6 +15,7 @@ # under the License. import mock +import six import yaml from fuelclient.tests.unit.v2.cli import test_engine @@ -86,3 +87,44 @@ class TestGraphActions(test_engine.BaseCLITest): nodes=[1, 2, 3] ) ) + + def test_download(self): + self._test_cmd( + 'download', + '--env 1 --all --file existing_graph.yaml --type custom_graph', + dict( + env_id=1, + level='all', + graph_type='custom_graph' + ) + ) + + def test_list(self): + with mock.patch('sys.stdout', new=six.moves.cStringIO()) as m_stdout: + self.m_get_client.reset_mock() + self.m_client.get_filtered.reset_mock() + self.m_client.list.return_value = [ + { + 'name': 'updated-graph-name', + 'tasks': [{ + 'id': 'test-task2', + 'type': 'puppet', + 'task_name': 'test-task2', + 'version': '2.0.0' + }], + 'relations': [{ + 'model': 'cluster', + 'model_id': 370, + 'type': 'custom-graph' + }], + 'id': 1 + } + ] + self.exec_command('graph list --env 1') + self.m_get_client.assert_called_once_with('graph', mock.ANY) + self.m_client.list.assert_called_once_with(env_id=1) + + self.assertIn('1', m_stdout.getvalue()) + self.assertIn('updated-graph-name', m_stdout.getvalue()) + self.assertIn('custom-graph', m_stdout.getvalue()) + self.assertIn('test-task2', m_stdout.getvalue()) diff --git a/fuelclient/tests/unit/v2/lib/test_deployment_graph.py b/fuelclient/tests/unit/v2/lib/test_deployment_graph.py index 5eb71cfb..92f0554b 100644 --- a/fuelclient/tests/unit/v2/lib/test_deployment_graph.py +++ b/fuelclient/tests/unit/v2/lib/test_deployment_graph.py @@ -115,3 +115,19 @@ class TestDeploymentGraphFacade(test_api.BaseLibTest): nodes=[1, 2, 3], graph_type="custom_graph") self.assertTrue(matcher_put.called) + + def test_graphs_list(self): + matcher_get = self.m_request.get( + '/api/v1/clusters/1/deployment_graphs/', + json=[] + ) + self.client.list(1) + self.assertTrue(matcher_get.called) + + def test_graphs_download(self): + matcher_get = self.m_request.get( + '/api/v1/clusters/1/deployment_tasks/?graph_type=custom_graph', + json=[] + ) + self.client.download(env_id=1, level='all', graph_type='custom_graph') + self.assertTrue(matcher_get.called) diff --git a/fuelclient/v1/graph.py b/fuelclient/v1/graph.py index 3d185a16..fa203f0f 100644 --- a/fuelclient/v1/graph.py +++ b/fuelclient/v1/graph.py @@ -31,6 +31,15 @@ class GraphClient(base_v1.BaseV1Client): cluster_deploy_api_path = "clusters/{env_id}/deploy/" + merged_cluster_tasks_api_path = "clusters/{env_id}/deployment_tasks" \ + "/?graph_type={graph_type}" + + merged_plugins_tasks_api_path = "clusters/{env_id}/deployment_tasks" \ + "/plugins/?graph_type={graph_type}" + + cluster_release_tasks_api_path = "clusters/{env_id}/deployment_tasks" \ + "/release/?graph_type={graph_type}" + @classmethod def update_graph_for_model( cls, data, related_model, related_model_id, graph_type=None): @@ -92,6 +101,57 @@ class GraphClient(base_v1.BaseV1Client): deploy_data = APIClient.put_request(url, {}) return objects.DeployTask.init_with_data(deploy_data) + # download + @classmethod + def get_merged_cluster_tasks(cls, env_id, graph_type=None): + return APIClient.get_request( + cls.merged_cluster_tasks_api_path.format( + env_id=env_id, + graph_type=graph_type or "")) + + @classmethod + def get_merged_plugins_tasks(cls, env_id, graph_type=None): + return APIClient.get_request( + cls.merged_plugins_tasks_api_path.format( + env_id=env_id, + graph_type=graph_type or "")) + + @classmethod + def get_release_tasks_for_cluster(cls, env_id, graph_type=None): + return APIClient.get_request( + cls.merged_plugins_tasks_api_path.format( + env_id=env_id, + graph_type=graph_type or "")) + + def download(self, env_id, level, graph_type): + tasks_levels = { + 'all': lambda: self.get_merged_cluster_tasks( + env_id=env_id, graph_type=graph_type), + + 'cluster': lambda: self.get_graph_for_model( + related_model='clusters', + related_model_id=env_id, + graph_type=graph_type).get('tasks', []), + + 'plugins': lambda: self.get_merged_plugins_tasks( + env_id=env_id, + graph_type=graph_type), + + 'release': lambda: self.get_release_tasks_for_cluster( + env_id=env_id, + graph_type=graph_type) + } + return tasks_levels[level]() + + # list + @classmethod + def list(cls, env_id): + # todo(ikutukov): extend lists to support all models + return APIClient.get_request( + cls.related_graphs_list_api_path.format( + related_model='clusters', + related_model_id=env_id)) + def get_client(): return GraphClient() diff --git a/setup.cfg b/setup.cfg index bbd03e7b..22659486 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,9 @@ fuelclient = env_spawn-vms=fuelclient.commands.environment:EnvSpawnVms env_update=fuelclient.commands.environment:EnvUpdate fuel-version=fuelclient.commands.fuelversion:FuelVersion + graph_download=fuelclient.commands.graph:GraphDownload graph_execute=fuelclient.commands.graph:GraphExecute + graph_list=fuelclient.commands.graph:GraphList graph_upload=fuelclient.commands.graph:GraphUpload network-group_create=fuelclient.commands.network_group:NetworkGroupCreate network-group_delete=fuelclient.commands.network_group:NetworkGroupDelete