rc.frisbee 15.7 KB
Newer Older
1
#!/bin/sh
Leigh B. Stoller's avatar
Leigh B. Stoller committed
2
#
3
# Copyright (c) 2000-2013 University of Utah and the Flux Group.
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
23
#
24
25
26
27
28
29
# Optional flag argument says "do not reboot"
#
reboot=1
if [ $# -eq 1 -a "$1" = "-noreboot" ]; then
    reboot=0
fi
Leigh B. Stoller's avatar
Leigh B. Stoller committed
30

31
32
echo "`date`: rc.frisbee starting"

33
34
35
36
37
38
#
# Amount of memory in MB to leave for everyone else in the system.  If you
# get out-of-memory or vm_pager error while running frisbee, increase this.
#
RESIDMEM=32

39
40
41
42
if [ -r /etc/emulab/paths.sh ]; then
	. /etc/emulab/paths.sh
else
	BINDIR=/etc/testbed
43
	BOOTDIR=/etc/testbed
44
45
46
47
48
49
50
	ETCDIR=/etc/testbed
fi

# Behave a little different on widearea nodes.
isrem=0
if [ -e $ETCDIR/isrem ]; then
    isrem=1
51
fi
52

53
54
55
56
57
58
59
60
61
62
63
64
65
#
# Update the MBR of the given disk to the indicated "version."
#
# XXX this is somewhat of a hack right now.  We recognize two
# versions of the MBR:
#	v1 (partition 1 size 6281352)
#	v2 (partition 1 size 12305790)
# Currently we only install a new MBR if the existing one is the
# wrong size, just in case the user has customized the boot program.
#
tweakmbr() {
    _DSK=$1
    _NEW=$2
66
    _ALWAYS=$3
67
68
69
70
71
72
73
74
75
76
77
78

    dd if=/dev/$_DSK of=/dev/null bs=512 count=1 2>/dev/null || {
	echo "WARNING: could not read from $_DSK, MBR not changed"
	return
    }

    _size=`fdisk -s $_DSK 2>/dev/null | sed -n -e 's/^ *1: *[0-9][0-9]* *\([0-9][0-9]*\).*$/\1/p'`
    case ${_size}s in
    6281352s)
	_CUR=1
	;;
    12305790s)
79
	_CUR=2
80
81
82
	;;
    s)
        # special case: no part1 so probably no MBR at all, make sure we install
83
	echo "Found no MBR on $_DSK, installing version $_NEW"
84
85
86
	_CUR=1000000
	;;
    *)
87
88
89
90
91
92
93
        if [ $_ALWAYS -eq 1 ]; then
	    echo "WARNING: overwriting unknown MBR on $_DSK with version $_NEW"
	    _CUR=1000000
	else
	    echo "WARNING: custom MBR on $_DSK, not changed"
	    return
	fi
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
    	;;
    esac

    if [ $_CUR = $_NEW ]; then
	return
    fi

    # now set it if we can
    if [ ! -r "/etc/emulab/mbr${_NEW}.dd" ]; then
	echo "WARNING: cannot find MBR version $_NEW, not installed"
	return
    fi

    echo "Installing MBR version $_NEW ..."
    dd if=/etc/emulab/mbr${_NEW}.dd of=/dev/$_DSK bs=512 count=1
}

111
find_disks() {
Mike Hibler's avatar
Mike Hibler committed
112
113
114
    _DISKS=""
    for d in `sed -n 's/^\([a-z]*[0-9][0-9]*\): [0-9][0-9]*MB/\1/p' /var/run/dmesg.boot`; do
	case $d in
115
	    ad*|da*|ar*|aacd*|amrd*|mfid*|mfisyspd*) _DISKS="$_DISKS $d"
Mike Hibler's avatar
Mike Hibler committed
116
117
	esac
    done
118

Mike Hibler's avatar
Mike Hibler committed
119
    echo $_DISKS
120
121
}

122
123
124
125
126
127
128
129
130
131
132
#
# Function to zero all potential superblocks in the DOS partitions that
# could interfere with the OSes on the image being loaded.
#
# FreeBSD 4 or 5 goes out of its way to make this hard.  In FBSD4, we
# cannot overwrite the beginning of partitions that have a legit superblock.
# In FBSD5, DOS partitions that have a zero type cannot even be accessed.
# So we have to use the whole-disk special file using offsets extracted
# via fdisk.
#
zapsuperblocks() {
Mike Hibler's avatar
Mike Hibler committed
133
    _DSK=$1
134
135
136
137
138

    #
    # Note we are not overly concerned about the consequences of misparsing
    # the fdisk output.  If we whack random blocks, it doesn't hurt anything.
    #
Mike Hibler's avatar
Mike Hibler committed
139
    offs=`fdisk -s $_DSK 2>/dev/null | sed -n -e 's/^[ 0-9]*: *\([0-9]*\).*$/\1/p'`
140
141
142
143
144

    if [ x"$offs" = x ]; then
        return
    fi

Mike Hibler's avatar
Mike Hibler committed
145
    echo -n "Invalidating old potential superblocks on $_DSK: "
146
147
    for off in $offs; do
        echo -n "$off "
Mike Hibler's avatar
Mike Hibler committed
148
	dd if=/dev/zero of=/dev/${_DSK} oseek=$off count=16 >/dev/null 2>&1 || {
149
150
151
152
153
154
155
156
	    echo "WARNING: failed to invalidate $off"
	}
    done
    echo ""

    return
}

157
158
159
160
#
# Function to load a single image on a disk
#
loadone() {
Mike Hibler's avatar
Mike Hibler committed
161
162
163
164
165
166
    _LOADINFO=$1
    _NUM=$2

    echo "Loading image #$_NUM"

    # Parse dem args
Mike Hibler's avatar
Mike Hibler committed
167
168
169
170
171
    ADDR=""
    SERVER=""
    PART=""
    PARTOS=""
    DISK=""
172
    BIOSDISK=""
Mike Hibler's avatar
Mike Hibler committed
173
174
175
    ZFILL=""
    ACPI=""
    ASF=""
176
    NOCLFLUSH=""
Mike Hibler's avatar
Mike Hibler committed
177
178
    MBRVERS=""
    PREPARE=""
179
    VGAONLY=""
180
    IMAGEID=""
181
    KEEPALIVE=""
182
    CONSOLE=""
183
    DOM0MEM=""
184

Mike Hibler's avatar
Mike Hibler committed
185
186
187
188
189
    for parm in $_LOADINFO; do
        case $parm in
	ADDR=*|\
	PART=*|\
	PARTOS=*|\
190
	SERVER=*|\
Mike Hibler's avatar
Mike Hibler committed
191
	DISK=*|\
192
	BIOSDISK=*|\
Mike Hibler's avatar
Mike Hibler committed
193
194
	ZFILL=*|\
	ACPI=*|\
195
	NOCLFLUSH=*|\
Mike Hibler's avatar
Mike Hibler committed
196
	MBRVERS=*|\
197
198
	ASF=*|\
	PREPARE=*|\
199
	VGAONLY=*|\
200
        IMAGEID=*|\
201
        KEEPALIVE=*|\
202
203
        DOM0MEM=*|\
        OSVERSION=*|\
204
	CONSOLE=*)
Mike Hibler's avatar
Mike Hibler committed
205
206
207
208
209
210
211
212
	    # XXX need to parse better, eval is dangerous!
	    eval $parm
	    ;;
        *)
	    echo "WARNING: bad loadinfo parameter \"$parm\" ignored"
	    ;;
	esac
    done
213

Mike Hibler's avatar
Mike Hibler committed
214
215
216
    #
    # Assign defaults where needed.
    #
217
    SERVER=${SERVER:-$BOSSIP}
Mike Hibler's avatar
Mike Hibler committed
218
219
    PART=${PART:-'0'}
    PARTOS=${PARTOS:-'unknown'}
220
221
222
223
    DISK=${DISK:-'ad0'}
    ZFILL=${ZFILL:-'0'}
    ACPI=${ACPI:-'unknown'}
    ASF=${ASF:-'unknown'}
224
    NOCLFLUSH=${NOCLFLUSH:-'unknown'}
225
    VGAONLY=${VGAONLY:-'unknown'}
Mike Hibler's avatar
Mike Hibler committed
226
    MBRVERS=${MBRVERS:-'1'}
227
    PREPARE=${PREPARE:-'0'}
228
    CONSOLE=${CONSOLE:-'unknown'}
229

230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
    #
    # XXX If KEEPALIVE is not explicitly set, attempt to intuit a value.
    #
    # It appears that FreeBSD 8.x's IGMP v3 implementation doesn't
    # properly sent V2 reports when it is connected to a V2-only querier
    # (switch). It insists on sending V3 reports event when the default
    # version is set to 2. So if detect that we have the newer IGMP
    # implementation, we will use the V2 keep alive mechanism in the
    # frisbee client.
    #
    if [ -z "$KEEPALIVE" ]; then
	igmpversion=`sysctl -n net.inet.igmp.default_version 2>/dev/null`
	if [ "$igmpversion"x != "x" ]; then
	    echo "WARNING: possible IGMP issues; using frisbee keep alive timer"
	    KEEPALIVE=30
	else
	    KEEPALIVE=0
	fi
    fi

250
251
252
    #
    # One of ADDR or IMAGEID must be set.
    #
253
254
255
256
257
258
259
260
261
262
263
    if [ x"$IMAGEID" != x ]; then
        ADDR=""
    	# IMAGEID=pid,gid,imagename
	pid=`echo $IMAGEID | awk -F, '{ printf $1 }'`
	name=`echo $IMAGEID | awk -F, '{ printf $3 }'`
	IMAGEID="$pid/$name"
    elif [ x"$ADDR" = x ]; then
	echo "Unable to get imageid or address for loading image"
	return 1
    fi

Mike Hibler's avatar
Mike Hibler committed
264
265
    if [ "$PART" != "0" ]; then
	SLICE="-s $PART"
266
267
268
269
270
271
272
273
274
	case $PARTOS in
	FreeBSD)
		SLICE="$SLICE -D 165"
		PTYPE=165
		;;
	OpenBSD)
		SLICE="$SLICE -D 166"
		PTYPE=166
		;;
275
	Fedora|Linux)
276
277
278
279
280
281
		SLICE="$SLICE -D 131"
		PTYPE=131
		;;
	*)
		;;
	esac
282
    fi
283

284
285
286
287
288
289
290
291
    #
    # set memory limits:
    #	allow $RESIDMEM MB for non-frisbee stuff
    #	split remaining memory (min of 2MB) between network/disk buffering
    #
    HOSTMEM=`sysctl -n hw.usermem`
    HOSTMEM=`expr $HOSTMEM / 1048576`
    if [ $HOSTMEM -ge `expr $RESIDMEM + 2` ]; then
292
	HOSTMEM=`expr $HOSTMEM - $RESIDMEM`
293
	KBYTES=`expr $HOSTMEM \* 1024`
294
	DATASEGSZ=`ulimit -d`
295
296
297
	if [ $KBYTES -gt $DATASEGSZ ]; then
	    KBYTES=$DATASEGSZ
	    HOSTMEM=`expr $KBYTES / 1024`
298
299
	    echo "WARNING: kernel limits buffering to $HOSTMEM MB"
	fi
300
	ulimit -v $KBYTES
301

302
	# Let the client split up the memory
303
	MEMARGS="-M $HOSTMEM"
304
    fi
305

306
    #
307
308
309
    # Make sure the necessary device files exist (only necessary on
    # FreeBSD 4.x).  Note that we create partition files for all slices,
    # not just slice 1, for the benefit of the slicefix script.
310
311
    #
    if [ -x /dev/MAKEDEV -a ! -e /dev/$DISK ]; then
312
	(cd /dev; ./MAKEDEV $DISK ${DISK}s2a ${DISK}s3a ${DISK}s4a)
313
    fi
314

Mike Hibler's avatar
Mike Hibler committed
315
316
317
    if [ x"$ADDR" != x ]; then
	isurl=`echo $ADDR | grep http -`
	ispath=`echo $ADDR | grep '^/' -`
318
319

	if [ x"$isurl" != x ]; then
Mike Hibler's avatar
Mike Hibler committed
320
	    echo "Need to download $ADDR"
321
322
323
324

	    isurl=1
	    if [ ! -d /images ]; then
		echo "Need to create or mount /images directory!"
325
		return 1
326
	    fi
327

328
329
330
	    #
	    # This needs a lot more work ...
	    #
Mike Hibler's avatar
Mike Hibler committed
331
	    imagefile=`echo $ADDR | sed -e 's,^http[s]*://[^/]*/,,'`
332
333
334
	    imagefile="/images/$imagefile"
	elif [ x"$ispath" != x ]; then
	    ispath=1
335

Mike Hibler's avatar
Mike Hibler committed
336
337
	    if [ ! -e $ADDR ]; then
		echo "$ADDR does not exist!"
338
		return 1
339
	    fi
Mike Hibler's avatar
Mike Hibler committed
340
	    imagefile="$ADDR"
341
	else
Mike Hibler's avatar
Mike Hibler committed
342
343
	    PORT=`echo $ADDR | awk -F: '{ printf $2 }'`
	    MCAST=`echo $ADDR | awk -F: '{ printf $1 }'`
344
345
346
	    if [ -e $BOOTDIR/myip ]; then
		MCASTIF="-i `cat $BOOTDIR/myip`"
	    else
347
		MCASTIF=""
348
349
	    fi
	    MCASTADDR="-m $MCAST -p $PORT"
350
	    IMAGEID="$MCASTIF $MCASTADDR"
351
352
	    isurl=0
	    ispath=0
353
	fi
354
    else
355
356
357
358
	#
	# Note: if you want to use broadcast rather that multicast as
	# the distribution method, add "-X bcast" to the IMAGEID= below.
	#
359
360
361
362
        IMAGEID="-B 30 -F $IMAGEID"
	isurl=0
	ispath=0
    fi
363

364
365
366
367
368
369
370
371
372
    #
    # ZFILL==1: use frisbee
    # ZFILL==2: separate disk-wipe pass (not yet implemented)
    #
    if [ "$ZFILL" != "0" ]; then
	ZFILL="-z"
    else
	ZFILL=""
    fi
373

374
375
376
377
378
379
    if [ "$KEEPALIVE" != "0" ]; then
	KA="-K $KEEPALIVE"
    else
	KA=""
    fi

380
381
382
383
384
385
386
387
388
389
390
    #
    # Make sure the write-cache is enabled on SCSI disks.  It makes a
    # huge difference.  We don't worry about data corruption in the
    # case of a crash, because we will just reload the disk again anyway
    # in that situation.
    #
    turncacheoff=0
    case $DISK in
    da*)
	if [ -x $BINDIR/camwce ] && $BINDIR/camwce on $DISK; then
	    turncacheoff=1;
391
	fi
392
393
	;;
    esac
394

395
396
397
398
399
400
    #
    # For slice images, ensure that the MBR is the correct version
    # and replace if not.
    #
    if [ $_NUM -eq 0 ]; then
	if [ "$PART" != "0" ]; then
401
	    tweakmbr $DISK $MBRVERS $PREPARE
402
	fi
403
404
405
406
        FIRSTMBR=$MBRVERS
    else
	if [ "$FIRSTMBR" != "$MBRVERS" ]; then
	    echo "MBR Mismatch: First MBR is \"$FIRSTMBR\" while image #$_NUM is \"$MBRVERS\""
407
	fi
408
    fi
409

410
411
412
413
414
415
416
    #
    # If a remote node and we have a URL, make sure that we have a place
    # to put it. Done after the MBR tweak of course. Then download the URL.
    #
    if [ $isrem -eq 1 -a $isurl -eq 1 ]; then
	echo "Downloading image \'$ADDR\' to /images directory ..."
	$BINDIR/mkextrafs.pl -c -s 4 -r $DISK /images || {
417
418
	    # XXX run growdisk to ensure we have a partition in the MBR
	    $BINDIR/growdisk -vW /dev/$DISK >/dev/null 2>&1
419
420
421
422
423
424
425
426
	    $BINDIR/mkextrafs.pl -n -f -s 4 -r $DISK /images || {
		echo "Could not create /images partition"
		return 1
	    }
	}
	wget -nv -N -P /images "$ADDR"
	wstat=$?
	case $wstat in
427
	0)
428
	    echo "wget succeeded getting the image"
429
	    ;;
430
	*)
431
432
	    echo "wget failed, status $wstat"
	    return 1
433
	    ;;
434
	esac
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
    fi

    #
    # If not zeroing the disk and we are loading a full disk image
    # we need to ensure that we at least invalidate any old superblocks
    # that might leak through (most likely in partition 4 which isn't
    # touched by our current image).  We do this before running frisbee
    # so that any legit filesystems loaded from the image work.
    #
    # Since we do it before frisbee, we are counting on the current
    # MBR being the same as the MBR being layed down.  While not
    # a reasonable assumption in general, it mostly works in our
    # environment and at least won't hurt anything if not true.
    #
    if [ $PREPARE -eq 1 -o \
         \( $isrem -eq 0 -a x"$ZFILL" = x -a "$PART" = "0" \) ]; then
	zapsuperblocks $DISK
    fi

    if [ x"$imagefile" != x ]; then
455
	echo "`date`: Running /usr/local/bin/imageunzip -o -O -W 32 $ZFILL $imagefile /dev/${DISK}s${PART}"
456
	/usr/local/bin/imageunzip -o -O -W 32 $ZFILL $imagefile /dev/${DISK}s${PART}
457
    else
458
	echo "`date`: Running $BINDIR/frisbee -S $SERVER $MEMARGS $KA $ZFILL $SLICE $IMAGEID /dev/$DISK"
459
	$BINDIR/frisbee -S $SERVER $MEMARGS $KA $ZFILL $SLICE $IMAGEID /dev/$DISK
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
    fi
    fstat=$?

    #
    # If we mounted a partition from the disk to store the image,
    # we must unmount it now so that slicefix and others don't fail
    # due to an in-use partition.
    #
    if [ $isrem -eq 1 -a $isurl -eq 1 ]; then
	umount /images || {
	    echo "WARNING: could not unmount /images"
	}
    fi

    #
    # Turn the cache back off if we turned it on.
    # Is this sufficient to ensure the cache gets flushed?
    #
    if [ $turncacheoff -eq 1 ]; then
	$BINDIR/camwce off $DISK
480
    fi
481
482
483

    case $fstat in
    0)
484
	echo "`date`: Adjusting slice-related files"
485
486
	export SLICEFIX_ACPI=$ACPI
	export SLICEFIX_ASF=$ASF
487
	export SLICEFIX_NOCLFLUSH=$NOCLFLUSH
488
	export SLICEFIX_VGAONLY=$VGAONLY
489
490
	export SLICEFIX_CONSOLE=$CONSOLE
	export SLICEFIX_BIOSDISK=$BIOSDISK
491
	export SLICEFIX_DOM0MEM=$DOM0MEM
492
	$BINDIR/slicefix $PART $DISK
493
	echo "`date`: Image #$_NUM load complete"
494
495
496
497
498
499
	return 0
	;;
    *)
	echo "Frisbee run failed, status $fstat"
	;;
    esac
500
501
502
503
504
505
506
507
508
509
    return 1
}

$BINDIR/tmcc state RELOADSETUP

BOSSINFO=`$BINDIR/tmcc bossinfo`
STATUS=`$BINDIR/tmcc status`

BOSSIP=`echo $BOSSINFO | awk '{ print $2 }'`

510
511
512
513
NTPIP=`grep -w ntp1 /etc/hosts 2>/dev/null | awk '{ print $1 }'`
if [ -z "$NTPIP" ]; then
    NTPIP=$BOSSIP
fi
514
if [ -x /usr/sbin/ntpdate ]; then
515
	/usr/sbin/ntpdate -b $NTPIP >/dev/null 2>&1
516
517
518
519
520
fi

# Enable IPoD
if [ -r $BINDIR/rc.ipod ]; then
    . $BINDIR/rc.ipod
521
fi
522
523

#
Mike Hibler's avatar
Mike Hibler committed
524
525
# Assign each line (one image) to one of the positional parameters.
# This is done by setting IFS to a newline and using set.
526
527
# XXX there must be a better way to do this!
#
Mike Hibler's avatar
Mike Hibler committed
528
OIFS="$IFS"
529
530
531
IFS='
'
set -- `$BINDIR/tmcc loadinfo`
Mike Hibler's avatar
Mike Hibler committed
532
IFS="$OIFS"
533
534
535
536
537
538
539
if [ "$1"x = x ]; then
    echo "No load information for node"
    exit 1
fi

$BINDIR/tmcc state RELOADING

540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# HACK ALERT: If we're reloading we need to zap the superblocks and
# MBRs of any other disks in the system.  This is to prevent Linux from
# finding an old filesystem with the same label or UUID and mounting
# that instead of the right one.  We skip the disks that are mounted
# and the disk we're going to write to.
# DOUBLE HACK ALERT: Changed this to zap all disks to avoid having
# to figure out what the other disks are when loading multiple images.
# Since a new MBR will be laid down anyway there is no harm in doing
# this as long as we are sure we are in the reloading experiment.
case $STATUS in
	*ALLOCATED=emulab-ops/reloading*)
		disks=`find_disks`
		for d in $disks; do
			#[ $d = $DISK ] && continue
			mount | grep "^/dev/$d" > /dev/null && continue
			zapsuperblocks $d
			echo "Invalidating MBR on $d"
557
			dd if=/dev/zero of=/dev/$d bs=512 count=16
558
		done
559
560
561
562
563
564
565
566
567
568

		# XXX tmp, gather HW info (including disk speed tests)
		if [ -x $BINDIR/rc.nodecheck ]; then
		    $BINDIR/rc.nodecheck boot gather
		fi

		## if we have nodecheck, perform disk speed tests here
		#if [ -x $BINDIR/rc.nodecheck ]; then
		#    true
		#fi
569
570
571
		;;
esac

572
573
574
575
576
577
578
579
580
#
# Load each image in turn.
# If a load fails, we exit non-zero so that the rc script will drop into
# single-user mode.  If all loads succeed we either reboot or continue with
# the rc script as desired by the caller.
#
NUM=0
while [ "$1"x != x ]; do
    loadone "$1" $NUM || {
581
	echo "`date`: Failed to load disk, dropping to login prompt"
582
583
584
585
586
        exit 1
    }
    shift
    NUM=`expr $NUM + 1`
done
587
echo "`date`: Frisbee run(s) finished"
588

589
echo "`date`: Resizing final disk partition"
590
$BINDIR/growdisk -vW /dev/$DISK
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608

#
# If requested to reboot, do so.
#
# Note: there is a race condition with stated here.
# If we reboot immediately after sending RELOADDONE,
# it is possible that, under heavy server load, we will
# finish the reboot and reach the bootinfo stage before
# stated gets and processes our RELOADDONE.  So now we
# wait around after sending the RELOADDONE.  stated should
# force us to reboot when the transition takes place.
# For backward compatibility we use a new state: RELOADDONEV2.
# For paranoia we just wait around for awhile and then
# reboot anyway, just in case stated's reboot fails for
# some reason.
#
if [ $reboot -eq 1 ]; then
    $BINDIR/tmcc state RELOADDONEV2
609
    echo "`date`: Waiting for server to reboot us ..."
610
611
612
    if [ $isrem -eq 1 ]; then
	sleep 30
    else
613
	sleep 300
614
    fi
615
    echo "`date`: No response from server, rebooting myself ..."
616
617
618
619
620
621
    /sbin/reboot
    sleep 100
else
    $BINDIR/tmcc state RELOADDONE
fi

622
echo "`date`: rc.frisbee finished"
623
624

exit 0