diff --git a/bin/vps b/bin/vps index fefdc05..8dfb8c7 100755 --- a/bin/vps +++ b/bin/vps @@ -495,13 +495,110 @@ mailcow:get_default_backup_host_ident() { echo "$dest $ident" } + +compose:service:containers() { + local project="$1" service="$2" + + docker ps \ + --filter label="com.docker.compose.project=$project" \ + --filter label="compose.master-service=$service" \ + --format="{{.ID}}" +} +export -f compose:service:containers + + +compose:service:container_one() { + local project="$1" service="$2" container_id + { + read-0a container_id || { + err "service ${DARKYELLOW}$service${NORMAL} has no running container." + return 1 + } + if read-0a _; then + err "service ${DARKYELLOW}$service${NORMAL} has more than one running container." + return 1 + fi + } < <(compose:service:containers "$project" "$service") + echo "$container_id" +} +export -f compose:service:container_one + + +compose:service:container_first() { + local project="$1" service="$2" container_id + { + read-0a container_id || { + err "service ${DARKYELLOW}$service${NORMAL} has no running container." + return 1 + } + if read-0a _; then + warn "service ${DARKYELLOW}$service${NORMAL} has more than one running container." + fi + } < <(compose:service:containers "$project" "$service") + echo "$container_id" +} +export -f compose:service:container_first + + +compose:charm:containers() { + local project="$1" charm="$2" + + docker ps \ + --filter label="com.docker.compose.project=$project" \ + --filter label="compose.charm=$charm" \ + --format="{{.ID}}" +} +export -f compose:charm:containers + + +compose:charm:container_one() { + local project="$1" charm="$2" container_id + { + read-0a container_id || { + err "charm ${DARKPINK}$charm${NORMAL} has no running container in project '$project'." + return 1 + } + if read-0a _; then + err "charm ${DARKPINK}$charm${NORMAL} has more than one running container." + return 1 + fi + } < <(compose:charm:containers "$project" "$charm") + echo "$container_id" +} +export -f compose:charm:container_one + + +compose:charm:container_first() { + local project="$1" charm="$2" container_id + { + read-0a container_id || { + warn "charm ${DARKYELLOW}$charm${NORMAL} has no running container in project '$project'." + } + if read-0a _; then + warn "charm ${DARKYELLOW}$charm${NORMAL} has more than one running container." + fi + } < <(compose:charm:containers "$project" "$charm") + echo "$container_id" +} +export -f compose:charm:container_first + + + compose:get_url() { - local service="$1" - ( - set -o pipefail - cat "/var/lib/compose/relations/myc/${service}-frontend/web-proxy/data" | - shyaml get-value url - ) || { + local project_name="$1" service="$2" data_file network ip + data_file="/var/lib/compose/relations/${project_name}/${service}-frontend/web-proxy/data" + if [ -e "$data_file" ]; then + ( + set -o pipefail + cat "$data_file" | shyaml get-value url + ) + else + ## Assume there are no frontend relation here, the url is direct IP + container_id=$(compose:service:container_one "${project_name}" "${service}") || return 1 + network_ip=$(docker:container:network_ip_one "${container_id}") || return 1 + IFS=":" read -r network ip <<<"$network_ip" + echo "http://$ip" + fi || { err "Failed querying ${service} to frontend relation to get url." return 1 } @@ -509,19 +606,90 @@ compose:get_url() { export -f compose:get_url +compose:container:service() { + local container="$1" service + if ! service=$(docker:container:label "$container" "compose.service"); then + err "Failed to get service name from container ${container}." + return 1 + fi + if [ -z "$service" ]; then + err "No service found for container ${container}." + return 1 + fi + echo "$service" +} +export -f compose:container:service + compose:psql() { - local dbname="$1" - docker exec -i myc_postgres_1 psql -U postgres "$dbname" + local project_name="$1" dbname="$2" + container_id=$(compose:charm:container_one "$project_name" "postgres") || return 1 + docker exec -i "${container_id}" psql -U postgres "$dbname" } export -f compose:psql +compose:pgm() { + local project_name="$1" container_network_ip container_ip container_network + shift + + container_id=$(compose:charm:container_one "$project_name" "postgres") || return 1 + service_name=$(compose:container:service "$container_id") || return 1 + image_id=$(docker:container:image "$container_id") || return 1 + container_network_ip=$(docker:container:network_ip_one "$container_id") || return 1 + IFS=":" read -r container_network container_ip <<<"$container_network_ip" + + pgpass="/srv/datastore/data/${service_name}/var/lib/postgresql/data/pgpass" + + local final_pgm_docker_run_opts+=( + -u 0 -e prefix_pg_local_command=" " + --network "${container_network}" + -e PGHOST="$container_ip" + -e PGUSER=postgres + -v "$pgpass:/root/.pgpass" + "${pgm_docker_run_opts[@]}" + ) + + cmd=(docker run --rm \ + "${final_pgm_docker_run_opts[@]}" \ + "${image_id}" pgm "$@" + ) + echo "${cmd[@]}" + "${cmd[@]}" +} +export -f compose:pgm + + +postgres:dump() { + local project_name="$1" src="$2" dst="$3" + + ( + settmpdir PGM_TMP_LOCATION + + pgm_docker_run_opts=('-v' "${PGM_TMP_LOCATION}:/tmp/dump") + compose:pgm "$project_name" cp -f "$src" "/tmp/dump/dump.gz" && + mv "$PGM_TMP_LOCATION/dump.gz" "$dst" + ) || return 1 +} +export -f postgres:dump + + +postgres:restore() { + local project_name="$1" src="$2" dst="$3" + + full_src_path=$(readlink -e "$src") || exit 1 + ( + pgm_docker_run_opts=('-v' "${full_src_path}:/tmp/dump.gz") + compose:pgm "$project_name" cp -f "/tmp/dump.gz" "$dst" + ) || return 1 +} +export -f postgres:restore + + cyclos:set_root_url() { - local dbname="$1" cyclos_service="$2" url + local project_name="$1" dbname="$2" url="$3" - url=$(compose:get_url "${cyclos_service}") || return 1 echo "UPDATE configurations SET root_url = '$url';" | - compose:psql "$dbname" || { + compose:psql "$project_name" "$dbname" || { err "Failed to set cyclos url value in '$dbname' database." return 1 } @@ -529,9 +697,30 @@ cyclos:set_root_url() { export -f cyclos:set_root_url +compose:project_name() { + if [ -z "$PROJECT_NAME" ]; then + PROJECT_NAME=$(compose --get-project-name) || { + err "Couldn't get project name." + return 1 + } + if [ -z "$PROJECT_NAME" -o "$PROJECT_NAME" == "orphan" ]; then + err "Couldn't get project name, probably because 'compose.yml' wasn't found." + echo " Please ensure to either configure a global 'compose.yml' or run this command" >&2 + echo " in a compose project (with 'compose.yml' on the top level directory)." >&2 + return 1 + fi + export PROJECT_NAME + fi + echo "$PROJECT_NAME" +} +export -f compose:project_name + + compose:get_cron_docker_cmd() { local cron_line cmd_line docker_cmd - if ! cron_line=$(docker exec myc_cron_1 cat /etc/cron.d/rsync-backup | grep "\* \* \*"); then + project_name=$(compose:project_name) || return 1 + + if ! cron_line=$(docker exec "${project_name}"_cron_1 cat /etc/cron.d/rsync-backup | grep "\* \* \*"); then err "Can't find cron_line in cron container." echo " Have you forgotten to run 'compose up' ?" >&2 return 1 @@ -559,9 +748,10 @@ compose:get_cron_docker_cmd() { compose:recover-target() { - local backup_host="$1" ident="$2" src="$3" dst="$4" service_name="${5:-rsync-backup}" + local backup_host="$1" ident="$2" src="$3" dst="$4" service_name="${5:-rsync-backup}" project_name + project_name=$(compose:project_name) || return 1 - docker_image="myc_${service_name}" + docker_image="${project_name}_${service_name}" if ! docker_has_image "$docker_image"; then compose build "${service_name}" || { err "Couldn't find nor build image for service '$service_name'." @@ -820,13 +1010,14 @@ set_errlvl() { return "${1:-1}"; } cmdline.spec:backup:cmd:compose:run() { local cron_line args + project_name=$(compose:project_name) || return 1 docker_cmd=$(compose:get_cron_docker_cmd) || return 1 - echo "${WHITE}Launching:${NORMAL} docker exec -i myc_cron_1 $docker_cmd" + echo "${WHITE}Launching:${NORMAL} docker exec -i "${project_name}_cron_1" $docker_cmd" { { - eval "docker exec -i myc_cron_1 $docker_cmd" | sed -r "s/^/ ${GRAY}|${NORMAL} /g" + eval "docker exec -i \"${project_name}_cron_1\" $docker_cmd" | sed -r "s/^/ ${GRAY}|${NORMAL} /g" set_errlvl "${PIPESTATUS[0]}" } 3>&1 1>&2 2>&3 | sed -r "s/^/ $DARKRED\!$NORMAL /g" set_errlvl "${PIPESTATUS[0]}" @@ -888,22 +1079,23 @@ cmdline.spec:odoo:cmd:restart:run() { local out odoo_service odoo_service="${opt_service:-odoo}" + project_name=$(compose:project_name) || return 1 - if ! out=$(docker restart "myc_${odoo_service}_1" 2>&1); then + if ! out=$(docker restart "${project_name}_${odoo_service}_1" 2>&1); then if [[ "$out" == *"no matching entries in passwd file" ]]; then warn "Catched docker bug. Restarting once more." - if ! out=$(docker restart "myc_${odoo_service}_1"); then - err "Can't restart container myc_${odoo_service}_1 (restarted twice)." + if ! out=$(docker restart "${project_name}_${odoo_service}_1"); then + err "Can't restart container ${project_name}_${odoo_service}_1 (restarted twice)." echo " output:" >&2 echo "$out" | prefix " ${GRAY}|${NORMAL} " >&2 exit 1 fi else - err "Couldn't restart container myc_${odoo_service}_1 (and no restart bug detected)." + err "Couldn't restart container ${project_name}_${odoo_service}_1 (and no restart bug detected)." exit 1 fi fi - info "Container myc_${odoo_service}_1 was ${DARKGREEN}successfully${NORMAL} restarted." + info "Container ${project_name}_${odoo_service}_1 was ${DARKGREEN}successfully${NORMAL} restarted." } @@ -921,7 +1113,8 @@ cmdline.spec:odoo:cmd:restore:run() { odoo_service="${opt_service:-odoo}" - if [[ "$ZIP_DUMP_LOCATION" == "http://"* ]]; then + if [[ "$ZIP_DUMP_LOCATION" == "http://"* ]] || + [[ "$ZIP_DUMP_LOCATION" == "https://"* ]]; then settmpdir ZIP_TMP_LOCATION tmp_location="$ZIP_TMP_LOCATION/dump.zip" curl -k -s -L "$ZIP_DUMP_LOCATION" > "$tmp_location" || { @@ -1015,11 +1208,11 @@ cmdline.spec:odoo:cmd:set-cyclos-url:run() { dbname=${opt_database:-odoo} cyclos_service="${opt_service:-cyclos}" - - URL=$(compose:get_url "${cyclos_service}") || exit 1 + project_name=$(compose:project_name) || exit 1 + URL=$(compose:get_url "${project_name}" "${cyclos_service}") || exit 1 echo "UPDATE res_company SET cyclos_server_url = '$URL/api' WHERE id=1;" | - compose:psql "$dbname" || { + compose:psql "$project_name" "$dbname" || { err "Failed to set cyclos url value in '$dbname' database." exit 1 } @@ -1043,13 +1236,18 @@ cmdline.spec:cyclos:cmd:dump:run() { cyclos_service="${opt_service:-cyclos}" cyclos_database="${opt_database:-cyclos}" + project_name=$(compose:project_name) || exit 1 + container_id=$(compose:service:container_one "$project_name" "${cyclos_service}") || exit 1 + + + Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \ + docker stop "$container_id" || exit 1 - Wrap -d "stop container 'myc_${cyclos_service}_1'" -- \ - docker stop "myc_${cyclos_service}_1" || exit 1 Wrap -d "Dump postgres database '${cyclos_database}'." -- \ - pgm cp "$cyclos_database" "$DUMP_GZFILE" || exit 1 - Wrap -d "start container 'myc_${cyclos_service}_1'." -- \ - docker start "myc_${cyclos_service}_1" || exit 1 + postgres:dump "${project_name}" "$cyclos_database" "$DUMP_GZFILE" || exit 1 + + Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \ + docker start "${container_id}" || exit 1 } @@ -1066,13 +1264,23 @@ cmdline.spec:cyclos:cmd:restore:run() { cyclos_service="${opt_service:-cyclos}" cyclos_database="${opt_database:-cyclos}" + project_name=$(compose:project_name) || exit 1 + url=$(compose:get_url "${project_name}" "${cyclos_service}") || return 1 + container_id=$(compose:service:container_one "$project_name" "${cyclos_service}") || exit 1 - if [[ "$GZ_DUMP_LOCATION" == "http://"* ]]; then + if [[ "$GZ_DUMP_LOCATION" == "http://"* ]] || + [[ "$GZ_DUMP_LOCATION" == "https://"* ]]; then settmpdir GZ_TMP_LOCATION tmp_location="$GZ_TMP_LOCATION/dump.gz" - Wrap -d "get '$GZ_DUMP_LOCATION'" < "$tmp_location" || exit 1 + Wrap -d "get '$GZ_DUMP_LOCATION'" < "$tmp_location" || { + echo "Error fetching ressource. Is url correct ?" >&2 + exit 1 + } if [[ "\$(dd if="$tmp_location" count=2 bs=1 2>/dev/null | hexdump -v -e "/1 \"%02x\"")" != "1f8b" ]]; then @@ -1089,27 +1297,27 @@ EOF exit 1 } - Wrap -d "stop container 'myc_${cyclos_service}_1'" -- \ - docker stop "myc_${cyclos_service}_1" || exit 1 + Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \ + docker stop "$container_id" || exit 1 ## XXXvlab: making the assumption that the postgres username should ## be the same as the cyclos service selected (which is the default, ## but not always the case). Wrap -d "restore postgres database '${cyclos_database}'." -- \ - pgm cp -f "$GZ_DUMP_LOCATION" "${cyclos_service}@${cyclos_database}" || exit 1 + postgres:restore "$project_name" "$GZ_DUMP_LOCATION" "${cyclos_service}@${cyclos_database}" || exit 1 ## ensure that the database is not locked ## XXXvlab: 70 is uid of user postgres, this avoids the docker bug ## but introduce hardwritten value - Wrap -d "check and remove database lock if any" <