[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:
jichenjc 2016-02-20 22:33:18 +08:00 committed by Dean Troyer
parent 9da02d14ea
commit 460846cef2
7 changed files with 458 additions and 0 deletions

View 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)

View File

@ -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

View 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)))

View 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)

View File

@ -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

View File

@ -0,0 +1,4 @@
---
features:
- |
Add support for the ``server backup create`` command

View File

@ -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