Backup/Restore nova tenant
Implement nova tenant backup and restore to swift, local and ssh storage. Add unit tests. Add new CLI option '--project-id' to perform tenant backup/restore. Add new option - '--backup-media' as well. Partially implements bp: tenant-backup Change-Id: Ia8e4412dfaf0c24364db692de78c27cd0e3b3c4c
This commit is contained in:
parent
08b04e1c5d
commit
b0c64f78cd
|
@ -35,7 +35,9 @@ def get_client_manager(backup_args):
|
|||
parse_osrc(backup_args['osrc']))
|
||||
else:
|
||||
options = osclients.OpenstackOpts.create_from_env().get_opts_dicts()
|
||||
|
||||
if backup_args['project_id']:
|
||||
options['project_name'] = None
|
||||
options['project_id'] = backup_args['project_id']
|
||||
client_manager = osclients.OSClientManager(
|
||||
auth_url=options.pop('auth_url', None),
|
||||
auth_method=options.pop('auth_method', 'password'),
|
||||
|
|
|
@ -79,7 +79,7 @@ DEFAULT_PARAMS = {
|
|||
'overwrite': False, 'incremental': None,
|
||||
'consistency_check': False, 'consistency_checksum': None,
|
||||
'nova_restore_network': None, 'cindernative_backup_id': None,
|
||||
'sync': True, 'engine_name': 'tar', 'timeout': 120
|
||||
'sync': True, 'engine_name': 'tar', 'timeout': 120, 'project_id': None,
|
||||
}
|
||||
|
||||
_COMMON = [
|
||||
|
@ -378,6 +378,11 @@ _COMMON = [
|
|||
default=DEFAULT_PARAMS['nova_inst_id'],
|
||||
help="Id of nova instance for backup"
|
||||
),
|
||||
cfg.StrOpt('project-id',
|
||||
dest='project_id',
|
||||
default=DEFAULT_PARAMS['project_id'],
|
||||
help="Id of project for backup"
|
||||
),
|
||||
cfg.StrOpt('sql-server-conf',
|
||||
dest='sql_server_conf',
|
||||
default=DEFAULT_PARAMS['sql_server_conf'],
|
||||
|
@ -473,7 +478,7 @@ _COMMON = [
|
|||
"used with any operation running with freezer and after "
|
||||
"this time it will raise a TimeOut Exception. Default is"
|
||||
" {0}".format(DEFAULT_PARAMS['timeout'])
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -599,14 +604,13 @@ def get_backup_args():
|
|||
backup_args.__dict__['windows_volume'] = \
|
||||
backup_args.path_to_backup[:3]
|
||||
|
||||
# TODO(enugaev): move it to new command line param backup_media
|
||||
|
||||
backup_media = 'fs'
|
||||
if backup_args.cinder_vol_id:
|
||||
backup_media = 'cinder'
|
||||
elif backup_args.cindernative_vol_id or backup_args.cindernative_backup_id:
|
||||
backup_media = 'cindernative'
|
||||
elif backup_args.nova_inst_id:
|
||||
elif backup_args.engine_name == 'nova' and (backup_args.project_id or
|
||||
backup_args.nova_inst_id):
|
||||
backup_media = 'nova'
|
||||
|
||||
backup_args.__dict__['backup_media'] = backup_media
|
||||
|
|
|
@ -183,7 +183,7 @@ class BackupEngine(object):
|
|||
"Engine error. Failed to backup.")
|
||||
|
||||
with open(freezer_meta, mode='wb') as b_file:
|
||||
b_file.write(json.dumps(self.metadata()))
|
||||
b_file.write(json.dumps(self.metadata(backup_resource)))
|
||||
self.storage.put_metadata(engine_meta, freezer_meta, backup)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
@ -312,5 +312,5 @@ class BackupEngine(object):
|
|||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def metadata(self):
|
||||
def metadata(self, backup_resource):
|
||||
pass
|
||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
|
||||
"""
|
||||
from concurrent import futures
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
@ -50,11 +52,38 @@ class NovaEngine(engine.BackupEngine):
|
|||
except EOFError:
|
||||
pass
|
||||
|
||||
def restore_nova_tenant(self, project_id, hostname_backup_name,
|
||||
overwrite, recent_to_date):
|
||||
# Load info about tenant instances in swift
|
||||
if self.storage._type == 'swift':
|
||||
swift_connection = self.client.create_swift()
|
||||
headers, data = swift_connection.get_object(
|
||||
self.storage.storage_path,
|
||||
"project_" + project_id)
|
||||
elif self.storage._type in ['local', 'ssh']:
|
||||
backup_basepath = os.path.join(self.storage.storage_path,
|
||||
'project_' + project_id)
|
||||
with self.storage.open(backup_basepath, 'rb') as backup_file:
|
||||
data = backup_file.readline()
|
||||
|
||||
instance_ids = json.loads(data)
|
||||
for instance_id in instance_ids:
|
||||
LOG.info("Restore nova instance ID: {0} from container {1}".
|
||||
format(instance_id, self.storage.storage_path))
|
||||
backup_name = os.path.join(hostname_backup_name,
|
||||
instance_id)
|
||||
self.restore(
|
||||
hostname_backup_name=backup_name,
|
||||
restore_resource=instance_id,
|
||||
overwrite=overwrite,
|
||||
recent_to_date=recent_to_date)
|
||||
|
||||
def restore_level(self, restore_resource, read_pipe, backup, except_queue):
|
||||
try:
|
||||
metadata = backup.metadata()
|
||||
engine_metadata = backup.engine_metadata()
|
||||
server_info = metadata.get('server', {})
|
||||
length = int(server_info.get('length'))
|
||||
length = int(engine_metadata.get('length'))
|
||||
available_networks = server_info.get('addresses')
|
||||
nova_networks = self.nova.networks.findall()
|
||||
|
||||
|
@ -83,7 +112,6 @@ class NovaEngine(engine.BackupEngine):
|
|||
" active".format(image.id),
|
||||
kwargs={"glance_client": self.glance, "image_id": image.id}
|
||||
)
|
||||
|
||||
server = self.nova.servers.create(
|
||||
name=server_info.get('name'),
|
||||
flavor=server_info['flavor']['id'],
|
||||
|
@ -96,6 +124,45 @@ class NovaEngine(engine.BackupEngine):
|
|||
except_queue.put(e)
|
||||
raise
|
||||
|
||||
def backup_nova_tenant(self, project_id, hostname_backup_name,
|
||||
no_incremental, max_level, always_level,
|
||||
restart_always_level):
|
||||
instance_ids = [server.id for server in
|
||||
self.nova.servers.list(detailed=False)]
|
||||
data = json.dumps(instance_ids)
|
||||
LOG.info("Saving information about instances {0}".format(data))
|
||||
|
||||
if self.storage._type == 'swift':
|
||||
swift_connection = self.client.create_swift()
|
||||
swift_connection.put_object(self.storage.storage_path,
|
||||
"project_{0}".format(project_id),
|
||||
data)
|
||||
elif self.storage._type in ['local', 'ssh']:
|
||||
backup_basepath = os.path.join(self.storage.storage_path,
|
||||
"project_" + project_id)
|
||||
with self.storage.open(backup_basepath, 'wb') as backup_file:
|
||||
backup_file.write(data)
|
||||
|
||||
executor = futures.ThreadPoolExecutor(
|
||||
max_workers=len(instance_ids))
|
||||
futures_list = []
|
||||
for instance_id in instance_ids:
|
||||
LOG.info("Backup nova instance ID: {0} to container {1}".
|
||||
format(instance_id, self.storage.storage_path))
|
||||
backup_name = os.path.join(hostname_backup_name,
|
||||
instance_id)
|
||||
|
||||
futures_list.append(executor.submit(
|
||||
self.backup,
|
||||
backup_resource=instance_id,
|
||||
hostname_backup_name=backup_name,
|
||||
no_incremental=no_incremental,
|
||||
max_level=max_level,
|
||||
always_level=always_level,
|
||||
restart_always_level=restart_always_level))
|
||||
|
||||
futures.wait(futures_list, CONF.timeout)
|
||||
|
||||
def backup_data(self, backup_resource, manifest_path):
|
||||
server = self.nova.servers.get(backup_resource)
|
||||
if not server:
|
||||
|
@ -145,8 +212,6 @@ class NovaEngine(engine.BackupEngine):
|
|||
|
||||
LOG.info("Deleting temporary image {0}".format(image.id))
|
||||
self.glance.images.delete(image.id)
|
||||
self.server_info = server.to_dict()
|
||||
self.server_info['length'] = len(stream)
|
||||
|
||||
@staticmethod
|
||||
def image_active(glance_client, image_id):
|
||||
|
@ -154,14 +219,20 @@ class NovaEngine(engine.BackupEngine):
|
|||
image = glance_client.images.get(image_id)
|
||||
return image.status == 'active'
|
||||
|
||||
def metadata(self):
|
||||
def metadata(self, backup_resource):
|
||||
"""Construct metadata"""
|
||||
server_info = self.nova.servers.get(backup_resource).to_dict()
|
||||
|
||||
return {
|
||||
"engine_name": self.name,
|
||||
"server": self.server_info
|
||||
"server": server_info,
|
||||
}
|
||||
|
||||
def set_tenant_meta(self, path, metadata):
|
||||
"""push data to the manifest file"""
|
||||
with open(path, 'wb') as fb:
|
||||
fb.writelines(json.dumps(metadata))
|
||||
|
||||
def get_tenant_meta(self, path):
|
||||
with open(path, 'rb') as fb:
|
||||
json.loads(fb.read())
|
||||
|
|
|
@ -68,7 +68,7 @@ class RsyncEngine(engine.BackupEngine):
|
|||
def name(self):
|
||||
return "rsync"
|
||||
|
||||
def metadata(self):
|
||||
def metadata(self, *args):
|
||||
return {
|
||||
"engine_name": self.name,
|
||||
"compression": self.compression_algo,
|
||||
|
|
|
@ -51,7 +51,7 @@ class TarEngine(engine.BackupEngine):
|
|||
def name(self):
|
||||
return "tar"
|
||||
|
||||
def metadata(self):
|
||||
def metadata(self, *args):
|
||||
return {
|
||||
"engine_name": self.name,
|
||||
"compression": self.compression_algo,
|
||||
|
|
|
@ -239,17 +239,28 @@ class BackupJob(Job):
|
|||
self.storage)
|
||||
|
||||
if backup_media == 'nova':
|
||||
LOG.info('Executing nova backup. Instance ID: {0}'.format(
|
||||
self.conf.nova_inst_id))
|
||||
hostname_backup_name = os.path.join(self.conf.hostname_backup_name,
|
||||
self.conf.nova_inst_id)
|
||||
return self.engine.backup(
|
||||
backup_resource=self.conf.nova_inst_id,
|
||||
hostname_backup_name=hostname_backup_name,
|
||||
no_incremental=self.conf.no_incremental,
|
||||
max_level=self.conf.max_level,
|
||||
always_level=self.conf.always_level,
|
||||
restart_always_level=self.conf.restart_always_level)
|
||||
if self.conf.project_id:
|
||||
return self.engine.backup_nova_tenant(
|
||||
project_id=self.conf.project_id,
|
||||
hostname_backup_name=self.conf.hostname_backup_name,
|
||||
no_incremental=self.conf.no_incremental,
|
||||
max_level=self.conf.max_level,
|
||||
always_level=self.conf.always_level,
|
||||
restart_always_level=self.conf.restart_always_level)
|
||||
else:
|
||||
LOG.info('Executing nova backup. Instance ID: {0}'.format(
|
||||
self.conf.nova_inst_id))
|
||||
hostname_backup_name = os.path.join(
|
||||
self.conf.hostname_backup_name,
|
||||
self.conf.nova_inst_id)
|
||||
return self.engine.backup(
|
||||
backup_resource=self.conf.nova_inst_id,
|
||||
hostname_backup_name=hostname_backup_name,
|
||||
no_incremental=self.conf.no_incremental,
|
||||
max_level=self.conf.max_level,
|
||||
always_level=self.conf.always_level,
|
||||
restart_always_level=self.conf.restart_always_level)
|
||||
|
||||
elif backup_media == 'cindernative':
|
||||
LOG.info('Executing cinder native backup. Volume ID: {0}, '
|
||||
'incremental: {1}'.format(self.conf.cindernative_vol_id,
|
||||
|
@ -269,9 +280,11 @@ class BackupJob(Job):
|
|||
class RestoreJob(Job):
|
||||
|
||||
def _validate(self):
|
||||
if not self.conf.restore_abs_path and not self.conf.nova_inst_id \
|
||||
and not self.conf.cinder_vol_id and not \
|
||||
self.conf.cindernative_vol_id:
|
||||
if not any([self.conf.restore_abs_path,
|
||||
self.conf.nova_inst_id,
|
||||
self.conf.cinder_vol_id,
|
||||
self.conf.cindernative_vol_id,
|
||||
self.conf.project_id]):
|
||||
raise ValueError("--restore-abs-path is required")
|
||||
if not self.conf.container:
|
||||
raise ValueError("--container is required")
|
||||
|
@ -316,19 +329,27 @@ class RestoreJob(Job):
|
|||
res = restore.RestoreOs(conf.client_manager, conf.container,
|
||||
self.storage)
|
||||
if conf.backup_media == 'nova':
|
||||
LOG.info("Restoring nova backup. Instance ID: {0}, timestamp: {1} "
|
||||
"network-id {2}".format(conf.nova_inst_id,
|
||||
restore_timestamp,
|
||||
conf.nova_restore_network))
|
||||
hostname_backup_name = os.path.join(self.conf.hostname_backup_name,
|
||||
self.conf.nova_inst_id)
|
||||
self.engine.restore(
|
||||
hostname_backup_name=hostname_backup_name,
|
||||
restore_resource=conf.nova_inst_id,
|
||||
overwrite=conf.overwrite,
|
||||
recent_to_date=restore_timestamp)
|
||||
# res.restore_nova(conf.nova_inst_id, restore_timestamp,
|
||||
# conf.nova_restore_network)
|
||||
if self.conf.project_id:
|
||||
return self.engine.restore_nova_tenant(
|
||||
project_id=self.conf.project_id,
|
||||
hostname_backup_name=self.conf.hostname_backup_name,
|
||||
overwrite=conf.overwrite,
|
||||
recent_to_date=restore_timestamp)
|
||||
else:
|
||||
LOG.info("Restoring nova backup. Instance ID: {0}, "
|
||||
"timestamp: {1} network-id {2}".format(
|
||||
conf.nova_inst_id,
|
||||
restore_timestamp,
|
||||
conf.nova_restore_network))
|
||||
hostname_backup_name = os.path.join(
|
||||
self.conf.hostname_backup_name,
|
||||
self.conf.nova_inst_id)
|
||||
self.engine.restore(
|
||||
hostname_backup_name=hostname_backup_name,
|
||||
restore_resource=conf.nova_inst_id,
|
||||
overwrite=conf.overwrite,
|
||||
recent_to_date=restore_timestamp)
|
||||
|
||||
elif conf.backup_media == 'cinder':
|
||||
LOG.info("Restoring cinder backup from glance. Volume ID: {0}, "
|
||||
"timestamp: {1}".format(conf.cinder_vol_id,
|
||||
|
|
|
@ -278,11 +278,17 @@ class Backup(object):
|
|||
level=int(increment[0])
|
||||
) for increment in increments}
|
||||
|
||||
def _get_file(self, filename):
|
||||
file = tempfile.NamedTemporaryFile('wb', delete=True)
|
||||
self.storage.get_file(filename, file.name)
|
||||
with open(file.name) as f:
|
||||
content = f.readlines()
|
||||
LOG.info("Content download {0}".format(content))
|
||||
file.close()
|
||||
return json.loads(content[0])
|
||||
|
||||
def metadata(self):
|
||||
metadata_file = tempfile.NamedTemporaryFile('wb', delete=True)
|
||||
self.storage.get_file(self.metadata_path, metadata_file.name)
|
||||
with open(metadata_file.name) as f:
|
||||
metadata_content = f.readlines()
|
||||
LOG.info("metadata content download {0}".format(metadata_content))
|
||||
metadata_file.close()
|
||||
return json.loads(metadata_content[0])
|
||||
return self._get_file(self.metadata_path)
|
||||
|
||||
def engine_metadata(self):
|
||||
return self._get_file(self.engine_metadata_path)
|
||||
|
|
|
@ -321,6 +321,7 @@ class BackupOpt1(object):
|
|||
self.max_level = '20'
|
||||
self.encrypt_pass_file = '/dev/random'
|
||||
self.always_level = '20'
|
||||
self.overwrite = False
|
||||
self.remove_from_date = '2014-12-03T23:23:23'
|
||||
self.restart_always_level = 100000
|
||||
self.restore_abs_path = '/tmp'
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
# (C) Copyright 2017 Mirantis, Inc.
|
||||
#
|
||||
# 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
|
||||
import mock
|
||||
import os
|
||||
|
||||
import ddt
|
||||
|
||||
from freezer.engine.nova import nova
|
||||
from freezer.tests import commons
|
||||
|
||||
|
||||
class FakeServer(object):
|
||||
def __init__(self, instance_id):
|
||||
self.id = instance_id
|
||||
|
||||
|
||||
class FakeContextManager(object):
|
||||
def __init__(self, mock_file):
|
||||
self.mock_file = mock_file
|
||||
|
||||
def __enter__(self):
|
||||
return self.mock_file
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class TestNovaEngine(commons.FreezerBaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestNovaEngine, self).setUp()
|
||||
self.backup_opt = commons.BackupOpt1()
|
||||
self.project_id = "test-project-id"
|
||||
self.instance_ids = ["instance-id-1", "instance-id-2", "instance-id-3"]
|
||||
self.instance_ids_str = json.dumps(self.instance_ids)
|
||||
servers_list = [FakeServer(instance_id) for instance_id in
|
||||
self.instance_ids]
|
||||
|
||||
self.mock_nova = mock.MagicMock()
|
||||
self.mock_nova.servers.list = mock.Mock(return_value=servers_list)
|
||||
self.backup_opt.client_manager = mock.MagicMock()
|
||||
self.backup_opt.client_manager.get_nova.return_value = self.mock_nova
|
||||
self.expected_backup_calls = [
|
||||
mock.call(
|
||||
backup_resource=inst_id,
|
||||
hostname_backup_name=os.path.join(self.backup_opt.backup_name,
|
||||
inst_id),
|
||||
no_incremental=self.backup_opt.no_incremental,
|
||||
max_level=self.backup_opt.max_level,
|
||||
always_level=self.backup_opt.always_level,
|
||||
restart_always_level=self.backup_opt.restart_always_level)
|
||||
for inst_id in self.instance_ids]
|
||||
self.expected_restore_calls = [
|
||||
mock.call(
|
||||
hostname_backup_name=os.path.join(self.backup_opt.backup_name,
|
||||
inst_id),
|
||||
restore_resource=inst_id,
|
||||
overwrite=self.backup_opt.overwrite,
|
||||
recent_to_date='test_timestamp')
|
||||
for inst_id in self.instance_ids]
|
||||
|
||||
|
||||
class TestNovaEngineSwiftStorage(TestNovaEngine):
|
||||
def setUp(self):
|
||||
super(TestNovaEngineSwiftStorage, self).setUp()
|
||||
self.mock_swift_connection = mock.MagicMock()
|
||||
self.mock_swift_connection.get_object.return_value = (
|
||||
None, self.instance_ids_str)
|
||||
self.mock_swift_connection.put_object = mock.MagicMock()
|
||||
|
||||
self.mock_swift_storage = mock.MagicMock()
|
||||
self.mock_swift_storage._type = 'swift'
|
||||
|
||||
self.engine = nova.NovaEngine(self.mock_swift_storage)
|
||||
self.engine.client = self.backup_opt.client_manager
|
||||
self.engine.client.create_swift = mock.Mock(
|
||||
return_value=self.mock_swift_connection)
|
||||
self.engine.backup = mock.Mock()
|
||||
self.engine.restore = mock.Mock()
|
||||
self.engine.nova = self.mock_nova
|
||||
|
||||
def test_backup_nova_tenant_to_swift_storage(self):
|
||||
self.engine.backup_nova_tenant(self.project_id,
|
||||
self.backup_opt.backup_name,
|
||||
self.backup_opt.no_incremental,
|
||||
self.backup_opt.max_level,
|
||||
self.backup_opt.always_level,
|
||||
self.backup_opt.restart_always_level)
|
||||
|
||||
self.mock_nova.servers.list.assert_called_once_with(detailed=False)
|
||||
self.engine.client.create_swift.assert_called_once()
|
||||
self.mock_swift_connection.put_object.assert_called_with(
|
||||
self.mock_swift_storage.storage_path,
|
||||
"project_test-project-id",
|
||||
self.instance_ids_str)
|
||||
self.engine.backup.assert_has_calls(self.expected_backup_calls)
|
||||
|
||||
def test_restore_nova_tenant_from_swift_storage(self):
|
||||
self.engine.restore_nova_tenant(self.project_id,
|
||||
self.backup_opt.backup_name,
|
||||
self.backup_opt.overwrite,
|
||||
'test_timestamp')
|
||||
|
||||
self.engine.client.create_swift.assert_called_once()
|
||||
self.mock_swift_connection.get_object.assert_called_with(
|
||||
self.mock_swift_storage.storage_path,
|
||||
"project_test-project-id")
|
||||
self.engine.restore.assert_has_calls(self.expected_restore_calls)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestNovaEngineFSLikeStorage(TestNovaEngine):
|
||||
def setUp(self):
|
||||
super(TestNovaEngineFSLikeStorage, self).setUp()
|
||||
self.mock_file = mock.Mock()
|
||||
self.mock_file.readline = mock.Mock(return_value=self.instance_ids_str)
|
||||
self.mock_file.write = mock.Mock()
|
||||
|
||||
self.mock_fslike_storage = mock.MagicMock()
|
||||
self.mock_fslike_storage.open = mock.Mock(
|
||||
return_value=FakeContextManager(self.mock_file))
|
||||
self.mock_fslike_storage.storage_path = 'test/storage/path'
|
||||
self.local_backup_file = os.path.join(
|
||||
self.mock_fslike_storage.storage_path,
|
||||
"project_test-project-id")
|
||||
|
||||
self.engine = nova.NovaEngine(self.mock_fslike_storage)
|
||||
self.engine.client = self.backup_opt.client_manager
|
||||
self.engine.backup = mock.Mock()
|
||||
self.engine.restore = mock.Mock()
|
||||
self.engine.nova = self.mock_nova
|
||||
|
||||
@ddt.data('local', 'ssh')
|
||||
def test_backup_nova_tenant_to_fslike_storage(self,
|
||||
storage_type):
|
||||
self.mock_fslike_storage._type = storage_type
|
||||
self.engine.backup_nova_tenant(self.project_id,
|
||||
self.backup_opt.backup_name,
|
||||
self.backup_opt.no_incremental,
|
||||
self.backup_opt.max_level,
|
||||
self.backup_opt.always_level,
|
||||
self.backup_opt.restart_always_level)
|
||||
|
||||
self.mock_nova.servers.list.assert_called_once_with(detailed=False)
|
||||
self.mock_fslike_storage.open.assert_called_once_with(
|
||||
self.local_backup_file,
|
||||
'wb')
|
||||
self.mock_file.write.assert_called_once_with(self.instance_ids_str)
|
||||
self.engine.backup.assert_has_calls(self.expected_backup_calls)
|
||||
|
||||
@ddt.data('local', 'ssh')
|
||||
def test_restore_nova_tenant_from_fslike_storage(self,
|
||||
storage_type):
|
||||
self.mock_fslike_storage._type = storage_type
|
||||
self.engine.restore_nova_tenant(self.project_id,
|
||||
self.backup_opt.backup_name,
|
||||
self.backup_opt.overwrite,
|
||||
'test_timestamp')
|
||||
|
||||
self.mock_fslike_storage.open.assert_called_once_with(
|
||||
self.local_backup_file,
|
||||
'rb')
|
||||
self.mock_file.readline.assert_called_once_with()
|
||||
self.engine.restore.assert_has_calls(self.expected_restore_calls)
|
|
@ -4,6 +4,7 @@
|
|||
flake8<2.6.0,>=2.5.4 # MIT
|
||||
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
|
||||
coverage>=4.0 # Apache-2.0
|
||||
ddt>=1.0.1 # MIT
|
||||
mock>=2.0 # BSD
|
||||
pylint==1.4.5 # GPLv2
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
|
|
Loading…
Reference in New Issue