Commit 18193a697ed684bde46efa4d6b890a9914e26b33

Authored by viktor
1 parent 9bf243dc

Refactored k8s configs (made 'basic' and 'high-availability' deployment modes)

  1 +# Can be either basic (with single instance of Zookeeper, Kafka and Redis) or high-availability (with Zookeeper, Kafka and Redis in cluster modes).
  2 +# According to the deployment type corresponding kubernetes resources will be deployed (see content of the directories ./basic and ./high-availability for details).
  3 +DEPLOYMENT_TYPE=basic
1 4
2 5 # Database used by ThingsBoard, can be either postgres (PostgreSQL) or cassandra (Cassandra).
3 6 # According to the database type corresponding kubernetes resources will be deployed (see postgres.yml, cassandra.yml for details).
  7 +DATABASE=cassandra
4 8
5   -DATABASE=postgres
... ...
... ... @@ -20,7 +20,7 @@ $ minikube addons enable ingress
20 20
21 21 ## Installation
22 22
23   -Before performing initial installation you can configure the type of database to be used with ThingsBoard.
  23 +Before performing initial installation you can configure the type of database to be used with ThingsBoard and the type of deployment.
24 24 In order to set database type change the value of `DATABASE` variable in `.env` file to one of the following:
25 25
26 26 - `postgres` - use PostgreSQL database;
... ... @@ -28,6 +28,13 @@ In order to set database type change the value of `DATABASE` variable in `.env`
28 28
29 29 **NOTE**: According to the database type corresponding kubernetes resources will be deployed (see `postgres.yml`, `cassandra.yml` for details).
30 30
  31 +In order to set deployment type change the value of `DEPLOYMENT_TYPE` variable in `.env` file to one of the following:
  32 +
  33 +- `basic` - start up with single instance of Zookeeper, Kafka and Redis;
  34 +- `cassandra` - start up with Zookeeper, Kafka and Redis in cluster modes;
  35 +
  36 +**NOTE**: According to the deployment type corresponding kubernetes resources will be deployed (see content of the directories `./basic` and `./high-availability` for details).
  37 +
31 38 Execute the following command to run installation:
32 39
33 40 `
... ... @@ -52,7 +59,7 @@ Get list of the running tb-redis pods and verify that all of them are in running
52 59 $ kubectl get pods -l app=tb-redis
53 60 `
54 61
55   -Execute the following command to create redis cluster:
  62 +If you are running ThingsBoard in `high-availability` `DEPLOYMENT_TYPE` execute the following command to create redis cluster:
56 63
57 64 `
58 65 $ kubectl exec -it tb-redis-0 -- redis-cli --cluster create --cluster-replicas 1 $(kubectl get pods -l app=tb-redis -o jsonpath='{range.items[*]}{.status.podIP}:6379 ')
... ...
  1 +#
  2 +# Copyright © 2016-2020 The Thingsboard Authors
  3 +#
  4 +# Licensed under the Apache License, Version 2.0 (the "License");
  5 +# you may not use this file except in compliance with the License.
  6 +# You may obtain a copy of the License at
  7 +#
  8 +# http://www.apache.org/licenses/LICENSE-2.0
  9 +#
  10 +# Unless required by applicable law or agreed to in writing, software
  11 +# distributed under the License is distributed on an "AS IS" BASIS,
  12 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +# See the License for the specific language governing permissions and
  14 +# limitations under the License.
  15 +#
  16 +
  17 +apiVersion: v1
  18 +kind: ConfigMap
  19 +metadata:
  20 + name: tb-node-cache-config
  21 + namespace: thingsboard
  22 + labels:
  23 + name: tb-node-cache-config
  24 +data:
  25 + CACHE_TYPE: redis
  26 + REDIS_HOST: tb-redis
\ No newline at end of file
... ...
  1 +#
  2 +# Copyright © 2016-2020 The Thingsboard Authors
  3 +#
  4 +# Licensed under the Apache License, Version 2.0 (the "License");
  5 +# you may not use this file except in compliance with the License.
  6 +# You may obtain a copy of the License at
  7 +#
  8 +# http://www.apache.org/licenses/LICENSE-2.0
  9 +#
  10 +# Unless required by applicable law or agreed to in writing, software
  11 +# distributed under the License is distributed on an "AS IS" BASIS,
  12 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +# See the License for the specific language governing permissions and
  14 +# limitations under the License.
  15 +#
  16 +
  17 +apiVersion: apps/v1
  18 +kind: Deployment
  19 +metadata:
  20 + name: zookeeper
  21 + namespace: thingsboard
  22 +spec:
  23 + selector:
  24 + matchLabels:
  25 + app: zookeeper
  26 + template:
  27 + metadata:
  28 + labels:
  29 + app: zookeeper
  30 + spec:
  31 + containers:
  32 + - name: server
  33 + imagePullPolicy: Always
  34 + image: zookeeper:3.5
  35 + ports:
  36 + - containerPort: 2181
  37 + readinessProbe:
  38 + periodSeconds: 5
  39 + tcpSocket:
  40 + port: 2181
  41 + livenessProbe:
  42 + initialDelaySeconds: 15
  43 + periodSeconds: 5
  44 + tcpSocket:
  45 + port: 2181
  46 + env:
  47 + - name: ZOO_MY_ID
  48 + value: "1"
  49 + - name: ZOO_SERVERS
  50 + value: "server.1=0.0.0.0:2888:3888;0.0.0.0:2181"
  51 + restartPolicy: Always
  52 +---
  53 +apiVersion: v1
  54 +kind: Service
  55 +metadata:
  56 + name: zookeeper
  57 + namespace: thingsboard
  58 +spec:
  59 + type: ClusterIP
  60 + selector:
  61 + app: zookeeper
  62 + ports:
  63 + - name: zk-port
  64 + port: 2181
  65 +---
  66 +apiVersion: apps/v1
  67 +kind: Deployment
  68 +metadata:
  69 + name: tb-kafka
  70 + namespace: thingsboard
  71 +spec:
  72 + selector:
  73 + matchLabels:
  74 + app: tb-kafka
  75 + template:
  76 + metadata:
  77 + labels:
  78 + app: tb-kafka
  79 + spec:
  80 + containers:
  81 + - name: server
  82 + imagePullPolicy: Always
  83 + image: wurstmeister/kafka:2.12-2.2.1
  84 + ports:
  85 + - containerPort: 9092
  86 + readinessProbe:
  87 + periodSeconds: 20
  88 + tcpSocket:
  89 + port: 9092
  90 + livenessProbe:
  91 + initialDelaySeconds: 25
  92 + periodSeconds: 5
  93 + tcpSocket:
  94 + port: 9092
  95 + env:
  96 + - name: KAFKA_ZOOKEEPER_CONNECT
  97 + value: "zookeeper:2181"
  98 + - name: KAFKA_LISTENERS
  99 + value: "INSIDE://:9093,OUTSIDE://:9092"
  100 + - name: KAFKA_ADVERTISED_LISTENERS
  101 + value: "INSIDE://:9093,OUTSIDE://tb-kafka:9092"
  102 + - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP
  103 + value: "INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT"
  104 + - name: KAFKA_INTER_BROKER_LISTENER_NAME
  105 + value: "INSIDE"
  106 + - name: KAFKA_CREATE_TOPICS
  107 + value: "js_eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_rule_engine:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600"
  108 + - name: KAFKA_AUTO_CREATE_TOPICS_ENABLE
  109 + value: "false"
  110 + - name: KAFKA_LOG_RETENTION_BYTES
  111 + value: "1073741824"
  112 + - name: KAFKA_LOG_SEGMENT_BYTES
  113 + value: "268435456"
  114 + - name: KAFKA_LOG_RETENTION_MS
  115 + value: "300000"
  116 + - name: KAFKA_LOG_CLEANUP_POLICY
  117 + value: "delete"
  118 + restartPolicy: Always
  119 +---
  120 +apiVersion: v1
  121 +kind: Service
  122 +metadata:
  123 + name: tb-kafka
  124 + namespace: thingsboard
  125 +spec:
  126 + type: ClusterIP
  127 + selector:
  128 + app: tb-kafka
  129 + ports:
  130 + - name: tb-kafka-port
  131 + port: 9092
  132 +---
  133 +apiVersion: apps/v1
  134 +kind: Deployment
  135 +metadata:
  136 + name: tb-redis
  137 + namespace: thingsboard
  138 +spec:
  139 + selector:
  140 + matchLabels:
  141 + app: tb-redis
  142 + template:
  143 + metadata:
  144 + labels:
  145 + app: tb-redis
  146 + spec:
  147 + containers:
  148 + - name: server
  149 + imagePullPolicy: Always
  150 + image: redis:4.0
  151 + ports:
  152 + - containerPort: 6379
  153 + readinessProbe:
  154 + periodSeconds: 5
  155 + tcpSocket:
  156 + port: 6379
  157 + livenessProbe:
  158 + periodSeconds: 5
  159 + tcpSocket:
  160 + port: 6379
  161 + volumeMounts:
  162 + - mountPath: /data
  163 + name: redis-data
  164 + volumes:
  165 + - name: redis-data
  166 + emptyDir: {}
  167 + restartPolicy: Always
  168 +---
  169 +apiVersion: v1
  170 +kind: Service
  171 +metadata:
  172 + name: tb-redis
  173 + namespace: thingsboard
  174 +spec:
  175 + type: ClusterIP
  176 + selector:
  177 + app: tb-redis
  178 + ports:
  179 + - name: tb-redis-port
  180 + port: 6379
  181 +---
\ No newline at end of file
... ...
k8s/common/cassandra.yml renamed from k8s/cassandra.yml
k8s/common/database-setup.yml renamed from k8s/database-setup.yml
k8s/common/postgres.yml renamed from k8s/postgres.yml
k8s/common/tb-coap-transport-configmap.yml renamed from k8s/tb-coap-transport-configmap.yml
k8s/common/tb-http-transport-configmap.yml renamed from k8s/tb-http-transport-configmap.yml
k8s/common/tb-mqtt-transport-configmap.yml renamed from k8s/tb-mqtt-transport-configmap.yml
k8s/common/tb-namespace.yml renamed from k8s/tb-namespace.yml
k8s/common/tb-node-cassandra-configmap.yml renamed from k8s/tb-node-cassandra-configmap.yml
k8s/common/tb-node-configmap.yml renamed from k8s/tb-node-configmap.yml
k8s/common/tb-node-postgres-configmap.yml renamed from k8s/tb-node-postgres-configmap.yml
  1 +#
  2 +# Copyright © 2016-2020 The Thingsboard Authors
  3 +#
  4 +# Licensed under the Apache License, Version 2.0 (the "License");
  5 +# you may not use this file except in compliance with the License.
  6 +# You may obtain a copy of the License at
  7 +#
  8 +# http://www.apache.org/licenses/LICENSE-2.0
  9 +#
  10 +# Unless required by applicable law or agreed to in writing, software
  11 +# distributed under the License is distributed on an "AS IS" BASIS,
  12 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +# See the License for the specific language governing permissions and
  14 +# limitations under the License.
  15 +#
  16 +
  17 +apiVersion: apps/v1
  18 +kind: Deployment
  19 +metadata:
  20 + name: tb-node
  21 + namespace: thingsboard
  22 +spec:
  23 + replicas: 2
  24 + selector:
  25 + matchLabels:
  26 + app: tb-node
  27 + template:
  28 + metadata:
  29 + labels:
  30 + app: tb-node
  31 + spec:
  32 + volumes:
  33 + - name: tb-node-config
  34 + configMap:
  35 + name: tb-node-config
  36 + items:
  37 + - key: conf
  38 + path: thingsboard.conf
  39 + - key: logback
  40 + path: logback.xml
  41 + containers:
  42 + - name: server
  43 + imagePullPolicy: Always
  44 + image: thingsboard/tb-node:latest
  45 + ports:
  46 + - containerPort: 8080
  47 + name: http
  48 + - containerPort: 9001
  49 + name: rpc
  50 + env:
  51 + - name: TB_SERVICE_ID
  52 + valueFrom:
  53 + fieldRef:
  54 + fieldPath: metadata.name
  55 + - name: TB_SERVICE_TYPE
  56 + value: "monolith"
  57 + - name: TB_QUEUE_TYPE
  58 + value: "kafka"
  59 + - name: ZOOKEEPER_ENABLED
  60 + value: "true"
  61 + - name: ZOOKEEPER_URL
  62 + value: "zookeeper:2181"
  63 + - name: TB_KAFKA_SERVERS
  64 + value: "tb-kafka:9092"
  65 + - name: JS_EVALUATOR
  66 + value: "remote"
  67 + - name: TRANSPORT_TYPE
  68 + value: "remote"
  69 + - name: HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE
  70 + value: "false"
  71 + envFrom:
  72 + - configMapRef:
  73 + name: tb-node-db-config
  74 + - configMapRef:
  75 + name: tb-node-cache-config
  76 + volumeMounts:
  77 + - mountPath: /config
  78 + name: tb-node-config
  79 + livenessProbe:
  80 + httpGet:
  81 + path: /login
  82 + port: http
  83 + initialDelaySeconds: 300
  84 + timeoutSeconds: 10
  85 + restartPolicy: Always
  86 +---
  87 +apiVersion: v1
  88 +kind: Service
  89 +metadata:
  90 + name: tb-node
  91 + namespace: thingsboard
  92 +spec:
  93 + type: ClusterIP
  94 + selector:
  95 + app: tb-node
  96 + ports:
  97 + - port: 8080
  98 + name: http
\ No newline at end of file
... ...
k8s/common/thingsboard.yml renamed from k8s/thingsboard.yml
... ... @@ -20,7 +20,7 @@ metadata:
20 20 name: tb-js-executor
21 21 namespace: thingsboard
22 22 spec:
23   - replicas: 20
  23 + replicas: 2
24 24 selector:
25 25 matchLabels:
26 26 app: tb-js-executor
... ... @@ -53,95 +53,6 @@ spec:
53 53 apiVersion: apps/v1
54 54 kind: Deployment
55 55 metadata:
56   - name: tb-node
57   - namespace: thingsboard
58   -spec:
59   - replicas: 2
60   - selector:
61   - matchLabels:
62   - app: tb-node
63   - template:
64   - metadata:
65   - labels:
66   - app: tb-node
67   - spec:
68   - volumes:
69   - - name: tb-node-config
70   - configMap:
71   - name: tb-node-config
72   - items:
73   - - key: conf
74   - path: thingsboard.conf
75   - - key: logback
76   - path: logback.xml
77   - containers:
78   - - name: server
79   - imagePullPolicy: Always
80   - image: thingsboard/tb-node:latest
81   - ports:
82   - - containerPort: 8080
83   - name: http
84   - - containerPort: 9001
85   - name: rpc
86   - env:
87   - - name: TB_SERVICE_ID
88   - valueFrom:
89   - fieldRef:
90   - fieldPath: metadata.name
91   - - name: TB_SERVICE_TYPE
92   - value: "monolith"
93   - - name: TB_QUEUE_TYPE
94   - value: "kafka"
95   - - name: ZOOKEEPER_ENABLED
96   - value: "true"
97   - - name: ZOOKEEPER_URL
98   - value: "zookeeper:2181"
99   - - name: TB_KAFKA_SERVERS
100   - value: "tb-kafka:9092"
101   - - name: JS_EVALUATOR
102   - value: "remote"
103   - - name: TRANSPORT_TYPE
104   - value: "remote"
105   - - name: CACHE_TYPE
106   - value: "redis"
107   - - name: REDIS_HOST
108   - value: "tb-redis"
109   - - name: REDIS_CONNECTION_TYPE
110   - value: "cluster"
111   - - name: REDIS_NODES
112   - value: "tb-redis:6379"
113   - - name: HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE
114   - value: "false"
115   - envFrom:
116   - - configMapRef:
117   - name: tb-node-db-config
118   - volumeMounts:
119   - - mountPath: /config
120   - name: tb-node-config
121   - livenessProbe:
122   - httpGet:
123   - path: /login
124   - port: http
125   - initialDelaySeconds: 120
126   - timeoutSeconds: 10
127   - restartPolicy: Always
128   ----
129   -apiVersion: v1
130   -kind: Service
131   -metadata:
132   - name: tb-node
133   - namespace: thingsboard
134   -spec:
135   - type: ClusterIP
136   - selector:
137   - app: tb-node
138   - ports:
139   - - port: 8080
140   - name: http
141   ----
142   -apiVersion: apps/v1
143   -kind: Deployment
144   -metadata:
145 56 name: tb-mqtt-transport
146 57 namespace: thingsboard
147 58 spec:
... ... @@ -193,6 +104,7 @@ spec:
193 104 tcpSocket:
194 105 port: 1883
195 106 livenessProbe:
  107 + initialDelaySeconds: 120
196 108 periodSeconds: 20
197 109 tcpSocket:
198 110 port: 1883
... ... @@ -266,6 +178,7 @@ spec:
266 178 tcpSocket:
267 179 port: 8080
268 180 livenessProbe:
  181 + initialDelaySeconds: 120
269 182 periodSeconds: 20
270 183 tcpSocket:
271 184 port: 8080
... ...
  1 +#
  2 +# Copyright © 2016-2020 The Thingsboard Authors
  3 +#
  4 +# Licensed under the Apache License, Version 2.0 (the "License");
  5 +# you may not use this file except in compliance with the License.
  6 +# You may obtain a copy of the License at
  7 +#
  8 +# http://www.apache.org/licenses/LICENSE-2.0
  9 +#
  10 +# Unless required by applicable law or agreed to in writing, software
  11 +# distributed under the License is distributed on an "AS IS" BASIS,
  12 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +# See the License for the specific language governing permissions and
  14 +# limitations under the License.
  15 +#
  16 +
  17 +apiVersion: v1
  18 +kind: ConfigMap
  19 +metadata:
  20 + name: tb-node-cache-config
  21 + namespace: thingsboard
  22 + labels:
  23 + name: tb-node-cache-config
  24 +data:
  25 + CACHE_TYPE: redis
  26 + REDIS_CONNECTION_TYPE: cluster
  27 + REDIS_NODES: tb-redis:6379
\ No newline at end of file
... ...
... ... @@ -169,7 +169,7 @@ spec:
169 169 - name: KAFKA_CONTROLLER_SHUTDOWN_ENABLE
170 170 value: "true"
171 171 - name: KAFKA_CREATE_TOPICS
172   - value: "js_eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600"
  172 + value: "js_eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_rule_engine:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600"
173 173 - name: KAFKA_AUTO_CREATE_TOPICS_ENABLE
174 174 value: "false"
175 175 - name: KAFKA_LOG_RETENTION_BYTES
... ...
... ... @@ -17,6 +17,9 @@
17 17
18 18 set -e
19 19
  20 +source .env
  21 +
20 22 kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
21   -kubectl delete -f thingsboard.yml
22   -kubectl delete -f thirdparty.yml
  23 +
  24 +kubectl delete -f common/thingsboard.yml
  25 +kubectl delete -f common/tb-node.yml
... ...
... ... @@ -17,5 +17,7 @@
17 17
18 18 set -e
19 19
  20 +source .env
  21 +
20 22 kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
21   -kubectl delete -f thirdparty.yml
  23 +kubectl delete -f $DEPLOYMENT_TYPE/thirdparty.yml
... ...
... ... @@ -17,10 +17,14 @@
17 17
18 18 set -e
19 19
20   -kubectl apply -f tb-namespace.yml
  20 +source .env
  21 +
  22 +kubectl apply -f common/tb-namespace.yml
21 23 kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
22   -kubectl apply -f tb-node-configmap.yml
23   -kubectl apply -f tb-mqtt-transport-configmap.yml
24   -kubectl apply -f tb-http-transport-configmap.yml
25   -kubectl apply -f tb-coap-transport-configmap.yml
26   -kubectl apply -f thingsboard.yml
  24 +kubectl apply -f common/tb-node-configmap.yml
  25 +kubectl apply -f common/tb-mqtt-transport-configmap.yml
  26 +kubectl apply -f common/tb-http-transport-configmap.yml
  27 +kubectl apply -f common/tb-coap-transport-configmap.yml
  28 +kubectl apply -f common/thingsboard.yml
  29 +kubectl apply -f $DEPLOYMENT_TYPE/tb-node-cache-configmap.yml
  30 +kubectl apply -f common/tb-node.yml
... ...
... ... @@ -17,6 +17,9 @@
17 17
18 18 set -e
19 19
20   -kubectl apply -f tb-namespace.yml
  20 +source .env
  21 +
  22 +kubectl apply -f common/tb-namespace.yml
21 23 kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
22   -kubectl apply -f thirdparty.yml
  24 +
  25 +kubectl apply -f $DEPLOYMENT_TYPE/thirdparty.yml
... ...
... ... @@ -19,8 +19,8 @@ function installTb() {
19 19
20 20 loadDemo=$1
21 21
22   - kubectl apply -f tb-node-configmap.yml
23   - kubectl apply -f database-setup.yml &&
  22 + kubectl apply -f common/tb-node-configmap.yml
  23 + kubectl apply -f common/database-setup.yml &&
24 24 kubectl wait --for=condition=Ready pod/tb-db-setup --timeout=120s &&
25 25 kubectl exec tb-db-setup -- sh -c 'export INSTALL_TB=true; export LOAD_DEMO='"$loadDemo"'; start-tb-node.sh; touch /tmp/install-finished;'
26 26
... ... @@ -30,16 +30,16 @@ function installTb() {
30 30
31 31 function installPostgres() {
32 32
33   - kubectl apply -f postgres.yml
34   - kubectl apply -f tb-node-postgres-configmap.yml
  33 + kubectl apply -f common/postgres.yml
  34 + kubectl apply -f common/tb-node-postgres-configmap.yml
35 35
36 36 kubectl rollout status deployment/postgres
37 37 }
38 38
39 39 function installCassandra() {
40 40
41   - kubectl apply -f cassandra.yml
42   - kubectl apply -f tb-node-cassandra-configmap.yml
  41 + kubectl apply -f common/cassandra.yml
  42 + kubectl apply -f common/tb-node-cassandra-configmap.yml
43 43
44 44 kubectl rollout status statefulset/cassandra
45 45
... ... @@ -75,9 +75,19 @@ fi
75 75
76 76 source .env
77 77
78   -kubectl apply -f tb-namespace.yml
  78 +kubectl apply -f common/tb-namespace.yml
79 79 kubectl config set-context $(kubectl config current-context) --namespace=thingsboard
80 80
  81 +case $DEPLOYMENT_TYPE in
  82 + basic)
  83 + ;;
  84 + high-availability)
  85 + ;;
  86 + *)
  87 + echo "Unknown DEPLOYMENT_TYPE value specified: '${DEPLOYMENT_TYPE}'. Should be either basic or high-availability." >&2
  88 + exit 1
  89 +esac
  90 +
81 91 case $DATABASE in
82 92 postgres)
83 93 installPostgres
... ... @@ -91,3 +101,4 @@ case $DATABASE in
91 101 echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or cassandra." >&2
92 102 exit 1
93 103 esac
  104 +
... ...