From 5e5edefe24d085308fb1b15e489b4c6e431f4752 Mon Sep 17 00:00:00 2001 From: tivaliy Date: Fri, 12 Aug 2016 08:20:29 +0300 Subject: [PATCH] Add fuel2 diagnostic snapshot commands This patch adds commands that allow to manipulate with diagnostic snapshot process creation: fuel2 snapshot get-default-config fuel2 snapshot create fuel2 snapshot get-link DocImpact Change-Id: I59542eba922d793406c50b864b1cd0c416c7c891 --- fuelclient/__init__.py | 1 + fuelclient/commands/snapshot.py | 132 ++++++++++++++++++ fuelclient/tests/unit/v2/cli/test_snapshot.py | 131 +++++++++++++++++ fuelclient/tests/unit/v2/lib/test_snapshot.py | 61 ++++++++ fuelclient/v1/__init__.py | 2 + fuelclient/v1/snapshot.py | 33 +++++ setup.cfg | 3 + 7 files changed, 363 insertions(+) create mode 100644 fuelclient/commands/snapshot.py create mode 100644 fuelclient/tests/unit/v2/cli/test_snapshot.py create mode 100644 fuelclient/tests/unit/v2/lib/test_snapshot.py create mode 100644 fuelclient/v1/snapshot.py diff --git a/fuelclient/__init__.py b/fuelclient/__init__.py index fa8a066..2a06328 100644 --- a/fuelclient/__init__.py +++ b/fuelclient/__init__.py @@ -72,6 +72,7 @@ def get_client(resource, version='v1', connection=None): 'openstack-config': v1.openstack_config, 'plugins': v1.plugins, 'release': v1.release, + 'snapshot': v1.snapshot, 'task': v1.task, 'vip': v1.vip } diff --git a/fuelclient/commands/snapshot.py b/fuelclient/commands/snapshot.py new file mode 100644 index 0000000..7c8e105 --- /dev/null +++ b/fuelclient/commands/snapshot.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 +# +# 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. + +import argparse +import os + +from oslo_utils import fileutils + +from fuelclient.cli import error +from fuelclient.commands import base +from fuelclient.common import data_utils +from fuelclient import utils + + +class SnapshotMixIn(object): + + entity_name = 'snapshot' + supported_file_formats = ('json', 'yaml') + + @staticmethod + def config_file(file_path): + if not utils.file_exists(file_path): + raise argparse.ArgumentTypeError( + 'File "{0}" does not exist'.format(file_path)) + return file_path + + @staticmethod + def get_config_path(directory, file_format): + return os.path.join(os.path.abspath(directory), + 'snapshot_conf.{}'.format(file_format)) + + +class SnapshotGenerate(SnapshotMixIn, base.BaseCommand): + """Generate diagnostic snapshot.""" + + def get_parser(self, prog_name): + parser = super(SnapshotGenerate, self).get_parser(prog_name) + parser.add_argument('-c', + '--config', + required=False, + type=self.config_file, + help='Configuration file.') + return parser + + def take_action(self, parsed_args): + file_path = parsed_args.config + + config = dict() + if file_path: + file_format = os.path.splitext(file_path)[1].lstrip('.') + try: + with open(file_path, 'r') as stream: + config = data_utils.safe_load(file_format, stream) + except (OSError, IOError): + msg = 'Could not read configuration at {}.' + raise error.InvalidFileException(msg.format(file_path)) + + result = self.client.create_snapshot(config) + + msg = "Diagnostic snapshot generation task with id {id} was started\n" + self.app.stdout.write(msg.format(id=result.id)) + + +class SnapshotConfigGetDefault(SnapshotMixIn, base.BaseCommand): + """Download default config to generate custom diagnostic snapshot.""" + + def get_parser(self, prog_name): + parser = super(SnapshotConfigGetDefault, self).get_parser(prog_name) + parser.add_argument('-f', + '--format', + required=True, + choices=self.supported_file_formats, + help='Format of serialized diagnostic snapshot ' + 'configuration data.') + parser.add_argument('-d', + '--directory', + required=False, + default=os.path.curdir, + help='Destination directory. Defaults to ' + 'the current directory.') + return parser + + def take_action(self, parsed_args): + file_path = self.get_config_path(parsed_args.directory, + parsed_args.format) + config = self.client.get_default_config() + + try: + fileutils.ensure_tree(os.path.dirname(file_path)) + fileutils.delete_if_exists(file_path) + + with open(file_path, 'w') as stream: + data_utils.safe_dump(parsed_args.format, stream, config) + except (OSError, IOError): + msg = 'Could not store configuration at {}.' + raise error.InvalidFileException(msg.format(file_path)) + + msg = "Configuration was stored in {path}\n" + self.app.stdout.write(msg.format(path=file_path)) + + +class SnapshotGetLink(SnapshotMixIn, base.BaseShowCommand): + """Show link to download diagnostic snapshot.""" + + columns = ('status', + 'link') + + def take_action(self, parsed_args): + data = self.client.get_by_id(parsed_args.id) + if data['name'] != 'dump': + msg = "Task with id {0} is not a snapshot generation task" + raise error.ActionException(msg.format(data['id'])) + if data['status'] != 'ready': + data['link'] = None + else: + data['link'] = self.client.connection.root + data['message'] + + data = data_utils.get_display_data_single(self.columns, data) + return self.columns, data diff --git a/fuelclient/tests/unit/v2/cli/test_snapshot.py b/fuelclient/tests/unit/v2/cli/test_snapshot.py new file mode 100644 index 0000000..d9f0457 --- /dev/null +++ b/fuelclient/tests/unit/v2/cli/test_snapshot.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 +# +# 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. + +import json +import mock +import yaml + +from fuelclient.cli import error +from fuelclient.tests.unit.v2.cli import test_engine + + +class TestSnapshotCommand(test_engine.BaseCLITest): + + @mock.patch('json.dump') + def test_snapshot_config_download_json(self, m_dump): + args = 'snapshot get-default-config -f json -d /tmp' + test_data = {'foo': 'bar'} + expected_path = '/tmp/snapshot_conf.json' + + self.m_client.get_default_config.return_value = test_data + + m_open = mock.mock_open() + with mock.patch('fuelclient.commands.snapshot.open', + m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(expected_path, 'w') + m_dump.assert_called_once_with(test_data, mock.ANY, indent=4) + self.m_get_client.assert_called_once_with('snapshot', mock.ANY) + self.m_client.get_default_config.assert_called_once_with() + + @mock.patch('yaml.safe_dump') + def test_snapshot_config_download_yaml(self, m_safe_dump): + args = 'snapshot get-default-config -f yaml -d /tmp' + test_data = {'foo': 'bar'} + expected_path = '/tmp/snapshot_conf.yaml' + + self.m_client.get_default_config.return_value = test_data + + m_open = mock.mock_open() + with mock.patch('fuelclient.commands.snapshot.open', + m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(expected_path, 'w') + m_safe_dump.assert_called_once_with(test_data, mock.ANY, + default_flow_style=False) + + self.m_get_client.assert_called_once_with('snapshot', mock.ANY) + self.m_client.get_default_config.assert_called_once_with() + + def test_snapshot_create(self): + args = 'snapshot create' + test_data = {} + self.exec_command(args) + + self.m_get_client.assert_called_once_with('snapshot', mock.ANY) + self.m_client.create_snapshot.assert_called_once_with(test_data) + + @mock.patch('fuelclient.utils.file_exists', mock.Mock(return_value=True)) + def test_snapshot_create_w_config_json(self): + args = 'snapshot create -c /tmp/snapshot_conf.json' + test_data = {'foo': 'bar'} + expected_path = '/tmp/snapshot_conf.json' + + m_open = mock.mock_open(read_data=json.dumps(test_data)) + with mock.patch('fuelclient.commands.snapshot.open', + m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(expected_path, 'r') + self.m_get_client.assert_called_once_with('snapshot', mock.ANY) + self.m_client.create_snapshot.assert_called_once_with(test_data) + + @mock.patch('fuelclient.utils.file_exists', mock.Mock(return_value=True)) + def test_snapshot_create_w_config_yaml(self): + args = 'snapshot create -c /tmp/snapshot_conf.yaml' + test_data = {'foo': 'bar'} + expected_path = '/tmp/snapshot_conf.yaml' + + m_open = mock.mock_open(read_data=yaml.dump(test_data)) + with mock.patch('fuelclient.commands.snapshot.open', + m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(expected_path, 'r') + self.m_get_client.assert_called_once_with('snapshot', mock.ANY) + self.m_client.create_snapshot.assert_called_once_with(test_data) + + def test_snapshot_get_link(self): + task_id = 45 + args = 'snapshot get-link {}'.format(task_id) + test_data = {'id': task_id, + 'name': 'dump', + 'status': 'ready', + 'message': 'fake_message'} + + self.m_client.get_by_id.return_value = test_data + + self.exec_command(args) + self.m_get_client.assert_called_once_with('snapshot', mock.ANY) + self.m_client.get_by_id.assert_called_once_with(task_id) + + @mock.patch('sys.stderr') + def test_snapshot_get_link_fail(self, mocked_stderr): + task_id = 45 + args = 'snapshot get-link {}'.format(task_id) + test_data = {'id': task_id, + 'name': 'not_dump_name', + 'status': 'ready', + 'message': 'fake_message'} + + self.m_client.get_by_id.return_value = test_data + + self.assertRaises(error.ActionException, self.exec_command, args) + self.assertIn('Task with id {} is not a snapshot generation ' + 'task'.format(task_id), + mocked_stderr.write.call_args_list[0][0][0]) diff --git a/fuelclient/tests/unit/v2/lib/test_snapshot.py b/fuelclient/tests/unit/v2/lib/test_snapshot.py new file mode 100644 index 0000000..fae69bf --- /dev/null +++ b/fuelclient/tests/unit/v2/lib/test_snapshot.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 +# +# 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. + +import fuelclient +from fuelclient.tests.unit.v2.lib import test_api +from fuelclient.tests import utils + + +class TestSnapshotFacade(test_api.BaseLibTest): + + def setUp(self): + super(TestSnapshotFacade, self).setUp() + + self.version = 'v1' + self.res_uri = '/api/{version}/logs/package'.format( + version=self.version) + self.client = fuelclient.get_client('snapshot', self.version) + self.fake_task = utils.get_fake_task() + + def test_snapshot_config_download(self): + fake_resp = {'test_key': 'test_value'} + expected_uri = '/api/{version}/logs/package/config/default/'.format( + version=self.version) + + matcher = self.m_request.get(expected_uri, json=fake_resp) + + conf = self.client.get_default_config() + self.assertTrue(matcher.called) + self.assertEqual(conf, fake_resp) + + def test_snapshot_create(self): + fake_config = {} + matcher = self.m_request.put(self.res_uri, json=self.fake_task) + + self.client.create_snapshot(fake_config) + + self.assertTrue(matcher.called) + self.assertEqual(fake_config, matcher.last_request.json()) + + def test_snapshot_create_w_config(self): + fake_config = {'key_value': 'data_value'} + + matcher = self.m_request.put(self.res_uri, json=self.fake_task) + + self.client.create_snapshot(fake_config) + + self.assertTrue(matcher.called) + self.assertEqual(fake_config, matcher.last_request.json()) diff --git a/fuelclient/v1/__init__.py b/fuelclient/v1/__init__.py index 909ca19..3f6a2b1 100644 --- a/fuelclient/v1/__init__.py +++ b/fuelclient/v1/__init__.py @@ -24,6 +24,7 @@ from fuelclient.v1 import node from fuelclient.v1 import openstack_config from fuelclient.v1 import release from fuelclient.v1 import plugins +from fuelclient.v1 import snapshot from fuelclient.v1 import task from fuelclient.v1 import vip @@ -40,5 +41,6 @@ __all__ = ('cluster_settings', 'openstack_config', 'plugins', 'release', + 'snapshot', 'task', 'vip') diff --git a/fuelclient/v1/snapshot.py b/fuelclient/v1/snapshot.py new file mode 100644 index 0000000..6ca2a12 --- /dev/null +++ b/fuelclient/v1/snapshot.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 +# +# 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. + +from fuelclient import objects +from fuelclient.v1 import base_v1 + + +class SnapshotClient(base_v1.BaseV1Client): + + _entity_wrapper = objects.SnapshotTask + + def create_snapshot(self, config): + return self._entity_wrapper.start_snapshot_task(config) + + def get_default_config(self): + return self._entity_wrapper.get_default_config() + + +def get_client(connection): + return SnapshotClient(connection) diff --git a/setup.cfg b/setup.cfg index 1340cfd..426a7e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,6 +103,9 @@ fuelclient = task_network-configuration_download=fuelclient.commands.task:TaskNetworkConfigurationDownload task_settings_download=fuelclient.commands.task:TaskClusterSettingsDownload task_show=fuelclient.commands.task:TaskShow + snapshot_create=fuelclient.commands.snapshot:SnapshotGenerate + snapshot_get-default-config=fuelclient.commands.snapshot:SnapshotConfigGetDefault + snapshot_get-link=fuelclient.commands.snapshot:SnapshotGetLink vip_create=fuelclient.commands.vip:VipCreate vip_download=fuelclient.commands.vip:VipDownload vip_upload=fuelclient.commands.vip:VipUpload