From 02406a337a3828ce46cd0f45ced2dbd2a21914af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Mon, 29 May 2017 13:52:41 -0400 Subject: [PATCH] Add new volume commands: create, attach and detach Change-Id: I2854e7f071108aa92e20bec9ca30ef9468aa383d --- almanachclient/commands/attach_volume.py | 35 ++++++++++ almanachclient/commands/create_volume.py | 44 ++++++++++++ almanachclient/commands/detach_volume.py | 35 ++++++++++ almanachclient/shell.py | 6 ++ .../tests/commands/test_attach_volume.py | 47 +++++++++++++ .../tests/commands/test_create_volume.py | 50 ++++++++++++++ .../tests/commands/test_detach_volume.py | 47 +++++++++++++ almanachclient/tests/v1/test_client.py | 55 +++++++++++++++ almanachclient/v1/client.py | 59 ++++++++++++++++ doc/source/installation.rst | 9 ++- doc/source/usage_cli.rst | 68 +++++++++++++++++++ 11 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 almanachclient/commands/attach_volume.py create mode 100644 almanachclient/commands/create_volume.py create mode 100644 almanachclient/commands/detach_volume.py create mode 100644 almanachclient/tests/commands/test_attach_volume.py create mode 100644 almanachclient/tests/commands/test_create_volume.py create mode 100644 almanachclient/tests/commands/test_detach_volume.py diff --git a/almanachclient/commands/attach_volume.py b/almanachclient/commands/attach_volume.py new file mode 100644 index 0000000..d641913 --- /dev/null +++ b/almanachclient/commands/attach_volume.py @@ -0,0 +1,35 @@ +# Copyright 2017 INAP +# +# 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 dateutil import parser as date_parser + +from cliff.command import Command + + +class AttachVolumeCommand(Command): + """Attach volume""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument('volume_id', help='Volume ID') + parser.add_argument('--date', help='Start date') + parser.add_argument('--attachment', action='append', help='Instance attached to the volume') + return parser + + def take_action(self, parsed_args): + date = date_parser.parse(parsed_args.date) if parsed_args.date else None + self.app.get_client().attach_volume(parsed_args.volume_id, + attachments=parsed_args.attachment, + attachment_date=date) + return 'Success' diff --git a/almanachclient/commands/create_volume.py b/almanachclient/commands/create_volume.py new file mode 100644 index 0000000..76e9dcc --- /dev/null +++ b/almanachclient/commands/create_volume.py @@ -0,0 +1,44 @@ +# Copyright 2017 INAP +# +# 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 dateutil import parser as date_parser + +from cliff.command import Command + + +class CreateVolumeCommand(Command): + """Create volume""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument('tenant_id', help='Tenant ID') + parser.add_argument('volume_id', help='Volume ID') + parser.add_argument('volume_type_id', help='Volume type ID') + parser.add_argument('volume_name', help='Volume name') + parser.add_argument('volume_size', help='Volume size') + parser.add_argument('--start', help='Start date') + parser.add_argument('--attachment', action='append', help='Instance attached to the volume') + return parser + + def take_action(self, parsed_args): + start_date = date_parser.parse(parsed_args.start) if parsed_args.start else None + + self.app.get_client().create_volume(parsed_args.tenant_id, + parsed_args.volume_id, + parsed_args.volume_type_id, + parsed_args.volume_name, + parsed_args.volume_size, + attachments=parsed_args.attachment, + start=start_date) + return 'Success' diff --git a/almanachclient/commands/detach_volume.py b/almanachclient/commands/detach_volume.py new file mode 100644 index 0000000..726e6f8 --- /dev/null +++ b/almanachclient/commands/detach_volume.py @@ -0,0 +1,35 @@ +# Copyright 2017 INAP +# +# 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 dateutil import parser as date_parser + +from cliff.command import Command + + +class DetachVolumeCommand(Command): + """Detach volume""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument('volume_id', help='Volume ID') + parser.add_argument('--date', help='Start date') + parser.add_argument('--attachment', action='append', help='Instance attached to the volume') + return parser + + def take_action(self, parsed_args): + date = date_parser.parse(parsed_args.date) if parsed_args.date else None + self.app.get_client().detach_volume(parsed_args.volume_id, + attachments=parsed_args.attachment, + attachment_date=date) + return 'Success' diff --git a/almanachclient/shell.py b/almanachclient/shell.py index 7a8dacc..5dc088d 100644 --- a/almanachclient/shell.py +++ b/almanachclient/shell.py @@ -18,10 +18,13 @@ import sys from cliff import app from cliff import commandmanager +from almanachclient.commands.attach_volume import AttachVolumeCommand from almanachclient.commands.create_instance import CreateInstanceCommand +from almanachclient.commands.create_volume import CreateVolumeCommand from almanachclient.commands.create_volume_type import CreateVolumeTypeCommand from almanachclient.commands.delete_instance import DeleteInstanceCommand from almanachclient.commands.delete_volume_type import DeleteVolumeTypeCommand +from almanachclient.commands.detach_volume import DetachVolumeCommand from almanachclient.commands.endpoint import EndpointCommand from almanachclient.commands.get_entity import GetEntityCommand from almanachclient.commands.get_volume_type import GetVolumeTypeCommand @@ -47,7 +50,10 @@ class AlmanachCommandManager(commandmanager.CommandManager): 'list-volume-types': ListVolumeTypeCommand, 'get-volume-type': GetVolumeTypeCommand, 'list-volumes': ListVolumeCommand, + 'create-volume': CreateVolumeCommand, 'resize-volume': ResizeVolumeCommand, + 'attach-volume': AttachVolumeCommand, + 'detach-volume': DetachVolumeCommand, 'list-instances': ListInstanceCommand, 'create-instance': CreateInstanceCommand, 'delete-instance': DeleteInstanceCommand, diff --git a/almanachclient/tests/commands/test_attach_volume.py b/almanachclient/tests/commands/test_attach_volume.py new file mode 100644 index 0000000..837add5 --- /dev/null +++ b/almanachclient/tests/commands/test_attach_volume.py @@ -0,0 +1,47 @@ +# Copyright 2017 INAP +# +# 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 argparse import Namespace +import datetime +from unittest import mock + +from almanachclient.commands.attach_volume import AttachVolumeCommand + +from almanachclient.tests import base + + +class TestAttachVolumeCommand(base.TestCase): + + def setUp(self): + super().setUp() + self.app = mock.Mock() + self.app_args = mock.Mock() + self.args = Namespace(volume_id='some uuid', attachment=['vm1', 'vm2'], date=None) + + self.client = mock.Mock() + self.app.get_client.return_value = self.client + self.command = AttachVolumeCommand(self.app, self.app_args) + + def test_execute_command(self): + self.assertEqual('Success', self.command.take_action(self.args)) + self.client.attach_volume.assert_called_once_with('some uuid', + attachments=['vm1', 'vm2'], + attachment_date=None) + + def test_execute_command_with_date(self): + self.args.date = '2017-01-01' + self.assertEqual('Success', self.command.take_action(self.args)) + self.client.attach_volume.assert_called_once_with('some uuid', + attachments=['vm1', 'vm2'], + attachment_date=datetime.datetime(2017, 1, 1, 0, 0)) diff --git a/almanachclient/tests/commands/test_create_volume.py b/almanachclient/tests/commands/test_create_volume.py new file mode 100644 index 0000000..303a9cf --- /dev/null +++ b/almanachclient/tests/commands/test_create_volume.py @@ -0,0 +1,50 @@ +# Copyright 2017 INAP +# +# 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 argparse import Namespace +import datetime +from unittest import mock + +from almanachclient.commands.create_volume import CreateVolumeCommand + +from almanachclient.tests import base + + +class TestCreateVolumeCommand(base.TestCase): + + def setUp(self): + super().setUp() + self.app = mock.Mock() + self.app_args = mock.Mock() + self.args = Namespace(tenant_id='tenant uuid', + volume_id='volume uuid', + volume_type_id='volume type uuid', + volume_name='volume', + volume_size=2, + start='2017-01-01', + attachment=None) + + self.client = mock.Mock() + self.app.get_client.return_value = self.client + self.command = CreateVolumeCommand(self.app, self.app_args) + + def test_execute_command(self): + self.assertEqual('Success', self.command.take_action(self.args)) + self.client.create_volume.assert_called_once_with('tenant uuid', + 'volume uuid', + 'volume type uuid', + 'volume', + 2, + start=datetime.datetime(2017, 1, 1, 0, 0), + attachments=None) diff --git a/almanachclient/tests/commands/test_detach_volume.py b/almanachclient/tests/commands/test_detach_volume.py new file mode 100644 index 0000000..fbe6159 --- /dev/null +++ b/almanachclient/tests/commands/test_detach_volume.py @@ -0,0 +1,47 @@ +# Copyright 2017 INAP +# +# 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 argparse import Namespace +import datetime +from unittest import mock + +from almanachclient.commands.detach_volume import DetachVolumeCommand + +from almanachclient.tests import base + + +class TestDetachVolumeCommand(base.TestCase): + + def setUp(self): + super().setUp() + self.app = mock.Mock() + self.app_args = mock.Mock() + self.args = Namespace(volume_id='some uuid', attachment=['vm1', 'vm2'], date=None) + + self.client = mock.Mock() + self.app.get_client.return_value = self.client + self.command = DetachVolumeCommand(self.app, self.app_args) + + def test_execute_command(self): + self.assertEqual('Success', self.command.take_action(self.args)) + self.client.detach_volume.assert_called_once_with('some uuid', + attachments=['vm1', 'vm2'], + attachment_date=None) + + def test_execute_command_with_date(self): + self.args.date = '2017-01-01' + self.assertEqual('Success', self.command.take_action(self.args)) + self.client.detach_volume.assert_called_once_with('some uuid', + attachments=['vm1', 'vm2'], + attachment_date=datetime.datetime(2017, 1, 1, 0, 0)) diff --git a/almanachclient/tests/v1/test_client.py b/almanachclient/tests/v1/test_client.py index 280ba1f..3d42c7b 100644 --- a/almanachclient/tests/v1/test_client.py +++ b/almanachclient/tests/v1/test_client.py @@ -250,3 +250,58 @@ class TestClient(base.TestCase): data=json.dumps({'size': 3, 'date': date.strftime(Client.DATE_FORMAT_BODY)}), headers=self.headers) + + @mock.patch('requests.post') + def test_create_volume(self, requests): + date = datetime.now() + requests.return_value = self.response + + self.response.headers['Content-Length'] = 0 + self.response.status_code = 201 + + self.assertTrue(self.client.create_volume('tenant_id', 'my_volume_id', 'volume_type_id', + 'volume name', 2, start=date)) + + requests.assert_called_once_with('{}{}'.format(self.url, '/v1/project/tenant_id/volume'), + params=None, + data=json.dumps({ + 'volume_id': 'my_volume_id', + 'volume_type': 'volume_type_id', + 'volume_name': 'volume name', + 'size': 2, + 'attached_to': [], + 'start': date.strftime(Client.DATE_FORMAT_BODY), + }), + headers=self.headers) + + @mock.patch('requests.put') + def test_attach_volume(self, requests): + date = datetime.now() + requests.return_value = self.response + + self.response.headers['Content-Length'] = 0 + self.response.status_code = 200 + + self.assertTrue(self.client.attach_volume('my_volume_id', ['instance_id'], date)) + + requests.assert_called_once_with('{}{}'.format(self.url, '/v1/volume/my_volume_id/attach'), + params=None, + data=json.dumps({'attachments': ['instance_id'], + 'date': date.strftime(Client.DATE_FORMAT_BODY)}), + headers=self.headers) + + @mock.patch('requests.put') + def test_detach_volume(self, requests): + date = datetime.now() + requests.return_value = self.response + + self.response.headers['Content-Length'] = 0 + self.response.status_code = 200 + + self.assertTrue(self.client.detach_volume('my_volume_id', ['instance_id'], date)) + + requests.assert_called_once_with('{}{}'.format(self.url, '/v1/volume/my_volume_id/detach'), + params=None, + data=json.dumps({'attachments': ['instance_id'], + 'date': date.strftime(Client.DATE_FORMAT_BODY)}), + headers=self.headers) diff --git a/almanachclient/v1/client.py b/almanachclient/v1/client.py index c296fc0..815924c 100644 --- a/almanachclient/v1/client.py +++ b/almanachclient/v1/client.py @@ -85,6 +85,31 @@ class Client(HttpClient): params = {'start': self._format_qs_datetime(start), 'end': self._format_qs_datetime(end)} return self._get(url, params) + def create_volume(self, tenant_id, volume_id, volume_type_id, name, size, attachments=None, start=None): + """Create a volume. + + :arg str tenant_id: Tenant UUID + :arg str volume_id: Volume UUID + :arg str volume_type_id: Volume type + :arg str name: Volume name + :arg int size: Volume size + :arg list attachments: List of instance attached to the volume + :arg datetime start: Creation date or now if None + :raises: ClientError + :rtype: bool + """ + data = { + 'volume_id': volume_id, + 'volume_type': volume_type_id, + 'volume_name': name, + 'size': size, + 'attached_to': attachments or [], + 'start': self._format_body_datetime(start or datetime.now()), + } + + self._post('{}/{}/project/{}/volume'.format(self.url, self.api_version, tenant_id), data=data) + return True + def resize_volume(self, volume_id, size, resize_date=None): """Resize a volume. @@ -102,6 +127,40 @@ class Client(HttpClient): self._put('{}/{}/volume/{}/resize'.format(self.url, self.api_version, volume_id), data=data) return True + def attach_volume(self, volume_id, attachments, attachment_date=None): + """Attach instances to a volume. + + :arg str volume_id: Volume UUID + :arg list attachments: List of instance ID + :arg datetime attachment_date: Attachment date + :raises: ClientError + :rtype: bool + """ + data = { + 'attachments': attachments, + 'date': self._format_body_datetime(attachment_date or datetime.now()), + } + + self._put('{}/{}/volume/{}/attach'.format(self.url, self.api_version, volume_id), data=data) + return True + + def detach_volume(self, volume_id, attachments, attachment_date=None): + """Detach instances from a volume. + + :arg str volume_id: Volume UUID + :arg list attachments: List of instance ID + :arg datetime attachment_date: Attachment date + :raises: ClientError + :rtype: bool + """ + data = { + 'attachments': attachments, + 'date': self._format_body_datetime(attachment_date or datetime.now()), + } + + self._put('{}/{}/volume/{}/detach'.format(self.url, self.api_version, volume_id), data=data) + return True + def get_instances(self, tenant_id, start, end): """List instances for a tenant. diff --git a/doc/source/installation.rst b/doc/source/installation.rst index 8f7ae68..96d6840 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -2,7 +2,14 @@ Installation ============ -The client requires at least the version 4.0.9 of Almanach. +Requirements +------------ + +- Python >= 3.4 +- Almanach >= 4.0.9 + +Instructions +------------ At the command line:: diff --git a/doc/source/usage_cli.rst b/doc/source/usage_cli.rst index 71cc9b0..32557f3 100644 --- a/doc/source/usage_cli.rst +++ b/doc/source/usage_cli.rst @@ -208,6 +208,35 @@ Arguments: * :code:`start`: Start date (ISO8601 format) * :code:`end`: End date (ISO8601 format) +Create Volume +------------- + +Usage: :code:`almanach create-volume --date --attachment ` + +.. code:: bash + + almanach create-volume \ + 8c3bc3aa-28d6-4863-b5ae-72e1b415f79d \ + 3e3b22e6-a10c-4c00-b8e5-05fcc8422b11 \ + f3786e9f-f8e6-4944-a3bc-e11b9f112706 \ + my-volume \ + 5 \ + --attachment=86dd5189-d9d6-40f7-a319-19231fbd4e07 \ + --attachment=252e49d8-abf2-486c-8478-b5f775134f54 + + Success + +Arguments: + +* :code:`tenant_id`: Tenant ID (UUID) +* :code:`volume_id`: Volume ID (UUID) +* :code:`volume_type_id`: Volume ID (UUID) +* :code:`volume_name`: Volume name (string) +* :code:`size`: Volume size (integer) +* :code:`date`: Creation date (ISO8601 format), if not specified the current datetime is used +* :code:`attachment`: Attach the volume to one or many instances (UUID) + + Resize Volume ------------- @@ -225,6 +254,45 @@ Arguments: * :code:`size`: Volume size (integer) * :code:`date`: Resize date (ISO8601 format), if not specified the current datetime is used +Attach Volume +------------- + +Usage: :code:`almanach attach-volume --date --attachment ` + +.. code:: bash + + almanach attach-volume \ + 8c3bc3aa-28d6-4863-b5ae-72e1b415f79d \ + --attachment=86dd5189-d9d6-40f7-a319-19231fbd4e07 + + Success + +Arguments: + +* :code:`volume_id`: Volume ID (UUID) +* :code:`date`: Attachment date (ISO8601 format), if not specified the current datetime is used +* :code:`attachment`: Attach the volume to one or many instances (UUID) + + +Detach Volume +------------- + +Usage: :code:`almanach detach-volume --date --attachment ` + +.. code:: bash + + almanach detach-volume \ + 8c3bc3aa-28d6-4863-b5ae-72e1b415f79d \ + --attachment=86dd5189-d9d6-40f7-a319-19231fbd4e07 + + Success + +Arguments: + +* :code:`volume_id`: Volume ID (UUID) +* :code:`date`: Attachment date (ISO8601 format), if not specified the current datetime is used +* :code:`attachment`: Attach the volume to one or many instances (UUID) + List Volume Types -----------------