From d65d86e15c7f50b2a5b7ad1422771601479c632f Mon Sep 17 00:00:00 2001 From: Bill Owen Date: Thu, 13 Feb 2014 14:55:47 -0700 Subject: [PATCH] TSM backup driver changes to support file backup Extend TSM backup driver to support backup of file based volumes, as well as restore and delete of the backups. This in addition to the current block based volume support. This involves determining the type of volume this is requested to be backed up, and modifying the dsmc command options accordingly. When a volume is backed up, the backup mode and backup path are saved in the backup service_metadata for use on restore and delete operations. This commit includes some further refactoring based on pylint feedback and system testing. Implements: blueprint tsm-backup-driver-enhancement Change-Id: Ic994a8c2f7302e59358dd858b50df746c9a7d76f --- cinder/backup/drivers/tsm.py | 564 ++++++++++++++++++-------------- cinder/tests/test_backup_tsm.py | 160 ++++++--- 2 files changed, 450 insertions(+), 274 deletions(-) diff --git a/cinder/backup/drivers/tsm.py b/cinder/backup/drivers/tsm.py index c871360df..a6d368ba0 100644 --- a/cinder/backup/drivers/tsm.py +++ b/cinder/backup/drivers/tsm.py @@ -17,13 +17,15 @@ Implementation of a backup service that uses IBM Tivoli Storage Manager (TSM) as the backend. The driver uses TSM command line dsmc utility to -run an image backup and restore. -This version supports backup of block devices, e.g, FC, iSCSI, local. +run the backup and restore operations. +This version supports backup of block devices, e.g, FC, iSCSI, local as well as +regular files. A prerequisite for using the IBM TSM backup service is configuring the Cinder host for using TSM. """ +import json import os import stat @@ -37,7 +39,7 @@ from cinder import utils LOG = logging.getLogger(__name__) -tsmbackup_service_opts = [ +tsm_opts = [ cfg.StrOpt('backup_tsm_volume_prefix', default='backup', help='Volume prefix for the backup id when backing up to TSM'), @@ -50,96 +52,250 @@ tsmbackup_service_opts = [ ] CONF = cfg.CONF -CONF.register_opts(tsmbackup_service_opts) +CONF.register_opts(tsm_opts) +VALID_BACKUP_MODES = ['image', 'file'] -class TSMBackupDriver(BackupDriver): - """Provides backup, restore and delete of volumes backup for TSM.""" - - DRIVER_VERSION = '1.0.0' - - def __init__(self, context, db_driver=None): - super(TSMBackupDriver, self).__init__(context, db_driver) - self.tsm_password = CONF.backup_tsm_password - self.volume_prefix = CONF.backup_tsm_volume_prefix - - def _make_link(self, volume_path, backup_path, vol_id): - """Create a hard link for the volume block device. - The IBM TSM client performs an image backup on a block device. - The name of the block device is the backup prefix plus the backup id - - :param volume_path: real device path name for volume - :param backup_path: path name TSM will use as volume to backup - :param vol_id: id of volume to backup (for reporting) - - :raises: InvalidBackup - """ +def _get_backup_metadata(backup, operation): + """Return metadata persisted with backup object.""" + svc_metadata = backup['service_metadata'] + try: + svc_dict = json.loads(svc_metadata) + backup_path = svc_dict.get('backup_path') + backup_mode = svc_dict.get('backup_mode') + except TypeError: + # for backwards compatibility + vol_prefix = CONF.backup_tsm_volume_prefix + backup_id = backup['id'] + backup_path = utils.make_dev_path('%s-%s' % + (vol_prefix, backup_id)) + backup_mode = 'image' - try: - utils.execute('ln', volume_path, backup_path, - run_as_root=True, - check_exit_code=True) - except processutils.ProcessExecutionError as e: - err = (_('backup: %(vol_id)s Failed to create device hardlink ' - 'from %(vpath)s to %(bpath)s.\n' - 'stdout: %(out)s\n stderr: %(err)s') - % {'vol_id': vol_id, - 'vpath': volume_path, - 'bpath': backup_path, - 'out': e.stdout, - 'err': e.stderr}) + if backup_mode not in VALID_BACKUP_MODES: + volume_id = backup['volume_id'] + backup_id = backup['id'] + err = (_('%(op)s: backup %(bck_id)s, volume %(vol_id)s failed. ' + 'Backup object has unexpected mode. Image or file ' + 'backups supported, actual mode is %(vol_mode)s.') + % {'op': operation, + 'bck_id': backup_id, + 'vol_id': volume_id, + 'vol_mode': backup_mode}) + LOG.error(err) + raise exception.InvalidBackup(reason=err) + return backup_path, backup_mode + + +def _image_mode(backup_mode): + """True if backup is image type.""" + return backup_mode == 'image' + + +def _make_link(volume_path, backup_path, vol_id): + """Create a hard link for the volume block device. + + The IBM TSM client performs an image backup on a block device. + The name of the block device is the backup prefix plus the backup id + + :param volume_path: real device path name for volume + :param backup_path: path name TSM will use as volume to backup + :param vol_id: id of volume to backup (for reporting) + + :raises: InvalidBackup + """ + + try: + utils.execute('ln', volume_path, backup_path, + run_as_root=True, + check_exit_code=True) + except processutils.ProcessExecutionError as exc: + err = (_('backup: %(vol_id)s failed to create device hardlink ' + 'from %(vpath)s to %(bpath)s.\n' + 'stdout: %(out)s\n stderr: %(err)s') + % {'vol_id': vol_id, + 'vpath': volume_path, + 'bpath': backup_path, + 'out': exc.stdout, + 'err': exc.stderr}) + LOG.error(err) + raise exception.InvalidBackup(reason=err) + + +def _create_unique_device_link(backup_id, volume_path, volume_id, bckup_mode): + """Create a consistent hardlink for the volume block device. + + Create a consistent hardlink using the backup id so TSM + will be able to backup and restore to the same block device. + + :param backup_id: the backup id + :param volume_path: real path of the backup/restore device + :param volume_id: Volume id for backup or as restore target + :param bckup_mode: TSM backup mode, either 'image' or 'file' + :raises: InvalidBackup + :returns str -- hardlink path of the volume block device + """ + if _image_mode(bckup_mode): + hardlink_path = utils.make_dev_path('%s-%s' % + (CONF.backup_tsm_volume_prefix, + backup_id)) + else: + dir, volname = os.path.split(volume_path) + hardlink_path = ('%s/%s-%s' % + (dir, + CONF.backup_tsm_volume_prefix, + backup_id)) + _make_link(volume_path, hardlink_path, volume_id) + return hardlink_path + + +def _check_dsmc_output(output, check_attrs, exact_match=True): + """Check dsmc command line utility output. + + Parse the output of the dsmc command and make sure that a given + attribute is present, and that it has the proper value. + TSM attribute has the format of "text : value". + + :param output: TSM output to parse + :param check_attrs: text to identify in the output + :param exact_match: if True, the check will pass only if the parsed + value is equal to the value specified in check_attrs. If false, the + check will pass if the parsed value is greater than or equal to the + value specified in check_attrs. This is needed because for file + backups, the parent directories may also be included the first a + volume is backed up. + :returns bool -- indicate if requited output attribute found in output + """ + + parsed_attrs = {} + for line in output.split('\n'): + # parse TSM output: look for "msg : value + key, sep, val = line.partition(':') + if sep is not None and key is not None and len(val.strip()) > 0: + parsed_attrs[key] = val.strip() + + for ckey, cval in check_attrs.iteritems(): + if ckey not in parsed_attrs: + return False + elif exact_match and parsed_attrs[ckey] != cval: + return False + elif not exact_match and int(parsed_attrs[ckey]) < int(cval): + return False + + return True + + +def _get_volume_realpath(volume_file, volume_id): + """Get the real path for the volume block device. + + If the volume is not a block device or a regular file issue an + InvalidBackup exception. + + :param volume_file: file object representing the volume + :param volume_id: Volume id for backup or as restore target + :raises: InvalidBackup + :returns str -- real path of volume device + :returns str -- backup mode to be used + """ + + try: + # Get real path + volume_path = os.path.realpath(volume_file.name) + # Verify that path is a block device + volume_mode = os.stat(volume_path).st_mode + if stat.S_ISBLK(volume_mode): + backup_mode = 'image' + elif stat.S_ISREG(volume_mode): + backup_mode = 'file' + else: + err = (_('backup: %(vol_id)s failed. ' + '%(path)s is unexpected file type. Block or regular ' + 'files supported, actual file mode is %(vol_mode)s.') + % {'vol_id': volume_id, + 'path': volume_path, + 'vol_mode': volume_mode}) LOG.error(err) raise exception.InvalidBackup(reason=err) - def _check_dsmc_output(self, output, check_attrs): - """Check dsmc command line utility output. + except AttributeError: + err = (_('backup: %(vol_id)s failed. Cannot obtain real path ' + 'to volume at %(path)s.') + % {'vol_id': volume_id, + 'path': volume_file}) + LOG.error(err) + raise exception.InvalidBackup(reason=err) + except OSError: + err = (_('backup: %(vol_id)s failed. ' + '%(path)s is not a file.') + % {'vol_id': volume_id, + 'path': volume_path}) + LOG.error(err) + raise exception.InvalidBackup(reason=err) + return volume_path, backup_mode + + +def _cleanup_device_hardlink(hardlink_path, volume_path, volume_id): + """Remove the hardlink for the volume block device. + + :param hardlink_path: hardlink to the volume block device + :param volume_path: real path of the backup/restore device + :param volume_id: Volume id for backup or as restore target + """ + + try: + utils.execute('rm', + '-f', + hardlink_path, + run_as_root=True) + except processutils.ProcessExecutionError as exc: + err = (_('backup: %(vol_id)s failed to remove backup hardlink' + ' from %(vpath)s to %(bpath)s.\n' + 'stdout: %(out)s\n stderr: %(err)s.') + % {'vol_id': volume_id, + 'vpath': volume_path, + 'bpath': hardlink_path, + 'out': exc.stdout, + 'err': exc.stderr}) + LOG.error(err) - Parse the output of the dsmc command and make sure that a given - attribute is present, and that it has the proper value. - TSM attribute has the format of "text : value". - :param output: TSM output to parse - :param check_attrs: text to identify in the output - :returns bool -- indicate if requited output attribute found in output - """ +class TSMBackupDriver(BackupDriver): + """Provides backup, restore and delete of volumes backup for TSM.""" - parsed_attrs = {} - for line in output.split('\n'): - # parse TSM output: look for "msg : value - key, sep, val = line.partition(':') - if (sep is not None and key is not None and len(val.strip()) > 0): - parsed_attrs[key] = val.strip() + DRIVER_VERSION = '1.0.0' - for k, v in check_attrs.iteritems(): - if k not in parsed_attrs or parsed_attrs[k] != v: - return False - return True + def __init__(self, context, db_driver=None): + super(TSMBackupDriver, self).__init__(context, db_driver) + self.tsm_password = CONF.backup_tsm_password + self.volume_prefix = CONF.backup_tsm_volume_prefix - def _do_backup(self, backup_path, vol_id): + def _do_backup(self, backup_path, vol_id, backup_mode): """Perform the actual backup operation. :param backup_path: volume path :param vol_id: volume id + :param backup_mode: file mode of source volume; 'image' or 'file' :raises: InvalidBackup """ backup_attrs = {'Total number of objects backed up': '1'} compr_flag = 'yes' if CONF.backup_tsm_compression else 'no' - out, err = utils.execute('dsmc', - 'backup', - 'image', - '-quiet', - '-compression=%s' % compr_flag, - '-password=%s' % CONF.backup_tsm_password, - backup_path, + backup_cmd = ['dsmc', 'backup'] + if _image_mode(backup_mode): + backup_cmd.append('image') + backup_cmd.extend(['-quiet', + '-compression=%s' % compr_flag, + '-password=%s' % self.tsm_password, + backup_path]) + + out, err = utils.execute(*backup_cmd, run_as_root=True, check_exit_code=False) - success = self._check_dsmc_output(out, backup_attrs) + success = _check_dsmc_output(out, backup_attrs, exact_match=False) if not success: - err = (_('backup: %(vol_id)s Failed to obtain backup ' + err = (_('backup: %(vol_id)s failed to obtain backup ' 'success notification from server.\n' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': vol_id, @@ -148,130 +304,51 @@ class TSMBackupDriver(BackupDriver): LOG.error(err) raise exception.InvalidBackup(reason=err) - def _do_restore(self, restore_path, vol_id): + def _do_restore(self, backup_path, restore_path, vol_id, backup_mode): """Perform the actual restore operation. - :param restore_path: volume path + :param backup_path: the path the backup was created from, this + identifes the backup to tsm + :param restore_path: volume path to restore into :param vol_id: volume id + :param backup_mode: mode used to create the backup ('image' or 'file') :raises: InvalidBackup """ restore_attrs = {'Total number of objects restored': '1'} - out, err = utils.execute('dsmc', - 'restore', - 'image', - '-quiet', - '-password=%s' % self.tsm_password, - '-noprompt', - restore_path, + restore_cmd = ['dsmc', 'restore'] + if _image_mode(backup_mode): + restore_cmd.append('image') + restore_cmd.append('-noprompt') # suppress prompt + else: + restore_cmd.append('-replace=yes') # suppress prompt + + restore_cmd.extend(['-quiet', + '-password=%s' % self.tsm_password, + backup_path]) + + if restore_path != backup_path: + restore_cmd.append(restore_path) + + out, err = utils.execute(*restore_cmd, run_as_root=True, check_exit_code=False) - success = self._check_dsmc_output(out, restore_attrs) + success = _check_dsmc_output(out, restore_attrs) if not success: - err = (_('restore: %(vol_id)s Failed.\n' - 'stdout: %(out)s\n stderr: %(err)s') + err = (_('restore: %(vol_id)s failed.\n' + 'stdout: %(out)s\n stderr: %(err)s.') % {'vol_id': vol_id, 'out': out, 'err': err}) LOG.error(err) raise exception.InvalidBackup(reason=err) - def _get_volume_realpath(self, volume_file, volume_id): - """Get the real path for the volume block device. - - If the volume is not a block device then issue an - InvalidBackup exception. - - :param volume_file: file object representing the volume - :param volume_id: Volume id for backup or as restore target - :raises: InvalidBackup - :returns str -- real path of volume device - """ - - try: - # Get real path - volume_path = os.path.realpath(volume_file.name) - # Verify that path is a block device - volume_mode = os.stat(volume_path).st_mode - if not stat.S_ISBLK(volume_mode): - err = (_('backup: %(vol_id)s Failed. ' - '%(path)s is not a block device.') - % {'vol_id': volume_id, - 'path': volume_path}) - LOG.error(err) - raise exception.InvalidBackup(reason=err) - except AttributeError as e: - err = (_('backup: %(vol_id)s Failed. Cannot obtain real path ' - 'to device %(path)s.') - % {'vol_id': volume_id, - 'path': volume_file}) - LOG.error(err) - raise exception.InvalidBackup(reason=err) - except OSError as e: - err = (_('backup: %(vol_id)s Failed. ' - '%(path)s is not a file.') - % {'vol_id': volume_id, - 'path': volume_path}) - LOG.error(err) - raise exception.InvalidBackup(reason=err) - return volume_path - - def _create_device_link_using_backupid(self, - backup_id, - volume_path, - volume_id): - """Create a consistent hardlink for the volume block device. - - Create a consistent hardlink using the backup id so TSM - will be able to backup and restore to the same block device. - - :param backup_id: the backup id - :param volume_path: real path of the backup/restore device - :param volume_id: Volume id for backup or as restore target - :raises: InvalidBackup - :returns str -- hardlink path of the volume block device - """ - - hardlink_path = utils.make_dev_path('%s-%s' % - (self.volume_prefix, - backup_id)) - self._make_link(volume_path, hardlink_path, volume_id) - return hardlink_path - - def _cleanup_device_hardlink(self, - hardlink_path, - volume_path, - volume_id): - """Remove the hardlink for the volume block device. - - :param hardlink_path: hardlink to the volume block device - :param volume_path: real path of the backup/restore device - :param volume_id: Volume id for backup or as restore target - """ - - try: - utils.execute('rm', - '-f', - hardlink_path, - run_as_root=True) - except processutils.ProcessExecutionError as e: - err = (_('backup: %(vol_id)s Failed to remove backup hardlink' - ' from %(vpath)s to %(bpath)s.\n' - 'stdout: %(out)s\n stderr: %(err)s') - % {'vol_id': volume_id, - 'vpath': volume_path, - 'bpath': hardlink_path, - 'out': e.stdout, - 'err': e.stderr}) - LOG.error(err) - def backup(self, backup, volume_file, backup_metadata=False): """Backup the given volume to TSM. - TSM performs an image backup of a volume. The volume_file is - used to determine the path of the block device that TSM will - back-up. + TSM performs a backup of a volume. The volume_file is used + to determine the path of the block device that TSM will back-up. :param backup: backup information for volume :param volume_file: file object representing the volume @@ -288,47 +365,54 @@ class TSMBackupDriver(BackupDriver): backup_id = backup['id'] volume_id = backup['volume_id'] - volume_path = self._get_volume_realpath(volume_file, volume_id) - - LOG.debug(_('starting backup of volume: %(volume_id)s to TSM,' - ' volume path: %(volume_path)s,') + volume_path, backup_mode = _get_volume_realpath(volume_file, + volume_id) + LOG.debug(_('Starting backup of volume: %(volume_id)s to TSM,' + ' volume path: %(volume_path)s, mode: %(mode)s.') % {'volume_id': volume_id, - 'volume_path': volume_path}) + 'volume_path': volume_path, + 'mode': backup_mode}) + + backup_path = _create_unique_device_link(backup_id, + volume_path, + volume_id, + backup_mode) + + service_metadata = {'backup_mode': backup_mode, + 'backup_path': backup_path} + self.db.backup_update(self.context, + backup_id, + {'service_metadata': + json.dumps(service_metadata)}) - backup_path = \ - self._create_device_link_using_backupid(backup_id, - volume_path, - volume_id) try: - self._do_backup(backup_path, volume_id) - except processutils.ProcessExecutionError as e: - err = (_('backup: %(vol_id)s Failed to run dsmc ' + self._do_backup(backup_path, volume_id, backup_mode) + except processutils.ProcessExecutionError as exc: + err = (_('backup: %(vol_id)s failed to run dsmc ' 'on %(bpath)s.\n' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': volume_id, 'bpath': backup_path, - 'out': e.stdout, - 'err': e.stderr}) + 'out': exc.stdout, + 'err': exc.stderr}) LOG.error(err) raise exception.InvalidBackup(reason=err) - except exception.Error as e: - err = (_('backup: %(vol_id)s Failed to run dsmc ' + except exception.Error as exc: + err = (_('backup: %(vol_id)s failed to run dsmc ' 'due to invalid arguments ' 'on %(bpath)s.\n' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': volume_id, 'bpath': backup_path, - 'out': e.stdout, - 'err': e.stderr}) + 'out': exc.stdout, + 'err': exc.stderr}) LOG.error(err) raise exception.InvalidBackup(reason=err) finally: - self._cleanup_device_hardlink(backup_path, - volume_path, - volume_id) + _cleanup_device_hardlink(backup_path, volume_path, volume_id) - LOG.debug(_('backup %s finished.') % backup_id) + LOG.debug(_('Backup %s finished.') % backup_id) def restore(self, backup, volume_id, volume_file): """Restore the given volume backup from TSM server. @@ -340,49 +424,56 @@ class TSMBackupDriver(BackupDriver): """ backup_id = backup['id'] - volume_path = self._get_volume_realpath(volume_file, volume_id) - LOG.debug(_('restore: starting restore of backup from TSM' - ' to volume %(volume_id)s, ' - ' backup: %(backup_id)s') - % {'volume_id': volume_id, - 'backup_id': backup_id}) + # backup_path is the path that was originally backed up. + backup_path, backup_mode = _get_backup_metadata(backup, 'restore') + + LOG.debug(_('Starting restore of backup from TSM ' + 'to volume %(volume_id)s, ' + 'backup: %(backup_id)s, ' + 'mode: %(mode)s.') % + {'volume_id': volume_id, + 'backup_id': backup_id, + 'mode': backup_mode}) - restore_path = \ - self._create_device_link_using_backupid(backup_id, - volume_path, - volume_id) + # volume_path is the path to restore into. This may + # be different than the original volume. + volume_path, unused = _get_volume_realpath(volume_file, + volume_id) + + restore_path = _create_unique_device_link(backup_id, + volume_path, + volume_id, + backup_mode) try: - self._do_restore(restore_path, volume_id) - except processutils.ProcessExecutionError as e: - err = (_('restore: %(vol_id)s Failed to run dsmc ' + self._do_restore(backup_path, restore_path, volume_id, backup_mode) + except processutils.ProcessExecutionError as exc: + err = (_('restore: %(vol_id)s failed to run dsmc ' 'on %(bpath)s.\n' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': volume_id, 'bpath': restore_path, - 'out': e.stdout, - 'err': e.stderr}) + 'out': exc.stdout, + 'err': exc.stderr}) LOG.error(err) raise exception.InvalidBackup(reason=err) - except exception.Error as e: - err = (_('restore: %(vol_id)s Failed to run dsmc ' + except exception.Error as exc: + err = (_('restore: %(vol_id)s failed to run dsmc ' 'due to invalid arguments ' 'on %(bpath)s.\n' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': volume_id, 'bpath': restore_path, - 'out': e.stdout, - 'err': e.stderr}) + 'out': exc.stdout, + 'err': exc.stderr}) LOG.error(err) raise exception.InvalidBackup(reason=err) finally: - self._cleanup_device_hardlink(restore_path, - volume_path, - volume_id) + _cleanup_device_hardlink(restore_path, volume_path, volume_id) - LOG.debug(_('restore %(backup_id)s to %(volume_id)s finished.') + LOG.debug(_('Restore %(backup_id)s to %(volume_id)s finished.') % {'backup_id': backup_id, 'volume_id': volume_id}) @@ -394,14 +485,12 @@ class TSMBackupDriver(BackupDriver): """ delete_attrs = {'Total number of objects deleted': '1'} - + delete_path, backup_mode = _get_backup_metadata(backup, 'restore') volume_id = backup['volume_id'] - backup_id = backup['id'] - LOG.debug('delete started, backup: %s', - backup['id']) - volume_path = utils.make_dev_path('%s-%s' % - (self.volume_prefix, backup_id)) + LOG.debug(_('Delete started for backup: %(backup)s, mode: %(mode)s.'), + {'backup': backup['id'], + 'mode': backup_mode}) try: out, err = utils.execute('dsmc', @@ -409,42 +498,43 @@ class TSMBackupDriver(BackupDriver): 'backup', '-quiet', '-noprompt', - '-objtype=image', - '-deltype=all', + '-objtype=%s' % backup_mode, '-password=%s' % self.tsm_password, - volume_path, + delete_path, run_as_root=True, check_exit_code=False) - except processutils.ProcessExecutionError as e: - err = (_('delete: %(vol_id)s Failed to run dsmc with ' + except processutils.ProcessExecutionError as exc: + err = (_('delete: %(vol_id)s failed to run dsmc with ' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': volume_id, - 'out': e.stdout, - 'err': e.stderr}) + 'out': exc.stdout, + 'err': exc.stderr}) LOG.error(err) raise exception.InvalidBackup(reason=err) - except exception.Error as e: - err = (_('restore: %(vol_id)s Failed to run dsmc ' + except exception.Error as exc: + err = (_('delete: %(vol_id)s failed to run dsmc ' 'due to invalid arguments with ' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': volume_id, - 'out': e.stdout, - 'err': e.stderr}) + 'out': exc.stdout, + 'err': exc.stderr}) LOG.error(err) raise exception.InvalidBackup(reason=err) - success = self._check_dsmc_output(out, delete_attrs) + success = _check_dsmc_output(out, delete_attrs) if not success: - err = (_('delete: %(vol_id)s Failed with ' + # log error if tsm cannot delete the backup object + # but do not raise exception so that cinder backup + # object can be removed. + err = (_('delete: %(vol_id)s failed with ' 'stdout: %(out)s\n stderr: %(err)s') % {'vol_id': volume_id, 'out': out, 'err': err}) LOG.error(err) - raise exception.InvalidBackup(reason=err) - LOG.debug(_('delete %s finished') % backup['id']) + LOG.debug(_('Delete %s finished.') % backup['id']) def get_backup_driver(context): diff --git a/cinder/tests/test_backup_tsm.py b/cinder/tests/test_backup_tsm.py index 37d528fcf..06ff419ac 100644 --- a/cinder/tests/test_backup_tsm.py +++ b/cinder/tests/test_backup_tsm.py @@ -18,6 +18,7 @@ Tests for volume backup to IBM Tivoli Storage Manager (TSM). """ import datetime +import json import os import posix @@ -32,11 +33,15 @@ from cinder import utils LOG = logging.getLogger(__name__) SIM = None +VOLUME_PATH = '/dev/null' class TSMBackupSimulator: - # The simulator simulates the execution of the 'dsmc' command. - # This allows the TSM backup test to succeed even if TSM is not installed. + """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 = [] @@ -122,7 +127,8 @@ class TSMBackupSimulator: def _cmd_to_dict(self, arg_list): """Convert command for kwargs (assumes a properly formed command).""" - + path = arg_list[-1] + other = arg_list[-2] ret = {'cmd': arg_list[0], 'type': arg_list[1], 'path': arg_list[-1]} @@ -137,7 +143,7 @@ class TSMBackupSimulator: return ret def _exec_dsmc_cmd(self, cmd): - # simulates the execution of the dsmc command + """Simulates the execution of the dsmc command.""" cmd_switch = {'backup': self._cmd_backup, 'restore': self._cmd_restore, 'delete': self._cmd_delete} @@ -152,7 +158,7 @@ class TSMBackupSimulator: return (out, err, ret) def exec_cmd(self, cmd): - # simulates the execution of dsmc, rm, and ln commands + """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': @@ -204,14 +210,30 @@ def fake_exec(*cmd, **kwargs): return (out, err) -def fake_stat(path): - # Simulate stat to retun the mode of a block device +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() @@ -221,7 +243,7 @@ class BackupTSMTestCase(test.TestCase): 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) + self.stubs.Set(os, 'stat', fake_stat_image) def tearDown(self): super(BackupTSMTestCase, self).tearDown() @@ -232,42 +254,106 @@ class BackupTSMTestCase(test.TestCase): 'status': 'available'} return db.volume_create(self.ctxt, vol)['id'] - def _create_backup_db_entry(self, backup_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'} + 'volume_id': '1234-5678-1234-8888', + 'service_metadata': service_metadata} return db.backup_create(self.ctxt, backup)['id'] - def test_backup(self): + 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) - self._create_backup_db_entry(backup_id2) - - volume_file = open('/dev/null', 'rw') - - # 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) - 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 - backup1 = db.backup_get(self.ctxt, 123) - 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) + 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) -- 2.45.2