Whamcloud - gitweb
LU-6051 utils: allow lfs_migrate to handle hard links 51/25851/9
authorSteve Guminski <stephenx.guminski@intel.com>
Thu, 9 Mar 2017 12:19:44 +0000 (07:19 -0500)
committerOleg Drokin <oleg.drokin@intel.com>
Mon, 6 Nov 2017 03:43:17 +0000 (03:43 +0000)
Detect files that are hard links to files that have already been
migrated, and skip them.  This prevents unnecessary work and speeds
up the migration process.  Sanity test 56xb has been added to test
the new behavior.  A new option '-S <stripe_size>' has been added
to lfs_migrate, and allows the test to be run with a single OST.

Previously, the default behavior was to skip migration of any
file name that contained hard links other than itself. Therefore,
the file in the link set would not be migrated at all.  The '-l'
option could be used to override.  However, there was no attempt to
detect which file names were hard linked to already migrated files,
so the file would be migrated multiple times.

The new behavior automatically migrates the file once and skips
other file names in the link set.

Test-Parameters: trivial
Signed-off-by: Steve Guminski <stephenx.guminski@intel.com>
Signed-off-by: Evan Felix <evan.felix@pnnl.gov>
Change-Id: I43678b34931507c0fe30c1c40ca2dc2d47f86aeb
Reviewed-on: https://review.whamcloud.com/25851
Tested-by: Jenkins
Tested-by: Maloo <hpdd-maloo@intel.com>
Reviewed-by: Andreas Dilger <andreas.dilger@intel.com>
Reviewed-by: Henri Doreau <henri.doreau@cea.fr>
lustre/doc/lfs_migrate.1
lustre/scripts/lfs_migrate
lustre/tests/sanity.sh

index c6f198a..8195ea4 100644 (file)
@@ -1,4 +1,4 @@
-.TH lfs_migrate 1 "Jul 21, 2010" Lustre "utilities"
+.TH lfs_migrate 1 "Jun 16, 2017" Lustre "utilities"
 .SH NAME
 .B lfs_migrate
 \- simple tool to migrate files between Lustre OSTs
 .SH NAME
 .B lfs_migrate
 \- simple tool to migrate files between Lustre OSTs
@@ -6,12 +6,13 @@
 .B lfs_migrate
 .RB [ -c <stripe_count> ]
 .RB [ -h ]
 .B lfs_migrate
 .RB [ -c <stripe_count> ]
 .RB [ -h ]
-.RB [ -l ]
 .RB [ -n ]
 .RB [ -q ]
 .RB [ -R ]
 .RB [ -s ]
 .RB [ -n ]
 .RB [ -q ]
 .RB [ -R ]
 .RB [ -s ]
+.RB [ -S <stripe_size> ]
 .RB [ -y ]
 .RB [ -y ]
+.RB [ -0 ]
 .RI [ file | "directory ..." ]
 .br
 .SH DESCRIPTION
 .RI [ file | "directory ..." ]
 .br
 .SH DESCRIPTION
@@ -64,28 +65,29 @@ specified at the same time as the -R option.
 .B \\-h
 Display help information.
 .TP
 .B \\-h
 Display help information.
 .TP
-.B \\-l
-Migrate files with hard links (skip by default).  Files with multiple
-hard links will be split into multiple separate files by
-.B lfs_migrate
-so they are skipped by default to avoid breaking the hard links.
-.TP
 .B \\-n
 .B \\-n
-Only print the names of files to be migrated
+Only print the names of files to be migrated.
 .TP
 .B \\-q
 .TP
 .B \\-q
-Run quietly (don't print filenames or status)
+Run quietly (don't print filenames or status).
 .TP
 .B \\-R
 Restripe file using default directory striping instead of keeping striping.
 .TP
 .B \\-R
 Restripe file using default directory striping instead of keeping striping.
-This option may not be specified at the same time as the -c option.
+This option may not be specified at the same time as the -c or -S options.
 .TP
 .B \\-s
 .TP
 .B \\-s
-skip file data comparison after migrate.  Default is to compare migrated file
+Skip file data comparison after migrate.  Default is to compare migrated file
 against original to verify correctness.
 .TP
 against original to verify correctness.
 .TP
+.B \\-S <stripe_size>
+Restripe file using the specified stripe size. This option may not be
+specified at the same time as the -R option.
+.TP
 .B \\-y
 Answer 'y' to usage warning without prompting (for scripts, use with caution).
 .B \\-y
 Answer 'y' to usage warning without prompting (for scripts, use with caution).
+.TP
+.B \\-0
+Input file names on stdin are separated by a null character.
 .SH EXAMPLES
 To rebalance all files within
 .I /testfs/jobs/2011
 .SH EXAMPLES
 To rebalance all files within
 .I /testfs/jobs/2011
@@ -103,9 +105,6 @@ a guarantee the files are not being modified, that is workload specific):
 lfs find /testfs -obd test-OST0004 -size +4G -mtime +2d |
     lfs_migrate -y
 .SH KNOWN BUGS
 lfs find /testfs -obd test-OST0004 -size +4G -mtime +2d |
     lfs_migrate -y
 .SH KNOWN BUGS
-Hard links could be handled correctly in Lustre 2.0 by using
-.BR lfs (1) " fid2path" .
-.PP
 Eventually, this functionality will be integrated into
 .BR lfs (1)
 itself and will integrate with the MDS layout locking to make it safe
 Eventually, this functionality will be integrated into
 .BR lfs (1)
 itself and will integrate with the MDS layout locking to make it safe
index 30e0664..0c080b7 100755 (executable)
@@ -1,6 +1,4 @@
 #!/bin/bash
 #!/bin/bash
-# set -x
-set -e
 
 # lfs_migrate: a simple tool to copy and check files.
 #
 
 # lfs_migrate: a simple tool to copy and check files.
 #
@@ -18,24 +16,51 @@ RSYNC=${RSYNC:-rsync}
 LFS_MIGRATE_RSYNC_MODE=${LFS_MIGRATE_RSYNC_MODE:-false}
 ECHO=echo
 LFS=${LFS:-lfs}
 LFS_MIGRATE_RSYNC_MODE=${LFS_MIGRATE_RSYNC_MODE:-false}
 ECHO=echo
 LFS=${LFS:-lfs}
+LFS_MIGRATE_RSYNC=${LFS_MIGRATE_RSYNC:-false}
+RSYNC_WITH_HLINKS=false
+LFS_MIGRATE_TMP=${TMPDIR:-/tmp}
+MIGRATED_SET="$(mktemp ${LFS_MIGRATE_TMP}/lfs_migrate-$$.links.XXXXXX)"
+NEWNAME=""
+REMOVE_FID='s/^\[[0-9a-fx:]*\] //'
+
+add_to_set() {
+       local old_fid="$1"
+       local path="$2"
+
+       echo -e "$old_fid $path" >> "$MIGRATED_SET"
+}
+
+path_in_set() {
+       local path="$1"
+
+       sed -e "$REMOVE_FID" $MIGRATED_SET | grep -q "^$path$"
+}
+
+old_fid_in_set() {
+       local old_fid="$1"
+
+       grep "^\\$old_fid" "$MIGRATED_SET" | head -n 1 |
+               sed -e "$REMOVE_FID"
+}
 
 usage() {
     cat -- <<USAGE 1>&2
 
 usage() {
     cat -- <<USAGE 1>&2
-usage: lfs_migrate [-c <stripe_count>] [-h] [-l] [-n] [-q] [-R] [-s] [-y] [-0]
-                   [file|dir ...]
+usage: lfs_migrate [-c <stripe_count>] [-h] [-n] [-q] [-R] [-s]
+                   [-S <stripe_size>] [-y] [-0] [file|dir ...]
     -c <stripe_count>
        restripe file using the specified stripe count
     -h show this usage message
     -c <stripe_count>
        restripe file using the specified stripe count
     -h show this usage message
-    -l migrate files with hard links (skip by default for rsync)
     -n only print the names of files to be migrated
     -q run quietly (don't print filenames or status)
     -R restripe file using default directory striping
     -s skip file data comparison after migrate
     -n only print the names of files to be migrated
     -q run quietly (don't print filenames or status)
     -R restripe file using default directory striping
     -s skip file data comparison after migrate
+    -S <stripe_size>
+       restripe file using the specified stripe size
     -y answer 'y' to usage question
     -0 input file names on stdin are separated by a null character
 
     -y answer 'y' to usage question
     -0 input file names on stdin are separated by a null character
 
-The -c <stripe_count> option may not be specified at the same time as
-the -R option.
+The -c <stripe_count> and -S <stripe_size> options may not be specified at
+the same time as the -R option.
 
 If a directory is an argument, all files in the directory are migrated.
 If no file/directory is given, the file list is read from standard input.
 
 If a directory is an argument, all files in the directory are migrated.
 If no file/directory is given, the file list is read from standard input.
@@ -46,17 +71,26 @@ USAGE
     exit 1
 }
 
     exit 1
 }
 
+cleanup() {
+       rm -f "$MIGRATED_SET"
+       [ -n "$NEWNAME" ] && rm -f "$NEWNAME"
+}
+
+trap cleanup EXIT
+
 OPT_CHECK=y
 OPT_STRIPE_COUNT=""
 OPT_CHECK=y
 OPT_STRIPE_COUNT=""
+OPT_STRIPE_SIZE=""
 
 
-while getopts "c:hlnqRsy0" opt $*; do
+while getopts "c:hlnqRsS:y0" opt $*; do
     case $opt in
        c) OPT_STRIPE_COUNT=$OPTARG;;
     case $opt in
        c) OPT_STRIPE_COUNT=$OPTARG;;
-       l) OPT_NLINK=y;;
+       l) ;; # maintained for backward compatibility
        n) OPT_DRYRUN=n; OPT_YES=y;;
        q) ECHO=:;;
        R) OPT_RESTRIPE=y;;
        s) OPT_CHECK="";;
        n) OPT_DRYRUN=n; OPT_YES=y;;
        q) ECHO=:;;
        R) OPT_RESTRIPE=y;;
        s) OPT_CHECK="";;
+       S) OPT_STRIPE_SIZE=$OPTARG;;
        y) OPT_YES=y;;
        0) OPT_NULL=y;;
        h|\?) usage;;
        y) OPT_YES=y;;
        0) OPT_NULL=y;;
        h|\?) usage;;
@@ -64,9 +98,10 @@ while getopts "c:hlnqRsy0" opt $*; do
 done
 shift $((OPTIND - 1))
 
 done
 shift $((OPTIND - 1))
 
-if [ "$OPT_STRIPE_COUNT" -a "$OPT_RESTRIPE" ]; then
+if [ -n "$OPT_STRIPE_COUNT""$OPT_STRIPE_SIZE" -a "$OPT_RESTRIPE" ]; then
        echo ""
        echo ""
-       echo "$(basename $0) error: The -c <stripe_count> option may not" 1>&2
+       echo "$(basename $0) error: The -c <stripe_count> option and" 1>&2
+       echo "-S <stripe_size> option may not" 1>&2
        echo "be specified at the same time as the -R option." 1>&2
        exit 1
 fi
        echo "be specified at the same time as the -R option." 1>&2
        exit 1
 fi
@@ -99,28 +134,76 @@ umask 0077
 
 lfs_migrate() {
        while IFS='' read -d '' OLDNAME; do
 
 lfs_migrate() {
        while IFS='' read -d '' OLDNAME; do
-               $ECHO -n "$OLDNAME: "
+               local hlinks=()
 
                # avoid duplicate stat if possible
 
                # avoid duplicate stat if possible
-               TYPE_LINK=($(LANG=C stat -c "%h %F" "$OLDNAME" || true))
+               local nlink_type=($(LANG=C stat -c "%h %F" "$OLDNAME" || true))
 
                # skip non-regular files, since they don't have any objects
                # and there is no point in trying to migrate them.
 
                # skip non-regular files, since they don't have any objects
                # and there is no point in trying to migrate them.
-               if [ "${TYPE_LINK[1]}" != "regular" ]; then
-                       echo -e "not a regular file, skipped"
+               if [ "${nlink_type[1]}" != "regular" ]; then
+                       echo -e "$OLDNAME: not a regular file, skipped"
                        continue
                fi
 
                # working out write perms is hard, let the shell do it
                if [ ! -w "$OLDNAME" ]; then
                        continue
                fi
 
                # working out write perms is hard, let the shell do it
                if [ ! -w "$OLDNAME" ]; then
-                       echo -e "no write permission, skipped"
+                       echo -e "$OLDNAME: no write permission, skipped"
                        continue
                fi
 
                if [ "$OPT_DRYRUN" ]; then
                        continue
                fi
 
                if [ "$OPT_DRYRUN" ]; then
-                       echo -e "dry run, skipped"
+                       echo -e "$OLDNAME: dry run, skipped"
+                       continue
+               fi
+
+               # xattrs use absolute file paths, so ensure provided path is
+               # also absolute so that the names can be compared
+               local oldname_absolute=$(readlink -f "$OLDNAME")
+               if [ $? -ne 0 ]; then
+                       echo -e "$OLDNAME: cannot resolve full path"
                        continue
                fi
                        continue
                fi
+               OLDNAME=$oldname_absolute
+
+               # In the future, the path2fid and fid2path calls below
+               # should be replaced with a single call to
+               # "lfs path2links" once that command is available.  The logic
+               # for detecting unlisted hard links could then be removed.
+               local fid=$(lfs path2fid "$OLDNAME" 2> /dev/null)
+               if [ $? -ne 0 ]; then
+                       echo -n "$OLDNAME: cannot determine FID; skipping; "
+                       echo "is this a Lustre file system?"
+                       continue
+               fi
+
+               if [[ ${nlink_type[0]} -gt 1 || $RSYNC_WITH_HLINKS == true ]]; then
+                       # don't migrate a hard link if it was already migrated
+                       if path_in_set "$OLDNAME"; then
+                               $ECHO -e "$OLDNAME: already migrated via another hard link"
+                               continue
+                       fi
+
+                       # There is limited space available in the xattrs
+                       # to store all of the hard links for a file, so it's
+                       # possible that $OLDNAME is part of a link set but is
+                       # not listed in xattrs and therefore not listed as
+                       # being migrated.
+                       local migrated=$(old_fid_in_set "$fid")
+                       if [ -n "$migrated" ]; then
+                               $ECHO -e "$OLDNAME: already migrated via another hard link"
+                               if [[ $LFS_MIGRATE_RSYNC == true ]]; then
+                                       # Only the rsync case has to relink.
+                                       # The lfs migrate case preserves the
+                                       # inode so the links are already
+                                       # correct.
+                                       [ "$migrated" != "$OLDNAME" ] &&
+                                               ln -f "$migrated" "$OLDNAME"
+                               fi
+                               add_to_set "$fid" "$OLDNAME"
+                               continue;
+                       fi
+               fi
 
                if [ "$OPT_RESTRIPE" ]; then
                        UNLINK=""
 
                if [ "$OPT_RESTRIPE" ]; then
                        UNLINK=""
@@ -130,57 +213,92 @@ lfs_migrate() {
                # then we don't need to do this getstripe/mktemp stuff.
                        UNLINK="-u"
 
                # then we don't need to do this getstripe/mktemp stuff.
                        UNLINK="-u"
 
-                       [ "$OPT_STRIPE_COUNT" ] && COUNT=$OPT_STRIPE_COUNT ||
-                               COUNT=$($LFS getstripe -c "$OLDNAME" \
-                                       2> /dev/null)
-                       SIZE=$($LFS getstripe -S "$OLDNAME" 2> /dev/null)
+                       [ "$OPT_STRIPE_COUNT" ] &&
+                               stripe_count=$OPT_STRIPE_COUNT ||
+                               stripe_count=$($LFS getstripe -c "$OLDNAME" \
+                                              2> /dev/null)
+                       [ "$OPT_STRIPE_SIZE" ] &&
+                               stripe_size=$OPT_STRIPE_SIZE ||
+                               stripe_size=$($LFS getstripe -S \
+                                             "$OLDNAME" 2> /dev/null)
 
 
-                       [ -z "$COUNT" -o -z "$SIZE" ] && UNLINK=""
-                       SIZE="-S${SIZE}"
-               fi
-
-               # first try to migrate inside lustre
-               # if failed go back to old rsync mode
-               if [[ $LFS_MIGRATE_RSYNC_MODE == false ]]; then
-                       if $LFS migrate -c${COUNT} ${SIZE} "$OLDNAME"; then
-                               $ECHO "done"
+                       if [ -z "$stripe_count" -o -z "$stripe_size" ]; then
+                               UNLINK=""
+                               echo -e "$OLDNAME: cannot determine stripe info; skipping"
                                continue
                                continue
-                       else
-                               echo "falling back to rsync-based migration"
-                               LFS_MIGRATE_RSYNC_MODE=true
                        fi
                        fi
+                       stripe_size="-S$stripe_size"
+                       stripe_count="-c$stripe_count"
                fi
 
                fi
 
-               if [ -z "$OPT_NLINK" -a ${TYPE_LINK[0]} -gt 1 ]; then
-                       echo -e "multiple hard links, skipped"
+               # detect other hard links and store them on a global
+               # list so we don't re-migrate them
+               local mntpoint=$(df -P "$OLDNAME" |
+                               awk 'NR==2 { print $NF; exit }')
+               if [ -z "$mntpoint" ]; then
+                       echo -e "$OLDNAME: cannot determine mount point; skipping"
+                       continue
+               fi
+               local hlinks=$(lfs fid2path "$mntpoint" "$fid" 2> /dev/null)
+               if [ $? -ne 0 ]; then
+                       echo -n "$OLDNAME: cannot determine hard link paths"
                        continue
                fi
                        continue
                fi
+               hlinks+=("$OLDNAME")
 
 
-               NEWNAME=$(mktemp $UNLINK "$OLDNAME.tmp.XXXXXX")
+               # first try to migrate via Lustre tools, then fall back to rsync
+               if [[ $LFS_MIGRATE_RSYNC == false ]]; then
+                       if $LFS migrate "$stripe_count" "$stripe_size" "$OLDNAME"; then
+                               $ECHO -e "$OLDNAME: done migrate"
+                               for link in ${hlinks[*]}; do
+                                       add_to_set "$fid" "$link"
+                               done
+                               continue
+                       else
+                               echo -e "$OLDNAME: falling back to rsync-based migration"
+                               LFS_MIGRATE_RSYNC=true
+                       fi
+               fi
+
+               NEWNAME=$(mktemp $UNLINK "$OLDNAME-lfs_migrate.tmp.XXXXXX")
                if [ $? -ne 0 -o -z "$NEWNAME" ]; then
                if [ $? -ne 0 -o -z "$NEWNAME" ]; then
-                       echo -e "\r$OLDNAME: can't make temp file, skipped" 1>&2
+                       echo -e "$OLDNAME: can't make temp file, skipped" 1>&2
                        continue
                fi
 
                        continue
                fi
 
-               [ "$UNLINK" ] && $LFS setstripe -c${COUNT} ${SIZE} "$NEWNAME"
+               [ "$UNLINK" ] && $LFS setstripe ${stripe_count} \
+                       ${stripe_size} "$NEWNAME"
 
                # we use --inplace, since we created our own temp file already
                if ! $RSYNC -a --inplace $RSYNC_OPTS "$OLDNAME" "$NEWNAME";then
 
                # we use --inplace, since we created our own temp file already
                if ! $RSYNC -a --inplace $RSYNC_OPTS "$OLDNAME" "$NEWNAME";then
-                       echo -e "\r$OLDNAME: copy error, exiting" 1>&2
-                       rm -f "$NEWNAME"
+                       echo -e "$OLDNAME: copy error, exiting" 1>&2
                        exit 4
                fi
 
                if [ "$OPT_CHECK" ] && ! cmp -s "$OLDNAME" "$NEWNAME"; then
                        exit 4
                fi
 
                if [ "$OPT_CHECK" ] && ! cmp -s "$OLDNAME" "$NEWNAME"; then
-                       echo -e "\r$NEWNAME: compare failed, exiting" 1>&2
+                       echo -e "$NEWNAME: compare failed, exiting" 1>&2
                        exit 8
                fi
 
                if ! mv "$NEWNAME" "$OLDNAME"; then
                        exit 8
                fi
 
                if ! mv "$NEWNAME" "$OLDNAME"; then
-                       echo -e "\r$OLDNAME: rename error, exiting" 1>&2
+                       echo -e "$OLDNAME: rename error, exiting" 1>&2
                        exit 12
                fi
                        exit 12
                fi
-               $ECHO "done"
+
+               $ECHO -e "$OLDNAME: done migrate via rsync"
+               for link in ${hlinks[*]}; do
+                       if [ "$link" != "$OLDNAME" ]; then
+                               ln -f "$OLDNAME" "$link"
+                       fi
+                       add_to_set "$fid" "$link"
+               done
+
+               # If the number of hlinks exceeds the space in the xattrs,
+               # when the final path is statted it will have a link count
+               # of 1 (all other links will point to the new inode).
+               # This flag indicates that even paths with a link count of
+               # 1 are potentially part of a link set.
+               [ ${#hlinks[*]} -gt 1 ] && RSYNC_WITH_HLINKS=true
        done
 }
 
        done
 }
 
@@ -193,10 +311,11 @@ if [ "$#" -eq 0 ]; then
 else
        while [ "$1" ]; do
                if [ -d "$1" ]; then
 else
        while [ "$1" ]; do
                if [ -d "$1" ]; then
-                       lfs find "$1" -type f -print0 | lfs_migrate
+                       $LFS find "$1" -type f -print0 | lfs_migrate
                else
                        echo -en "$1\0" | lfs_migrate
                fi
                shift
        done
 fi
                else
                        echo -en "$1\0" | lfs_migrate
                fi
                shift
        done
 fi
+
index 6acddd5..85d65df 100755 (executable)
@@ -5150,16 +5150,15 @@ test_56x() {
        check_swap_layouts_support && return 0
        [[ $OSTCOUNT -lt 2 ]] && skip_env "needs >= 2 OSTs" && return
 
        check_swap_layouts_support && return 0
        [[ $OSTCOUNT -lt 2 ]] && skip_env "needs >= 2 OSTs" && return
 
-       local dir0=$DIR/$tdir/$testnum
-       test_mkdir -p $dir0
-
+       local dir0=$DIR/$tdir
        local ref1=/etc/passwd
        local file1=$dir0/file1
 
        local ref1=/etc/passwd
        local file1=$dir0/file1
 
-       $SETSTRIPE -c 2 $file1
+       test_mkdir $dir0 || error "creating dir $dir0"
+       $LFS setstripe -c 2 $file1
        cp $ref1 $file1
        $LFS migrate -c 1 $file1 || error "migrate failed rc = $?"
        cp $ref1 $file1
        $LFS migrate -c 1 $file1 || error "migrate failed rc = $?"
-       stripe=$($GETSTRIPE -c $file1)
+       stripe=$($LFS getstripe -c $file1)
        [[ $stripe == 1 ]] || error "stripe of $file1 is $stripe != 1"
        cmp $file1 $ref1 || error "content mismatch $file1 differs from $ref1"
 
        [[ $stripe == 1 ]] || error "stripe of $file1 is $stripe != 1"
        cmp $file1 $ref1 || error "content mismatch $file1 differs from $ref1"
 
@@ -5178,10 +5177,10 @@ test_56xa() {
        local ref1=/etc/passwd
        local file1=$dir0/file1
 
        local ref1=/etc/passwd
        local file1=$dir0/file1
 
-       $SETSTRIPE -c 2 $file1
+       $LFS setstripe -c 2 $file1
        cp $ref1 $file1
        $LFS migrate --block -c 1 $file1 || error "migrate failed rc = $?"
        cp $ref1 $file1
        $LFS migrate --block -c 1 $file1 || error "migrate failed rc = $?"
-       local stripe=$($GETSTRIPE -c $file1)
+       local stripe=$($LFS getstripe -c $file1)
        [[ $stripe == 1 ]] || error "stripe of $file1 is $stripe != 1"
        cmp $file1 $ref1 || error "content mismatch $file1 differs from $ref1"
 
        [[ $stripe == 1 ]] || error "stripe of $file1 is $stripe != 1"
        cmp $file1 $ref1 || error "content mismatch $file1 differs from $ref1"
 
@@ -5190,6 +5189,110 @@ test_56xa() {
 }
 run_test 56xa "lfs migration --block support"
 
 }
 run_test 56xa "lfs migration --block support"
 
+check_migrate_links() {
+       local dir="$1"
+       local file1="$dir/file1"
+       local begin="$2"
+       local count="$3"
+       local total_count=$(($begin + $count - 1))
+       local symlink_count=10
+       local uniq_count=10
+
+       if [ ! -f "$file1" ]; then
+               echo -n "creating initial file..."
+               $LFS setstripe -c 1 -S "512k" "$file1" ||
+                       error "cannot setstripe initial file"
+               echo "done"
+
+               echo -n "creating symlinks..."
+               for s in $(seq 1 $symlink_count); do
+                       ln -s "$file1" "$dir/slink$s" ||
+                               error "cannot create symlinks"
+               done
+               echo "done"
+
+               echo -n "creating nonlinked files..."
+               createmany -o "$dir/uniq" 1 10 &> /dev/null ||
+                       error "cannot create nonlinked files"
+               echo "done"
+       fi
+
+       # create hard links
+       if [ ! -f "$dir/file$total_count" ]; then
+               echo -n "creating hard links $begin:$total_count..."
+               createmany -l"$file1" "$dir/file" "$begin" "$count" &>  \
+                       /dev/null || error "cannot create hard links"
+               echo "done"
+       fi
+
+       echo -n "checking number of hard links listed in xattrs..."
+       local fid=$($LFS getstripe -F "$file1")
+       local paths=($($LFS fid2path "$MOUNT" "$fid" 2> /dev/null))
+
+       echo "${#paths[*]}"
+       if [ ${#paths[*]} -lt $total_count -a "$begin" -eq 2  ]; then
+                       echo "hard link list has unexpected size, skipping test"
+                       return 0
+       fi
+       if [ ${#paths[*]} -ge $total_count -a "$begin" -ne 2  ]; then
+                       error "link names should exceed xattrs size"
+       fi
+
+       echo -n "migrating files..."
+       local migrate_out=$($LFS_MIGRATE -y -S '1m' $dir)
+       local rc=$?
+       [ $rc -eq 0 ] || error "migrate failed rc = $rc"
+       echo "done"
+
+       # make sure all links have been properly migrated
+       echo -n "verifying files..."
+       fid=$($LFS getstripe -F "$file1") ||
+               error "cannot get fid for file $file1"
+       for i in $(seq 2 $total_count); do
+               local fid2=$($LFS getstripe -F $dir/file$i)
+               [ "$fid2" == "$fid" ] ||
+                       error "migrated hard link has mismatched FID"
+       done
+
+       # make sure hard links were properly detected, and migration was
+       # performed only once for the entire link set; nonlinked files should
+       # also be migrated
+       local actual=$(grep -c 'done migrate' <<< "$migrate_out")
+       local expected=$(($uniq_count + 1))
+       [ "$actual" -eq  "$expected" ] ||
+               error "hard links individually migrated ($actual != $expected)"
+
+       # make sure the correct number of hard links are present
+       local hardlinks=$(stat -c '%h' "$file1")
+       [ $hardlinks -eq $total_count ] ||
+               error "num hard links $hardlinks != $total_count"
+       echo "done"
+
+       return 0
+}
+
+test_56xb() {
+       local dir0="$DIR/$tdir"
+
+       test_mkdir "$dir0" || error "cannot create dir $dir0"
+
+       echo "testing lfs migrate mode when all links fit within xattrs"
+       LFS_MIGRATE_RSYNC=false check_migrate_links "$dir0" 2 99
+
+       echo "testing rsync mode when all links fit within xattrs"
+       LFS_MIGRATE_RSYNC=true check_migrate_links "$dir0" 2 99
+
+       echo "testing lfs migrate mode when all links do not fit within xattrs"
+       LFS_MIGRATE_RSYNC=false check_migrate_links "$dir0" 101 100
+
+       echo "testing rsync mode when all links do not fit within xattrs"
+       LFS_MIGRATE_RSYNC=true check_migrate_links "$dir0" 101 100
+
+       # clean up
+       rm -rf $dir0
+}
+run_test 56xb "lfs migration hard link support"
+
 test_56y() {
        [ $(lustre_version_code $SINGLEMDS) -lt $(version_code 2.4.53) ] &&
                skip "No HSM $(lustre_build_version $SINGLEMDS) MDS < 2.4.53" &&
 test_56y() {
        [ $(lustre_version_code $SINGLEMDS) -lt $(version_code 2.4.53) ] &&
                skip "No HSM $(lustre_build_version $SINGLEMDS) MDS < 2.4.53" &&