1#!/bin/bash
2# SOURCE: GRUNTWORKS
3# This script can be used to install Consul and its dependencies. This script has been tested with the following
4# operating systems:
5#
6# 1. Ubuntu 16.04
7# 1. Ubuntu 18.04
8# 1. Amazon Linux 2
9
10set -e
11
12readonly DEFAULT_INSTALL_PATH="/opt/consul"
13readonly DEFAULT_CONSUL_USER="consul"
14readonly DOWNLOAD_PACKAGE_PATH="/tmp/consul.zip"
15
16readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17readonly SYSTEM_BIN_DIR="/usr/local/bin"
18
19readonly SCRIPT_NAME="$(basename "$0")"
20
21function print_usage {
22  echo
23  echo "Usage: install-consul [OPTIONS]"
24  echo
25  echo "This script can be used to install Consul and its dependencies. This script has been tested with Ubuntu 16.04 and Amazon Linux 2."
26  echo
27  echo "Options:"
28  echo
29  echo -e "  --version\t\tThe version of Consul to install. Optional if download-url is provided."
30  echo -e "  --download-url\t\tUrl to exact Consul package to be installed. Optional if version is provided."
31  echo -e "  --path\t\tThe path where Consul should be installed. Optional. Default: $DEFAULT_INSTALL_PATH."
32  echo -e "  --user\t\tThe user who will own the Consul install directories. Optional. Default: $DEFAULT_CONSUL_USER."
33  echo -e "  --ca-file-path\t\tPath to a PEM-encoded certificate authority used to encrypt and verify authenticity of client and server connections. Will be installed under <install-path>/tls/ca."
34  echo -e "  --cert-file-path\t\tPath to a PEM-encoded certificate, which will be provided to clients or servers to verify the agent's authenticity. Will be installed under <install-path>/tls. Must be provided along with --key-file-path."
35  echo -e "  --key-file-path\t\tPath to a PEM-encoded private key, used with the certificate to verify the agent's authenticity. Will be installed under <install-path>/tls. Must be provided along with --cert-file-path"
36  echo
37  echo "Example:"
38  echo
39  echo "  install-consul --version 1.2.2"
40}
41
42function log {
43  local -r level="$1"
44  local -r message="$2"
45  local -r timestamp=$(date +"%Y-%m-%d %H:%M:%S")
46  >&2 echo -e "${timestamp} [${level}] [$SCRIPT_NAME] ${message}"
47}
48
49function log_info {
50  local -r message="$1"
51  log "INFO" "$message"
52}
53
54function log_warn {
55  local -r message="$1"
56  log "WARN" "$message"
57}
58
59function log_error {
60  local -r message="$1"
61  log "ERROR" "$message"
62}
63
64function assert_not_empty {
65  local -r arg_name="$1"
66  local -r arg_value="$2"
67
68  if [[ -z "$arg_value" ]]; then
69    log_error "The value for '$arg_name' cannot be empty"
70    print_usage
71    exit 1
72  fi
73}
74
75function assert_either_or {
76  local -r arg1_name="$1"
77  local -r arg1_value="$2"
78  local -r arg2_name="$3"
79  local -r arg2_value="$4"
80
81  if [[ -z "$arg1_value" && -z "$arg2_value" ]]; then
82    log_error "Either the value for '$arg1_name' or '$arg2_name' must be passed, both cannot be empty"
83    print_usage
84    exit 1
85  fi
86}
87
88# A retry function that attempts to run a command a number of times and returns the output
89function retry {
90  local -r cmd="$1"
91  local -r description="$2"
92
93  for i in $(seq 1 5); do
94    log_info "$description"
95
96    # The boolean operations with the exit status are there to temporarily circumvent the "set -e" at the
97    # beginning of this script which exits the script immediatelly for error status while not losing the exit status code
98    output=$(eval "$cmd") && exit_status=0 || exit_status=$?
99    log_info "$output"
100    if [[ $exit_status -eq 0 ]]; then
101      echo "$output"
102      return
103    fi
104    log_warn "$description failed. Will sleep for 10 seconds and try again."
105    sleep 10
106  done;
107
108  log_error "$description failed after 5 attempts."
109  exit $exit_status
110}
111
112function has_yum {
113  [ -n "$(command -v yum)" ]
114}
115
116function has_apt_get {
117  [ -n "$(command -v apt-get)" ]
118}
119
120function install_dependencies {
121  log_info "Installing dependencies"
122
123  if $(has_apt_get); then
124    sudo apt-get update -y
125    sudo apt-get install -y awscli curl unzip jq
126  elif $(has_yum); then
127    sudo yum update -y
128    sudo yum install -y aws curl unzip jq
129  else
130    log_error "Could not find apt-get or yum. Cannot install dependencies on this OS."
131    exit 1
132  fi
133}
134
135function user_exists {
136  local -r username="$1"
137  id "$username" >/dev/null 2>&1
138}
139
140function create_consul_user {
141  local -r username="$1"
142
143  if $(user_exists "$username"); then
144    echo "User $username already exists. Will not create again."
145  else
146    log_info "Creating user named $username"
147    sudo useradd "$username"
148  fi
149}
150
151function create_consul_install_paths {
152  local -r path="$1"
153  local -r username="$2"
154
155  log_info "Creating install dirs for Consul at $path"
156  sudo mkdir -p "$path"
157  sudo mkdir -p "$path/bin"
158  sudo mkdir -p "$path/config"
159  sudo mkdir -p "$path/data"
160  sudo mkdir -p "$path/tls/ca"
161
162  log_info "Changing ownership of $path to $username"
163  sudo chown -R "$username:$username" "$path"
164}
165
166function fetch_binary {
167  local -r version="$1"
168  local download_url="$2"
169
170  if [[ -z "$download_url" && -n "$version" ]];  then
171    download_url="https://releases.hashicorp.com/consul/${version}/consul_${version}_linux_amd64.zip"
172  fi
173
174  retry \
175    "curl -o '$DOWNLOAD_PACKAGE_PATH' '$download_url' --location --silent --fail --show-error" \
176    "Downloading Consul to $DOWNLOAD_PACKAGE_PATH"
177}
178
179function install_binary {
180  local -r install_path="$1"
181  local -r username="$2"
182
183  local -r bin_dir="$install_path/bin"
184  local -r consul_dest_path="$bin_dir/consul"
185  local -r run_consul_dest_path="$bin_dir/run-consul"
186
187  unzip -d /tmp "$DOWNLOAD_PACKAGE_PATH"
188
189  log_info "Moving Consul binary to $consul_dest_path"
190  sudo mv "/tmp/consul" "$consul_dest_path"
191  sudo chown "$username:$username" "$consul_dest_path"
192  sudo chmod a+x "$consul_dest_path"
193
194  local -r symlink_path="$SYSTEM_BIN_DIR/consul"
195  if [[ -f "$symlink_path" ]]; then
196    log_info "Symlink $symlink_path already exists. Will not add again."
197  else
198    log_info "Adding symlink to $consul_dest_path in $symlink_path"
199    sudo ln -s "$consul_dest_path" "$symlink_path"
200  fi
201
202  log_info "Copying Consul run script to $run_consul_dest_path"
203  sudo cp "$SCRIPT_DIR/run-consul" "$run_consul_dest_path"
204  sudo chown "$username:$username" "$run_consul_dest_path"
205  sudo chmod a+x "$run_consul_dest_path"
206}
207
208function install_tls_certificates {
209  local -r path="$1"
210  local -r user="$2"
211  local -r ca_file_path="$3"
212  local -r cert_file_path="$4"
213  local -r key_file_path="$5"
214
215  local -r consul_tls_certs_path="$path/tls"
216  local -r ca_certs_path="$consul_tls_certs_path/ca"
217
218  log_info "Moving TLS certs to $consul_tls_certs_path and $ca_certs_path"
219
220  sudo mkdir -p "$ca_certs_path"
221  sudo mv "$ca_file_path" "$ca_certs_path/"
222  sudo mv "$cert_file_path" "$consul_tls_certs_path/"
223  sudo mv "$key_file_path" "$consul_tls_certs_path/"
224
225  sudo chown -R "$user:$user" "$consul_tls_certs_path/"
226  sudo find "$consul_tls_certs_path/" -type f -exec chmod u=r,g=,o= {} \;
227}
228
229function install {
230  local version=""
231  local download_url=""
232  local path="$DEFAULT_INSTALL_PATH"
233  local user="$DEFAULT_CONSUL_USER"
234  local ca_file_path=""
235  local cert_file_path=""
236  local key_file_path=""
237
238  while [[ $# > 0 ]]; do
239    local key="$1"
240
241    case "$key" in
242      --version)
243        version="$2"
244        shift
245        ;;
246      --download-url)
247        download_url="$2"
248        shift
249        ;;
250      --path)
251        path="$2"
252        shift
253        ;;
254      --user)
255        user="$2"
256        shift
257        ;;
258      --ca-file-path)
259        assert_not_empty "$key" "$2"
260        ca_file_path="$2"
261        shift
262        ;;
263      --cert-file-path)
264        assert_not_empty "$key" "$2"
265        cert_file_path="$2"
266        shift
267        ;;
268      --key-file-path)
269        assert_not_empty "$key" "$2"
270        key_file_path="$2"
271        shift
272        ;;
273      --help)
274        print_usage
275        exit
276        ;;
277      *)
278        log_error "Unrecognized argument: $key"
279        print_usage
280        exit 1
281        ;;
282    esac
283
284    shift
285  done
286
287  assert_either_or "--version" "$version" "--download-url" "$download_url"
288  assert_not_empty "--path" "$path"
289  assert_not_empty "--user" "$user"
290
291  log_info "Starting Consul install"
292
293  install_dependencies
294  create_consul_user "$user"
295  create_consul_install_paths "$path" "$user"
296
297  fetch_binary "$version" "$download_url"
298  install_binary "$path" "$user"
299
300  if [[ -n "$ca_file_path" || -n "$cert_file_path" || -n "$key_file_path" ]]; then
301    install_tls_certificates "$path" "$user" "$ca_file_path" "$cert_file_path" "$key_file_path"
302  fi
303
304  if command -v consul; then
305    log_info "Consul install complete!";
306  else
307    log_info "Could not find consul command. Aborting.";
308    exit 1;
309  fi
310}
311
312install "$@"
313