2020-12-11 04:43:14 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
set -o nounset
|
|
|
|
set -o errexit
|
|
|
|
set -o pipefail
|
|
|
|
|
|
|
|
# FUNCTIONS
|
|
|
|
function ask_questions {
|
|
|
|
|
|
|
|
while [[ -z "$API_HOST" ]] && [[ "$API_HOST" != *[.]*[.]* ]]
|
|
|
|
do
|
|
|
|
echo -ne "Enter the subdomain for the backend (e.g. api.example.com): "
|
|
|
|
read API_HOST
|
|
|
|
done
|
|
|
|
echo "API_HOST is set to ${API_HOST}"
|
|
|
|
|
|
|
|
while [[ -z "$APP_HOST" ]] && [[ "$APP_HOST" != *[.]*[.]* ]]
|
|
|
|
do
|
|
|
|
echo -ne "Enter the subdomain for the frontend (e.g. rmm.example.com): "
|
|
|
|
read APP_HOST
|
|
|
|
done
|
|
|
|
echo "APP_HOST is set to ${APP_HOST}"
|
|
|
|
|
|
|
|
while [[ -z "$MESH_HOST" ]] && [[ "$MESH_HOST" != *[.]*[.]* ]]
|
|
|
|
do
|
|
|
|
echo -ne "Enter the subdomain for meshcentral (e.g. mesh.example.com): "
|
|
|
|
read MESH_HOST
|
|
|
|
done
|
|
|
|
echo "MESH_HOST is set to ${MESH_HOST}"
|
|
|
|
|
|
|
|
while [[ -z "$EMAIL" ]] && [[ "$EMAIL" != *[@]*[.]* ]]
|
|
|
|
do
|
|
|
|
echo -ne "Enter a valid email address for django and meshcentral: "
|
|
|
|
read EMAIL
|
|
|
|
done
|
|
|
|
echo "EMAIL is set to ${EMAIL}"
|
|
|
|
|
|
|
|
while [[ -z "$USERNAME" ]]
|
|
|
|
do
|
|
|
|
echo -ne "Set username for mesh and tactical login: "
|
|
|
|
read USERNAME
|
|
|
|
done
|
|
|
|
echo "USERNAME is set to ${USERNAME}"
|
|
|
|
|
|
|
|
while [[ -z "$PASSWORD" ]]
|
|
|
|
do
|
|
|
|
echo -ne "Set password for mesh and tactical password: "
|
|
|
|
read PASSWORD
|
|
|
|
done
|
|
|
|
echo "PASSWORD is set"
|
|
|
|
|
|
|
|
# check if let's encrypt or cert-keys options were set
|
|
|
|
if [[ -z "$LETS_ENCRYPT" ]] && [[ -z "$CERT_PRIV_FILE" ]] || [[ -z "$CERT_PUB_FILE" ]]; then
|
|
|
|
echo -ne "Create a let's encrypt certificate?[Y,n]: "
|
|
|
|
read USE_LETS_ENCRYPT
|
|
|
|
|
|
|
|
[[ "$USE_LETS_ENCRYPT" == "" ]] || [[ "$USE_LETS_ENCRYPT" ~= [Yy] ]] && LETS_ENCRYPT=1
|
|
|
|
|
|
|
|
if [[ -z "$LET_ENCRYPT" ]]; then
|
|
|
|
echo "Let's Encrypt will not be used"
|
|
|
|
|
|
|
|
echo -ne "Do you want to specify paths to a certificate public key and private key?[Y,n]: "
|
|
|
|
read PRIVATE_CERTS
|
|
|
|
|
|
|
|
if [[ "$PRIVATE_CERTS" == "" ]] || [[ "$PRIVATE_CERTS" ~= [yY] ]]; then
|
|
|
|
|
|
|
|
# check for valid public certificate file
|
|
|
|
while [[ ! -f $CERT_PUB_FILE ]]
|
|
|
|
do
|
|
|
|
echo -ne "Enter a valid full path to public key file: "
|
|
|
|
read CERT_PUB_FILE
|
|
|
|
done
|
|
|
|
|
|
|
|
# check for valid private key file
|
|
|
|
while [[ ! -f $CERT_PRIV_FILE ]]
|
|
|
|
do
|
|
|
|
echo -ne "Enter a valid full path to private key file: "
|
|
|
|
read CERT_PRIV_FILE
|
|
|
|
done
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function encode_certificates {
|
|
|
|
echo "Base64 encoding certificates"
|
|
|
|
CERT_PUB_BASE64="$(sudo base64 -w 0 ${CERT_PUB_FILE})"
|
|
|
|
CERT_PRIV_BASE64="$(sudo base64 -w 0 ${CERT_PRIV_FILE})"
|
|
|
|
}
|
|
|
|
|
|
|
|
function generate_env {
|
|
|
|
[[ -f "$ENV_FILE" ]] && echo "Env file already exists"; return 0;
|
|
|
|
|
|
|
|
local mongodb_user=$(cat /dev/urandom | tr -dc 'a-z' | fold -w 8 | head -n 1)
|
|
|
|
local mongodb_pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)
|
|
|
|
local postgres_user=$(cat /dev/urandom | tr -dc 'a-z' | fold -w 8 | head -n 1)
|
|
|
|
local postgres_pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)
|
|
|
|
|
|
|
|
echo "Generating env file in ${INSTALL_DIR}"
|
|
|
|
local config_file="$(cat << EOF
|
2021-01-15 19:13:55 +00:00
|
|
|
IMAGE_REPO=${DOCKER_REPO}
|
2020-12-11 04:43:14 +00:00
|
|
|
VERSION=${VERSION}
|
|
|
|
TRMM_USER=${USERNAME}
|
|
|
|
TRMM_PASS=${PASSWORD}
|
|
|
|
APP_HOST=${APP_HOST}
|
|
|
|
API_HOST=${API_HOST}
|
|
|
|
MESH_HOST=${MESH_HOST}
|
|
|
|
MESH_USER=${USERNAME}
|
|
|
|
MESH_PASS=${PASSWORD}
|
|
|
|
MONGODB_USER=${mongogb_user}
|
|
|
|
MONGODB_PASSWORD=${mongodb_pass}
|
|
|
|
POSTGRES_USER=${postgres_user}
|
|
|
|
POSTGRES_PASS=${postgres_pass}
|
|
|
|
EOF
|
|
|
|
)"
|
|
|
|
echo "${env_file}" > "$ENV_FILE"
|
|
|
|
}
|
|
|
|
|
|
|
|
function update_env_field {
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function get_env_field {
|
|
|
|
local search_field="$1"
|
|
|
|
awk -F "=" '{if ($1==$search_field) { print $2" } }' $ENV_FILE
|
|
|
|
}
|
|
|
|
|
|
|
|
function initiate_letsencrypt {
|
|
|
|
echo "Starting Let's Encrypt"
|
|
|
|
|
|
|
|
ROOT_DOMAIN=$(echo ${API_HOST} | cut -d "." -f2- )
|
|
|
|
|
|
|
|
echo "Root domain is ${ROOTDOMAIN}"
|
2022-04-21 20:45:47 +00:00
|
|
|
sudo certbot certonly --manual -d *.${ROOT_DOMAIN} --agree-tos --no-bootstrap --preferred-challenges dns -m ${EMAIL} --no-eff-email
|
2020-12-11 04:43:14 +00:00
|
|
|
while [[ $? -ne 0 ]]
|
|
|
|
do
|
2022-04-21 20:45:47 +00:00
|
|
|
sudo certbot certonly --manual -d *.${ROOT_DOMAIN} --agree-tos --no-bootstrap --preferred-challenges dns -m ${EMAIL} --no-eff-email
|
2020-12-11 04:43:14 +00:00
|
|
|
done
|
|
|
|
|
|
|
|
CERT_PRIV_FILE=/etc/letsencrypt/live/${ROOT_DOMAIN}/privkey.pem
|
|
|
|
CERT_PUB_FILE=/etc/letsencrypt/live/${ROOT_DOMAIN}/fullchain.pem
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# setup defaults
|
|
|
|
# keep track of first arg
|
|
|
|
FIRST_ARG="$1"
|
|
|
|
|
|
|
|
# defaults
|
2021-01-15 19:13:55 +00:00
|
|
|
DOCKER_REPO="tacticalrmm/"
|
2022-03-18 18:09:58 +00:00
|
|
|
REPO="amidaware"
|
2020-12-11 04:43:14 +00:00
|
|
|
BRANCH="master"
|
|
|
|
VERSION="latest"
|
|
|
|
|
|
|
|
# file locations
|
|
|
|
INSTALL_DIR=/opt/tactical
|
|
|
|
ENV_FILE=/opt/tactical/.env
|
|
|
|
|
|
|
|
# check prerequisites
|
|
|
|
command -v docker >/dev/null 2>&1 || { echo >&2 "Docker must be installed. Exiting..."; exit 1; }
|
|
|
|
command -v docker-compose >/dev/null 2>&1 || { echo >&2 "Docker Compose must be installed. Exiting..."; exit 1; }
|
|
|
|
command -v curl >/dev/null 2>&1 || { echo >&2 "Curl must be installed. Exiting..."; exit 1; }
|
|
|
|
command -v bash >/dev/null 2>&1 || { echo >&2 "Bash must be installed. Exiting..."; exit 1; }
|
|
|
|
|
|
|
|
# check for arguments
|
|
|
|
[ -z "$1" ] && echo >&2 "No arguments supplied. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
# parse arguments
|
|
|
|
while [[ $# -gt 0 ]]
|
|
|
|
do
|
|
|
|
key="$1"
|
|
|
|
|
|
|
|
case $key in
|
|
|
|
# install arg
|
|
|
|
-i|install)
|
|
|
|
[[ "$key" != "$FIRST_ARG" ]] && echo >&2 "install must be the first argument. Exiting.."; exit 1;
|
|
|
|
MODE="install"
|
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
|
|
|
# update arg
|
|
|
|
-u|update)
|
|
|
|
[[ "$key" != "$FIRST_ARG" ]] && echo >&2 "update must be the first argument. Exiting..."; exit 1;
|
|
|
|
MODE="update"
|
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
|
|
|
# backup arg
|
|
|
|
-b|backup)
|
|
|
|
[[ "$key" != "$FIRST_ARG" ]] && echo >&2 "backup must be the first argument. Exiting..."; exit 1;
|
|
|
|
MODE="backup"
|
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
|
|
|
# restore arg
|
|
|
|
-r|restore)
|
|
|
|
[[ "$key" != "$FIRST_ARG" ]] && echo >&2 "restore must be the first argument. Exiting..."; exit 1;
|
|
|
|
MODE="restore"
|
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
|
|
|
# update-cert arg
|
|
|
|
-c|update-cert)
|
|
|
|
[[ "$key" != "$FIRST_ARG" ]] && echo >&2 "update-cert must be the first argument. Exiting..."; exit 1;
|
|
|
|
MODE="update-cert"
|
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
|
|
|
# use-lets-encrypt arg
|
|
|
|
--use-lets-encrypt)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install or update-cert as first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update-cert" ]] && \
|
|
|
|
echo >&2 "--use-lets-encrypt option only valid for install and update-cert. Exiting..."; exit 1;
|
|
|
|
LETS_ENCRYPT=1
|
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
|
|
|
# cert-priv-file arg
|
|
|
|
--cert-priv-file)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install or update-cert first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update-cert" ]] && \
|
|
|
|
echo >&2 "--cert-priv-file option only valid for install and update-cert. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
[ ! -f "$key" ] && echo >&2 "Certificate private key file $key does not exist. Use absolute paths. Exiting..."; exit 1;
|
|
|
|
CERT_PRIV_FILE="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# cert-pub-file arg
|
|
|
|
--cert-pub-file)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install or update-cert first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update-cert" ]] && \
|
|
|
|
echo >&2 "--cert-pub-file option only valid for install and update-cert. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
[ ! -f "$key" ] && echo >&2 "Certificate public Key file ${key} does not exist. Use absolute paths. Exiting..."; exit 1;
|
|
|
|
CERT_PUB_FILE="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# local arg
|
|
|
|
--local)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \
|
|
|
|
echo >&2 "--local option only valid for install and update. Exiting..."; exit 1;
|
2021-01-15 19:13:55 +00:00
|
|
|
DOCKER_REPO=""
|
2020-12-11 04:43:14 +00:00
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
2021-01-15 19:13:55 +00:00
|
|
|
# repo arg
|
|
|
|
--repo)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \
|
|
|
|
echo >&2 "--repo option only valid for install and update. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
REPO="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
2020-12-11 04:43:14 +00:00
|
|
|
# branch arg
|
|
|
|
--branch)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \
|
|
|
|
echo >&2 "--branch option only valid for install and update. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
BRANCH="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# version arg
|
|
|
|
--version)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \
|
|
|
|
echo ">&2 --version option only valid for install and update. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
VERSION="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# noninteractive arg
|
|
|
|
--noninteractive)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] && echo >&2 "--noninteractive option only valid for install. Exiting..."; exit 1;
|
|
|
|
NONINTERACTIVE=1
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
;;
|
|
|
|
|
|
|
|
# app host arg
|
|
|
|
--app-host)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] && echo >&2 "--app-host option only valid for install. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
APP_HOST="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# api host arg
|
|
|
|
--api-host)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] && echo >&2 "--api-host option only valid for install. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
API_HOST="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# mesh host arg
|
|
|
|
--mesh-host)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] && echo >&2 "--mesh-host option only valid for install. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
MESH_HOST="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# tactical user arg
|
|
|
|
--tactical-user)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] && echo >&2 "--tactical-user option only valid for install. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
USERNAME="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# tactical password arg
|
|
|
|
--tactical-password)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] && echo >&2 "--tactical-password option only valid for install. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
PASSWORD="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# email arg
|
|
|
|
--email)
|
|
|
|
[[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1;
|
|
|
|
[[ "$MODE" != "install" ]] && echo >&2 "--email option only valid for install. Exiting..."; exit 1;
|
|
|
|
|
|
|
|
shift # past argument
|
|
|
|
EMAIL="$key"
|
|
|
|
shift # past value
|
|
|
|
;;
|
|
|
|
|
|
|
|
# Unknown arg
|
|
|
|
*)
|
|
|
|
echo "Unknown argument ${$1}. Exiting..."
|
|
|
|
exit 1
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
# for install mode
|
|
|
|
if [[ "$MODE" == "install" ]]; then
|
|
|
|
echo "Starting installation in ${INSTALL_DIR}"
|
|
|
|
|
|
|
|
# move to install dir
|
|
|
|
mkdir -p "${INSTALL_DIR}"
|
|
|
|
cd "$INSTALL_DIR"
|
|
|
|
|
|
|
|
# pull docker-compose.yml file
|
2021-01-15 19:13:55 +00:00
|
|
|
echo "Downloading docker-compose.yml from branch ${BRANCH}"
|
|
|
|
COMPOSE_FILE="https://raw.githubusercontent.com/${REPO}/tacticalrmm/${BRANCH}/docker/docker-compose.yml"
|
2020-12-11 04:43:14 +00:00
|
|
|
if ! curl -sS "${COMPOSE_FILE}"; then
|
|
|
|
echo >&2 "Failed to download installation package ${COMPOSE_FILE}"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
# check if install is noninteractive
|
|
|
|
if [[ -z "$NONINTERACTIVE" ]]; then
|
|
|
|
# ask user for information not supplied as arguments
|
|
|
|
ask_questions
|
|
|
|
|
|
|
|
else
|
|
|
|
echo "NonInteractive mode set."
|
|
|
|
# check for required noninteractive arguments
|
|
|
|
[[ -z "$API_HOST" ]] || \
|
|
|
|
[[ -z "$APP_HOST" ]] || \
|
|
|
|
[[ -z "$MESH_HOST" ]] || \
|
|
|
|
[[ -z "$EMAIL" ]] || \
|
|
|
|
[[ -z "$USERNAME" ]] || \
|
|
|
|
[[ -z "$PASSWORD" ]] && \
|
|
|
|
echo "You must supply additional arguments for noninteractive install."; exit 1;
|
|
|
|
fi
|
|
|
|
|
|
|
|
# if certificates are available base64 encode them
|
|
|
|
if [[ -n "$LET_ENCRYPT" ]] && [[ -z "$NONINTERACTIVE" ]]; then
|
|
|
|
initiate_letsencrypt
|
|
|
|
encode_certificates
|
|
|
|
elif [[ -n "$CERT_PUB_FILE" ]] && [[ -n "$CERT_PRIV_FILE" ]]; then
|
|
|
|
encode_certificates
|
|
|
|
|
|
|
|
# generate config file
|
|
|
|
generate_config
|
|
|
|
|
|
|
|
# generate env file
|
|
|
|
generate_env
|
|
|
|
|
|
|
|
echo "Configuration complete. Starting environment."
|
|
|
|
# start environment
|
|
|
|
docker-compose pull
|
|
|
|
docker-compose up -d
|
|
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
# for update mode
|
|
|
|
if [[ "$MODE" == "update" ]]; then
|
|
|
|
[[ "$VERSION" != "latest" ]]
|
|
|
|
docker-compose pull
|
|
|
|
docker-compose up -d
|
|
|
|
fi
|
|
|
|
|
|
|
|
# for update cert mode
|
|
|
|
if [[ "$MODE" == "update-cert" ]]; then
|
|
|
|
# check for required parameters
|
|
|
|
[[ -z "$LET_ENCRYPT" ]] || \
|
|
|
|
[[ -z "$CERT_PUB_FILE" ]] && \
|
|
|
|
[[ -z "$CERT_PRIV_FILE" ]] && \
|
|
|
|
echo >&2 "Provide the --lets-encrypt option or use --cert-pub-file and --cert-priv-file. Exiting..."; exit;
|
|
|
|
|
|
|
|
if [[ -n "$LET_ENCRYPT" ]]; then
|
|
|
|
initiate_letsencrypt
|
|
|
|
encode_certificates
|
|
|
|
generate_env
|
|
|
|
elif [[ -n "$CERT_PUB_FILE" ]] && [[ -n "$CERT_PRIV_FILE" ]]; then
|
|
|
|
encode_certificates
|
|
|
|
generate_env
|
|
|
|
|
|
|
|
docker-compose restart
|
|
|
|
fi
|
|
|
|
|
|
|
|
# for backup mode
|
|
|
|
if [[ "$MODE" == "backup" ]]; then
|
|
|
|
echo "backup not yet implemented"
|
|
|
|
fi
|
|
|
|
|
|
|
|
# for restore mode
|
|
|
|
if [[ "$MODE" == "restore" ]] then;
|
|
|
|
echo "restore not yet implemented"
|
|
|
|
fi
|