Commit 24739695 authored by Marco Nenciarini's avatar Marco Nenciarini

Updated version 2.3 from 'upstream/2.3'

with Debian dir 61aa4945f25da88f61fd484cf988704ee47ad713
parents b512f27d 54261906
2017-09-04 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Update the ChangeLog file
Set version to 2.3
2017-09-01 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Support `path_prefix` for PostgreSQL 10
With PostgreSQL 10, `pg_receivexlog` has been renamed to
`pg_receivewal`. Barman will try both and will pick `pg_receivewal` in
case it find both of them.
The user can override this behavior by using the `path_prefix` setting
and prepending a directory containing only the desired command
version.
2017-08-30 Gabriele Bartolini <gabriele.bartolini@2ndQuadrant.it>
Improve naming consistency with PostgreSQL 10
The `switch-xlog` command has been renamed to `switch-wal`.
In commands output, the `xlog` word has been changed to `WAL` and
`location` has been changed to `LSN` when appropriate.
Improve documentation about recovery target
2017-08-29 Gabriele Bartolini <gabriele.bartolini@2ndQuadrant.it>
Fix typo in doc for createuser
2017-08-26 Gabriele Bartolini <gabriele.bartolini@2ndQuadrant.it>
Add `--target-immediate` option to `recover` command
Throught the `--target-immediate` option, Barman writes the
`recovery_target = 'immediate'` line to recovery.conf.
By doing so, PostgreSQL will exit from recovery mode as soon as a
consistent state is reached (end of the base backup).
Only available for PostgreSQL 9.4 or newer.
2017-08-25 Gabriele Bartolini <gabriele.bartolini@2ndQuadrant.it>
Show cluster state with `barman status`
Similarly to `pg_controldata`, the `status` command now
displays the cluster state of a PostgreSQL server ('in production'
or 'in archive recovery').
2017-08-23 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Support PostgreSQL 10 in `barman replication-status` command
Invoke pg_basebackup with --no-slot option in PostgreSQL 10+
Starting from PostgreSQL 10 pg_basebackup uses a temporary
replication slot unless explicitly instructed.
Barman don't need it because it already stores the full
WAL stream
2017-08-02 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Fix exception during exclusive backup with PostgreSQL 10
2017-08-01 Gabriele Bartolini <gabriele.bartolini@2ndQuadrant.it>
Add `--network-compression` option to remote recovery
Enable/disable network compression at run-time to remote recovery
with `--network-compression` and `--no-network-compression` options.
2017-08-01 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Describe the actual error when invoking archiving hooks script
In case of error the `BARMAN_ERROR` variable will contain a string
starting with the name of the actual WALFileException subclass.
2017-07-28 Giulio Calacoci <giulio.calacoci@2ndquadrant.it>
Ignore `.tmp` files in `incoming` and `streaming` directories
The pg_receivewal utility occasionally writes to a `.tmp` file and
then renames it to the final name to ensure an atomic creation.
Barman must ignore this temporary file.
Other programs could do the same, so Barman will ignore any file name
with a `.tmp` suffix in the incoming and streaming directories.
2017-07-20 Leonardo Cecchi <leonardo.cecchi@2ndquadrant.com>
Add support to PostgreSQL 10
PostgreSQL 10 renames SQL functions, tools and options that references
`xlog` to `wal`. Also WAL related functions referencing `location` have
been renamed to `lsn`.
That change have been made to improve consistency of terminology.
This patch improves Barman adding support to PostgreSQL 10.
2017-07-26 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Make flake8 happy again
Improve `barman diagnose` robustness
2017-07-22 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Fix high memory usage with parallel_jobs > 1
This patch avoids bloating the memory with objects duplicated by
pickle/unpickle cycles.
Fixes #116
2017-07-21 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Improve travis integration
Fix unit test for FileWalArchiver to work with Python 3.6
Python 3.6 is stricter about what the `os.path.join` function can
receive as input. This change has highlighted missing prerequisite
setting in the FileWalArchiver.archive test.
Set version 2.3a1
Fix exception in `barman get-wal --peek` requiring history files
Fix exception during check if ssh works and conninfo is invalid
Make sure that parallel workers are cleaned up in case of errors
In CopyController code, at the end of parallel copy, it is necessary
to terminate all the parallel workers, whether the copy finishes
naturally or it is terminated by an error.
Closes: #114
2017-07-17 Marco Nenciarini <marco.nenciarini@2ndquadrant.it>
Update the ChangeLog file
......
Barman News - History of user-visible changes
Copyright (C) 2011-2017 2ndQuadrant Limited
Version 2.3 - 5 Sep 2017
- Add support to PostgreSQL 10
- Follow naming changes in PostgreSQL 10:
- The switch-xlog command has been renamed to switch-wal.
- In commands output, the xlog word has been changed to WAL and
location has been changed to LSN when appropriate.
- Add the --network-compression/--no-network-compression options to
barman recover to enable or disable network compression at run-time
- Add --target-immediate option to recover command, in order to exit
recovery when a consistent state is reached (end of the backup,
available from PostgreSQL 9.4)
- Show cluster state (master or standby) with barman status command
- Documentation improvements
- Bug fixes:
- Fix high memory usage with parallel_jobs > 1 (#116)
- Better handling of errors using parallel copy (#114)
- Make barman diagnose more robust with system exceptions
- Let archive-wal ignore files with .tmp extension
Version 2.2 - 17 Jul 2017
- Implement parallel copy for backup/recovery through the
......
Metadata-Version: 1.1
Name: barman
Version: 2.2
Version: 2.3
Summary: Backup and Recovery Manager for PostgreSQL
Home-page: http://www.pgbarman.org/
Author: 2ndQuadrant Limited
......@@ -29,3 +29,4 @@ Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Metadata-Version: 1.1
Name: barman
Version: 2.2
Version: 2.3
Summary: Backup and Recovery Manager for PostgreSQL
Home-page: http://www.pgbarman.org/
Author: 2ndQuadrant Limited
......@@ -29,3 +29,4 @@ Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
psycopg2 >= 2.4.2
argh >= 0.21.2, <= 0.26.2
psycopg2>=2.4.2
argh<=0.26.2,>=0.21.2
python-dateutil
argcomplete
......@@ -385,7 +385,7 @@ class BackupManager(RemoteStatusMixin):
'\n'.join(msg_lines[1:]))
else:
output.info("Backup end at xlog location: %s (%s, %08X)",
output.info("Backup end at LSN: %s (%s, %08X)",
backup_info.end_xlog,
backup_info.end_wal,
backup_info.end_offset)
......@@ -427,7 +427,7 @@ class BackupManager(RemoteStatusMixin):
def recover(self, backup_info, dest, tablespaces=None, target_tli=None,
target_time=None, target_xid=None, target_name=None,
exclusive=False, remote_command=None):
target_immediate=False, exclusive=False, remote_command=None):
"""
Performs a recovery of a backup
......@@ -440,6 +440,8 @@ class BackupManager(RemoteStatusMixin):
:param str|None target_xid: the target xid
:param str|None target_name: the target name created previously with
pg_create_restore_point() function call
:param bool|None target_immediate: end recovery as soon as consistency
is reached
:param bool exclusive: whether the recovery is exclusive or not
:param str|None remote_command: default None. The remote command
to recover the base backup, in case of remote backup.
......@@ -453,7 +455,8 @@ class BackupManager(RemoteStatusMixin):
dest, tablespaces,
target_tli, target_time,
target_xid, target_name,
exclusive, remote_command)
target_immediate, exclusive,
remote_command)
# Output recovery results
output.result('recovery', recovery_info['results'])
......@@ -782,7 +785,7 @@ class BackupManager(RemoteStatusMixin):
wal_info = WalFileInfo.from_xlogdb_line(line)
if not xlog.is_any_xlog_file(wal_info.name):
output.error(
"invalid xlog segment name %r\n"
"invalid WAL segment name %r\n"
"HINT: Please run \"barman rebuild-xlogdb %s\" "
"to solve this issue",
wal_info.name, self.config.name)
......
......@@ -310,12 +310,12 @@ class PostgresBackupExecutor(BackupExecutor):
backup_info.save()
if backup_info.begin_wal is not None:
output.info("Backup start at xlog location: %s (%s, %08X)",
output.info("Backup start at LSN: %s (%s, %08X)",
backup_info.begin_xlog,
backup_info.begin_wal,
backup_info.begin_offset)
else:
output.info("Backup start at xlog location: %s",
output.info("Backup start at LSN: %s",
backup_info.begin_xlog)
# Start the copy
......@@ -723,12 +723,12 @@ class SshBackupExecutor(with_metaclass(ABCMeta, BackupExecutor)):
backup_info.save()
if backup_info.begin_wal is not None:
output.info("Backup start at xlog location: %s (%s, %08X)",
output.info("Backup start at LSN: %s (%s, %08X)",
backup_info.begin_xlog,
backup_info.begin_wal,
backup_info.begin_offset)
else:
output.info("Backup start at xlog location: %s",
output.info("Backup start at LSN: %s",
backup_info.begin_xlog)
# If this is the first backup, purge eventually unused WAL files
......@@ -794,7 +794,7 @@ class SshBackupExecutor(with_metaclass(ABCMeta, BackupExecutor)):
# If SSH works but PostgreSQL is not responding
if (cmd is not None and
self.server.get_remote_status()['server_txt_version']
self.server.get_remote_status().get('server_txt_version')
is None):
# Check for 'backup_label' presence
last_backup = self.server.get_backup(
......@@ -857,6 +857,9 @@ class SshBackupExecutor(with_metaclass(ABCMeta, BackupExecutor)):
cmd = UnixRemoteCommand(self.ssh_command,
self.ssh_options,
path=self.server.path)
# Here the name of the PostgreSQL WALs directory is
# hardcoded, but that doesn't represent a problem as
# this code runs only for PostgreSQL < 9.4
archive_dir = os.path.join(
self.server.postgres.get_setting('data_directory'),
'pg_xlog', 'archive_status')
......@@ -1382,11 +1385,11 @@ class PostgresBackupStrategy(BackupStrategy):
self._backup_info_from_stop_location(
backup_info, current_xlog_info)
# Ask PostgreSQL to switch to another XLOG file. This is needed
# Ask PostgreSQL to switch to another WAL file. This is needed
# to archive the transaction log file containing the backup
# end position, which is required to recover from the backup.
try:
postgres.switch_xlog()
postgres.switch_wal()
except PostgresIsInRecovery:
# Skip switching XLOG if a standby server
pass
......@@ -1569,12 +1572,12 @@ class ConcurrentBackupStrategy(BackupStrategy):
self.current_action = "writing backup label"
self._write_backup_label(backup_info)
# Ask PostgreSQL to switch to another XLOG file. This is needed
# Ask PostgreSQL to switch to another WAL file. This is needed
# to archive the transaction log file containing the backup
# end position, which is required to recover from the backup.
postgres = self.executor.server.postgres
try:
postgres.switch_xlog()
postgres.switch_wal()
except PostgresIsInRecovery:
# Skip switching XLOG if a standby server
pass
......
......@@ -24,6 +24,7 @@ import os
import sys
from argparse import SUPPRESS, ArgumentTypeError
from contextlib import closing
from functools import wraps
from argh import ArghParser, arg, expects_obj, named
......@@ -326,6 +327,10 @@ def rebuild_xlogdb(args):
@arg('--target-name',
help='target name created previously with '
'pg_create_restore_point() function call')
@arg('--target-immediate',
help='end recovery as soon as a consistent state is reached',
action='store_true',
default=False)
@arg('--exclusive',
help='set target xid to be non inclusive', action="store_true")
@arg('--tablespace',
......@@ -364,6 +369,16 @@ def rebuild_xlogdb(args):
dest='get_wal',
action='store_false',
default=SUPPRESS)
@arg('--network-compression',
help='Enable network compression during remote recovery.',
dest='network_compression',
action='store_true',
default=SUPPRESS)
@arg('--no-network-compression',
help='Disable network compression during remote recovery.',
dest='network_compression',
action='store_false',
default=SUPPRESS)
@expects_obj
def recover(args):
"""
......@@ -425,6 +440,35 @@ def recover(args):
server.config.recovery_options.remove(RecoveryOptions.GET_WAL)
if args.jobs is not None:
server.config.parallel_jobs = args.jobs
# PostgreSQL supports multiple parameters to specify when the recovery
# process will end, and in that case the last entry in recovery.conf
# will be used. See [1]
#
# Since the meaning of the target options is not dependent on the order
# of parameters, we decided to make the target options mutually exclusive.
#
# [1]: https://www.postgresql.org/docs/current/static/
# recovery-target-settings.html
target_options = ['target_tli', 'target_time', 'target_xid',
'target_name', 'target_immediate']
specified_target_options = len(
[option for option in target_options if getattr(args, option)])
if specified_target_options > 1:
output.error(
"You cannot specify multiple targets for the recovery operation")
output.close_and_exit()
if hasattr(args, 'network_compression'):
if args.network_compression and args.remote_ssh_command is None:
output.error(
"Network compression can only be used with "
"remote recovery.\n"
"HINT: If you want to do a remote recovery "
"you have to use the --remote-ssh-command option")
output.close_and_exit()
server.config.network_compression = args.network_compression
with closing(server):
server.recover(backup_id,
args.destination_directory,
......@@ -433,6 +477,7 @@ def recover(args):
target_time=args.target_time,
target_xid=args.target_xid,
target_name=args.target_name,
target_immediate=args.target_immediate,
exclusive=args.exclusive,
remote_command=args.remote_ssh_command)
......@@ -471,30 +516,30 @@ def show_server(args):
output.close_and_exit()
@named('switch-xlog')
@named('switch-wal')
@arg('server_name', nargs='+',
completer=server_completer_all,
help="specifies the server name target of the switch-xlog command")
help="specifies the server name target of the switch-wal command")
@arg('--force',
help='forces the switch of a xlog by executing a checkpoint before',
help='forces the switch of a WAL by executing a checkpoint before',
dest='force',
action='store_true',
default=False)
@arg('--archive',
help='wait for one xlog file to be archived',
help='wait for one WAL file to be archived',
dest='archive',
action='store_true',
default=False)
@arg('--archive-timeout',
help='the time, in seconds, the archiver will wait for a new xlog file '
help='the time, in seconds, the archiver will wait for a new WAL file '
'to be archived before timing out',
metavar='TIMEOUT',
default='30',
type=check_non_negative)
@expects_obj
def switch_xlog(args):
def switch_wal(args):
"""
Execute the switch-xlog command on the target server
Execute the switch-wal command on the target server
"""
servers = get_server_list(args, skip_inactive=True)
for name in sorted(servers):
......@@ -503,10 +548,20 @@ def switch_xlog(args):
if not manage_server_command(server, name):
continue
with closing(server):
server.switch_xlog(args.force, args.archive, args.archive_timeout)
server.switch_wal(args.force, args.archive, args.archive_timeout)
output.close_and_exit()
@named('switch-xlog')
# Set switch-xlog as alias of switch-wal.
# We cannot use the @argh.aliases decorator, because it needs Python >= 3.2,
# so we create a wraqpper function and use @wraps to copy all the function
# attributes needed by argh
@wraps(switch_wal)
def switch_xlog(args):
return switch_wal(args)
@arg('server_name', nargs='+',
completer=server_completer_all,
help="specifies the server names to check "
......@@ -1059,6 +1114,7 @@ def main():
show_server,
replication_status,
status,
switch_wal,
switch_xlog,
]
)
......
......@@ -679,7 +679,16 @@ class PostgreSQLClient(Command):
Superclass of all the PostgreSQL client commands.
"""
COMMAND = None
COMMAND_ALTERNATIVES = None
"""
Sometimes the name of a command has been changed during the PostgreSQL
evolution. I.e. that happened with pg_receivexlog, that has been renamed
to pg_receivewal. In that case, we should try using pg_receivewal (the
newer auternative) and, if that command doesn't exist, we should try
using `pg_receivewal`.
This is a list of command names to be used to find the installed command.
"""
def __init__(self,
connection,
......@@ -718,6 +727,74 @@ class PostgreSQLClient(Command):
self.enable_signal_forwarding(signal.SIGINT)
self.enable_signal_forwarding(signal.SIGTERM)
@classmethod
def find_command(cls, path=None):
"""
Find the active command, given all the alternatives as set in the
property named `COMMAND_ALTERNATIVES` in this class.
:param str path: The path to use while searching for the command
:rtype: Command
"""
# TODO: Unit tests of this one
# To search for an available command, testing if the command
# exists in PATH is not sufficient. Debian will install wrappers for
# all commands, even if the real command doesn't work.
#
# I.e. we may have a wrapper for `pg_receivewal` even it PostgreSQL
# 10 isn't installed.
#
# This is an example of what can happen in this case:
#
# ```
# $ pg_receivewal --version; echo $?
# Error: pg_wrapper: pg_receivewal was not found in
# /usr/lib/postgresql/9.6/bin
# 1
# $ pg_receivexlog --version; echo $?
# pg_receivexlog (PostgreSQL) 9.6.3
# 0
# ```
#
# That means we should not only ensure the existence of the command,
# but we also need to invoke the command to see if it is a shim
# or not.
# Get the system path if needed
if path is None:
path = os.getenv('PATH')
# If the path is None at this point we have nothing to search
if path is None:
path = ''
# Search the requested executable in every directory present
# in path and return a Command object first occurrence that exists,
# is executable and runs without errors.
for path_entry in path.split(os.path.pathsep):
for cmd in cls.COMMAND_ALTERNATIVES:
full_path = barman.utils.which(cmd, path_entry)
# It doesn't exist try another
if not full_path:
continue
# It exists, let's try invoking it with `--version` to check if
# it's real or not.
try:
command = Command(full_path, path=path, check=True)
command("--version")
return command
except CommandFailedException:
# It's only a inactive shim
continue
# We don't have such a command
raise CommandFailedException(
'command not in PATH, tried: %s' %
' '.join(cls.COMMAND_ALTERNATIVES))
@classmethod
def get_version_info(cls, path=None):
"""
......@@ -726,7 +803,7 @@ class PostgreSQLClient(Command):
:param str path: the PATH env
"""
if cls.COMMAND is None:
if cls.COMMAND_ALTERNATIVES is None:
raise NotImplementedError(
"get_version_info cannot be invoked on %s" % cls.__name__)
......@@ -737,16 +814,21 @@ class PostgreSQLClient(Command):
# Get the version string
try:
command = Command(cls.COMMAND, path=path, check=True)
version_info['full_path'] = command.cmd
command("--version")
command = cls.find_command(path)
except CommandFailedException as e:
_logger.debug("Error invoking %s: %s", cls.COMMAND, e)
_logger.debug("Error invoking %s: %s", cls.__name__, e)
return version_info
version_info['full_path'] = command.cmd
# Parse the full text version
full_version = command.out.strip().split()[-1]
version_info['full_version'] = Version(full_version)
try:
full_version = command.out.strip().split()[-1]
version_info['full_version'] = Version(full_version)
except IndexError:
_logger.debug("Error parsing %s version output",
version_info['full_path'])
return version_info
# Extract the major version
version_info['major_version'] = Version(barman.utils.simplify_version(
full_version))
......@@ -759,12 +841,12 @@ class PgBaseBackup(PostgreSQLClient):
Wrapper class for the pg_basebackup system command
"""
COMMAND = 'pg_basebackup'
COMMAND_ALTERNATIVES = ['pg_basebackup']
def __init__(self,
connection,
destination,
command=COMMAND,
command,
version=None,
app_name=None,
bwlimit=None,
......@@ -798,6 +880,13 @@ class PgBaseBackup(PostgreSQLClient):
# Set the backup destination
self.args += ['-v', '--no-password', '--pgdata=%s' % destination]
if version and version >= Version("10"):
# If version of the client is >= 10 it would use
# a temporary replication slot by default to keep WALs.
# We don't need it because Barman already stores the full
# WAL stream, so we disable this feature to avoid wasting one slot.
self.args += ['--no-slot']
# The tablespace mapping option is repeated once for each tablespace
if tbs_mapping:
for (tbs_source, tbs_destination) in tbs_mapping.items():
......@@ -822,12 +911,12 @@ class PgReceiveXlog(PostgreSQLClient):
Wrapper class for pg_receivexlog
"""
COMMAND = "pg_receivexlog"
COMMAND_ALTERNATIVES = ["pg_receivewal", "pg_receivexlog"]
def __init__(self,
connection,
destination,
command=COMMAND,
command,
version=None,
app_name=None,
synchronous=False,
......
......@@ -87,10 +87,10 @@ class _RsyncJob(object):
"""
A job to be executed by a worker Process
"""
def __init__(self, item, description,
def __init__(self, item_idx, description,
id=None, file_list=None, checksum=None):
"""
:param _RsyncCopyItem item: The copy item containing this job
:param int item_idx: The index of copy item containing this job
:param str description: The description of the job, used for logging
:param int id: Job ID (as in bucket)
:param list[RsyncCopyController._FileItem] file_list: Path to the file
......@@ -98,7 +98,7 @@ class _RsyncJob(object):
:param bool checksum: Whether to force the checksum verification
"""
self.id = id
self.item = item
self.item_idx = item_idx
self.description = description
self.file_list = file_list
self.checksum = checksum
......@@ -385,7 +385,6 @@ class RsyncCopyController(object):
and logging.
:param str src: source directory.
:param str dst: destination directory.
:param bwlimit: bandwidth limit to be enforced. (KiB)
:param str item_class: If specified carries a meta information about
what the object to be copied is.
:param bool optional: Whether a failure copying this object should be
......@@ -456,7 +455,9 @@ class RsyncCopyController(object):
# Create a temporary directory to hold the file lists.
self.temp_dir = tempfile.mkdtemp(suffix='', prefix='barman-')
# The following try block is to make sure the temporary directory
# will be removed on exit.
# will be removed on exit and all the pool workers
# have been terminated.
pool = None
try:
# Initialize the counters used by progress reporting
self._progress_init()
......@@ -510,13 +511,8 @@ class RsyncCopyController(object):
self.jobs_done.append(job)
except KeyboardInterrupt:
# The parent process has been interrupted with a Ctrl-C. Since we
# handle this signal only from the main process (the workers will
# discard it), let's ask the pool to terminate its workers, wait
# for them to be really terminated, and than recover from the
# interruption in the usual way.
pool.terminate()
pool.join()
_logger.info("Copy interrupted by the user (safe before %s)",
self.safe_horizon)
raise
except:
_logger.info("Copy failed (safe before %s)", self.safe_horizon)
......@@ -524,9 +520,20 @@ class RsyncCopyController(object):
else:
_logger.info("Copy finished (safe before %s)", self.safe_horizon)
finally:
# Clean tmp dir and log, exception management is delegated to
# the executor class
shutil.rmtree(self.temp_dir)
# The parent process may have finished naturally or have been
# interrupted by an exception (i.e. due to a copy error or
# the user pressing Ctrl-C).
# At this point we must make sure that all the workers have been
# correctly terminated before continuing.
if pool:
pool.terminate()
pool.join()
# Clean up the temp dir, any exception raised here is logged
# and discarded to not clobber an eventual exception being handled.
try:
shutil.rmtree(self.temp_dir)
except EnvironmentError as e:
_logger.error("Error cleaning up '%s' (%s)", self.temp_dir, e)
self.temp_dir = None
# Store the end time
......@@ -542,7 +549,7 @@ class RsyncCopyController(object):
which have one of the specified classes.
:rtype: iter[_RsyncJob]
"""
for item in self.item_list:
for item_idx, item in enumerate(self.item_list):
# Skip items of classes which are not required
if include_classes and item.item_class not in include_classes:
......@@ -561,7 +568,7 @@ class RsyncCopyController(object):
for i, bucket in enumerate(
self._fill_buckets(item.safe_list)):
phase_skipped = False
yield _RsyncJob(item,
yield _RsyncJob(item_idx,
id=i,
description=msg,
file_list=bucket,
......@@ -576,7 +583,7 @@ class RsyncCopyController(object):
for i, bucket in enumerate(
self._fill_buckets(item.check_list)):
phase_skipped = False
yield _RsyncJob(item,
yield _RsyncJob(item_idx,
id=i,
description=msg,
file_list=bucket,
......@@ -587,7 +594,7 @@ class RsyncCopyController(object):
else:
# Copy the file using plain rsync
msg = self._progress_message("[%%s] %%s copy %s" % item)
yield _RsyncJob(item, description=msg)
yield _RsyncJob(item_idx, description=msg)