add better failed to bootstrap message
[discourse_docker.git] / launcher
1 #!/bin/bash
2
3 command=$1
4 config=$2
5 opt=$3
6
7 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
8 re='[A-Z/ !@#$%^&*()+~`=]'
9 if [[ $config =~ $re ]];
10 then
11 echo
12 echo "ERROR: Config name must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
13 echo
14 exit 1
15 fi
16
17 cd "$(dirname "$0")"
18
19 docker_min_version='1.2.0'
20 docker_rec_version='1.2.0'
21
22 config_file=containers/"$config".yml
23 cidbootstrap=cids/"$config"_bootstrap.cid
24 local_discourse=local_discourse
25 image=samsaffron/discourse:1.0.12
26 docker_path=`which docker.io || which docker`
27
28 if [ "${SUPERVISED}" = "true" ]; then
29 restart_policy="--restart=no"
30 attach_on_start="-a"
31 attach_on_run="-a stdout -a stderr"
32 else
33 attach_on_run="-d"
34 fi
35
36 if [ -n "$DOCKER_HOST" ]; then
37 docker_ip=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
38 elif [ -x "$(which ip 2>/dev/null)" ]; then
39 docker_ip=`ip addr show docker0 | \
40 grep 'inet ' | \
41 awk '{ split($2,a,"/"); print a[1] }';`
42 else
43 docker_ip=`ifconfig | \
44 grep -B1 "inet addr" | \
45 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
46 grep docker0 | \
47 awk -F: '{ print $3 }';`
48 fi
49
50
51 usage () {
52 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs]"
53 echo "Commands:"
54 echo " start: Start/initialize a container"
55 echo " stop: Stop a running container"
56 echo " restart: Restart a container"
57 echo " destroy: Stop and remove a container"
58 echo " enter: Use nsenter to enter a container"
59 echo " ssh: Start a bash shell in a running container"
60 echo " logs: Docker logs for container"
61 echo " mailtest: Test the mail settings in a container"
62 echo " bootstrap: Bootstrap a container for the config based on a template"
63 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
64 echo " cleanup: Remove all containers that have stopped for > 24 hours"
65 echo
66 echo "Options:"
67 echo " --skip-prereqs Don't check prerequisites"
68 echo " --docker-args Extra arguments to pass when running docker"
69 exit 1
70 }
71
72 compare_version() {
73 declare -a ver_a
74 declare -a ver_b
75 IFS=. read -a ver_a <<< "$1"
76 IFS=. read -a ver_b <<< "$2"
77
78 while [[ -n $ver_a ]]; do
79 if (( ver_a > ver_b )); then
80 return 0
81 elif (( ver_b > ver_a )); then
82 return 1
83 else
84 unset ver_a[0]
85 ver_a=("${ver_a[@]}")
86 unset ver_b[0]
87 ver_b=("${ver_b[@]}")
88 fi
89 done
90 return 1 # They are equal
91 }
92
93 prereqs() {
94
95 # 1. docker daemon running?
96 # we send stderr to /dev/null cause we don't care about warnings,
97 # it usually complains about swap which does not matter
98 test=`$docker_path info 2> /dev/null`
99
100 if [[ $? -ne 0 ]] ; then
101 echo "Cannot connect to the docker daemon - verify it is running and you have access"
102 exit 1
103 fi
104
105 # 2. running aufs or btrfs
106 test=`$docker_path info 2> /dev/null | grep 'Driver: '`
107 if [[ "$test" =~ [aufs|btrfs|zfs|overlay] ]] ; then : ; else
108 echo "Your Docker installation is not using a supported filesystem if we were to proceed you may have a broken install."
109 echo "aufs is the recommended filesystem you should be using (zfs/btrfs and overlay may work as well)"
110 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the driver"
111 echo ""
112 echo "If you wish to continue anyway using your existing unsupported filesystem"
113 echo "read the source code of launcher and figure out how to bypass this."
114 exit 1
115 fi
116
117 # 3. running recommended docker version
118 test=($($docker_path --version)) # Get docker version string
119 test=${test[2]//,/} # Get version alone and strip comma if exists
120
121 [[ "$test" =~ "0.12.0" ]] && echo "You are running a broken version of Docker, please upgrade ASAP. See: https://meta.discourse.org/t/the-installation-stopped-in-the-middle/16311/ for more details." && exit 1
122
123 # At least minimum version
124 if compare_version "${docker_min_version}" "${test}"; then
125 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
126 exit 1
127 fi
128
129 # Recommend best version
130 if compare_version "${docker_rec_version}" "${test}"; then
131 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
132 fi
133
134 # 4. discourse docker image is downloaded
135 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
136
137 if [ -z "$test" ]; then
138 echo
139 echo "WARNING: We are about to start downloading the Discourse base image"
140 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
141 echo
142 echo "Please be patient"
143 echo
144
145 fi
146
147 # 5. able to attach stderr / out / tty
148 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
149 if [[ "$test" =~ "working" ]] ; then : ; else
150 echo "Your Docker installation is not working correctly"
151 echo
152 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
153 exit 1
154 fi
155 }
156
157 if [ "$opt" != "--skip-prereqs" ] ; then
158 prereqs
159 fi
160
161 if [ "$opt" == "--docker-args" ] ; then
162 user_args=$4
163 else
164 user_args=""
165 fi
166
167 get_ssh_pub_key() {
168 local ${ssh_key_locations}
169 ssh_key_locations=(
170 ~/.ssh/id_ed25519.pub
171 ~/.ssh/id_ecdsa.pub
172 ~/.ssh/id_rsa.pub
173 ~/.ssh/id_dsa.pub
174 ~core/.ssh/authorized_keys
175 )
176
177 local $keyfile
178 for keyfile in "${ssh_key_locations[@]}"; do
179 if [[ -e ${keyfile} ]] ; then
180 ssh_pub_key="$(cat ${keyfile})"
181 return 0
182 fi
183 done
184
185 return 0
186 }
187
188
189 install_docker() {
190
191 echo "Docker is not installed, make sure you are running on the 3.8 kernel"
192 echo "The best supported Docker release is Ubuntu 12.04.03 for it run the following"
193 echo
194 echo "sudo apt-get update"
195 echo "sudo apt-get install linux-image-generic-lts-raring linux-headers-generic-lts-raring"
196 echo "sudo reboot"
197 echo
198
199 echo "sudo sh -c \"wget -qO- https://get.docker.io/gpg | apt-key add -\""
200 echo "sudo sh -c \"echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list\""
201 echo "sudo apt-get update"
202 echo "sudo apt-get install lxc-docker"
203
204 exit 1
205 }
206
207 host_run() {
208 read -r -d '' env_ruby << 'RUBY'
209 require 'yaml'
210
211 input = STDIN.readlines.join
212 yaml = YAML.load(input)
213
214 if host_run = yaml['host_run']
215 params = yaml['params'] || {}
216 host_run.each do |run|
217 params.each do |k,v|
218 run = run.gsub("$#{k}", v)
219 end
220 STDOUT.write "#{run}--SEP--"
221 end
222 end
223 RUBY
224
225 host_run=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
226
227 while [ "$host_run" ] ; do
228 iter=${host_run%%--SEP--*}
229 echo
230 echo "Host run: $iter"
231 $iter || exit 1
232 echo
233 host_run="${host_run#*--SEP--}"
234 done
235 }
236
237
238 set_volumes() {
239 volumes=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
240 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
241 }
242
243 set_links() {
244 links=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
245 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
246 }
247
248 set_template_info() {
249
250 templates=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
251 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
252
253
254 arrTemplates=(${templates// / })
255 config_data=$(cat $config_file)
256
257 input="hack: true"
258
259
260 for template in "${arrTemplates[@]}"
261 do
262 [ ! -z $template ] && {
263 input="$input _FILE_SEPERATOR_ $(cat $template)"
264 }
265 done
266
267 # we always want our config file last so it takes priority
268 input="$input _FILE_SEPERATOR_ $config_data"
269
270 read -r -d '' env_ruby << 'RUBY'
271 require 'yaml'
272
273 input=STDIN.readlines.join
274 # default to UTF-8 for the dbs sake
275 env = {'LANG' => 'en_US.UTF-8'}
276 input.split('_FILE_SEPERATOR_').each do |yml|
277 yml.strip!
278 begin
279 env.merge!(YAML.load(yml)['env'] || {})
280 rescue Psych::SyntaxError => e
281 puts e
282 puts "*ERROR."
283 rescue => e
284 puts yml
285 p e
286 end
287 end
288 puts env.map{|k,v| "-e\n#{k}=#{v}" }.join("\n")
289 RUBY
290
291 raw=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
292
293 env=()
294 ok=1
295 while read i; do
296 if [ "$i" == "*ERROR." ]; then
297 ok=0
298 elif [ -n "$i" ]; then
299 env[${#env[@]}]=$i
300 fi
301 done <<< "$raw"
302
303 if [ "$ok" -ne 1 ]; then
304 echo "${env[@]}"
305 echo "YAML syntax error. Please check your /var/discourse/containers/*.yml config files."
306 exit 1
307 fi
308 }
309
310 [ -z $docker_path ] && {
311 install_docker
312 }
313
314 [ $command == "cleanup" ] && {
315 echo
316 echo "The following command will"
317 echo "- Delete all docker images for old containers"
318 echo "- Delete all stopped and orphan containers"
319 echo
320 read -p "Are you sure (Y/n): " -n 1 -r && echo
321 if [[ $REPLY =~ ^[Yy]$ || ! $REPLY ]]
322 then
323 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
324
325 echo "Starting Cleanup"
326
327 if [[ ! -z `docker ps -aq` ]]; then
328 docker inspect -f '{{.Id}},{{.State.Running}},{{.State.FinishedAt}}' $(docker ps -qa) | \
329 awk -F, 'BEGIN { TIME=strftime("%FT%T.000000000Z",systime()-60*60*24); } $2=="false" && $3 < TIME {print $1;}' | \
330 xargs --no-run-if-empty docker rm >/dev/null 2>/dev/null
331 fi
332
333 docker rmi `docker images -a | grep '<none>' | awk '{print $3}'` 2> /dev/null
334
335 let freed=$space-$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
336
337 echo $space
338 echo $(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
339
340
341 output="$freed" | awk '{sum=$1;hum[1024**3]="GB"; hum[1024**2]="MB"; hum[1024]="KB"; for (x=1024**3;x>=1024; x/=1024){ if (sum>=x) { printf "%.2f %s\n",sum/x,hum[x];break }}}'
342 [ -z "$output" ] && {
343 [[ $freed > 0 ]] && { echo "./launcher cleanup cleared up $freed of disk space."; } || { echo "./launcher cleanup has finished, no files were removed."; }
344 } || { echo "./launcher cleanup cleared up $freed of disk space."; }
345 else
346 exit 1
347 fi
348 exit 0
349 }
350
351 [ $# -lt 2 ] && {
352 usage
353 }
354
355 if [ ! -e $config_file ]
356 then
357 echo "Config file was not found, ensure $config_file exists"
358 echo ""
359 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
360 exit 1
361 fi
362
363
364 docker_version=($($docker_path --version))
365 docker_version=${test[2]//,/}
366
367 if compare_version "1.2.0" "$docker_version"; then
368 echo "We recommend you upgrade docker, the version you are running has no restart policies, on reboot your container may not start up"
369 restart_policy=""
370 else
371 restart_policy=${restart_policy:---restart=always}
372 fi
373
374
375 run_mailtest(){
376 if [ ! -e $config_file ]; then
377 echo "Config does not exist: $config_file" >&2
378 exit 1
379 fi
380 exec scripts/mailtest $config_file
381 }
382
383 set_existing_container(){
384 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
385 }
386
387 run_stop(){
388
389 set_existing_container
390
391 if [ ! -z $existing ]
392 then
393 (
394 set -x
395 $docker_path stop -t 10 $config
396 )
397 else
398 echo "$config was not started !"
399 exit 1
400 fi
401 }
402
403 run_start(){
404
405 existing=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
406 echo $existing
407 if [ ! -z $existing ]
408 then
409 echo "Nothing to do, your container has already started!"
410 exit 1
411 fi
412
413 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
414 if [ ! -z $existing ]
415 then
416 echo "starting up existing container"
417 (
418 set -x
419 $docker_path start $config
420 )
421 exit 0
422 fi
423
424 host_run
425 ports=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
426 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| '-p ' << p.to_s << ' '}.join"`
427
428 set_template_info
429 set_volumes
430 set_links
431
432 (
433 hostname=`hostname`
434 set -x
435 $docker_path run $user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname-$config" \
436 -e DOCKER_HOST_IP=$docker_ip --name $config -t $ports $volumes $local_discourse/$config /sbin/boot
437
438 )
439 exit 0
440
441 }
442
443 run_bootstrap(){
444
445 host_run
446
447 get_ssh_pub_key
448
449 # Is the image available?
450 # If not, pull it here so the user is aware what's happening.
451 $docker_path history $image >/dev/null 2>&1 || $docker_path pull $image
452
453 set_template_info
454
455 base_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
456 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
457
458 update_pups=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
459 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
460
461 if [[ ! X"" = X"$base_image" ]]; then
462 image=$base_image
463 fi
464
465 set_volumes
466 set_links
467
468 rm -f $cidbootstrap
469
470 run_command="cd /pups &&"
471 if [[ ! "false" = $update_pups ]]; then
472 run_command="$run_command git pull &&"
473 fi
474 run_command="$run_command /pups/bin/pups --stdin"
475
476 echo $run_command
477
478 env=("${env[@]}" "-e" "SSH_PUB_KEY=$ssh_pub_key")
479
480 (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 \
481 /bin/bash -c "$run_command") \
482 || ($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
483
484 [ ! -e $cidbootstrap ] && echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages" && exit 1
485
486 sleep 5
487
488 $docker_path commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
489 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
490 }
491
492 case "$command" in
493 bootstrap)
494 run_bootstrap
495 echo "Successfully bootstrapped, to startup use ./launcher start $config"
496 exit 0
497 ;;
498
499 mailtest)
500 run_mailtest
501 exit 0
502 ;;
503
504 enter)
505 exec $docker_path exec -it $config /bin/bash
506 ;;
507
508 ssh)
509 existing=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
510
511 if [[ ! -z $existing ]]; then
512 address="`$docker_path port $config 22`"
513 split=(${address//:/ })
514 exec ssh -o StrictHostKeyChecking=no root@${split[0]} -p ${split[1]}
515 else
516 echo "$config is not running!"
517 exit 1
518 fi
519 ;;
520
521 stop)
522 run_stop
523 exit 0
524 ;;
525
526 logs)
527
528 $docker_path logs $config
529 exit 0
530 ;;
531
532 restart)
533 run_stop
534 run_start
535 exit 0
536 ;;
537
538 start)
539 run_start
540 exit 0
541 ;;
542
543 rebuild)
544 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
545 echo "Ensuring discourse docker is up to date"
546
547 git remote update
548
549 LOCAL=$(git rev-parse @)
550 REMOTE=$(git rev-parse @{u})
551 BASE=$(git merge-base @ @{u})
552
553 if [ $LOCAL = $REMOTE ]; then
554 echo "Discourse Docker is up-to-date"
555
556 elif [ $LOCAL = $BASE ]; then
557 echo "Updating Discourse Docker"
558 git pull || (echo 'failed to update' && exit 1)
559 exec /bin/bash $0 $@
560
561 elif [ $REMOTE = $BASE ]; then
562 echo "Your version of Discourse Docker is ahead of origin"
563
564 else
565 echo "Discourse Docker has diverged source, this is only expected in Dev mode"
566 fi
567
568 fi
569
570 set_existing_container
571
572 if [ ! -z $existing ]
573 then
574 echo "Stopping old container"
575 (
576 set -x
577 $docker_path stop -t 10 $config
578 )
579 fi
580
581 run_bootstrap
582
583 if [ ! -z $existing ]
584 then
585 echo "Removing old container"
586 (
587 set -x
588 $docker_path rm $config
589 )
590 fi
591
592 run_start
593 exit 0
594 ;;
595
596
597 destroy)
598 (set -x; $docker_path stop -t 10 $config && $docker_path rm $config) || (echo "$config was not found" && exit 0)
599 exit 0
600 ;;
601 esac
602
603 usage