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