[ANSIBLE] Configure custom Grafana dashboard
[oom/offline-installer.git] / tools / helm-healer.sh
1 #!/bin/bash
2
3 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
4
5 #
6 # globals and defaults
7 #
8
9 NAMESPACE=
10 OVERRIDES=
11 HELM_CHART_RELEASE_NAME=
12 HELM_DELETE_ALL=
13 HELM_SKIP_DEPLOY=
14 VOLUME_STORAGE=
15 HELM_TIMEOUT=3600
16 RELEASE_PREFIX=onap
17
18 #
19 # control variables
20 #
21
22 CMD=$(basename "$0")
23 COLOR_ON_RED='\033[0;31;1m'
24 COLOR_ON_GREEN='\033[0;32;1m'
25 COLOR_OFF='\033[0m'
26
27
28 #
29 # functions
30 #
31
32 help()
33 {
34 cat <<EOF
35 ${CMD} - simple tool for fixing onap helm deployment
36
37 DESCRIPTION
38     This script does nothing smart or special it just tries to
39     redeploy onap component. It can fix only problems related to
40     race conditions or timeouts. Nothing else. It will not fix
41     broken ONAP - there is no such ambition - that effort should
42     be directed in the upstream.
43
44 USAGE
45     ${CMD} -h|--help
46         This help
47
48     ${CMD} -n|--namespace <namespace>
49            (-f|--file <override>)...
50            (-s|--storage <directory>)|--no-storage-deletion
51            [-p|--release-prefix <release prefix>]
52            [-t|--timeout <secs>]
53            [(-c|--component <component release name>)...|
54             (-D|--delete-all)]
55            [-C|--clean-only]
56
57 EXAMPLES
58
59     Usage 1: (simple heuristics - redeploy failed components):
60         ${CMD} -n onap -f /some/override1.yml -s /dockerdata-nfs/onap
61
62     Usage 2: (redeploy ONLY explicitly listed components):
63         ${CMD} -n onap -f /some/override1.yml -s /dockerdata-nfs/onap \\
64                -c onap-aaf -c onap-sdc -c onap-portal
65
66     Usage 3: (delete EVERYTHING and redeploy):
67         ${CMD} -n onap -f /some/override1.yml -s /dockerdata-nfs/onap --delete-all
68
69     Usage 4: (delete EVERYTHING and DO NOT redeploy - clean env.)
70         ${CMD} -n onap -s /dockerdata-nfs/onap --delete-all --clean-only
71
72 NOTES
73
74     Namespace argument (always) and at least one override file (if you don't
75     use '--delete-all') are mandatory for this script to execute. Also you must
76     provide path to the storage ('--storage') OR explicitly request to not
77     delete file storage of the component ('--no-storage-deletion').
78
79     The storage should be a directory where persistent volume resides. It will
80     work only if the component created the persistent volume with the same
81     filename as its release name. Otherwise no files are deleted. The exception
82     is when '--delete-all' is used - in that case all content of the storage is
83     deleted (because ONAP is not consistent with the volume directory names
84     - e.g.: sdnc).
85
86     '--file' can be used multiple of times and it is used for override files
87     which are passed on to helm. The order is significant because if two
88     override files modify one value the latest one is used. This option is
89     ignored if '--clean-only' is used.
90
91     CAUTION 1: filename of an override file cannot contain whitespace! This is
92     actually helm/onap deploy plugin issue which does not handle such files. So
93     I dropped the more complicated version of this script when there is no
94     reason to support something on what will helm deploy choke anyway.
95
96     '--prefix' option is helm release argument - it is actually prefix when you
97     list the helm releases - helm is little confusing here.
98
99     CAUTION 2: By default release prefix is 'onap' - if you deployed release
100     'onap' and now run this script with different prefix then it will skip all
101     'onap-*' components and will deploy a new release with new prefix - BEWARE
102     TO USE PROPER RELEASE PREFIX!
103
104     Timeout sets the waiting time for helm deploy per component.
105
106     '--component' references to the release name of the chart which you want to
107     redeploy excplicitly - otherwise 'ALL FAILED' components will be
108     redeployed. You can target more than one component at once - just use the
109     argument multiple times.
110
111     Component option is mutually exclusive with the '--delete-all' which will
112     delete all components - healthy or not. Actually it will delete the whole
113     NAMESPACE and everything in it. Also to be sure it will cleanup all
114     orphaned images and volumes on all kubernetes nodes.
115
116     '--clean-only' can be used with any usage: heuristics, explicit component
117     list or with '--delete-all'. It basically just skips the last step - the
118     actual redeploy.
119 EOF
120 }
121
122 use_help()
123 {
124     printf "Try help: ${CMD} --help\n"
125 }
126
127 msg()
128 {
129     printf "${COLOR_ON_GREEN}INFO: $@ ${COLOR_OFF}\n"
130 }
131
132 error()
133 {
134     printf "${COLOR_ON_RED}ERROR: $@ ${COLOR_OFF}\n"
135 }
136
137 on_exit()
138 {
139     printf "$COLOR_OFF"
140 }
141
142 # remove all successfully completed jobs
143 clean_jobs()
144 {
145     kubectl get jobs -n ${NAMESPACE} \
146         --ignore-not-found=true \
147         --no-headers=true | \
148         while read -r _job _completion _duration _age ; do
149             _done=$(echo ${_completion} | awk 'BEGIN {FS="/";} {print $1;}')
150             _desired=$(echo ${_completion} | awk 'BEGIN {FS="/";} {print $2;}')
151             if [ "$_desired" -eq "$_done" ] ; then
152                 delete_job "$_job"
153             fi
154         done
155 }
156
157 get_failed_labels()
158 {
159     get_labels 'status.phase==Failed'
160 }
161
162 # arg: [optional: selector]
163 get_labels()
164 {
165     if [ -n "$1" ] ; then
166         _selector="--field-selector=${1}"
167     else
168         _selector=
169     fi
170
171     kubectl get pods -n ${NAMESPACE} \
172         --show-labels=true \
173         ${_selector} \
174         --ignore-not-found=true \
175         --no-headers=true | \
176         while read -r _pod _ready _status _restart _age _labels ; do
177             [ -z "$_labels" ] && break
178             for _label in $(echo "$_labels" | tr ',' ' ') ; do
179                 case "$_label" in
180                     release=*)
181                         _label=$(echo "$_label" | sed 's/release=//')
182                         echo "$_label"
183                         ;;
184                 esac
185             done
186         done | sort -u
187 }
188
189 # arg: <release name>
190 helm_undeploy()
191 {
192     msg "Undeploy helm release name: ${1}"
193     # Helm v3 does not support "--purge" flag since it's a default behavior for v3
194     if [[ $(helm version --template "{{.Version}}") =~ ^v3 ]];then
195         helm undeploy ${1}
196     else
197         helm undeploy ${1} --purge
198     fi
199     sleep 15s
200 }
201
202 helm_deploy()
203 {
204   # Helm v3 need "--create-namespace" to create namespace if don't exist
205   if [[ $(helm version --template "{{.Version}}") =~ ^v3 ]];then
206     msg helm deploy ${RELEASE_PREFIX} local/onap --create-namespace --namespace ${NAMESPACE} ${OVERRIDES} --timeout ${HELM_TIMEOUT}
207     helm deploy ${RELEASE_PREFIX} local/onap --create-namespace --namespace ${NAMESPACE} ${OVERRIDES} --timeout ${HELM_TIMEOUT}
208   else
209     msg helm deploy ${RELEASE_PREFIX} local/onap --namespace ${NAMESPACE} ${OVERRIDES} --timeout ${HELM_TIMEOUT}
210     helm deploy ${RELEASE_PREFIX} local/onap --namespace ${NAMESPACE} ${OVERRIDES} --timeout ${HELM_TIMEOUT}
211   fi
212 }
213
214 # arg: <job name>
215 delete_job()
216 {
217     kubectl delete job -n ${NAMESPACE} \
218         --cascade=true \
219         --now=true \
220         --wait=true \
221         ${1}
222
223     # wait for job to be deleted
224     _output=start
225     while [ -n "$_output" ] && sleep 1 ; do
226         _output=$(kubectl get pods -n ${NAMESPACE} \
227             --ignore-not-found=true \
228             --no-headers=true \
229             --selector="job-name=${1}")
230     done
231 }
232
233 #arg: <component>
234 get_resources_for_component()
235 {
236
237 helm status $1 | awk -f <(cat - <<-'EOD'
238 BEGIN {
239   work="no"
240   kind=""
241   a["dummy"]=""
242 }
243
244 $1 ~ ":" {
245   if ( $1 == "RESOURCES:" ) {
246           work="yes"
247 } else {
248   work="no"
249 }
250
251 }
252
253 $1 == "==>" {
254   split($2, a, "[/(]")
255   kind=a[2]
256 }
257
258 $1 != "NAME" && $1 != "==>" && work == "yes" && $1 !~ ":" && $1 != "" {
259   printf "%s/%s\n", kind, $1
260 }
261
262 EOD
263 )
264 }
265
266 # arg: <resource>
267 delete_resource()
268 {
269     local _resource="$1"
270     local _kind="${_resource%/*}"
271     local _name="${_resource#*/}"
272
273
274     if kubectl get ${_resource} >/dev/null 2>&1; then
275         msg "${_resource} has not been removed with helm undeploy, manual removal is required. Proceeding"
276         kubectl delete ${_resource} -n ${NAMESPACE} \
277             --cascade=true \
278             --now=true \
279             --wait=true \
280             2>&1 | grep -iv 'not[[:space:]]*found'
281
282         # wait for resource to be deleted
283         _output=start
284         while [ -n "$_output" ] && sleep 1 ; do
285             _output=$(kubectl get ${_kind} ${_name} -n ${NAMESPACE} \
286                 --ignore-not-found=true \
287                 --no-headers=true )
288         done
289         msg "Done"
290     fi
291 }
292
293 delete_namespace()
294 {
295     msg "Delete the whole namespace: ${NAMESPACE}"
296     kubectl delete namespace \
297         --cascade=true \
298         --now=true \
299         --wait=true \
300         "$NAMESPACE"
301
302     # wait for namespace to be deleted
303     _output=start
304     while [ -n "$_output" ] && sleep 1 ; do
305         _output=$(kubectl get all -n ${NAMESPACE} \
306             --ignore-not-found=true \
307             --no-headers=true)
308     done
309 }
310
311 delete_persistent_volume()
312 {
313     _persistent_volume=$1
314      if kubectl get ${_persistent_volume} >/dev/null 2>&1; then
315           msg "${_persistent_volume} has not been removed with helm undeploy, manual removal is required. Proceeding"
316           #very often k8s hangs on Terminating state for pv due to  still active pvc. It is better to delete pvc directly
317          _claim=$(kubectl get ${_persistent_volume} -o jsonpath='{ .spec.claimRef.name}')
318          delete_resource PersistentVolumeClaim/${_claim}
319      fi
320 }
321
322 # arg: [optional: directory]
323 delete_storage()
324 {
325     _node=$(kubectl get nodes \
326         --selector=node-role.kubernetes.io/worker \
327         -o wide \
328         --no-headers=true | \
329         awk '{print $6}' | head -n 1)
330
331     if [ -z "$_node" ] ; then
332         error "Could not list kubernetes nodes - SKIPPING DELETION"
333     else
334         if [ -n "$1" ] ; then
335             msg "Delete directory '${1}' on $_node"
336             ssh $_node "rm -rf '${1}'"
337         else
338             msg "Delete directories '${VOLUME_STORAGE}/*' on $_node"
339             ssh $_node "find '${VOLUME_STORAGE}' -maxdepth 1 -mindepth 1 -exec rm -rf '{}' \;"
340         fi
341     fi
342 }
343
344 docker_cleanup()
345 {
346     _nodes=$(kubectl get nodes \
347         --selector=node-role.kubernetes.io/worker \
348         -o wide \
349         --no-headers=true | \
350         awk '{print $6}')
351
352     if [ -z "$_nodes" ] ; then
353         error "Could not list kubernetes nodes - SKIPPING docker cleanup"
354         return
355     fi
356
357     for _node in $_nodes ; do
358         msg "Docker cleanup on $_node"
359         ssh $_node "docker system prune --force --all --volumes" >/dev/null &
360     done
361
362     msg "We are waiting now for docker cleanup to finish on all nodes..."
363     wait
364 }
365
366 is_helm_serve_running()
367 {
368     # healthy result: HTTP/1.1 200 OK
369     _helm_serve_result=$(curl -w %{http_code} --silent --connect-timeout 3 http://127.0.0.1:8879/ -o /dev/null)
370
371     if [ "$_helm_serve_result" == "200" ] ; then
372         return 0
373     else
374         return 1
375     fi
376 }
377
378 # arg: <release name>
379 undeploy_component()
380 {
381     local _component=$1
382
383     #Because Helm undeploy is not reliable: Gathering resources assigned to componen to track and remove orphans later
384     _component_resources=($(get_resources_for_component ${_component}))
385
386     declare -a _persistent_volumes
387     declare -a _standard
388     declare -a _unknown_kinds
389
390     for resource in ${_component_resources[@]}; do
391         case $resource in
392             CronJob/* | Job/* | Secret/* | ConfigMap/* | Pod/* | Service/* | Deployment/* | StatefulSet/*)
393                 _standard+=(${resource});;
394             #Ignoring PVC, they will be handled along with PV as 'helm' status does not return them for some components
395             PersistentVolumeClaim/*)
396                 ;;
397             PersistentVolume/*)
398                 _persistent_volumes+=(${resource});;
399             *)
400                 _unknown_kinds+=(${resource})
401         esac
402     done
403
404
405     #Gathering physical location of directories for persistent volumes to delete them after undeploy
406     declare -a _physical_locations
407     for volume in ${_persistent_volumes[@]}; do
408         _physical_locations+=($(kubectl get ${volume} -o jsonpath='{ .spec.hostPath.path}' ))
409     done
410
411     helm_undeploy ${_component}
412
413     #Manual items removal
414     for resource in ${_standard[@]}; do
415         delete_resource ${resource}
416     done
417
418     for volume in ${_persistent_volumes[@]}; do
419         delete_persistent_volume ${volume}
420     done
421
422     for subdir in ${_physical_locations[@]}; do
423         delete_storage ${subdir}
424     done
425
426     if [ "${#_unknown_kinds[@]}" -ne 0 ] ; then
427         for resource in ${_unknown_kinds[@]}; do
428             error "Untracked resource kind present: ${resource}, attempting to delete it..."
429             delete_resource ${resource}
430         done
431         return
432     fi
433 }
434
435 # arg: <release name>
436 deploy_component()
437 {
438     # TODO: until I can verify that this does the same for this component as helm deploy
439     #msg "Redeployment of the component ${1}..."
440     #helm install "local/${_chart}" --name ${1} --namespace ${NAMESPACE} --wait --timeout ${HELM_TIMEOUT}
441     error "NOT IMPLEMENTED"
442 }
443
444
445 #
446 # arguments
447 #
448
449 state=nil
450 arg_namespace=
451 arg_overrides=
452 arg_timeout=
453 arg_storage=
454 arg_nostorage=
455 arg_components=
456 arg_prefix=
457 arg_deleteall=
458 arg_cleanonly=
459 while [ -n "$1" ] ; do
460     case $state in
461         nil)
462             case "$1" in
463                 -h|--help)
464                     help
465                     exit 0
466                     ;;
467                 -n|--namespace)
468                     state=namespace
469                     ;;
470                 -f|--file)
471                     state=override
472                     ;;
473                 -t|--timeout)
474                     state=timeout
475                     ;;
476                 -s|--storage)
477                     state=storage
478                     ;;
479                 --no-storage-deletion)
480                     if [ -n "$arg_storage" ] ; then
481                         error "Usage of storage argument together with no storage deletion option!"
482                         use_help
483                         exit 1
484                     elif [ -z "$arg_nostorage" ] ; then
485                         arg_nostorage=nostorage
486                     else
487                         error "Duplicit argument for no storage option! (IGNORING)"
488                     fi
489                     ;;
490                 -c|--component)
491                     if [ -n "$arg_deleteall" ] ; then
492                         error "'Delete all components' used already - argument mismatch"
493                         use_help
494                         exit 1
495                     fi
496                     state=component
497                     ;;
498                 -D|--delete-all)
499                     if [ -n "$arg_components" ] ; then
500                         error "Explicit component(s) provided already - argument mismatch"
501                         use_help
502                         exit 1
503                     elif [ -z "$arg_deleteall" ] ; then
504                         arg_deleteall=deleteall
505                     else
506                         error "Duplicit argument for 'delete all' option! (IGNORING)"
507                     fi
508                     ;;
509                 -p|--prefix)
510                     state=prefix
511                     ;;
512                 -C|--clean-only)
513                     if [ -z "$arg_cleanonly" ] ; then
514                         arg_cleanonly=cleanonly
515                     else
516                         error "Duplicit argument for 'clean only' option! (IGNORING)"
517                     fi
518                     ;;
519                 *)
520                     error "Unknown parameter: $1"
521                     use_help
522                     exit 1
523                     ;;
524             esac
525             ;;
526         namespace)
527             if [ -z "$arg_namespace" ] ; then
528                 arg_namespace="$1"
529                 state=nil
530             else
531                 error "Duplicit argument for namespace!"
532                 use_help
533                 exit 1
534             fi
535             ;;
536         override)
537             if ! [ -f "$1" ] ; then
538                 error "Wrong filename for override file: $1"
539                 use_help
540                 exit 1
541             fi
542             arg_overrides="${arg_overrides} -f $1"
543             state=nil
544             ;;
545         component)
546             arg_components="${arg_components} $1"
547             state=nil
548             ;;
549         prefix)
550             if [ -z "$arg_prefix" ] ; then
551                 arg_prefix="$1"
552                 state=nil
553             else
554                 error "Duplicit argument for release prefix!"
555                 use_help
556                 exit 1
557             fi
558             ;;
559         timeout)
560             if [ -z "$arg_timeout" ] ; then
561                 if ! echo "$1" | grep -q '^[0-9]\+$' ; then
562                     error "Timeout must be an integer: $1"
563                     use_help
564                     exit 1
565                 fi
566                 arg_timeout="$1"
567                 state=nil
568             else
569                 error "Duplicit argument for timeout!"
570                 use_help
571                 exit 1
572             fi
573             ;;
574         storage)
575             if [ -n "$arg_nostorage" ] ; then
576                 error "Usage of storage argument together with no storage deletion option!"
577                 use_help
578                 exit 1
579             elif [ -z "$arg_storage" ] ; then
580                 arg_storage="$1"
581                 state=nil
582             else
583                 error "Duplicit argument for storage!"
584                 use_help
585                 exit 1
586             fi
587             ;;
588     esac
589     shift
590 done
591
592 # sanity checks
593
594 if [ -z "$arg_namespace" ] ; then
595     error "Missing namespace"
596     use_help
597     exit 1
598 else
599     NAMESPACE="$arg_namespace"
600 fi
601
602 if [ -z "$arg_overrides" ] && [ -z "$arg_cleanonly" ] ; then
603     error "Missing override file(s) or use '--clean-only'"
604     use_help
605     exit 1
606 else
607     OVERRIDES="$arg_overrides"
608 fi
609
610 if [ -n "$arg_prefix" ] ; then
611     RELEASE_PREFIX="$arg_prefix"
612 fi
613
614 if [ -n "$arg_timeout" ] ; then
615     HELM_TIMEOUT="$arg_timeout"
616 fi
617
618 if [ -n "$arg_storage" ] ; then
619     VOLUME_STORAGE="$arg_storage"
620 elif [ -z "$arg_nostorage" ] ; then
621     error "Missing storage argument! If it is intended then use '--no-storage-deletion' option"
622     use_help
623     exit 1
624 fi
625
626 if [ -n "$arg_components" ] ; then
627     HELM_CHART_RELEASE_NAME="$arg_components"
628 fi
629
630 if [ -n "$arg_deleteall" ] ; then
631     HELM_DELETE_ALL=yes
632 fi
633
634 if [ -n "$arg_cleanonly" ] ; then
635     HELM_SKIP_DEPLOY=yes
636 fi
637
638 # If running with helm v3 a time unit has to be appended to HELM_TIMEOUT
639 if [[ $(helm version --template "{{.Version}}") =~ ^v3 ]];then
640     HELM_TIMEOUT="${HELM_TIMEOUT}s"
641 fi
642
643 #
644 # main
645 #
646
647 # set trap for this script cleanup
648 trap on_exit INT QUIT TERM EXIT
649
650 # another sanity checks
651 for tool in helm kubectl curl ; do
652     if ! which "$tool" >/dev/null 2>&1 ; then
653         error "Missing '${tool}' command"
654         exit 1
655     fi
656 done
657
658 if ! is_helm_serve_running ; then
659     error "'helm serve' is not running (http://localhost:8879)"
660     exit 1
661 fi
662
663 # if --delete-all is used then redeploy all components (the current namespace is deleted)
664 if [ -n "$HELM_DELETE_ALL" ] ; then
665     # undeploy helm release (prefix)
666     helm_undeploy "$RELEASE_PREFIX"
667
668     # we will delete the whole namespace
669     delete_namespace
670
671     # we will cleanup docker on each node
672     docker_cleanup
673
674     # we will delete the content of storage (volumes)
675     if [ -n "$VOLUME_STORAGE" ] ; then
676         delete_storage
677     fi
678 # delete and redeploy explicit or failed components...
679 else
680     # if a helm chart release name was given then just redeploy said component and quit
681     if [ -n "$HELM_CHART_RELEASE_NAME" ] ; then
682         msg "Explicitly asked for component redeploy: ${HELM_CHART_RELEASE_NAME}"
683         _COMPONENTS="$HELM_CHART_RELEASE_NAME"
684     # simple heuristics: redeploy only failed components
685     else
686         msg "Delete successfully completed jobs..."
687         clean_jobs
688
689         msg "Find failed components..."
690         _COMPONENTS=$(get_failed_labels)
691     fi
692
693     for _component in ${_COMPONENTS} ; do
694         if echo "$_component" | grep -q "^${RELEASE_PREFIX}-" ; then
695             msg "Redeploy component: ${_component}"
696             undeploy_component ${_component}
697         else
698             error "Component release name '${_component}' does not match release prefix: ${RELEASE_PREFIX} (SKIP)"
699         fi
700     done
701 fi
702
703 if [ -z "$HELM_SKIP_DEPLOY" ] ; then
704     # TODO: this is suboptimal - find a way how to deploy only the affected component...
705     msg "Redeploy onap..."
706     helm_deploy
707 else
708     msg "Clean only option used: Skipping redeploy..."
709 fi
710
711 msg DONE
712
713 exit $?
714