UX: Suggest commands to run in order to free up disk space.
[discourse_docker.git] / launcher
1 #!/bin/bash
2
3 usage () {
4 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs]"
5 echo "Commands:"
6 echo " start: Start/initialize a container"
7 echo " stop: Stop a running container"
8 echo " restart: Restart a container"
9 echo " destroy: Stop and remove a container"
10 echo " enter: Use nsenter to enter a container"
11 echo " logs: Docker logs for container"
12 echo " bootstrap: Bootstrap a container for the config based on a template"
13 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
14 echo " cleanup: Remove all containers that have stopped for > 24 hours"
15 echo
16 echo "Options:"
17 echo " --skip-prereqs Don't check prerequisites or resource requirements"
18 echo " --docker-args Extra arguments to pass when running docker"
19 exit 1
20 }
21
22 command=$1
23 config=$2
24 opt=$3
25
26 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
27 re='[A-Z/ !@#$%^&*()+~`=]'
28 if [[ $config =~ $re ]];
29 then
30 echo
31 echo "ERROR: Config name must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
32 echo
33 exit 1
34 fi
35
36 cd "$(dirname "$0")"
37
38 docker_min_version='1.6.0'
39 docker_rec_version='1.6.0'
40
41 config_file=containers/"$config".yml
42 cidbootstrap=cids/"$config"_bootstrap.cid
43 local_discourse=local_discourse
44 image=discourse/discourse:1.0.17
45 docker_path=`which docker.io || which docker`
46 template_path=samples/standalone.yml
47 changelog=/tmp/changelog # used to test whether sed did anything
48
49 if [ "${SUPERVISED}" = "true" ]; then
50 restart_policy="--restart=no"
51 attach_on_start="-a"
52 attach_on_run="-a stdout -a stderr"
53 else
54 attach_on_run="-d"
55 fi
56
57 if [ -n "$DOCKER_HOST" ]; then
58 docker_ip=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
59 elif [ -x "$(which ip 2>/dev/null)" ]; then
60 docker_ip=`ip addr show docker0 | \
61 grep 'inet ' | \
62 awk '{ split($2,a,"/"); print a[1] }';`
63 else
64 docker_ip=`ifconfig | \
65 grep -B1 "inet addr" | \
66 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
67 grep docker0 | \
68 awk -F: '{ print $3 }';`
69 fi
70
71 compare_version() {
72 declare -a ver_a
73 declare -a ver_b
74 IFS=. read -a ver_a <<< "$1"
75 IFS=. read -a ver_b <<< "$2"
76
77 while [[ -n $ver_a ]]; do
78 if (( ver_a > ver_b )); then
79 return 0
80 elif (( ver_b > ver_a )); then
81 return 1
82 else
83 unset ver_a[0]
84 ver_a=("${ver_a[@]}")
85 unset ver_b[0]
86 ver_b=("${ver_b[@]}")
87 fi
88 done
89 return 1 # They are equal
90 }
91
92
93 install_docker() {
94
95 echo "Docker is not installed, you will need to install Docker in order to run Discourse"
96 echo "Please visit https://docs.docker.com/installation/ for instructions on how to do this for your system"
97 echo
98 echo "If you are running a recent Ubuntu Server, try the following:"
99 echo "sudo apt-get install docker-engine"
100
101 exit 1
102 }
103
104 prereqs() {
105
106 if [ -z $docker_path ]; then
107 install_docker
108 fi
109
110 # 1. docker daemon running?
111 # we send stderr to /dev/null cause we don't care about warnings,
112 # it usually complains about swap which does not matter
113 test=`$docker_path info 2> /dev/null`
114 if [[ $? -ne 0 ]] ; then
115 echo "Cannot connect to the docker daemon - verify it is running and you have access"
116 exit 1
117 fi
118
119 # 2. running aufs or btrfs
120 test=`$docker_path info 2> /dev/null | grep 'Driver: '`
121 if [[ "$test" =~ [aufs|btrfs|zfs|overlay] ]] ; then : ; else
122 echo "Your Docker installation is not using a supported filesystem if we were to proceed you may have a broken install."
123 echo "aufs is the recommended filesystem you should be using (zfs/btrfs and overlay may work as well)"
124 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the driver"
125 echo ""
126 echo "If you wish to continue anyway using your existing unsupported filesystem"
127 echo "read the source code of launcher and figure out how to bypass this."
128 exit 1
129 fi
130
131 # 3. running recommended docker version
132 test=($($docker_path --version)) # Get docker version string
133 test=${test[2]//,/} # Get version alone and strip comma if exists
134
135 # At least minimum version
136 if compare_version "${docker_min_version}" "${test}"; then
137 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
138 exit 1
139 fi
140
141 # Recommend best version
142 if compare_version "${docker_rec_version}" "${test}"; then
143 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
144 fi
145
146 # 4. discourse docker image is downloaded
147 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
148
149 if [ -z "$test" ]; then
150 echo
151 echo "WARNING: We are about to start downloading the Discourse base image"
152 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
153 echo
154 echo "Please be patient"
155 echo
156
157 fi
158
159 # 5. able to attach stderr / out / tty
160 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
161 if [[ "$test" =~ "working" ]] ; then : ; else
162 echo "Your Docker installation is not working correctly"
163 echo
164 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
165 exit 1
166 fi
167
168 }
169
170 check_resources() {
171 # Memory
172 resources="ok"
173 avail_mem="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
174 if [ "$avail_mem" -lt 900 ]; then
175 resources="insufficient"
176 echo "WARNING: You do not appear to have sufficient memory to run Discourse."
177 echo
178 echo "Your system may not work properly, or future upgrades of Discourse may"
179 echo "not complete successfully."
180 echo
181 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#create-new-cloud-server"
182 elif [ "$avail_mem" -lt 1800 ]; then
183 total_swap="$(LANG=C free -m | grep ^Swap: | awk '{print $2}')"
184 if [ "$total_swap" -lt 1000 ]; then
185 resources="insufficient"
186 echo "WARNING: You must have at least 1GB of swap when running with less"
187 echo "than 2GB of RAM."
188 echo
189 echo "Your system may not work properly, or future upgrades of Discourse may"
190 echo "not complete successfully."
191 echo
192 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#set-up-swap-if-needed"
193 fi
194 fi
195
196 # Disk space
197 free_disk="$(df /var | tail -n 1 | awk '{print $4}')"
198 if [ "$free_disk" -lt 5000 ]; then
199 resources="insufficient"
200 echo "WARNING: You must have at least 5GB of *free* disk space to run Discourse."
201 echo
202 echo "Insufficient disk space may result in problems running your site, and may"
203 echo "not even allow Discourse installation to complete successfully."
204 echo
205 echo "Please free up some space, or expand your disk, before continuing."
206 echo
207 echo "Run \`apt-get autoremove && apt-get autoclean\` to clean up unused packages and \`./launcher cleanup\` to remove stale Docker containers."
208 exit 1
209 fi
210
211 if [ -t 0 ] && [ "$resources" != "ok" ]; then
212 echo
213 read -p "Press ENTER to continue, or Ctrl-C to exit and give your system more resources"
214 fi
215 }
216
217 check_ports() {
218 local valid=$(netstat -tln | awk '{print $4}' | grep ":${1}\$")
219
220 if [ -n "$valid" ]; then
221 echo "Launcher has detected that port ${1} is in use."
222 echo ""
223 echo "If you are trying to run Discourse simultaneously with another web server like Apache or nginx, you will need to bind to a different port."
224 echo "See https://meta.discourse.org/t/17247 for help."
225 echo "To continue anyway, re-run Launcher with --skip-prereqs"
226 exit 1
227 fi
228 }
229
230 if [ "$opt" != "--skip-prereqs" ] ; then
231 prereqs
232 fi
233
234 if [ "$opt" == "--docker-args" ] ; then
235 user_args=$4
236 else
237 user_args=""
238 fi
239
240 host_run() {
241 read -r -d '' env_ruby << 'RUBY'
242 require 'yaml'
243
244 input = STDIN.readlines.join
245 yaml = YAML.load(input)
246
247 if host_run = yaml['host_run']
248 params = yaml['params'] || {}
249 host_run.each do |run|
250 params.each do |k,v|
251 run = run.gsub("$#{k}", v)
252 end
253 STDOUT.write "#{run}--SEP--"
254 end
255 end
256 RUBY
257
258 host_run=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
259
260 while [ "$host_run" ] ; do
261 iter=${host_run%%--SEP--*}
262 echo
263 echo "Host run: $iter"
264 $iter || exit 1
265 echo
266 host_run="${host_run#*--SEP--}"
267 done
268 }
269
270
271 set_volumes() {
272 volumes=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
273 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
274 }
275
276 set_links() {
277 links=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
278 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
279 }
280
281 set_template_info() {
282
283 templates=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
284 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
285
286 arrTemplates=(${templates// / })
287 config_data=$(cat $config_file)
288
289 input="hack: true"
290
291 for template in "${arrTemplates[@]}"
292 do
293 [ ! -z $template ] && {
294 input="$input _FILE_SEPERATOR_ $(cat $template)"
295 }
296 done
297
298 # we always want our config file last so it takes priority
299 input="$input _FILE_SEPERATOR_ $config_data"
300
301 read -r -d '' env_ruby << 'RUBY'
302 require 'yaml'
303
304 input=STDIN.readlines.join
305 # default to UTF-8 for the dbs sake
306 env = {'LANG' => 'en_US.UTF-8'}
307 input.split('_FILE_SEPERATOR_').each do |yml|
308 yml.strip!
309 begin
310 env.merge!(YAML.load(yml)['env'] || {})
311 rescue Psych::SyntaxError => e
312 puts e
313 puts "*ERROR."
314 rescue => e
315 puts yml
316 p e
317 end
318 end
319 puts env.map{|k,v| "-e\n#{k}=#{v}" }.join("\n")
320 RUBY
321
322 raw=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
323
324 env=()
325 ok=1
326 while read i; do
327 if [ "$i" == "*ERROR." ]; then
328 ok=0
329 elif [ -n "$i" ]; then
330 env[${#env[@]}]=$i
331 fi
332 done <<< "$raw"
333
334 if [ "$ok" -ne 1 ]; then
335 echo "${env[@]}"
336 echo "YAML syntax error. Please check your /var/discourse/containers/*.yml config files."
337 exit 1
338 fi
339 }
340
341 if [ -z $docker_path ]; then
342 install_docker
343 fi
344
345 [ "$command" == "cleanup" ] && {
346 echo
347 echo "The following command will"
348 echo "- Delete all docker images for old containers"
349 echo "- Delete all stopped and orphan containers"
350 echo
351 read -p "Are you sure (Y/n): " -n 1 -r && echo
352 if [[ $REPLY =~ ^[Yy]$ || ! $REPLY ]]
353 then
354 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
355 echo "Starting Cleanup (bytes free $space)"
356
357 STATE_DIR=./.gc-state scripts/docker-gc
358
359 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
360 echo "Finished Cleanup (bytes free $space)"
361
362 else
363 exit 1
364 fi
365 exit 0
366 }
367
368 [ $# -lt 2 ] && {
369 usage
370 }
371
372 if [[ ! -e $config_file && $command -ne "memconfig" ]]
373 then
374 echo "Config file was not found, ensure $config_file exists"
375 echo ""
376 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
377 exit 1
378 fi
379
380 docker_version=($($docker_path --version))
381 docker_version=${test[2]//,/}
382 restart_policy=${restart_policy:---restart=always}
383
384 set_existing_container(){
385 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
386 }
387
388 run_stop() {
389
390 set_existing_container
391
392 if [ ! -z $existing ]
393 then
394 (
395 set -x
396 $docker_path stop -t 10 $config
397 )
398 else
399 echo "$config was not started !"
400 exit 1
401 fi
402 }
403
404 set_run_image() {
405 run_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
406 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
407
408 if [ -z "$run_image" ]; then
409 run_image="$local_discourse/$config"
410 fi
411 }
412
413 set_boot_command() {
414 boot_command=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
415 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
416
417 if [ -z "$boot_command" ]; then
418
419 no_boot_command=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
420 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
421
422 if [ -z "$no_boot_command" ]; then
423 boot_command="/sbin/boot"
424 fi
425 fi
426 }
427
428 scale_ram_and_cpu() {
429
430 # get free mem
431 avail_mem="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
432 avail_gb=`expr $(($avail_mem / 950))`
433 avail_cores=`grep -c processor /proc/cpuinfo`
434 echo "Found ${avail_gb}GB of memory and $avail_cores CPU cores"
435
436 # set db_shared_buffers: "128MB" (1GB) or 256MB * GB
437 if [ "$avail_gb" -eq "1" ]
438 then
439 db_shared_buffers="128"
440 else
441 db_shared_buffers=`expr $avail_gb \* 256`
442 fi
443 sed -i -e "s/^ #db_shared_buffers:.*/ db_shared_buffers: \"${db_shared_buffers}MB\"/w $changelog" $config_file
444 if [ -s $changelog ]
445 then
446 echo "setting db_shared_buffers = ${db_shared_buffers}MB based on detected CPU/RAM"
447 rm $changelog
448 fi
449
450
451 # set UNICORN_WORKERS: 2*GB or 2*cores (the same on DO)
452 if [ "$avail_gb" -le "2" ]
453 then
454 unicorn_workers=`expr $avail_gb \* 2`
455 else
456 unicorn_workers=`expr \$avail_cores \* 2`
457 fi
458 sed -i -e "s/^ #UNICORN_WORKERS:.*/ UNICORN_WORKERS: ${unicorn_workers}/w $changelog" $config_file
459 if [ -s $changelog ]
460 then
461 echo "setting UNICORN_WORKERS = ${unicorn_workers} based on detected CPU/RAM"
462 rm $changelog
463 fi
464
465 }
466
467 run_start() {
468
469 existing=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
470 echo $existing
471 if [ ! -z $existing ]
472 then
473 echo "Nothing to do, your container has already started!"
474 exit 1
475 fi
476
477 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
478 if [ ! -z $existing ]
479 then
480 echo "starting up existing container"
481 (
482 set -x
483 $docker_path start $config
484 )
485 exit 0
486 fi
487
488 host_run
489 ports=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
490 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
491
492 IFS='-p ' read -a array <<< "$ports"
493 for element in "${array[@]}"
494 do
495 IFS=':' read -a args <<< "$element"
496 if [ "${#args[@]}" == "2" ]; then
497 check_ports "${args[0]}"
498 elif [ "${#args[@]}" == "3" ]; then
499 check_ports "${args[1]}"
500 fi
501 done
502
503 docker_args=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
504 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
505
506 set_template_info
507 set_volumes
508 set_links
509 set_run_image
510 set_boot_command
511
512 # get hostname and settings from container configuration
513 for envar in "${env[@]}"
514 do
515 if [[ $envar == DOCKER_USE_HOSTNAME* ]] || [[ $envar == DISCOURSE_HOSTNAME* ]]
516 then
517 # use as environment variable
518 eval $envar
519 fi
520 done
521
522 (
523 hostname=`hostname -s`
524 # overwrite hostname
525 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
526 then
527 hostname=$DISCOURSE_HOSTNAME
528 else
529 hostname=$hostname-$config
530 fi
531
532 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
533 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
534 # docker added more hostname rules
535 hostname=${hostname/_/-}
536
537 set -x
538 $docker_path run $user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname" \
539 -e DOCKER_HOST_IP=$docker_ip --name $config -t $ports $volumes $docker_args $run_image $boot_command
540
541 )
542 exit 0
543
544 }
545
546 valid_config_check() {
547
548 valid_config="y"
549 for x in DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD \
550 DISCOURSE_DEVELOPER_EMAILS DISCOURSE_HOSTNAME
551 do
552 mail_var=`grep "^ $x:" $config_file`
553 result=$?
554 if (( result == 0 ))
555 then
556 if [[ $mail_var = *"example.com"* ]]
557 then
558 echo "Warning: $x left at incorrect default of example.com"
559 valid_config="n"
560 fi
561 else
562 echo "Warning: $x not configured."
563 valid_config="n"
564 fi
565 done
566 if [ -t 0 ] && [ "$valid_config" != "y" ]; then
567 echo ""
568 echo "Please edit $config_file to add missing settings."
569 exit 1
570 fi
571 }
572
573 run_bootstrap() {
574
575 # Does your system meet the minimum requirements?
576 if [ "$opt" != "--skip-prereqs" ] ; then
577 check_resources
578 fi
579
580 # is our configuration file valid?
581 valid_config_check
582
583 # make minor scaling adjustments for RAM and CPU
584 scale_ram_and_cpu
585
586 host_run
587
588 # Is the image available?
589 # If not, pull it here so the user is aware what's happening.
590 $docker_path history $image >/dev/null 2>&1 || $docker_path pull $image
591
592 set_template_info
593
594 base_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
595 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
596
597 update_pups=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
598 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
599
600 if [[ ! X"" = X"$base_image" ]]; then
601 image=$base_image
602 fi
603
604 set_volumes
605 set_links
606
607 rm -f $cidbootstrap
608
609 run_command="cd /pups &&"
610 if [[ ! "false" = $update_pups ]]; then
611 run_command="$run_command git pull &&"
612 fi
613 run_command="$run_command /pups/bin/pups --stdin"
614
615 echo $run_command
616
617 (exec echo "$input" | $docker_path run $user_args $links "${env[@]}" -e DOCKER_HOST_IP=$docker_ip --cidfile $cidbootstrap -i -a stdin -a stdout -a stderr $volumes $image \
618 /bin/bash -c "$run_command") \
619 || ($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
620
621 [ ! -e $cidbootstrap ] && echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one" && exit 1
622
623 sleep 5
624
625 $docker_path commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
626 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
627 }
628
629
630
631 case "$command" in
632 bootstrap)
633 run_bootstrap
634 echo "Successfully bootstrapped, to startup use ./launcher start $config"
635 exit 0
636 ;;
637
638 enter)
639 exec $docker_path exec -it $config /bin/bash --login
640 ;;
641
642 stop)
643 run_stop
644 exit 0
645 ;;
646
647 logs)
648
649 $docker_path logs $config
650 exit 0
651 ;;
652
653 restart)
654 run_stop
655 run_start
656 exit 0
657 ;;
658
659 start)
660 run_start
661 exit 0
662 ;;
663
664 rebuild)
665 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
666 echo "Ensuring discourse docker is up to date"
667
668 git remote update
669
670 LOCAL=$(git rev-parse @)
671 REMOTE=$(git rev-parse @{u})
672 BASE=$(git merge-base @ @{u})
673
674 if [ $LOCAL = $REMOTE ]; then
675 echo "Discourse Docker is up-to-date"
676
677 elif [ $LOCAL = $BASE ]; then
678 echo "Updating Discourse Docker"
679 git pull || (echo 'failed to update' && exit 1)
680 exec /bin/bash $0 $@
681
682 elif [ $REMOTE = $BASE ]; then
683 echo "Your version of Discourse Docker is ahead of origin"
684
685 else
686 echo "Discourse Docker has diverged source, this is only expected in Dev mode"
687 fi
688
689 fi
690
691 set_existing_container
692
693 if [ ! -z $existing ]
694 then
695 echo "Stopping old container"
696 (
697 set -x
698 $docker_path stop -t 10 $config
699 )
700 fi
701
702 run_bootstrap
703
704 if [ ! -z $existing ]
705 then
706 echo "Removing old container"
707 (
708 set -x
709 $docker_path rm $config
710 )
711 fi
712
713 run_start
714 exit 0
715 ;;
716
717
718 destroy)
719 (set -x; $docker_path stop -t 10 $config && $docker_path rm $config) || (echo "$config was not found" && exit 0)
720 exit 0
721 ;;
722 esac
723
724 usage