summaryrefslogtreecommitdiffstats
path: root/src/rgw/rgw-restore-bucket-index
blob: a38d97068a231a16cd8162bb4a604c711bbac119 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
#!/usr/bin/env bash

# version 2024-03-11

# rgw-restore-bucket-index is an EXPERIMENTAL tool to use in case
# bucket index entries for objects in the bucket are somehow lost. It
# is expected to be needed and used rarely. A bucket name is provided
# and the data pool for that bucket is scanned for all head objects
# matching the bucket's marker. The rgw object name is then extracted
# from the rados object name, and `radosgw-admin bucket reindex ...`
# is used to add the bucket index entry.
#
# Because this script must process json objects, the `jq` tool must be
# installed on the system.
#
# Usage: see the usage() function below for details
#
# This tool is designed to be interactive, allowing the user to
# examine the list of objects to be reindexed before
# proceeding. However, if the "--proceed" option is provided, the
# script will not prompt the user and simply proceed.

trap "clean ; exit 1" TERM
export TOP_PID=$$

# IMPORTANT: affects order produced by 'sort' and 'ceph-diff-sorted'
# relies on this ordering
export LC_ALL=C

# whether or not the temporary files are cleaned on completion
export clean_temps=1

# make explicit tabs easier to see in code
export TAB="	"


#
# helper functions
#

super_exit() {
    kill -s TERM -${TOP_PID}
}

usage() {
  >&2 cat << EOF

Usage: $0 -b <bucket-name> [-l <rados-ls-file>] [-p <pool>] [-y]

where:
  -b <bucket-name>     Required - name of the bucket to operate on
  -l <rados-ls-file>   Optional - file containing the output of 'rados ls -p <pool>'
  -r <realm-name>      Optional - specify the realm if not applying to the default realm"
  -g <zonegroup-name>  Optional - specify the zonegroup if not applying to the default zonegroup"
  -z <zone-name>       Optional - specify the zone if not applying to the default zone"
  -p <pool>            Optional - data pool; if not provided will be inferred from bucket and zone information
  -t <tmp-dir>         Optional - specify a directory for temporary files other than the default of /tmp
  -y                   Optional - proceed with restoring without confirming with the user
                       USE WITH CAUTION.
  -d                   Optional - run with debugging output
EOF
  super_exit
}

# cleans all temporary files
clean() {
  if [ "$clean_temps" == 1 ] ;then
    rm -f $bkt_entry $temp_file_list \
       $zone_info $olh_info_enc $olh_info_json
  fi
}

test_temp_space() {
    # use df to determine percentage of data and inodes used; strip
    # out spaces and percent signs from the output, so we just have a
    # number from 0 to 100
    pcent=$(df -k $temp_dir --output=pcent | tail -1 | sed 's/[ %]//g')
    ipcent=$(df -k $temp_dir --output=ipcent | tail -1 | sed 's/[ %]//g')
    if [ "$pcent" -eq 100 -o "$ipcent" -eq 100 ] ;then
	>&2 echo "ERROR: the temporary directory's partition is full, preventing continuation."
	>&2 echo "    NOTE: the temporary directory is \"${temp_dir}\"."
	>&2 df -k $temp_dir -h --output="target,used,avail,pcent,iused,iavail,ipcent"
	>&2 echo "    NOTE: cleaning temporary files before exiting...."
	super_exit
    fi
}

# number of seconds for a bucket index pending op to be completed via
# dir_suggest mechanism
export pending_op_secs=120

#
# sanity checks
#

export exit_code=0

tool_list="radosgw-admin ceph-dencoder jq"
for t in $tool_list ;do
    if which $t > /dev/null ;then
	:
    else
	echo "ERROR: must have tool \`$t\` installed and on \$PATH for operation."
	exit_code=1
    fi
done
if [ "$exit_code" -ne 0 ] ;then
    exit $exit_code
fi

dencode_list="RGWOLHInfo"
for t in $dencode_list ;do
    if ceph-dencoder list_types | grep -q $t ;then
	:
    else
	echo "ERROR: ceph-dencoder lacking module to decode ${t}."
	exit_code=1
    fi
done
if [ "$exit_code" -ne 0 ] ;then
    exit $exit_code
fi

# Determines the name of the data pool. Expects the optional
# command-line argument to appear as $1 if there is one. The
# command-line has the highest priority, then the "explicit_placement"
# in the bucket instance data, and finally the "placement_rule" in the
# bucket instance data.
get_pool() {
  # explicit_placement
  expl_pool=$(jq -r '.data.bucket_info.bucket.explicit_placement.data_pool' $bkt_inst)
  if [ -n "$expl_pool" ] ;then
    echo "$expl_pool"
    exit 0
  fi

  # placement_rule
  plmt_rule=$(jq -r '.data.bucket_info.placement_rule' $bkt_inst)
  plmt_pool=$(echo "$plmt_rule" | awk -F / '{print $1}')
  plmt_class=$(echo "$plmt_rule" | awk -F / '{print $2}')
  if [ -z "$plmt_class" ] ;then
    plmt_class=STANDARD
  fi

  radosgw-admin zone get $multisite_spec >$zone_info 2>/dev/null
  test_temp_space
  pool=$(jq -r ".placement_pools [] | select(.key | contains(\"${plmt_pool}\")) .val .storage_classes.${plmt_class}.data_pool" $zone_info)

  if [ -z "$pool" ] ;then
      echo ERROR: unable to determine pool.
      super_exit
  fi
  echo "$pool"
}

export bucket=""
export temp_dir=/tmp
pool=""
multisite_spec=""
lsoutput=""
debug=0

while getopts "b:l:p:r:g:z:ydt:" o; do
    case "${o}" in
	b)
	    bucket="${OPTARG}"
	    ;;
	l)
	    if [ -e "${OPTARG}" ]; then
		lsoutput="${OPTARG}"
	    else
		echo 
		echo "ERROR: Provided 'rados ls' output file name does not exist. ${OPTARG}"
		exit 1
	    fi
	    ;;
	p)
	    pool="${OPTARG}"
	    ;;
	r)
	    multisite_spec="$multisite_spec --rgw-realm=${OPTARG}"
	    ;;
	g)
	    multisite_spec="$multisite_spec --rgw-zonegroup=${OPTARG}"
	    ;;
	z)
	    multisite_spec="$multisite_spec --rgw-zone=${OPTARG}"
	    ;;
	y)
	    echo "NOTICE: This tool is currently considered EXPERIMENTAL."
	    proceed=1
	    ;;
	d)
	    echo Debugging On
	    debug=1
	    clean_temps=0
	    ;;
	t)
	    temp_dir="${OPTARG}"
	    ;;
	*)
	    echo
	    usage
	    exit 1 # useage should exit also
	    ;;
    esac
done
shift $((OPTIND-1))

if [ "$debug" == 1 ] ;then
    export debugging_rgwadmin=" --debug-rgw=20 --debug-ms=20 "
else
    export debugging_rgwadmin=" 2>/dev/null "
fi

if [ ! -d "$temp_dir" ] ;then
    echo "ERROR: temporary directory $temp_dir is not a directory"
    exit 1
fi

# temporary files
export bkt_entry=${temp_dir}/rgwrbi-bkt-entry.$$
export bkt_inst=${temp_dir}/rgwrbi-bkt-inst.$$
export marker_ls=${temp_dir}/rgwrbi-marker-ls.$$
export obj_list=${temp_dir}/rgwrbi-object-list.$$
export obj_list_ver=${temp_dir}/rgwrbi-object-list-ver.$$
export zone_info=${temp_dir}/rgwrbi-zone-info.$$
export olh_info_enc=${temp_dir}/rgwrbi-olh-info-enc.$$
export olh_info_json=${temp_dir}/rgwrbi-olh-info-json.$$
export debug_log=${temp_dir}/rgwrbi-debug-log.$$

export temp_file_list="$bkt_entry $bkt_inst $marker_ls $obj_list $obj_list_ver $zone_info $olh_info_enc $olh_info_json"

# special code path for versioned buckets
handle_versioned() {
    while read o ;do

	# determine object and locator for OLH
	olh_line=$(awk "/_$o(\t.*)?$/"' && !/__:/' $marker_ls)
	olh_obj=$(echo "$olh_line" | sed 's/\t.*//') # obj everything before tab
	olh_loc=$(echo "$olh_line" | sed 's/^.*\t\(.*\)/\1/') # locator everything after tab

	# process OLH object; determine final instance or delete-marker
	rados -p $pool getxattr $olh_obj user.rgw.olh.info --object-locator "$olh_loc" >$olh_info_enc
	test_temp_space
	ceph-dencoder import $olh_info_enc type RGWOLHInfo decode dump_json >$olh_info_json
	test_temp_space
	last_instance=$(jq -r ".target.key.instance" $olh_info_json)
	if [ -z "$last_instance" ] ;then
	    # filters out entry without an instance
	    filter_out_last_instance="${marker}_[^_]"
	else
	    # filters out entry with an instance
	    filter_out_last_instance="$last_instance"
	fi

	if [ "$debug" == 1 ] ;then
	    echo "working on versioned $o"
	    echo "last instance is $last_instance"
	    echo "filter_out_last_instance is $filter_out_last_instance"
	fi >>$debug_log
	test_temp_space

	# we currently don't need the delete marker, but we can have access to it
	# delete_marker=$(jq -r ".removed" $olh_info_json) # true or false

	IFS='\t' grep -E "(__:.*[^_])?_$o(\t.*)?$" $marker_ls | # versioned head objects
	    while read obj loc ;do
		if [ "$debug" == 1 ] ;then
		    echo "obj=$obj ; loc=$loc" >>$debug_log
		fi
		test_temp_space
		rados -p $pool stat2 $obj --object-locator "$loc"
	    done | # output of stat2, which includes mtime
	    sort -T $temp_dir -k 3 | # stat2 but sorted by mtime earlier to later
	    grep -v -e "$filter_out_last_instance" | # remove the final instance in case it's not last

	    # sed 1) removes pool and marker, 2) removes indicator of
	    # version id, 3) removes obj name including escaped
	    # leading underscores, 4) inserts object name and tab at
	    # front of line, and 5) removes trailing tab. Note: after
	    # 3) the line will be empty or contain a version id, so 5)
	    # is for when that line is empty and 4 inserts a tab
	    sed -E \
		-e "s/.*${marker}//" \
		-e 's/^__://' \
		-e "s/_+${o}.*//" \
		-e "s/^/${o}\t/"
	echo "${o}${TAB}$last_instance" # now add the final instance; could be delete marker
    done <$obj_list 2>/dev/null | sed 's/\t$//' >$obj_list_ver
    test_temp_space

} # handle_versioned

if [ -z "$bucket" ]; then
  echo
  echo "ERROR: Bucket option ( -b ) is required."
  usage
fi

# read bucket entry metadata
eval "radosgw-admin metadata get bucket:$bucket $debugging_rgwadmin $multisite_spec >$bkt_entry"
test_temp_space
export marker=$(jq -r ".data.bucket.marker" $bkt_entry)
export bucket_id=$(jq -r ".data.bucket.bucket_id" $bkt_entry)
if [ -z "$marker" -o -z "$bucket_id" ] ;then
    echo "ERROR: Unable to read entry-point metadata for bucket \"$bucket\"."
    echo "       Please make sure that <bucket-name> is correct and, if not using"
    echo "       the defaults, that <realm-name>, <zonegroup-name>, and/or"
    echo "       <zone-name> are correctly specified."
    clean
    usage
    exit 1
fi

echo marker is $marker
echo bucket_id is $bucket_id

# read bucket instance metadata
eval "radosgw-admin metadata get bucket.instance:${bucket}:$bucket_id $multisite_spec $debugging_rgwadmin >$bkt_inst"
test_temp_space

# examine number of bucket index shards
num_shards=$(jq ".data.bucket_info.num_shards" $bkt_inst)
echo number of bucket index shards is $num_shards

# determine data pool
if [ -z "$pool" ]; then
  pool=$(get_pool)
fi
echo data pool is $pool

# handle versioned buckets
export bkt_flags=$(jq ".data.bucket_info.flags" $bkt_inst)
if [ -z "$bkt_flags" ] ;then
    echo "ERROR: unable to read instance metadata for bucket \"$bucket\"."
    clean
    exit 1
fi

# search the data pool for all of the head objects that begin with the
# marker that are not in namespaces (indicated by an extra underscore
# and colon) and then strip away all but the rgw object name,
# including optional locator that follows a tab. Initial underscores
# are quoted with an underscore, so swap the first double with a
# single.
if [ -z "$lsoutput" ]; then
  ( rados -p $pool ls | grep "^${marker}_" >$marker_ls ) 2>/dev/null
  test_temp_space
else
  ( grep "^${marker}_" "${lsoutput}" >$marker_ls ) 2>/dev/null
  test_temp_space
fi

( sed -E 's/\t.*//' $marker_ls | grep -v -E "^${marker}__[^_]+_" | sed -E "s/^${marker}_(.*)/\1/" | sed 's/^__/_/' >$obj_list ) 2>/dev/null
test_temp_space

# mask bit indicating it's a versioned bucket
export is_versioned=$(( $bkt_flags & 2))
export is_suspended=$(( $bkt_flags & 4))
if [ "$is_versioned" -ne 0 ] ;then
    echo "INFO: this bucket appears to be versioned."
    handle_versioned
else
    # no additional versioned handling, so just hard link
    ln $obj_list $obj_list_ver
fi

# handle the case where the resulting object list file is empty
if [ -s $obj_list_ver ] ;then
    :
else
    echo "NOTICE: No head objects for bucket \"$bucket\" were found in pool \"$pool\", so nothing was recovered."
    clean
    exit 0
fi

if [ -z "$proceed" ] ;then
    # warn user and get permission to proceed
    echo "NOTICE: This tool is currently considered EXPERIMENTAL."
    echo "The list of objects that we will attempt to restore can be found in \"$obj_list_ver\"."
    echo "Please review the object names in that file (either below or in another window/terminal) before proceeding."
    while true ; do
	read -p "Type \"proceed!\" to proceed, \"view\" to view object list, or \"q\" to quit: " action
	if [ "$action" == "q" ] ;then
	    echo "Exiting..."
	    clean
	    exit 0
	elif [ "$action" == "view" ] ;then
	    echo "Viewing..."
	    less $obj_list_ver
	elif [ "$action" == "proceed!" ] ;then
	    echo "Proceeding..."
	    break
	else
	    echo "Error: response \"$action\" is not understood."
	fi
    done
fi

eval "radosgw-admin object reindex --bucket=$bucket --objects-file=$obj_list_ver $multisite_spec --yes-i-really-mean-it $debugging_rgwadmin"

clean
echo Done