Whamcloud - gitweb
LU-10278 utils: allow to migrate without direct io
[fs/lustre-release.git] / lustre / scripts / lfs_migrate
1 #!/bin/bash
2
3 # lfs_migrate: a simple tool to copy and check files.
4 #
5 # To avoid allocating objects on one or more OSTs, they should be
6 # deactivated on the MDS via "lctl --device {device_number} deactivate",
7 # where {device_number} is from the output of "lctl dl" on the MDS.
8 #
9 # To guard against corruption, the file is compared after migration
10 # to verify the copy is correct and the file has not been modified.
11 # This is not a protection against the file being open by another
12 # process, but it would catch the worst cases of in-use files, but
13 # to be 100% safe the administrator needs to ensure this is safe.
14
15 RSYNC=${RSYNC:-rsync}
16 LFS_MIGRATE_RSYNC_MODE=${LFS_MIGRATE_RSYNC_MODE:-false}
17 ECHO=echo
18 LFS=${LFS:-lfs}
19 RSYNC_WITH_HLINKS=false
20 LFS_MIGRATE_TMP=${TMPDIR:-/tmp}
21 MIGRATED_SET="$(mktemp ${LFS_MIGRATE_TMP}/lfs_migrate.links.XXXXXX)"
22 NEWNAME=""
23 REMOVE_FID='s/^\[[0-9a-fx:]*\] //'
24
25 add_to_set() {
26         local old_fid="$1"
27         local path="$2"
28
29         echo -e "$old_fid $path" >> "$MIGRATED_SET"
30 }
31
32 path_in_set() {
33         local path="$1"
34
35         sed -e "$REMOVE_FID" $MIGRATED_SET | grep -q "^$path$"
36 }
37
38 old_fid_in_set() {
39         local old_fid="$1"
40
41         grep "^\\$old_fid" "$MIGRATED_SET" | head -n 1 |
42                 sed -e "$REMOVE_FID"
43 }
44
45 usage() {
46     cat -- <<USAGE 1>&2
47 usage: lfs_migrate [--dry-run] [-D] [-h] [--no-rsync|--rsync] [-q] [-R]
48                    [-s] [-v] [-y] [-0] [FILE|DIR...]
49         --dry-run only print the names of files to be migrated
50         -D do not use direct I/O to copy file contents
51         -h show this usage message
52         --no-rsync do not fall back to rsync mode even if lfs migrate fails
53         -q run quietly (don't print filenames or status)
54         --rsync force rsync mode instead of using lfs migrate
55         -R restripe file using default directory striping
56         -s skip file data comparison after migrate
57         -v show verbose debug messages
58         -y answer 'y' to usage question
59         -0 input file names on stdin are separated by a null character
60
61 The -c <stripe_count> and -S <stripe_size> options may not be specified at
62 the same time as the -R option.
63
64 The --rsync and --no-rsync options may not be specified at the same time.
65
66 If a directory is an argument, all files in the directory are migrated.
67 If no file/directory is given, the file list is read from standard input.
68
69 Any arguments that are not explicitly recognized by the script are passed
70 through to the 'lfs migrate' utility.
71
72 Examples:
73       lfs_migrate /mnt/lustre/dir
74       lfs_migrate -p newpool /mnt/lustre/dir
75       lfs find /test -O test-OST0004 -size +4G | lfs_migrate -y
76 USAGE
77     exit 1
78 }
79
80 cleanup() {
81         rm -f "$MIGRATED_SET"
82         [ -n "$NEWNAME" ] && rm -f "$NEWNAME"
83 }
84
85 trap cleanup EXIT
86
87 OPT_CHECK=true
88 OPT_DEBUG=false
89 OPT_NO_RSYNC=false
90 OPT_DRYRUN=false
91 OPT_YES=false
92 OPT_RESTRIPE=false
93 OPT_NULL=false
94 OPT_PASSTHROUGH=()
95 STRIPE_COUNT=""
96 STRIPE_SIZE=""
97 POOL=""
98 LFS_OPT_DIRECTIO=""
99
100 # Examine any long options and arguments.  getopts does not support long
101 # options, so they must be stripped out and classified as either options
102 # for the script, or passed through to "lfs migrate".
103 LONG_OPT=false
104 SHORT_OPT=false
105 OPTS=()
106
107 for f in $(seq 1 $#); do
108         arg=${!f}
109         if [ "${arg:0:2}" = "--" ]; then
110                 SHORT_OPT=false
111                 if [ "$arg" = "--block" ]; then
112                         BLOCK="$arg"
113                         OPT_YES=true
114                 elif [ "$arg" = "--non-block" ]; then
115                         BLOCK="$arg"
116                 elif [ "$arg" = "--dry-run" ]; then
117                         OPT_DRYRUN=true
118                         OPT_YES=true
119                 elif [ "$arg" = "--rsync" ]; then
120                         LFS_MIGRATE_RSYNC_MODE=true
121                 elif [ "$arg" = "--no-rsync" ]; then
122                         OPT_NO_RSYNC=true
123                         OPT_YES=true
124                 else
125                         LONG_OPT=true
126                         OPT_PASSTHROUGH+=("$arg")
127                         PREV="$arg"
128                 fi
129         elif [ "${arg:0:1}" = "-" ]; then
130                 LONG_OPT=false
131                 if [ "$arg" == "-b" ]; then
132                         BLOCK="$arg"
133                 else
134                         SHORT_OPT=true
135                         OPTS+=("$arg")
136                         PREV="$arg"
137                 fi
138         elif $LONG_OPT; then
139                 LONG_OPT=false
140                 # This will prevent long options from having file name
141                 # arguments, but allows long options with no arguments to work.
142                 if [ -f "$arg" -o -d "$arg" ]; then
143                         OPTS+=("$arg")
144                 else
145                         [ "$PREV" = "--stripe-count" ] &&
146                                 STRIPE_COUNT="$arg"
147                         [ "$PREV" = "--stripe-size" ] &&
148                                 STRIPE_SIZE="$arg"
149                         [ "$PREV" = "--pool" ] &&
150                                 POOL="$arg"
151                         OPT_PASSTHROUGH+=("$arg")
152                 fi
153         elif $SHORT_OPT; then
154                 [ "$PREV" = "-c" ] &&
155                         STRIPE_COUNT="$arg"
156                 [ "$PREV" = "-S" ] &&
157                         STRIPE_SIZE="$arg"
158                 [ "$PREV" = "-p" ] &&
159                         POOL="$arg"
160                 SHORT_OPT=false
161                 OPTS+=("$arg")
162         else
163                 OPTS+=("$arg")
164         fi
165 done
166
167 # Reset the argument list to include only the short options and file names
168 set -- "${OPTS[@]}"
169
170 while getopts ":DhlnqRsvy0" opt $*; do
171     case $opt in
172         D) LFS_OPT_DIRECTIO="-D";;
173         h) usage;;
174         l) ;; # maintained for backward compatibility
175         n) OPT_DRYRUN=true
176            OPT_YES=true
177            echo "$(basename $0): -n deprecated, use --dry-run instead" 1>&2
178            echo "$(basename $0): to specify non-block, use --non-block instead" 1>&2;;
179         q) ECHO=:;;
180         R) OPT_RESTRIPE=true;;
181         s) OPT_CHECK=false;;
182         v) OPT_DEBUG=true; ECHO=echo; OPT_PASSTHROUGH+=("-v");;
183         y) OPT_YES=true;;
184         0) OPT_NULL=true;;
185         *) # Pass through any unrecognized options to 'lfs migrate'
186            OPT_PASSTHROUGH+=("-$OPTARG")
187            if [[ ${!OPTIND:0:1} != "-" && ! -f "${!OPTIND}" &&
188                  ! -d "${!OPTIND}" ]]; then
189                 OPT_PASSTHROUGH+=("${!OPTIND}")
190                 ((OPTIND++))
191            fi;;
192     esac
193 done
194 shift $((OPTIND - 1))
195
196 if $OPT_RESTRIPE && [[ "$STRIPE_COUNT" || "$STRIPE_SIZE" ]]; then
197         echo "$(basename $0): Options -c <stripe_count> and -S <stripe_size> "\
198         "may not be specified at the same time as the -R option." 1>&2
199         exit 1
200 fi
201
202 if $LFS_MIGRATE_RSYNC_MODE && $OPT_NO_RSYNC; then
203         echo "$(basename $0): Options --rsync and --no-rsync may not be "\
204         "specified at the same time." 1>&2
205         exit 1
206 fi
207
208 if ! $OPT_YES; then
209         echo ""
210         echo "lfs_migrate is currently NOT SAFE for moving in-use files." 1>&2
211         echo "Use it only when you are sure migrated files are unused." 1>&2
212         echo "" 1>&2
213         echo "If emptying an OST that is active on the MDS, new files may" 1>&2
214         echo "use it.  To stop allocating any new objects on OSTNNNN run:" 1>&2
215         echo "  lctl set_param osp.<fsname>-OSTNNNN*.max_create_count=0'" 1>&2
216         echo "on each MDS using the OST(s) being emptied." 1>&2
217         echo -n "Continue? (y/n) "
218         read CHECK
219         [ "$CHECK" != "y" -a "$CHECK" != "yes" ] && exit 1
220 fi
221
222 # if rsync has --xattr support, then try to copy the xattrs.
223 $RSYNC --help 2>&1 | grep -q xattr && RSYNC_OPTS="$RSYNC_OPTS -X"
224 $RSYNC --help 2>&1 | grep -q acls && RSYNC_OPTS="$RSYNC_OPTS -A"
225 # If rsync copies lustre xattrs in the future, then we can skip lfs (bug 22189)
226 strings $(which $RSYNC) 2>&1 | grep -q lustre && LFS=:
227
228 # rsync creates its temporary files with lenient permissions, even if
229 # permissions on the original files are more strict. Tighten umask here
230 # to avoid the brief window where unprivileged users might be able to
231 # access the temporary file.
232 umask 0077
233
234 lfs_migrate() {
235         while IFS='' read -d '' OLDNAME; do
236                 local hlinks=()
237                 local stripe_size="$STRIPE_SIZE"
238                 local stripe_count="$STRIPE_COUNT"
239                 local parent_count=""
240                 local parent_size=""
241
242                 $ECHO -n "$OLDNAME: "
243
244                 # avoid duplicate stat if possible
245                 local nlink_type=($(LANG=C stat -c "%h %F" "$OLDNAME"   \
246                                  2> /dev/null))
247
248                 # skip non-regular files, since they don't have any objects
249                 # and there is no point in trying to migrate them.
250                 if [ "${nlink_type[1]}" != "regular" ]; then
251                         echo -e "\r\e[K$OLDNAME: not a regular file, skipped"
252                         continue
253                 fi
254
255                 # working out write perms is hard, let the shell do it
256                 if [ ! -w "$OLDNAME" ]; then
257                         echo -e "\r\e[K$OLDNAME: no write permission, skipped"
258                         continue
259                 fi
260
261                 if $OPT_DRYRUN && ! $OPT_DEBUG; then
262                         $ECHO "dry run, skipped"
263                         continue
264                 fi
265
266                 # xattrs use absolute file paths, so ensure provided path is
267                 # also absolute so that the names can be compared
268                 local oldname_absolute=$(readlink -f "$OLDNAME")
269                 if [ -z "$oldname_absolute" ]; then
270                         echo -e "\r\e[K$OLDNAME: cannot resolve full path, skipped"
271                         continue
272                 fi
273                 OLDNAME=$oldname_absolute
274
275                 # In the future, the path2fid and fid2path calls below
276                 # should be replaced with a single call to
277                 # "lfs path2links" once that command is available.  The logic
278                 # for detecting unlisted hard links could then be removed.
279                 local fid=$($LFS path2fid "$OLDNAME" 2> /dev/null)
280                 if [ $? -ne 0 ]; then
281                         echo -n "\r\e[K$OLDNAME: cannot determine FID; skipping; "
282                         echo "is this a Lustre file system?"
283                         continue
284                 fi
285
286                 if [[ ${nlink_type[0]} -gt 1 ]] || $RSYNC_WITH_HLINKS; then
287                         # don't migrate a hard link if it was already migrated
288                         if path_in_set "$OLDNAME"; then
289                                 $ECHO -e "$OLDNAME: already migrated via another hard link"
290                                 continue
291                         fi
292
293                         # There is limited space available in the xattrs
294                         # to store all of the hard links for a file, so it's
295                         # possible that $OLDNAME is part of a link set but is
296                         # not listed in xattrs and therefore not listed as
297                         # being migrated.
298                         local migrated=$(old_fid_in_set "$fid")
299                         if [ -n "$migrated" ]; then
300                                 $ECHO -e "$OLDNAME: already migrated via another hard link"
301                                 if $LFS_MIGRATE_RSYNC_MODE; then
302                                         # Only the rsync case has to relink.
303                                         # The lfs migrate case preserves the
304                                         # inode so the links are already
305                                         # correct.
306                                         [ "$migrated" != "$OLDNAME" ] &&
307                                                 ln -f "$migrated" "$OLDNAME"
308                                 fi
309                                 add_to_set "$fid" "$OLDNAME"
310                                 continue;
311                         fi
312                 fi
313
314                 if $OPT_RESTRIPE; then
315                         UNLINK=""
316                 else
317                 # if rsync copies Lustre xattrs properly in the future
318                 # (i.e. before the file data, so that it preserves striping)
319                 # then we don't need to do this getstripe/mktemp stuff.
320                         UNLINK="-u"
321
322                         [ -z "$stripe_count" ] &&
323                                 stripe_count=$($LFS getstripe -c "$OLDNAME" 2> /dev/null)
324
325                         [ -z "$stripe_size" ] &&
326                                 stripe_size=$($LFS getstripe -S "$OLDNAME" 2> /dev/null)
327
328                         [ -z "$stripe_count" -o -z "$stripe_size" ] && UNLINK=""
329                 fi
330
331                 if $OPT_DEBUG; then
332                         if $OPT_RESTRIPE; then
333                                 parent_count=$($LFS getstripe -c \
334                                                $(dirname "$OLDNAME") 2> \
335                                                /dev/null)
336                                 parent_size=$($LFS getstripe -S \
337                                               $(dirname "$OLDNAME") 2> \
338                                               /dev/null)
339                         fi
340
341                         $ECHO -n "stripe" \
342                                 "count=${stripe_count:-$parent_count}," \
343                                 "size=${stripe_size:-$parent_size}," \
344                                 "pool=${POOL:-not in a pool}: "
345                 fi
346
347                 if $OPT_DRYRUN; then
348                         $ECHO "dry run, skipped"
349                         continue
350                 fi
351
352                 if [[ "$stripe_count" && -z "$STRIPE_COUNT" ]]; then
353                         stripe_count="-c $stripe_count"
354                 else
355                         stripe_count=""
356                 fi
357                 if [[ "$stripe_size" && -z "$STRIPE_SIZE" ]]; then
358                         stripe_size="-S $stripe_size"
359                 else
360                         stripe_size=""
361                 fi
362
363                 # detect other hard links and store them on a global
364                 # list so we don't re-migrate them
365                 local mntpoint=$(df -P "$OLDNAME" |
366                                 awk 'NR==2 { print $NF; exit }')
367                 if [ -z "$mntpoint" ]; then
368                         echo -e "\r\e[K$OLDNAME: cannot determine mount point; skipped"
369                         continue
370                 fi
371                 hlinks=$($LFS fid2path "$mntpoint" "$fid" 2> /dev/null)
372                 if [ $? -ne 0 ]; then
373                         echo -e "\r\e[K$OLDNAME: cannot determine hard link paths, skipped"
374                         continue
375                 fi
376                 hlinks+=("$OLDNAME")
377
378                 # first try to migrate via Lustre tools, then fall back to rsync
379                 if ! $LFS_MIGRATE_RSYNC_MODE; then
380                         if $LFS migrate "${OPT_PASSTHROUGH[@]}" ${BLOCK} \
381                            $LFS_OPT_DIRECTIO ${stripe_count} ${stripe_size} \
382                             "$OLDNAME" &> /dev/null; then
383                                 $ECHO "done migrate"
384                                 for link in ${hlinks[*]}; do
385                                         add_to_set "$fid" "$link"
386                                 done
387                                 continue
388                         elif $OPT_NO_RSYNC; then
389                                 echo -e "\r\e[K$OLDNAME: refusing to fall back to rsync, skipped" 1>&2
390                                 continue
391                         else
392                                 $ECHO -n "falling back to rsync: "
393                                 LFS_MIGRATE_RSYNC_MODE=true
394                         fi
395                 fi
396
397                 NEWNAME=$(mktemp $UNLINK "$OLDNAME-lfs_migrate.tmp.XXXXXX")
398                 if [ $? -ne 0 -o -z "$NEWNAME" ]; then
399                         echo -e "\r\e[K$OLDNAME: can't make temp file, skipped" 1>&2
400                         continue
401                 fi
402
403                 [ "$UNLINK" ] && $LFS setstripe ${OPT_PASSTHROUGH}      \
404                                  ${stripe_count} ${stripe_size}         \
405                                  "$NEWNAME" &> /dev/null
406
407                 # we use --inplace, since we created our own temp file already
408                 if ! $RSYNC -a --inplace $RSYNC_OPTS "$OLDNAME" "$NEWNAME";then
409                         echo -e "\r\e[K$OLDNAME: copy error, exiting" 1>&2
410                         exit 4
411                 fi
412
413                 if $OPT_CHECK && ! cmp -s "$OLDNAME" "$NEWNAME"; then
414                         echo -e "\r\e[K$NEWNAME: compare failed, exiting" 1>&2
415                         exit 8
416                 fi
417
418                 if ! mv "$NEWNAME" "$OLDNAME"; then
419                         echo -e "\r\e[K$OLDNAME: rename error, exiting" 1>&2
420                         exit 12
421                 fi
422
423                 $ECHO "done migrate via rsync"
424                 for link in ${hlinks[*]}; do
425                         if [ "$link" != "$OLDNAME" ]; then
426                                 ln -f "$OLDNAME" "$link"
427                         fi
428                         add_to_set "$fid" "$link"
429                 done
430
431                 # If the number of hlinks exceeds the space in the xattrs,
432                 # when the final path is statted it will have a link count
433                 # of 1 (all other links will point to the new inode).
434                 # This flag indicates that even paths with a link count of
435                 # 1 are potentially part of a link set.
436                 [ ${#hlinks[*]} -gt 1 ] && RSYNC_WITH_HLINKS=true
437         done
438 }
439
440 if [ "$#" -eq 0 ]; then
441         if $OPT_NULL; then
442                 lfs_migrate
443         else
444                 tr '\n' '\0' | lfs_migrate
445         fi
446 else
447         while [ "$1" ]; do
448                 if [ -d "$1" ]; then
449                         $LFS find "$1" -type f -print0
450                 else
451                         echo -en "$1\0"
452                 fi
453                 shift
454         done | lfs_migrate
455 fi
456