6ee7901df9
Infra team has indicated that tearDown should not be used and should be replaced with addCleanup in all places. All addCleanup methods will be executed even if one of them fails, while a failure in tearDown method can leave the rest of the tearDown un-executed, which can leave stale state laying around. Moreover, tearDown methods won't run if an exception raises in setUp method, while addCleanup will run in such case. This patch replaces tearDown with addCleanup or removes redundant tearDown methods in cinder unit tests. Implements blueprint replace-teardown-with-addcleanup Change-Id: I6947eafa419ed7dda53582484090cd5210274f73
354 lines
12 KiB
Python
354 lines
12 KiB
Python
# Copyright 2013 IBM Corp
|
|
# 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.
|
|
#
|
|
"""
|
|
Tests for volume backup to IBM Tivoli Storage Manager (TSM).
|
|
"""
|
|
|
|
import datetime
|
|
import json
|
|
import os
|
|
import posix
|
|
|
|
from cinder.backup.drivers import tsm
|
|
from cinder import context
|
|
from cinder import db
|
|
from cinder import exception
|
|
from cinder.openstack.common import log as logging
|
|
from cinder.openstack.common import processutils as putils
|
|
from cinder import test
|
|
from cinder import utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
SIM = None
|
|
VOLUME_PATH = '/dev/null'
|
|
|
|
|
|
class TSMBackupSimulator:
|
|
"""Simulates TSM dsmc command.
|
|
|
|
The simulator simulates the execution of the 'dsmc' command.
|
|
This allows the TSM backup test to succeed even if TSM is not installed.
|
|
"""
|
|
def __init__(self):
|
|
self._backup_list = {}
|
|
self._hardlinks = []
|
|
self._next_cmd_error = {
|
|
'backup': '',
|
|
}
|
|
self._intro_msg = ('IBM Tivoli Storage Manager\n'
|
|
'Command Line Backup-Archive Client Interface\n'
|
|
'...\n\n')
|
|
|
|
def _cmd_backup(self, **kwargs):
|
|
# simulates the execution of the dsmc backup command
|
|
ret_msg = self._intro_msg
|
|
path = kwargs['path']
|
|
|
|
ret_msg += ('Image backup of volume \'%s\'\n\n'
|
|
'Total number of objects inspected: 1\n'
|
|
% path)
|
|
|
|
if self._next_cmd_error['backup'] == 'fail':
|
|
ret_msg += ('ANS1228E Sending of object \'%s\' '
|
|
'failed\n' % path)
|
|
ret_msg += ('ANS1063E The specified path is not a valid file '
|
|
'system or logical volume name.')
|
|
self._next_cmd_error['backup'] = ''
|
|
retcode = 12
|
|
else:
|
|
ret_msg += 'Total number of objects backed up: 1'
|
|
if path not in self._backup_list:
|
|
self._backup_list[path] = []
|
|
else:
|
|
self._backup_list[path][-1]['active'] = False
|
|
date = datetime.datetime.now()
|
|
datestr = date.strftime("%m/%d/%Y %H:%M:%S")
|
|
self._backup_list[path].append({'date': datestr, 'active': True})
|
|
retcode = 0
|
|
|
|
return (ret_msg, '', retcode)
|
|
|
|
def _backup_exists(self, path):
|
|
if path not in self._backup_list:
|
|
return ('ANS4000E Error processing \'%s\': file space does '
|
|
'not exist.' % path)
|
|
|
|
return 'OK'
|
|
|
|
def _cmd_restore(self, **kwargs):
|
|
|
|
ret_msg = self._intro_msg
|
|
path = kwargs['path']
|
|
exists = self._backup_exists(path)
|
|
|
|
if exists == 'OK':
|
|
ret_msg += ('Total number of objects restored: 1\n'
|
|
'Total number of objects failed: 0')
|
|
retcode = 0
|
|
else:
|
|
ret_msg += exists
|
|
retcode = 12
|
|
|
|
return (ret_msg, '', retcode)
|
|
|
|
def _cmd_delete(self, **kwargs):
|
|
# simulates the execution of the dsmc delete command
|
|
ret_msg = self._intro_msg
|
|
path = kwargs['path']
|
|
exists = self._backup_exists(path)
|
|
|
|
if exists == 'OK':
|
|
ret_msg += ('Total number of objects deleted: 1\n'
|
|
'Total number of objects failed: 0')
|
|
retcode = 0
|
|
index = len(self._backup_list[path]) - 1
|
|
del self._backup_list[path][index]
|
|
if not len(self._backup_list[path]):
|
|
del self._backup_list[path]
|
|
else:
|
|
ret_msg += exists
|
|
retcode = 12
|
|
|
|
return (ret_msg, '', retcode)
|
|
|
|
def _cmd_to_dict(self, arg_list):
|
|
"""Convert command for kwargs (assumes a properly formed command)."""
|
|
ret = {'cmd': arg_list[0],
|
|
'type': arg_list[1],
|
|
'path': arg_list[-1]}
|
|
|
|
for i in range(2, len(arg_list) - 1):
|
|
arg = arg_list[i].split('=')
|
|
if len(arg) == 1:
|
|
ret[arg[0]] = True
|
|
else:
|
|
ret[arg[0]] = arg[1]
|
|
|
|
return ret
|
|
|
|
def _exec_dsmc_cmd(self, cmd):
|
|
"""Simulates the execution of the dsmc command."""
|
|
cmd_switch = {'backup': self._cmd_backup,
|
|
'restore': self._cmd_restore,
|
|
'delete': self._cmd_delete}
|
|
|
|
kwargs = self._cmd_to_dict(cmd)
|
|
if kwargs['cmd'] != 'dsmc' or kwargs['type'] not in cmd_switch:
|
|
raise putils.ProcessExecutionError(exit_code=1,
|
|
stdout='',
|
|
stderr='Not dsmc command',
|
|
cmd=' '.join(cmd))
|
|
out, err, ret = cmd_switch[kwargs['type']](**kwargs)
|
|
return (out, err, ret)
|
|
|
|
def exec_cmd(self, cmd):
|
|
"""Simulates the execution of dsmc, rm, and ln commands."""
|
|
if cmd[0] == 'dsmc':
|
|
out, err, ret = self._exec_dsmc_cmd(cmd)
|
|
elif cmd[0] == 'ln':
|
|
dest = cmd[2]
|
|
out = ''
|
|
if dest in self._hardlinks:
|
|
err = ('ln: failed to create hard link `%s\': '
|
|
'File exists' % dest)
|
|
ret = 1
|
|
else:
|
|
self._hardlinks.append(dest)
|
|
err = ''
|
|
ret = 0
|
|
elif cmd[0] == 'rm':
|
|
dest = cmd[2]
|
|
out = ''
|
|
if dest not in self._hardlinks:
|
|
err = ('rm: cannot remove `%s\': No such file or '
|
|
'directory' % dest)
|
|
ret = 1
|
|
else:
|
|
index = self._hardlinks.index(dest)
|
|
del self._hardlinks[index]
|
|
err = ''
|
|
ret = 0
|
|
else:
|
|
raise putils.ProcessExecutionError(exit_code=1,
|
|
stdout='',
|
|
stderr='Unsupported command',
|
|
cmd=' '.join(cmd))
|
|
return (out, err, ret)
|
|
|
|
def error_injection(self, cmd, error):
|
|
self._next_cmd_error[cmd] = error
|
|
|
|
|
|
def fake_exec(*cmd, **kwargs):
|
|
# Support only bool
|
|
check_exit_code = kwargs.pop('check_exit_code', True)
|
|
global SIM
|
|
|
|
out, err, ret = SIM.exec_cmd(cmd)
|
|
if ret and check_exit_code:
|
|
raise putils.ProcessExecutionError(
|
|
exit_code=-1,
|
|
stdout=out,
|
|
stderr=err,
|
|
cmd=' '.join(cmd))
|
|
return (out, err)
|
|
|
|
|
|
def fake_stat_image(path):
|
|
# Simulate stat to return the mode of a block device
|
|
# make sure that st_mode (the first in the sequence(
|
|
# matches the mode of a block device
|
|
return posix.stat_result((25008, 5753, 5L, 1, 0, 6, 0,
|
|
1375881199, 1375881197, 1375881197))
|
|
|
|
|
|
def fake_stat_file(path):
|
|
# Simulate stat to return the mode of a block device
|
|
# make sure that st_mode (the first in the sequence(
|
|
# matches the mode of a block device
|
|
return posix.stat_result((33188, 5753, 5L, 1, 0, 6, 0,
|
|
1375881199, 1375881197, 1375881197))
|
|
|
|
|
|
def fake_stat_illegal(path):
|
|
# Simulate stat to return the mode of a block device
|
|
# make sure that st_mode (the first in the sequence(
|
|
# matches the mode of a block device
|
|
return posix.stat_result((17407, 5753, 5L, 1, 0, 6, 0,
|
|
1375881199, 1375881197, 1375881197))
|
|
|
|
|
|
class BackupTSMTestCase(test.TestCase):
|
|
def setUp(self):
|
|
super(BackupTSMTestCase, self).setUp()
|
|
global SIM
|
|
SIM = TSMBackupSimulator()
|
|
self.sim = SIM
|
|
self.ctxt = context.get_admin_context()
|
|
self.driver = tsm.TSMBackupDriver(self.ctxt)
|
|
self.stubs.Set(utils, 'execute', fake_exec)
|
|
self.stubs.Set(os, 'stat', fake_stat_image)
|
|
|
|
def _create_volume_db_entry(self, volume_id):
|
|
vol = {'id': volume_id,
|
|
'size': 1,
|
|
'status': 'available'}
|
|
return db.volume_create(self.ctxt, vol)['id']
|
|
|
|
def _create_backup_db_entry(self, backup_id, mode):
|
|
if mode == 'file':
|
|
backup_path = VOLUME_PATH
|
|
else:
|
|
backup_path = '/dev/backup-%s' % backup_id
|
|
service_metadata = json.dumps({'backup_mode': mode,
|
|
'backup_path': backup_path})
|
|
backup = {'id': backup_id,
|
|
'size': 1,
|
|
'container': 'test-container',
|
|
'volume_id': '1234-5678-1234-8888',
|
|
'service_metadata': service_metadata}
|
|
return db.backup_create(self.ctxt, backup)['id']
|
|
|
|
def test_backup_image(self):
|
|
volume_id = '1234-5678-1234-7777'
|
|
mode = 'image'
|
|
self._create_volume_db_entry(volume_id)
|
|
|
|
backup_id1 = 123
|
|
backup_id2 = 456
|
|
backup_id3 = 666
|
|
self._create_backup_db_entry(backup_id1, mode)
|
|
self._create_backup_db_entry(backup_id2, mode)
|
|
self._create_backup_db_entry(backup_id3, mode)
|
|
|
|
with open(VOLUME_PATH, 'rw') as volume_file:
|
|
# Create two backups of the volume
|
|
backup1 = db.backup_get(self.ctxt, backup_id1)
|
|
self.driver.backup(backup1, volume_file)
|
|
backup2 = db.backup_get(self.ctxt, backup_id2)
|
|
self.driver.backup(backup2, volume_file)
|
|
|
|
# Create a backup that fails
|
|
fail_back = db.backup_get(self.ctxt, backup_id3)
|
|
self.sim.error_injection('backup', 'fail')
|
|
self.assertRaises(exception.InvalidBackup,
|
|
self.driver.backup, fail_back, volume_file)
|
|
|
|
# Try to restore one, then the other
|
|
self.driver.restore(backup1, volume_id, volume_file)
|
|
self.driver.restore(backup2, volume_id, volume_file)
|
|
|
|
# Delete both backups
|
|
self.driver.delete(backup2)
|
|
self.driver.delete(backup1)
|
|
|
|
def test_backup_file(self):
|
|
volume_id = '1234-5678-1234-8888'
|
|
mode = 'file'
|
|
self.stubs.Set(os, 'stat', fake_stat_file)
|
|
self._create_volume_db_entry(volume_id)
|
|
|
|
backup_id1 = 123
|
|
backup_id2 = 456
|
|
self._create_backup_db_entry(backup_id1, mode)
|
|
self._create_backup_db_entry(backup_id2, mode)
|
|
|
|
with open(VOLUME_PATH, 'rw') as volume_file:
|
|
# Create two backups of the volume
|
|
backup1 = db.backup_get(self.ctxt, 123)
|
|
self.driver.backup(backup1, volume_file)
|
|
backup2 = db.backup_get(self.ctxt, 456)
|
|
self.driver.backup(backup2, volume_file)
|
|
|
|
# Create a backup that fails
|
|
self._create_backup_db_entry(666, mode)
|
|
fail_back = db.backup_get(self.ctxt, 666)
|
|
self.sim.error_injection('backup', 'fail')
|
|
self.assertRaises(exception.InvalidBackup,
|
|
self.driver.backup, fail_back, volume_file)
|
|
|
|
# Try to restore one, then the other
|
|
self.driver.restore(backup1, volume_id, volume_file)
|
|
self.driver.restore(backup2, volume_id, volume_file)
|
|
|
|
# Delete both backups
|
|
self.driver.delete(backup1)
|
|
self.driver.delete(backup2)
|
|
|
|
def test_backup_invalid_mode(self):
|
|
volume_id = '1234-5678-1234-9999'
|
|
mode = 'illegal'
|
|
self.stubs.Set(os, 'stat', fake_stat_illegal)
|
|
self._create_volume_db_entry(volume_id)
|
|
|
|
backup_id1 = 123
|
|
self._create_backup_db_entry(backup_id1, mode)
|
|
|
|
with open(VOLUME_PATH, 'rw') as volume_file:
|
|
# Create two backups of the volume
|
|
backup1 = db.backup_get(self.ctxt, 123)
|
|
self.assertRaises(exception.InvalidBackup,
|
|
self.driver.backup, backup1, volume_file)
|
|
|
|
self.assertRaises(exception.InvalidBackup,
|
|
self.driver.restore,
|
|
backup1,
|
|
volume_id,
|
|
volume_file)
|
|
|
|
self.assertRaises(exception.InvalidBackup,
|
|
self.driver.delete, backup1)
|