]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Backup driver for IBM Tivoli Storage manager (TSM)
authorRonen Kat <ronenkat@il.ibm.com>
Thu, 8 Aug 2013 09:12:37 +0000 (12:12 +0300)
committerAvishay Traeger <avishay@il.ibm.com>
Thu, 22 Aug 2013 20:10:03 +0000 (23:10 +0300)
An implementation of Cinder backup driver using TSM as a backend for
Cinder backups. The driver is a wrapper for the TSM command line
utility dsmc, and uses TSM image backup and restore.

Re-added make_dev_path from cinder/utils.py which was removed by commit
d65425453d215ad64e2bf31b66a3b613c6e7f879

Change-Id: Id105c91ffd3ca953a9fc67dc13f31c3b885bccd7

cinder/backup/drivers/tsm.py [new file with mode: 0644]
cinder/tests/test_backup_tsm.py [new file with mode: 0644]
cinder/utils.py
etc/cinder/cinder.conf.sample
etc/cinder/rootwrap.d/volume.filters

diff --git a/cinder/backup/drivers/tsm.py b/cinder/backup/drivers/tsm.py
new file mode 100644 (file)
index 0000000..84f90f4
--- /dev/null
@@ -0,0 +1,442 @@
+# 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.
+
+"""Backup driver for IBM Tivoli Storage Manager (TSM).
+
+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.
+
+A prerequisite for using the IBM TSM backup service is configuring the
+Cinder host for using TSM.
+"""
+
+import os
+import stat
+
+from cinder.backup.driver import BackupDriver
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder import utils
+from oslo.config import cfg
+
+LOG = logging.getLogger(__name__)
+
+tsmbackup_service_opts = [
+    cfg.StrOpt('backup_tsm_volume_prefix',
+               default='backup',
+               help='Volume prefix for the backup id when backing up to TSM'),
+    cfg.StrOpt('backup_tsm_password',
+               default='password',
+               help='TSM password for the running username'),
+    cfg.BoolOpt('backup_tsm_compression',
+                default=True,
+                help='Enable or Disable compression for backups'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(tsmbackup_service_opts)
+
+
+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):
+        self.context = context
+        self.tsm_password = CONF.backup_tsm_password
+        self.volume_prefix = CONF.backup_tsm_volume_prefix
+        super(TSMBackupDriver, self).__init__(db_driver)
+
+    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
+        """
+
+        try:
+            utils.execute('ln', volume_path, backup_path,
+                          run_as_root=True,
+                          check_exit_code=True)
+        except exception.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})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+
+    def _check_dsmc_output(self, output, check_attrs):
+        """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
+        :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 k, v in check_attrs.iteritems():
+            if k not in parsed_attrs or parsed_attrs[k] != v:
+                return False
+        return True
+
+    def _do_backup(self, backup_path, vol_id):
+        """Perform the actual backup operation.
+
+       :param backup_path: volume path
+       :param vol_id: volume id
+       :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,
+                                 run_as_root=True,
+                                 check_exit_code=False)
+
+        success = self._check_dsmc_output(out, backup_attrs)
+        if not success:
+            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,
+                      'out': out,
+                      'err': err})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+
+    def _do_restore(self, restore_path, vol_id):
+        """Perform the actual restore operation.
+
+        :param restore_path: volume path
+        :param vol_id: volume id
+        :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,
+                                 run_as_root=True,
+                                 check_exit_code=False)
+
+        success = self._check_dsmc_output(out, restore_attrs)
+        if not success:
+            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 exsception.
+
+        :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 exception.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 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.
+
+        :param backup: backup information for volume
+        :param volume_file: file object representing the volume
+        :raises InvalidBackup
+        """
+
+        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_id': volume_id,
+                     'volume_path': volume_path})
+
+        backup_path = \
+            self._create_device_link_using_backupid(backup_id,
+                                                    volume_path,
+                                                    volume_id)
+        try:
+            self._do_backup(backup_path, volume_id)
+        except exception.ProcessExecutionError as e:
+            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})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+        except exception.Error as e:
+            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})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+
+        finally:
+            self._cleanup_device_hardlink(backup_path,
+                                          volume_path,
+                                          volume_id)
+
+        LOG.debug(_('backup %s finished.') % backup_id)
+
+    def restore(self, backup, volume_id, volume_file):
+        """Restore the given volume backup from TSM server.
+
+        :param backup: backup information for volume
+        :param volume_id: volume id
+        :param volume_file: file object representing the volume
+        :raises InvalidBackup
+        """
+
+        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})
+
+        restore_path = \
+            self._create_device_link_using_backupid(backup_id,
+                                                    volume_path,
+                                                    volume_id)
+
+        try:
+            self._do_restore(restore_path, volume_id)
+        except exception.ProcessExecutionError as e:
+            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})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+        except exception.Error as e:
+            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})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+
+        finally:
+            self._cleanup_device_hardlink(restore_path,
+                                          volume_path,
+                                          volume_id)
+
+        LOG.debug(_('restore %(backup_id)s to %(volume_id)s finished.')
+                  % {'backup_id': backup_id,
+                     'volume_id': volume_id})
+
+    def delete(self, backup):
+        """Delete the given backup from TSM server.
+
+        :param backup: backup information for volume
+        :raises InvalidBackup
+        """
+
+        delete_attrs = {'Total number of objects deleted': '1'}
+
+        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))
+
+        try:
+            out, err = utils.execute('dsmc',
+                                     'delete',
+                                     'backup',
+                                     '-quiet',
+                                     '-noprompt',
+                                     '-objtype=image',
+                                     '-deltype=all',
+                                     '-password=%s' % self.tsm_password,
+                                     volume_path,
+                                     run_as_root=True,
+                                     check_exit_code=False)
+
+        except exception.ProcessExecutionError as e:
+            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})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+        except exception.Error as e:
+            err = (_('restore: %(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})
+            LOG.error(err)
+            raise exception.InvalidBackup(reason=err)
+
+        success = self._check_dsmc_output(out, delete_attrs)
+        if not success:
+            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'])
+
+
+def get_backup_driver(context):
+    return TSMBackupDriver(context)
diff --git a/cinder/tests/test_backup_tsm.py b/cinder/tests/test_backup_tsm.py
new file mode 100644 (file)
index 0000000..f7a7425
--- /dev/null
@@ -0,0 +1,272 @@
+# 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 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 import test
+from cinder import utils
+
+LOG = logging.getLogger(__name__)
+SIM = None
+
+
+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.
+    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
+            for idx, backup in enumerate(self._backup_list[path]):
+                index = idx
+            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 exception.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 exception.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 exception.ProcessExecutionError(
+            exit_code=-1,
+            stdout=out,
+            stderr=err,
+            cmd=' '.join(cmd))
+    return (out, err)
+
+
+def fake_stat(path):
+    # Simulate stat to retun 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))
+
+
+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)
+
+    def tearDown(self):
+        super(BackupTSMTestCase, self).tearDown()
+
+    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):
+        backup = {'id': backup_id,
+                  'size': 1,
+                  'container': 'test-container',
+                  'volume_id': '1234-5678-1234-8888'}
+        return db.backup_create(self.ctxt, backup)['id']
+
+    def test_backup(self):
+        volume_id = '1234-5678-1234-8888'
+        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)
index 08d1ff57aa4fde0cafa035155b3fe61ba22e1609..1a18a08d7740ac4fdd2d024335998815536c3277 100644 (file)
@@ -795,6 +795,21 @@ def logging_error(message):
             LOG.exception(message)
 
 
+def make_dev_path(dev, partition=None, base='/dev'):
+    """Return a path to a particular device.
+
+    >>> make_dev_path('xvdc')
+    /dev/xvdc
+
+    >>> make_dev_path('xvdc', 1)
+    /dev/xvdc1
+    """
+    path = os.path.join(base, dev)
+    if partition:
+        path += str(partition)
+    return path
+
+
 def total_seconds(td):
     """Local total_seconds implementation for compatibility with python 2.6"""
     if hasattr(td, 'total_seconds'):
index 547e107ae17a639bde30bc827213e544535186be..9232b03f308a398fec2fff233c2eb95030ecd7e7 100644 (file)
 #nova_api_insecure=false
 
 
+
+#
+# Options defined in cinder.backup.services.tsm
+#
+
+# Volume prefix for the backup id when backing up to TSM
+#backup_tsm_volume_prefix=backup
+
+# TSM password for the running username
+#backup_tsm_password=password
+
+# Enable or Disable compression for backups
+#backup_tsm_compression=True
+
+
 #
 # Options defined in cinder.db.api
 #
index 908ec0894d837122f1ba8ed81702f8d0ebaa98ab..4e59bd09cf0850ee0fb1f49e87262d0a6b2866b2 100644 (file)
@@ -83,3 +83,6 @@ mkfs: CommandFilter, mkfs, root
 aoe-revalidate: CommandFilter, aoe-revalidate, root
 aoe-discover: CommandFilter, aoe-discover, root
 aoe-flush: CommandFilter, aoe-flush, root
+
+#cinder/backup/services/tsm.py
+dsmc:CommandFilter,/usr/bin/dsmc,root