Implement share backup

Add share backup feature. This will allow the user to create,
restore and delete backups as well as listing backups and showing
the details of a specific backup.

Implement: blueprint share-backup
Depends-On: Ice01ab7892b1eb52b3202f2c79957977f73f3aca
Change-Id: I2c3848cbbeb921ede74756e25e58ef82277e0d2b
This commit is contained in:
Kiran Pawar 2022-04-30 19:42:11 +05:30
parent 1734b45fa5
commit 5e24577904
11 changed files with 1332 additions and 0 deletions

View File

@ -89,6 +89,19 @@ RESOURCE_LOCK_SORT_KEY_VALUES = (
'lock_reason', 'lock_reason',
) )
BACKUP_SORT_KEY_VALUES = (
'id',
'status',
'size',
'share_id',
'progress',
'restore_progress',
'name',
'host',
'topic',
'project_id',
)
TASK_STATE_MIGRATION_SUCCESS = 'migration_success' TASK_STATE_MIGRATION_SUCCESS = 'migration_success'
TASK_STATE_MIGRATION_ERROR = 'migration_error' TASK_STATE_MIGRATION_ERROR = 'migration_error'
TASK_STATE_MIGRATION_CANCELLED = 'migration_cancelled' TASK_STATE_MIGRATION_CANCELLED = 'migration_cancelled'

View File

@ -0,0 +1,416 @@
# Copyright 2023 Cloudification GmbH.
# All Rights Reserved.
#
# 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 logging
from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils as osc_utils
from manilaclient.common._i18n import _
from manilaclient.common import constants
from manilaclient.osc import utils
LOG = logging.getLogger(__name__)
class CreateShareBackup(command.ShowOne):
"""Create a share backup."""
_description = _("Create a backup of the given share")
def get_parser(self, prog_name):
parser = super(CreateShareBackup, self).get_parser(prog_name)
parser.add_argument(
"share",
metavar="<share>",
help=_("Name or ID of the share to backup.")
)
parser.add_argument(
'--name',
metavar='<name>',
default=None,
help=_('Optional share backup name. (Default=None).')
)
parser.add_argument(
'--description',
metavar='<description>',
default=None,
help=_('Optional share backup description. (Default=None).')
)
parser.add_argument(
"--backup-options",
metavar="<key=value>",
default={},
action=parseractions.KeyValueAction,
help=_("Backup driver option key=value pairs (Optional, "
"Default=None)."),
)
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
share = osc_utils.find_resource(
share_client.shares, parsed_args.share)
body = {}
if parsed_args.backup_options:
body['backup_options'] = utils.extract_key_value_options(
parsed_args.backup_options)
if parsed_args.description:
body['description'] = parsed_args.description
if parsed_args.name:
body['name'] = parsed_args.name
share_backup = share_client.share_backups.create(share, **body)
share_backup._info.pop('links', None)
return self.dict2columns(share_backup._info)
class DeleteShareBackup(command.Command):
"""Delete one or more share backups."""
_description = _("Delete one or more share backups")
def get_parser(self, prog_name):
parser = super(DeleteShareBackup, self).get_parser(prog_name)
parser.add_argument(
"backup",
metavar="<backup>",
nargs="+",
help=_("Name or ID of the backup(s) to delete")
)
parser.add_argument(
"--wait",
action='store_true',
default=False,
help=_("Wait for share backup deletion")
)
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
result = 0
for backup in parsed_args.backup:
try:
share_backup_obj = osc_utils.find_resource(
share_client.share_backups, backup)
share_client.share_backups.delete(share_backup_obj)
if parsed_args.wait:
if not osc_utils.wait_for_delete(
manager=share_client.share_backups,
res_id=share_backup_obj.id):
result += 1
except Exception as e:
result += 1
LOG.error(_(
"Failed to delete a share backup with "
"name or ID '%(backup)s': %(e)s"),
{'backup': backup, 'e': e})
if result > 0:
total = len(parsed_args.backup)
msg = (_("%(result)s of %(total)s backups failed "
"to delete.") % {'result': result, 'total': total})
raise exceptions.CommandError(msg)
class ListShareBackup(command.Lister):
"""List share backups."""
_description = _("List share backups")
def get_parser(self, prog_name):
parser = super(ListShareBackup, self).get_parser(prog_name)
parser.add_argument(
"--share",
metavar="<share>",
default=None,
help=_("Name or ID of the share to list backups for.")
)
parser.add_argument(
"--name",
metavar="<name>",
default=None,
help=_("Filter results by name. Default=None.")
)
parser.add_argument(
'--description',
metavar="<description>",
default=None,
help=_("Filter results by description. Default=None.")
)
parser.add_argument(
"--name~",
metavar="<name~>",
default=None,
help=_("Filter results matching a share backup name pattern. ")
)
parser.add_argument(
'--description~',
metavar="<description~>",
default=None,
help=_("Filter results matching a share backup description ")
)
parser.add_argument(
'--status',
metavar="<status>",
default=None,
help=_('Filter results by status. Default=None.')
)
parser.add_argument(
"--limit",
metavar="<num-backups>",
type=int,
default=None,
action=parseractions.NonNegativeAction,
help=_("Limit the number of backups returned. Default=None.")
)
parser.add_argument(
'--offset',
metavar="<offset>",
default=None,
help='Start position of backup records listing.')
parser.add_argument(
'--sort-key', '--sort_key',
metavar='<sort_key>',
type=str,
default=None,
help='Key to be sorted, available keys are %(keys)s. '
'Default=None.'
% {'keys': constants.BACKUP_SORT_KEY_VALUES})
parser.add_argument(
'--sort-dir', '--sort_dir',
metavar='<sort_dir>',
type=str,
default=None,
help='Sort direction, available values are %(values)s. '
'OPTIONAL: Default=None.' % {
'values': constants.SORT_DIR_VALUES})
parser.add_argument(
'--detail',
dest='detail',
metavar='<0|1>',
nargs='?',
type=int,
const=1,
default=0,
help="Show detailed information about share backups.")
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
share_id = None
if parsed_args.share:
share_id = osc_utils.find_resource(share_client.shares,
parsed_args.share).id
columns = [
'ID',
'Name',
'Share ID',
'Status'
]
if parsed_args.detail:
columns.extend(['Description', 'Size', 'Created At',
'Updated At', 'Availability Zone', 'Progress',
'Restore Progress', 'Host', 'Topic'])
search_opts = {
'limit': parsed_args.limit,
'offset': parsed_args.offset,
'name': parsed_args.name,
'description': parsed_args.description,
'status': parsed_args.status,
'share_id': share_id,
}
search_opts['name~'] = getattr(parsed_args, 'name~')
search_opts['description~'] = getattr(parsed_args, 'description~')
backups = share_client.share_backups.list(
detailed=parsed_args.detail, search_opts=search_opts,
sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir)
return (columns,
(osc_utils.get_item_properties(b, columns) for b in backups))
class ShowShareBackup(command.ShowOne):
"""Show share backup."""
_description = _("Show details of a backup")
def get_parser(self, prog_name):
parser = super(ShowShareBackup, self).get_parser(prog_name)
parser.add_argument(
"backup",
metavar="<backup>",
help=_("ID of the share backup. ")
)
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
backup = osc_utils.find_resource(share_client.share_backups,
parsed_args.backup)
backup._info.pop('links', None)
return self.dict2columns(backup._info)
class RestoreShareBackup(command.Command):
"""Restore share backup to share"""
_description = _("Attempt to restore share backup")
def get_parser(self, prog_name):
parser = super(RestoreShareBackup, self).get_parser(prog_name)
parser.add_argument(
"backup",
metavar="<backup>",
help=_('ID of backup to restore.')
)
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
share_backup = osc_utils.find_resource(
share_client.share_backups,
parsed_args.backup)
share_client.share_backups.restore(share_backup.id)
class SetShareBackup(command.Command):
"""Set share backup properties."""
_description = _("Set share backup properties")
def get_parser(self, prog_name):
parser = super(SetShareBackup, self).get_parser(prog_name)
parser.add_argument(
"backup",
metavar="<backup>",
help=_('Name or ID of the backup to set a property for')
)
parser.add_argument(
"--name",
metavar="<name>",
default=None,
help=_("Set a name to the backup.")
)
parser.add_argument(
"--description",
metavar="<description>",
default=None,
help=_("Set a description to the backup.")
)
parser.add_argument(
"--status",
metavar="<status>",
choices=['available', 'error', 'creating', 'deleting',
'restoring'],
help=_("Assign a status to the backup(Admin only). "
"Options include : available, error, creating, "
"deleting, restoring.")
)
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
result = 0
share_backup = osc_utils.find_resource(
share_client.share_backups,
parsed_args.backup)
kwargs = {}
if parsed_args.name is not None:
kwargs['name'] = parsed_args.name
if parsed_args.description is not None:
kwargs['description'] = parsed_args.description
try:
share_client.share_backups.update(share_backup, **kwargs)
except Exception as e:
result += 1
LOG.error(_(
"Failed to set share backup properties "
"'%(properties)s': %(exception)s"),
{'properties': kwargs,
'exception': e})
if parsed_args.status:
try:
share_client.share_backups.reset_status(
share_backup,
parsed_args.status
)
except Exception as e:
result += 1
LOG.error(_(
"Failed to update backup status to "
"'%(status)s': %(e)s"),
{'status': parsed_args.status, 'e': e})
if result > 0:
raise exceptions.CommandError(_("One or more of the "
"set operations failed"))
class UnsetShareBackup(command.Command):
"""Unset share backup properties."""
_description = _("Unset share backup properties")
def get_parser(self, prog_name):
parser = super(UnsetShareBackup, self).get_parser(prog_name)
parser.add_argument(
"backup",
metavar="<backup>",
help=_('Name or ID of the backup to unset a property for')
)
parser.add_argument(
"--name",
action='store_true',
help=_("Unset a name to the backup.")
)
parser.add_argument(
"--description",
action='store_true',
help=_("Unset a description to the backup.")
)
return parser
def take_action(self, parsed_args):
share_client = self.app.client_manager.share
share_backup = osc_utils.find_resource(
share_client.share_backups,
parsed_args.backup)
kwargs = {}
if parsed_args.name:
kwargs['name'] = None
if parsed_args.description:
kwargs['description'] = None
if not kwargs:
msg = "Either name or description must be provided."
raise exceptions.CommandError(msg)
try:
share_client.share_backups.update(share_backup, **kwargs)
except Exception as e:
LOG.error(_(
"Failed to unset share backup properties "
"'%(properties)s': %(exception)s"),
{'properties': kwargs,
'exception': e})

View File

@ -434,3 +434,31 @@ class OSCClientTestBase(base.ClientTestBase):
'share lock delete %s' % lock['id'], 'share lock delete %s' % lock['id'],
client=client) client=client)
return lock return lock
def create_backup(self, share_id, name=None, description=None,
backup_options=None, add_cleanup=True):
name = name or data_utils.rand_name('autotest_backup_name')
cmd = (f'backup create {share_id} ')
if name:
cmd += f' --name {name}'
if description:
cmd += f' --description {description}'
if backup_options:
options = ' --backup-options'
for key, value in backup_options.items():
options += f' {key}={value}'
cmd += options
backup_object = self.dict_result('share', cmd)
self._wait_for_object_status(
'share backup', backup_object['id'], 'available')
if add_cleanup:
self.addCleanup(
self.openstack,
f'share backup delete {backup_object["id"]} --wait')
return backup_object

View File

@ -0,0 +1,152 @@
# 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
from manilaclient.tests.functional.osc import base
class ShareBackupCLITest(base.OSCClientTestBase):
"""Functional tests for share backup."""
def test_share_backup_create(self):
share = self.create_share()
backup = self.create_backup(
share_id=share['id'],
name='test_backup_create',
description='Description',
backup_options={'dummy': True})
# fetch latest after periodic callback updates status
backup = json.loads(self.openstack(
f'share backup show -f json {backup["id"]}'))
self.assertEqual(share["id"], backup["share_id"])
self.assertEqual('test_backup_create', backup["name"])
self.assertEqual('Description', backup["description"])
self.assertEqual('available', backup["status"])
backups_list = self.listing_result('share backup', 'list')
self.assertIn(backup['id'],
[item['ID'] for item in backups_list])
def test_share_backup_delete(self):
share = self.create_share()
backup = self.create_backup(
share_id=share['id'],
backup_options={'dummy': True},
add_cleanup=False)
self.openstack(
f'share backup delete {backup["id"]} --wait')
self.check_object_deleted('share backup', backup["id"])
def test_share_backup_show(self):
share = self.create_share()
backup = self.create_backup(
share_id=share['id'],
name='test_backup_show',
description='Description',
backup_options={'dummy': True})
show_result = self.dict_result(
'share backup', f'show {backup["id"]}')
self.assertEqual(backup["id"], show_result["id"])
self.assertEqual('test_backup_show', show_result["name"])
self.assertEqual('Description', show_result["description"])
def test_share_backup_set(self):
share = self.create_share()
backup = self.create_backup(share_id=share['id'],
backup_options={'dummy': True})
self.openstack(
f'share backup set {backup["id"]} '
f'--name test_backup_set --description Description')
show_result = self.dict_result(
'share backup ', f'show {backup["id"]}')
self.assertEqual(backup['id'], show_result["id"])
self.assertEqual('test_backup_set', show_result["name"])
self.assertEqual('Description', show_result["description"])
def test_share_backup_unset(self):
share = self.create_share()
backup = self.create_backup(
share_id=share['id'],
name='test_backup_unset',
description='Description',
backup_options={'dummy': True})
self.openstack(
f'share backup unset {backup["id"]} --name --description')
show_result = json.loads(self.openstack(
f'share backup show -f json {backup["id"]}'))
self.assertEqual(backup['id'], show_result["id"])
self.assertIsNone(show_result["name"])
self.assertIsNone(show_result["description"])
def test_share_backup_list(self):
share_1 = self.create_share()
share_2 = self.create_share()
backup_1 = self.create_backup(share_id=share_1['id'],
backup_options={'dummy': True})
backup_2 = self.create_backup(share_id=share_2['id'],
backup_options={'dummy': True})
backups_list = self.listing_result(
'share backup', f'list --name {backup_2["name"]} '
)
self.assertTableStruct(backups_list, [
'ID',
'Name',
'Share ID',
'Status'
])
self.assertEqual(1, len(backups_list))
self.assertIn(backup_2['id'],
[item['ID'] for item in backups_list])
backups_list = self.listing_result(
'share backup', f'list --share {share_1["id"]} --detail'
)
self.assertTableStruct(backups_list, [
'ID',
'Name',
'Share ID',
'Status',
'Description',
'Availability Zone',
'Created At',
'Updated At',
'Size',
'Progress',
'Restore Progress',
'Host',
'Topic',
])
self.assertEqual(1, len(backups_list))
self.assertIn(backup_1['id'],
[item['ID'] for item in backups_list])

View File

@ -37,6 +37,7 @@ class FakeShareClient(object):
self.share_type_access = mock.Mock() self.share_type_access = mock.Mock()
self.quotas = mock.Mock() self.quotas = mock.Mock()
self.quota_classes = mock.Mock() self.quota_classes = mock.Mock()
self.share_backups = mock.Mock()
self.share_snapshots = mock.Mock() self.share_snapshots = mock.Mock()
self.share_group_snapshots = mock.Mock() self.share_group_snapshots = mock.Mock()
self.share_snapshot_export_locations = mock.Mock() self.share_snapshot_export_locations = mock.Mock()
@ -1580,3 +1581,61 @@ class FakeResourceLock(object):
FakeResourceLock.create_one_lock(attrs)) FakeResourceLock.create_one_lock(attrs))
return resource_locks return resource_locks
class FakeShareBackup(object):
"""Fake a share Backup"""
@staticmethod
def create_one_backup(attrs=None, methods=None):
"""Create a fake share backup
:param Dictionary attrs:
A dictionary with all attributes
:return:
A FakeResource object, with project_id, resource and so on
"""
attrs = attrs or {}
methods = methods or {}
share_backup = {
'id': 'backup-id-' + uuid.uuid4().hex,
'share_id': 'share-id-' + uuid.uuid4().hex,
'status': None,
'name': None,
'description': None,
'size': '0',
'created_at': datetime.datetime.now().isoformat(),
'updated_at': datetime.datetime.now().isoformat(),
'availability_zone': None,
'progress': None,
'restore_progress': None,
'host': None,
'topic': None,
}
share_backup.update(attrs)
share_backup = osc_fakes.FakeResource(info=copy.deepcopy(
share_backup),
methods=methods,
loaded=True)
return share_backup
@staticmethod
def create_share_backups(attrs=None, count=2):
"""Create multiple fake backups.
:param Dictionary attrs:
A dictionary with all attributes
:param Integer count:
The number of share backups to be faked
:return:
A list of FakeResource objects
"""
share_backups = []
for n in range(0, count):
share_backups.append(
FakeShareBackup.create_one_backup(attrs))
return share_backups

View File

@ -0,0 +1,420 @@
# 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 osc_lib import exceptions
from osc_lib import utils as oscutils
from manilaclient import api_versions
from manilaclient.api_versions import MAX_VERSION
from manilaclient.osc.v2 import share_backups as osc_share_backups
from manilaclient.tests.unit.osc import osc_utils
from manilaclient.tests.unit.osc.v2 import fakes as manila_fakes
class TestShareBackup(manila_fakes.TestShare):
def setUp(self):
super(TestShareBackup, self).setUp()
self.shares_mock = self.app.client_manager.share.shares
self.shares_mock.reset_mock()
self.backups_mock = self.app.client_manager.share.share_backups
self.backups_mock.reset_mock()
self.app.client_manager.share.api_version = api_versions.APIVersion(
MAX_VERSION)
class TestShareBackupCreate(TestShareBackup):
def setUp(self):
super(TestShareBackupCreate, self).setUp()
self.share = manila_fakes.FakeShare.create_one_share()
self.shares_mock.get.return_value = self.share
self.share_backup = (
manila_fakes.FakeShareBackup.create_one_backup(
attrs={'status': 'available'}
))
self.backups_mock.create.return_value = self.share_backup
self.backups_mock.get.return_value = self.share_backup
self.cmd = osc_share_backups.CreateShareBackup(self.app, None)
self.data = tuple(self.share_backup._info.values())
self.columns = tuple(self.share_backup._info.keys())
def test_share_backup_create_missing_args(self):
arglist = []
verifylist = []
self.assertRaises(
osc_utils.ParserException,
self.check_parser, self.cmd, arglist, verifylist)
def test_share_backup_create(self):
arglist = [
self.share.id
]
verifylist = [
('share', self.share.id)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.backups_mock.create.assert_called_with(
self.share,
)
self.assertCountEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
def test_share_backup_create_name(self):
arglist = [
self.share.id,
'--name', "FAKE_SHARE_BACKUP_NAME"
]
verifylist = [
('share', self.share.id),
('name', "FAKE_SHARE_BACKUP_NAME")
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.backups_mock.create.assert_called_with(
self.share,
name="FAKE_SHARE_BACKUP_NAME",
)
self.assertCountEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
class TestShareBackupDelete(TestShareBackup):
def setUp(self):
super(TestShareBackupDelete, self).setUp()
self.share_backup = (
manila_fakes.FakeShareBackup.create_one_backup())
self.backups_mock.get.return_value = self.share_backup
self.cmd = osc_share_backups.DeleteShareBackup(self.app, None)
def test_share_backup_delete_missing_args(self):
arglist = []
verifylist = []
self.assertRaises(osc_utils.ParserException,
self.check_parser, self.cmd, arglist, verifylist)
def test_share_backup_delete(self):
arglist = [
self.share_backup.id
]
verifylist = [
('backup', [self.share_backup.id])
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.backups_mock.delete.assert_called_with(self.share_backup)
self.assertIsNone(result)
def test_share_backup_delete_multiple(self):
share_backups = (
manila_fakes.FakeShareBackup.create_share_backups(
count=2))
arglist = [
share_backups[0].id,
share_backups[1].id
]
verifylist = [
('backup', [share_backups[0].id, share_backups[1].id])
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.assertEqual(self.backups_mock.delete.call_count,
len(share_backups))
self.assertIsNone(result)
def test_share_backup_delete_exception(self):
arglist = [
self.share_backup.id
]
verifylist = [
('backup', [self.share_backup.id])
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.backups_mock.delete.side_effect = exceptions.CommandError()
self.assertRaises(exceptions.CommandError,
self.cmd.take_action,
parsed_args)
class TestShareBackupList(TestShareBackup):
columns = [
'ID',
'Name',
'Share ID',
'Status',
]
detailed_columns = [
'ID',
'Name',
'Share ID',
'Status',
'Description',
'Size',
'Created At',
'Updated At',
'Availability Zone',
'Progress',
'Restore Progress',
'Host',
'Topic',
]
def setUp(self):
super(TestShareBackupList, self).setUp()
self.share = manila_fakes.FakeShare.create_one_share()
self.shares_mock.get.return_value = self.share
self.backups_list = (
manila_fakes.FakeShareBackup.create_share_backups(
count=2))
self.backups_mock.list.return_value = self.backups_list
self.values = (oscutils.get_dict_properties(
i._info, self.columns) for i in self.backups_list)
self.detailed_values = (oscutils.get_dict_properties(
i._info, self.detailed_columns) for i in self.backups_list)
self.cmd = osc_share_backups.ListShareBackup(self.app, None)
def test_share_backup_list(self):
arglist = []
verifylist = []
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.backups_mock.list.assert_called_with(
detailed=0,
search_opts={
'offset': None, 'limit': None, 'name': None,
'description': None, 'name~': None, 'description~': None,
'status': None, 'share_id': None
},
sort_key=None, sort_dir=None
)
self.assertEqual(self.columns, columns)
self.assertEqual(list(self.values), list(data))
def test_share_backup_list_detail(self):
arglist = [
'--detail'
]
verifylist = [
('detail', True)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.backups_mock.list.assert_called_with(
detailed=1,
search_opts={
'offset': None, 'limit': None, 'name': None,
'description': None, 'name~': None, 'description~': None,
'status': None, 'share_id': None
},
sort_key=None, sort_dir=None
)
self.assertEqual(self.detailed_columns, columns)
self.assertEqual(list(self.detailed_values), list(data))
def test_share_backup_list_for_share(self):
arglist = [
'--share', self.share.id
]
verifylist = [
('share', self.share.id)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.backups_mock.list.assert_called_with(
detailed=0,
search_opts={
'offset': None, 'limit': None, 'name': None,
'description': None, 'name~': None, 'description~': None,
'status': None, 'share_id': self.share.id
},
sort_key=None, sort_dir=None
)
self.assertEqual(self.columns, columns)
self.assertEqual(list(self.values), list(data))
class TestShareBackupShow(TestShareBackup):
def setUp(self):
super(TestShareBackupShow, self).setUp()
self.share_backup = (
manila_fakes.FakeShareBackup.create_one_backup()
)
self.backups_mock.get.return_value = self.share_backup
self.cmd = osc_share_backups.ShowShareBackup(self.app, None)
self.data = tuple(self.share_backup._info.values())
self.columns = tuple(self.share_backup._info.keys())
def test_share_backup_show_missing_args(self):
arglist = []
verifylist = []
self.assertRaises(
osc_utils.ParserException,
self.check_parser, self.cmd, arglist, verifylist)
def test_share_backup_show(self):
arglist = [
self.share_backup.id
]
verifylist = [
('backup', self.share_backup.id)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.backups_mock.get.assert_called_with(
self.share_backup.id
)
self.assertCountEqual(self.columns, columns)
self.assertCountEqual(self.data, data)
class TestShareBackupRestore(TestShareBackup):
def setUp(self):
super(TestShareBackupRestore, self).setUp()
self.share_backup = (
manila_fakes.FakeShareBackup.create_one_backup()
)
self.backups_mock.get.return_value = self.share_backup
self.cmd = osc_share_backups.RestoreShareBackup(
self.app, None)
def test_share_backup_restore(self):
arglist = [
self.share_backup.id,
]
verifylist = [
('backup', self.share_backup.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.backups_mock.restore.assert_called_with(self.share_backup.id)
self.assertIsNone(result)
class TestShareBackupSet(TestShareBackup):
def setUp(self):
super(TestShareBackupSet, self).setUp()
self.share_backup = (
manila_fakes.FakeShareBackup.create_one_backup()
)
self.backups_mock.get.return_value = self.share_backup
self.cmd = osc_share_backups.SetShareBackup(self.app, None)
def test_set_share_backup_name(self):
arglist = [
self.share_backup.id,
'--name', "FAKE_SHARE_BACKUP_NAME"
]
verifylist = [
('backup', self.share_backup.id),
('name', "FAKE_SHARE_BACKUP_NAME")
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.backups_mock.update.assert_called_with(self.share_backup,
name=parsed_args.name)
self.assertIsNone(result)
def test_set_backup_status(self):
arglist = [
self.share_backup.id,
'--status', 'available'
]
verifylist = [
('backup', self.share_backup.id),
('status', 'available')
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.backups_mock.reset_status.assert_called_with(
self.share_backup,
parsed_args.status)
self.assertIsNone(result)
class TestShareBackupUnset(TestShareBackup):
def setUp(self):
super(TestShareBackupUnset, self).setUp()
self.share_backup = (
manila_fakes.FakeShareBackup.create_one_backup()
)
self.backups_mock.get.return_value = self.share_backup
self.cmd = osc_share_backups.UnsetShareBackup(self.app, None)
def test_unset_backup_name(self):
arglist = [
self.share_backup.id,
'--name'
]
verifylist = [
('backup', self.share_backup.id),
('name', True)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.backups_mock.update.assert_called_with(
self.share_backup,
name=None)
self.assertIsNone(result)
def test_unset_backup_description(self):
arglist = [
self.share_backup.id,
'--description'
]
verifylist = [
('backup', self.share_backup.id),
('description', True)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.backups_mock.update.assert_called_with(
self.share_backup,
description=None)
self.assertIsNone(result)

View File

@ -0,0 +1,91 @@
# 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 unittest import mock
import ddt
from manilaclient import api_versions
from manilaclient.tests.unit import utils
from manilaclient.tests.unit.v2 import fakes
from manilaclient.v2 import share_backups
FAKE_BACKUP = 'fake_backup'
@ddt.ddt
class ShareBackupsTest(utils.TestCase):
class _FakeShareBackup(object):
id = 'fake_share_backup_id'
def setUp(self):
super(ShareBackupsTest, self).setUp()
microversion = api_versions.APIVersion("2.80")
self.manager = share_backups.ShareBackupManager(
fakes.FakeClient(api_version=microversion))
def test_delete_str(self):
with mock.patch.object(self.manager, '_delete', mock.Mock()):
self.manager.delete(FAKE_BACKUP)
self.manager._delete.assert_called_once_with(
share_backups.RESOURCE_PATH % FAKE_BACKUP)
def test_delete_obj(self):
backup = self._FakeShareBackup
with mock.patch.object(self.manager, '_delete', mock.Mock()):
self.manager.delete(backup)
self.manager._delete.assert_called_once_with(
share_backups.RESOURCE_PATH % backup.id)
def test_get(self):
with mock.patch.object(self.manager, '_get', mock.Mock()):
self.manager.get(FAKE_BACKUP)
self.manager._get.assert_called_once_with(
share_backups.RESOURCE_PATH % FAKE_BACKUP,
share_backups.RESOURCE_NAME)
def test_restore(self):
with mock.patch.object(self.manager, '_action', mock.Mock()):
self.manager.restore(FAKE_BACKUP)
self.manager._action.assert_called_once_with(
'restore', FAKE_BACKUP)
def test_list(self):
with mock.patch.object(self.manager, '_list', mock.Mock()):
self.manager.list()
self.manager._list.assert_called_once_with(
share_backups.RESOURCES_PATH + '/detail',
share_backups.RESOURCES_NAME)
def test_list_with_share(self):
with mock.patch.object(self.manager, '_list', mock.Mock()):
self.manager.list(search_opts={'share_id': 'fake_share_id'})
share_uri = '?share_id=fake_share_id'
self.manager._list.assert_called_once_with(
(share_backups.RESOURCES_PATH + '/detail' + share_uri),
share_backups.RESOURCES_NAME)
def test_reset_state(self):
with mock.patch.object(self.manager, '_action', mock.Mock()):
self.manager.reset_status(FAKE_BACKUP, 'fake_status')
self.manager._action.assert_called_once_with(
'reset_status', FAKE_BACKUP, {'status': 'fake_status'})
def test_update(self):
backup = self._FakeShareBackup
with mock.patch.object(self.manager, '_update', mock.Mock()):
data = dict(name='backup1')
self.manager.update(backup, **data)
self.manager._update.assert_called_once_with(
share_backups.RESOURCE_PATH % backup.id,
{'share_backup': data})

View File

@ -28,6 +28,7 @@ from manilaclient.v2 import scheduler_stats
from manilaclient.v2 import security_services from manilaclient.v2 import security_services
from manilaclient.v2 import services from manilaclient.v2 import services
from manilaclient.v2 import share_access_rules from manilaclient.v2 import share_access_rules
from manilaclient.v2 import share_backups
from manilaclient.v2 import share_export_locations from manilaclient.v2 import share_export_locations
from manilaclient.v2 import share_group_snapshots from manilaclient.v2 import share_group_snapshots
from manilaclient.v2 import share_group_type_access from manilaclient.v2 import share_group_type_access
@ -237,6 +238,7 @@ class Client(object):
self.pools = scheduler_stats.PoolManager(self) self.pools = scheduler_stats.PoolManager(self)
self.share_access_rules = ( self.share_access_rules = (
share_access_rules.ShareAccessRuleManager(self)) share_access_rules.ShareAccessRuleManager(self))
self.share_backups = share_backups.ShareBackupManager(self)
self._load_extensions(extensions) self._load_extensions(extensions)

View File

@ -0,0 +1,137 @@
# 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 manilaclient import api_versions
from manilaclient import base
from manilaclient.common import constants
RESOURCES_NAME = 'share_backups'
RESOURCE_NAME = 'share_backup'
RESOURCES_PATH = '/share-backups'
RESOURCE_PATH = '/share-backups/%s'
RESOURCE_PATH_ACTION = '/share-backups/%s/action'
class ShareBackup(base.Resource):
def __repr__(self):
return "<Share Backup: %s>" % self.id
class ShareBackupManager(base.ManagerWithFind):
"""Manage :class:`ShareBackup` resources."""
resource_class = ShareBackup
@api_versions.wraps("2.80")
@api_versions.experimental_api
def get(self, backup):
"""Get a share backup.
:param backup: either backup object or its UUID.
:rtype: :class:`ShareBackup`
"""
backup_id = base.getid(backup)
return self._get(RESOURCE_PATH % backup_id, RESOURCE_NAME)
@api_versions.wraps("2.80")
@api_versions.experimental_api
def list(self, detailed=True, search_opts=None, sort_key=None,
sort_dir=None):
"""List all share backups or list backups belonging to a share.
:param detailed: list backups with detailed fields.
:param search_opts: Search options to filter out shares.
:param sort_key: Key to be sorted.
:param sort_dir: Sort direction, should be 'desc' or 'asc'.
:rtype: list of :class:`ShareBackup`
"""
search_opts = search_opts or {}
if sort_key is not None:
if sort_key in constants.BACKUP_SORT_KEY_VALUES:
search_opts['sort_key'] = sort_key
else:
raise ValueError(
'sort_key must be one of the following: %s.'
% ', '.join(constants.BACKUP_SORT_KEY_VALUES))
if sort_dir is not None:
if sort_dir in constants.SORT_DIR_VALUES:
search_opts['sort_dir'] = sort_dir
else:
raise ValueError(
'sort_dir must be one of the following: %s.'
% ', '.join(constants.SORT_DIR_VALUES))
query_string = self._build_query_string(search_opts)
if detailed:
path = "/share-backups/detail%s" % (query_string,)
else:
path = "/share-backups%s" % (query_string,)
return self._list(path, 'share_backups')
@api_versions.wraps("2.80")
@api_versions.experimental_api
def create(self, share, backup_options=None, description=None, name=None):
"""Create a backup for a share.
:param share: The share to create the backup of. Can be the share
object or its UUID.
:param backup_options: dict - custom set of key-values
:param name: text - name of new share
:param description: - description for new share
"""
share_id = base.getid(share)
body = {
'share_id': share_id,
'backup_options': backup_options,
'description': description,
'name': name,
}
return self._create(RESOURCES_PATH,
{RESOURCE_NAME: body},
RESOURCE_NAME)
@api_versions.wraps("2.80")
@api_versions.experimental_api
def delete(self, backup):
backup_id = base.getid(backup)
url = RESOURCE_PATH % backup_id
self._delete(url)
@api_versions.wraps("2.80")
@api_versions.experimental_api
def restore(self, backup):
return self._action('restore', backup)
@api_versions.wraps("2.80")
@api_versions.experimental_api
def reset_status(self, backup, state):
return self._action('reset_status', backup, {"status": state})
@api_versions.wraps("2.80")
@api_versions.experimental_api
def update(self, backup, **kwargs):
if not kwargs:
return
backup_id = base.getid(backup)
body = {'share_backup': kwargs}
return self._update(RESOURCE_PATH % backup_id, body)
def _action(self, action, backup, info=None, **kwargs):
body = {action: info}
self.run_hooks('modify_body_for_action', body, **kwargs)
backup_id = base.getid(backup)
url = RESOURCE_PATH_ACTION % backup_id
return self.api.client.post(url, body=body)

View File

@ -0,0 +1,7 @@
---
features:
- |
Added support for share backup APIs in the SDK and the openstackclient
plugin. You can use the openstack client to create a backup, restore a
backup, delete a backup, list backups with filters, and update the name
and description fields of a backup. Available from microversion 2.80.

View File

@ -59,6 +59,13 @@ openstack.share.v2 =
share_access_show = manilaclient.osc.v2.share_access_rules:ShowShareAccess share_access_show = manilaclient.osc.v2.share_access_rules:ShowShareAccess
share_access_set = manilaclient.osc.v2.share_access_rules:SetShareAccess share_access_set = manilaclient.osc.v2.share_access_rules:SetShareAccess
share_access_unset = manilaclient.osc.v2.share_access_rules:UnsetShareAccess share_access_unset = manilaclient.osc.v2.share_access_rules:UnsetShareAccess
share_backup_create = manilaclient.osc.v2.share_backups:CreateShareBackup
share_backup_delete = manilaclient.osc.v2.share_backups:DeleteShareBackup
share_backup_list = manilaclient.osc.v2.share_backups:ListShareBackup
share_backup_show = manilaclient.osc.v2.share_backups:ShowShareBackup
share_backup_restore = manilaclient.osc.v2.share_backups:RestoreShareBackup
share_backup_set = manilaclient.osc.v2.share_backups:SetShareBackup
share_backup_unset = manilaclient.osc.v2.share_backups:UnsetShareBackup
share_type_create = manilaclient.osc.v2.share_types:CreateShareType share_type_create = manilaclient.osc.v2.share_types:CreateShareType
share_type_delete = manilaclient.osc.v2.share_types:DeleteShareType share_type_delete = manilaclient.osc.v2.share_types:DeleteShareType
share_type_set = manilaclient.osc.v2.share_types:SetShareType share_type_set = manilaclient.osc.v2.share_types:SetShareType