[compute] Add server backup function
Add server backup function There is no return value for this command per following doc http://developer.openstack.org/api-ref-compute-v2.1.html#createBackup, also novaclient can't be updated now due to backward compatible issue http://lists.openstack.org/pipermail/openstack-dev/2016-March/089376.html, so we have to get the information ourselves. The Image tests were not using warlock images, so that needed to be fixed before we could completely test things like --wait. Change-Id: I30159518c4d3fdec89f15963bda641a0b03962d1
This commit is contained in:
		
							
								
								
									
										44
									
								
								doc/source/command-objects/server-backup.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								doc/source/command-objects/server-backup.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| ============= | ||||
| server backup | ||||
| ============= | ||||
|  | ||||
| A server backup is a disk image created in the Image store from a running server | ||||
| instance.  The backup command manages the number of archival copies to retain. | ||||
|  | ||||
| Compute v2 | ||||
|  | ||||
| server backup create | ||||
| -------------------- | ||||
|  | ||||
| Create a server backup image | ||||
|  | ||||
| .. program:: server create | ||||
| .. code:: bash | ||||
|  | ||||
|     os server backup create | ||||
|         [--name <image-name>] | ||||
|         [--type <backup-type>] | ||||
|         [--rotate <count>] | ||||
|         [--wait] | ||||
|         <server> | ||||
|  | ||||
| .. option:: --name <image-name> | ||||
|  | ||||
|     Name of the backup image (default: server name) | ||||
|  | ||||
| .. option:: --type <backup-type> | ||||
|  | ||||
|     Used to populate the ``backup_type`` property of the backup | ||||
|     image (default: empty) | ||||
|  | ||||
| .. option:: --rotate <count> | ||||
|  | ||||
|     Number of backup images to keep (default: 1) | ||||
|  | ||||
| .. option:: --wait | ||||
|  | ||||
|     Wait for operation to complete | ||||
|  | ||||
| .. describe:: <server> | ||||
|  | ||||
|     Server to back up (name or ID) | ||||
| @@ -118,6 +118,7 @@ referring to both Compute and Volume quotas. | ||||
| * ``security group``: (**Compute**, **Network**) - groups of network access rules | ||||
| * ``security group rule``: (**Compute**, **Network**) - the individual rules that define protocol/IP/port access | ||||
| * ``server``: (**Compute**) virtual machine instance | ||||
| * ``server backup``: (**Compute**) backup server disk image by using snapshot method | ||||
| * ``server dump``: (**Compute**) a dump file of a server created by features like kdump | ||||
| * ``server group``: (**Compute**) a grouping of servers | ||||
| * ``server image``: (**Compute**) saved server disk image | ||||
|   | ||||
							
								
								
									
										134
									
								
								openstackclient/compute/v2/server_backup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								openstackclient/compute/v2/server_backup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| #   Copyright 2012-2013 OpenStack Foundation | ||||
| # | ||||
| #   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. | ||||
| # | ||||
|  | ||||
| """Compute v2 Server action implementations""" | ||||
|  | ||||
| import sys | ||||
|  | ||||
| from oslo_utils import importutils | ||||
| import six | ||||
|  | ||||
| from openstackclient.common import command | ||||
| from openstackclient.common import exceptions | ||||
| from openstackclient.common import utils | ||||
| from openstackclient.i18n import _ | ||||
|  | ||||
|  | ||||
| def _show_progress(progress): | ||||
|     if progress: | ||||
|         sys.stderr.write('\rProgress: %s' % progress) | ||||
|         sys.stderr.flush() | ||||
|  | ||||
|  | ||||
| class CreateServerBackup(command.ShowOne): | ||||
|     """Create a server backup image""" | ||||
|  | ||||
|     IMAGE_API_VERSIONS = { | ||||
|         "1": "openstackclient.image.v1.image", | ||||
|         "2": "openstackclient.image.v2.image", | ||||
|     } | ||||
|  | ||||
|     def get_parser(self, prog_name): | ||||
|         parser = super(CreateServerBackup, self).get_parser(prog_name) | ||||
|         parser.add_argument( | ||||
|             'server', | ||||
|             metavar='<server>', | ||||
|             help=_('Server to back up (name or ID)'), | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             '--name', | ||||
|             metavar='<image-name>', | ||||
|             help=_('Name of the backup image (default: server name)'), | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             '--type', | ||||
|             metavar='<backup-type>', | ||||
|             help=_( | ||||
|                 'Used to populate the backup_type property of the backup ' | ||||
|                 'image (default: empty)' | ||||
|             ), | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             '--rotate', | ||||
|             metavar='<count>', | ||||
|             type=int, | ||||
|             help=_('Number of backups to keep (default: 1)'), | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             '--wait', | ||||
|             action='store_true', | ||||
|             help=_('Wait for backup image create to complete'), | ||||
|         ) | ||||
|         return parser | ||||
|  | ||||
|     def take_action(self, parsed_args): | ||||
|         compute_client = self.app.client_manager.compute | ||||
|  | ||||
|         server = utils.find_resource( | ||||
|             compute_client.servers, | ||||
|             parsed_args.server, | ||||
|         ) | ||||
|  | ||||
|         # Set sane defaults as this API wants all mouths to be fed | ||||
|         if parsed_args.name is None: | ||||
|             backup_name = server.name | ||||
|         else: | ||||
|             backup_name = parsed_args.name | ||||
|         if parsed_args.type is None: | ||||
|             backup_type = "" | ||||
|         else: | ||||
|             backup_type = parsed_args.type | ||||
|         if parsed_args.rotate is None: | ||||
|             backup_rotation = 1 | ||||
|         else: | ||||
|             backup_rotation = parsed_args.rotate | ||||
|  | ||||
|         compute_client.servers.backup( | ||||
|             server.id, | ||||
|             backup_name, | ||||
|             backup_type, | ||||
|             backup_rotation, | ||||
|         ) | ||||
|  | ||||
|         image_client = self.app.client_manager.image | ||||
|         image = utils.find_resource( | ||||
|             image_client.images, | ||||
|             backup_name, | ||||
|         ) | ||||
|  | ||||
|         if parsed_args.wait: | ||||
|             if utils.wait_for_status( | ||||
|                 image_client.images.get, | ||||
|                 image.id, | ||||
|                 callback=_show_progress, | ||||
|             ): | ||||
|                 sys.stdout.write('\n') | ||||
|             else: | ||||
|                 msg = _('Error creating server backup: %s') % parsed_args.name | ||||
|                 raise exceptions.CommandError(msg) | ||||
|  | ||||
|         if self.app.client_manager._api_version['image'] == '1': | ||||
|             info = {} | ||||
|             info.update(image._info) | ||||
|             info['properties'] = utils.format_dict(info.get('properties', {})) | ||||
|         else: | ||||
|             # Get the right image module to format the output | ||||
|             image_module = importutils.import_module( | ||||
|                 self.IMAGE_API_VERSIONS[ | ||||
|                     self.app.client_manager._api_version['image'] | ||||
|                 ] | ||||
|             ) | ||||
|             info = image_module._format_image(image) | ||||
|         return zip(*sorted(six.iteritems(info))) | ||||
							
								
								
									
										270
									
								
								openstackclient/tests/compute/v2/test_server_backup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								openstackclient/tests/compute/v2/test_server_backup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,270 @@ | ||||
| #   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 mock | ||||
|  | ||||
| from openstackclient.common import exceptions | ||||
| from openstackclient.common import utils as common_utils | ||||
| from openstackclient.compute.v2 import server_backup | ||||
| from openstackclient.tests.compute.v2 import fakes as compute_fakes | ||||
| from openstackclient.tests.image.v2 import fakes as image_fakes | ||||
|  | ||||
|  | ||||
| class TestServerBackup(compute_fakes.TestComputev2): | ||||
|  | ||||
|     def setUp(self): | ||||
|         super(TestServerBackup, self).setUp() | ||||
|  | ||||
|         # Get a shortcut to the compute client ServerManager Mock | ||||
|         self.servers_mock = self.app.client_manager.compute.servers | ||||
|         self.servers_mock.reset_mock() | ||||
|  | ||||
|         # Get a shortcut to the image client ImageManager Mock | ||||
|         self.images_mock = self.app.client_manager.image.images | ||||
|         self.images_mock.reset_mock() | ||||
|  | ||||
|         # Set object attributes to be tested. Could be overwriten in subclass. | ||||
|         self.attrs = {} | ||||
|  | ||||
|         # Set object methods to be tested. Could be overwriten in subclass. | ||||
|         self.methods = {} | ||||
|  | ||||
|     def setup_servers_mock(self, count): | ||||
|         servers = compute_fakes.FakeServer.create_servers( | ||||
|             attrs=self.attrs, | ||||
|             methods=self.methods, | ||||
|             count=count, | ||||
|         ) | ||||
|  | ||||
|         # This is the return value for utils.find_resource() | ||||
|         self.servers_mock.get = compute_fakes.FakeServer.get_servers( | ||||
|             servers, | ||||
|             0, | ||||
|         ) | ||||
|         return servers | ||||
|  | ||||
|  | ||||
| class TestServerBackupCreate(TestServerBackup): | ||||
|  | ||||
|     # Just return whatever Image is testing with these days | ||||
|     def image_columns(self, image): | ||||
|         columnlist = tuple(sorted(image.keys())) | ||||
|         return columnlist | ||||
|  | ||||
|     def image_data(self, image): | ||||
|         datalist = ( | ||||
|             image['id'], | ||||
|             image['name'], | ||||
|             image['owner'], | ||||
|             image['protected'], | ||||
|             'active', | ||||
|             common_utils.format_list(image.get('tags')), | ||||
|             image['visibility'], | ||||
|         ) | ||||
|         return datalist | ||||
|  | ||||
|     def setUp(self): | ||||
|         super(TestServerBackupCreate, self).setUp() | ||||
|  | ||||
|         # Get the command object to test | ||||
|         self.cmd = server_backup.CreateServerBackup(self.app, None) | ||||
|  | ||||
|         self.methods = { | ||||
|             'backup': None, | ||||
|         } | ||||
|  | ||||
|     def setup_images_mock(self, count, servers=None): | ||||
|         if servers: | ||||
|             images = image_fakes.FakeImage.create_images( | ||||
|                 attrs={ | ||||
|                     'name': servers[0].name, | ||||
|                     'status': 'active', | ||||
|                 }, | ||||
|                 count=count, | ||||
|             ) | ||||
|         else: | ||||
|             images = image_fakes.FakeImage.create_images( | ||||
|                 attrs={ | ||||
|                     'status': 'active', | ||||
|                 }, | ||||
|                 count=count, | ||||
|             ) | ||||
|  | ||||
|         self.images_mock.get = mock.MagicMock(side_effect=images) | ||||
|         return images | ||||
|  | ||||
|     def test_server_backup_defaults(self): | ||||
|         servers = self.setup_servers_mock(count=1) | ||||
|         images = self.setup_images_mock(count=1, servers=servers) | ||||
|  | ||||
|         arglist = [ | ||||
|             servers[0].id, | ||||
|         ] | ||||
|         verifylist = [ | ||||
|             ('name', None), | ||||
|             ('type', None), | ||||
|             ('rotate', None), | ||||
|             ('wait', False), | ||||
|             ('server', servers[0].id), | ||||
|         ] | ||||
|         parsed_args = self.check_parser(self.cmd, arglist, verifylist) | ||||
|  | ||||
|         # In base command class ShowOne in cliff, abstract method take_action() | ||||
|         # returns a two-part tuple with a tuple of column names and a tuple of | ||||
|         # data to be shown. | ||||
|         columns, data = self.cmd.take_action(parsed_args) | ||||
|  | ||||
|         # ServerManager.backup(server, backup_name, backup_type, rotation) | ||||
|         self.servers_mock.backup.assert_called_with( | ||||
|             servers[0].id, | ||||
|             servers[0].name, | ||||
|             '', | ||||
|             1, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(self.image_columns(images[0]), columns) | ||||
|         self.assertEqual(self.image_data(images[0]), data) | ||||
|  | ||||
|     def test_server_backup_create_options(self): | ||||
|         servers = self.setup_servers_mock(count=1) | ||||
|         images = self.setup_images_mock(count=1, servers=servers) | ||||
|  | ||||
|         arglist = [ | ||||
|             '--name', 'image', | ||||
|             '--type', 'daily', | ||||
|             '--rotate', '2', | ||||
|             servers[0].id, | ||||
|         ] | ||||
|         verifylist = [ | ||||
|             ('name', 'image'), | ||||
|             ('type', 'daily'), | ||||
|             ('rotate', 2), | ||||
|             ('server', servers[0].id), | ||||
|         ] | ||||
|         parsed_args = self.check_parser(self.cmd, arglist, verifylist) | ||||
|  | ||||
|         # In base command class ShowOne in cliff, abstract method take_action() | ||||
|         # returns a two-part tuple with a tuple of column names and a tuple of | ||||
|         # data to be shown. | ||||
|         columns, data = self.cmd.take_action(parsed_args) | ||||
|  | ||||
|         # ServerManager.backup(server, backup_name, backup_type, rotation) | ||||
|         self.servers_mock.backup.assert_called_with( | ||||
|             servers[0].id, | ||||
|             'image', | ||||
|             'daily', | ||||
|             2, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(self.image_columns(images[0]), columns) | ||||
|         self.assertEqual(self.image_data(images[0]), data) | ||||
|  | ||||
|     @mock.patch.object(common_utils, 'wait_for_status', return_value=False) | ||||
|     def test_server_backup_wait_fail(self, mock_wait_for_status): | ||||
|         servers = self.setup_servers_mock(count=1) | ||||
|         images = image_fakes.FakeImage.create_images( | ||||
|             attrs={ | ||||
|                 'name': servers[0].name, | ||||
|                 'status': 'active', | ||||
|             }, | ||||
|             count=5, | ||||
|         ) | ||||
|  | ||||
|         self.images_mock.get = mock.MagicMock( | ||||
|             side_effect=images, | ||||
|         ) | ||||
|  | ||||
|         arglist = [ | ||||
|             '--name', 'image', | ||||
|             '--type', 'daily', | ||||
|             '--wait', | ||||
|             servers[0].id, | ||||
|         ] | ||||
|         verifylist = [ | ||||
|             ('name', 'image'), | ||||
|             ('type', 'daily'), | ||||
|             ('wait', True), | ||||
|             ('server', servers[0].id), | ||||
|         ] | ||||
|         parsed_args = self.check_parser(self.cmd, arglist, verifylist) | ||||
|  | ||||
|         self.assertRaises( | ||||
|             exceptions.CommandError, | ||||
|             self.cmd.take_action, | ||||
|             parsed_args, | ||||
|         ) | ||||
|  | ||||
|         # ServerManager.backup(server, backup_name, backup_type, rotation) | ||||
|         self.servers_mock.backup.assert_called_with( | ||||
|             servers[0].id, | ||||
|             'image', | ||||
|             'daily', | ||||
|             1, | ||||
|         ) | ||||
|  | ||||
|         mock_wait_for_status.assert_called_once_with( | ||||
|             self.images_mock.get, | ||||
|             images[0].id, | ||||
|             callback=mock.ANY | ||||
|         ) | ||||
|  | ||||
|     @mock.patch.object(common_utils, 'wait_for_status', return_value=True) | ||||
|     def test_server_backup_wait_ok(self, mock_wait_for_status): | ||||
|         servers = self.setup_servers_mock(count=1) | ||||
|         images = image_fakes.FakeImage.create_images( | ||||
|             attrs={ | ||||
|                 'name': servers[0].name, | ||||
|                 'status': 'active', | ||||
|             }, | ||||
|             count=5, | ||||
|         ) | ||||
|  | ||||
|         self.images_mock.get = mock.MagicMock( | ||||
|             side_effect=images, | ||||
|         ) | ||||
|  | ||||
|         arglist = [ | ||||
|             '--name', 'image', | ||||
|             '--type', 'daily', | ||||
|             '--wait', | ||||
|             servers[0].id, | ||||
|         ] | ||||
|         verifylist = [ | ||||
|             ('name', 'image'), | ||||
|             ('type', 'daily'), | ||||
|             ('wait', True), | ||||
|             ('server', servers[0].id), | ||||
|         ] | ||||
|         parsed_args = self.check_parser(self.cmd, arglist, verifylist) | ||||
|  | ||||
|         # In base command class ShowOne in cliff, abstract method take_action() | ||||
|         # returns a two-part tuple with a tuple of column names and a tuple of | ||||
|         # data to be shown. | ||||
|         columns, data = self.cmd.take_action(parsed_args) | ||||
|  | ||||
|         # ServerManager.backup(server, backup_name, backup_type, rotation) | ||||
|         self.servers_mock.backup.assert_called_with( | ||||
|             servers[0].id, | ||||
|             'image', | ||||
|             'daily', | ||||
|             1, | ||||
|         ) | ||||
|  | ||||
|         mock_wait_for_status.assert_called_once_with( | ||||
|             self.images_mock.get, | ||||
|             images[0].id, | ||||
|             callback=mock.ANY | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(self.image_columns(images[0]), columns) | ||||
|         self.assertEqual(self.image_data(images[0]), data) | ||||
| @@ -105,6 +105,9 @@ class FakeClient(object): | ||||
|  | ||||
|  | ||||
| class FakeClientManager(object): | ||||
|     _api_version = { | ||||
|         'image': '2', | ||||
|     } | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.compute = None | ||||
|   | ||||
| @@ -0,0 +1,4 @@ | ||||
| --- | ||||
| features: | ||||
|   - | | ||||
|     Add support for the ``server backup create`` command | ||||
| @@ -131,6 +131,8 @@ openstack.compute.v2 = | ||||
|     server_unset = openstackclient.compute.v2.server:UnsetServer | ||||
|     server_unshelve = openstackclient.compute.v2.server:UnshelveServer | ||||
|  | ||||
|     server_backup_create = openstackclient.compute.v2.server_backup:CreateServerBackup | ||||
|  | ||||
|     server_group_create = openstackclient.compute.v2.server_group:CreateServerGroup | ||||
|     server_group_delete = openstackclient.compute.v2.server_group:DeleteServerGroup | ||||
|     server_group_list = openstackclient.compute.v2.server_group:ListServerGroup | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 jichenjc
					jichenjc