[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:
parent
9da02d14ea
commit
460846cef2
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``: (**Compute**, **Network**) - groups of network access rules
|
||||||
* ``security group rule``: (**Compute**, **Network**) - the individual rules that define protocol/IP/port access
|
* ``security group rule``: (**Compute**, **Network**) - the individual rules that define protocol/IP/port access
|
||||||
* ``server``: (**Compute**) virtual machine instance
|
* ``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 dump``: (**Compute**) a dump file of a server created by features like kdump
|
||||||
* ``server group``: (**Compute**) a grouping of servers
|
* ``server group``: (**Compute**) a grouping of servers
|
||||||
* ``server image``: (**Compute**) saved server disk image
|
* ``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):
|
class FakeClientManager(object):
|
||||||
|
_api_version = {
|
||||||
|
'image': '2',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.compute = None
|
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_unset = openstackclient.compute.v2.server:UnsetServer
|
||||||
server_unshelve = openstackclient.compute.v2.server:UnshelveServer
|
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_create = openstackclient.compute.v2.server_group:CreateServerGroup
|
||||||
server_group_delete = openstackclient.compute.v2.server_group:DeleteServerGroup
|
server_group_delete = openstackclient.compute.v2.server_group:DeleteServerGroup
|
||||||
server_group_list = openstackclient.compute.v2.server_group:ListServerGroup
|
server_group_list = openstackclient.compute.v2.server_group:ListServerGroup
|
||||||
|
Loading…
Reference in New Issue
Block a user