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