From e16fa1efc6e74e0830dd6090b5d7e97c7c748723 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Tue, 8 Sep 2020 12:07:56 +0200 Subject: [PATCH] new: [mariadb] added charm. Signed-off-by: Valentin Lab --- mariadb/hooks/init | 61 +++++++++++++++ mariadb/hooks/mysql_database-relation-joined | 62 +++++++++++++++ .../hooks/schedule_command-relation-joined | 44 +++++++++++ mariadb/lib/common | 76 +++++++++++++++++++ mariadb/metadata.yml | 18 +++++ mariadb/resources/bin/mysql-backup | 52 +++++++++++++ 6 files changed, 313 insertions(+) create mode 100755 mariadb/hooks/init create mode 100755 mariadb/hooks/mysql_database-relation-joined create mode 100755 mariadb/hooks/schedule_command-relation-joined create mode 100644 mariadb/lib/common create mode 100644 mariadb/metadata.yml create mode 100755 mariadb/resources/bin/mysql-backup diff --git a/mariadb/hooks/init b/mariadb/hooks/init new file mode 100755 index 0000000..2e8a84c --- /dev/null +++ b/mariadb/hooks/init @@ -0,0 +1,61 @@ +#!/bin/bash + +## Init is run on host +## For now it is run every time the script is launched, but +## it should be launched only once after build. + +## Accessible variables are: +## - SERVICE_NAME Name of current service +## - DOCKER_BASE_IMAGE Base image from which this service might be built if any +## - SERVICE_DATASTORE Location on host of the DATASTORE of this service +## - SERVICE_CONFIGSTORE Location on host of the CONFIGSTORE of this service + + +. lib/common + +set -e + +if [ "${HOST_DATASTORE+x}" ]; then + export HOST_DB_PASSFILE="$HOST_DATASTORE/${SERVICE_NAME}$DB_DATADIR/my.cnf" +else + export HOST_DB_PASSFILE="$CLIENT_DB_PASSFILE" +fi + + +if ! [ -d "$HOST_DATASTORE/${SERVICE_NAME}$DB_DATADIR" ]; then + + MYSQL_ROOT_PASSWORD="$(gen_password)" + + debug docker run -e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD" \ + --rm \ + -v "$DATA_DIR:$DB_DATADIR" \ + --entrypoint /bin/bash "$DOCKER_BASE_IMAGE" + docker run -e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD" \ + --rm \ + -v "$DATA_DIR:$DB_DATADIR" \ + --entrypoint /bin/bash "$DOCKER_BASE_IMAGE" -c ' + mysqld() { + echo "diverted mysqld call..." >&2; + echo "$*" | grep -E "(--help|--skip-networking)" >/dev/null 2>&1 || return; + echo " .. Allowing call." >&2; + /usr/sbin/mysqld "$@"; + } + export -f mysqld; + /docker-entrypoint.sh mysqld' || true + ## docker errorlevel is still 0 even if it failed. + ## AND we must ignore mysqld error ! + [ "$(find "$DATA_DIR" \ + -maxdepth 0 -type d -empty 2>/dev/null)" ] && { + err "Docker run probably failed to do it's job." + exit 1 + } + + ## XXXvlab: this won't help support multiple project running on the + ## same host + cat < "$HOST_DB_PASSFILE" +[client] +password=$MYSQL_ROOT_PASSWORD +EOF + chmod 600 "$HOST_DB_PASSFILE" + info "New root password for mysql. " +fi \ No newline at end of file diff --git a/mariadb/hooks/mysql_database-relation-joined b/mariadb/hooks/mysql_database-relation-joined new file mode 100755 index 0000000..b291ff2 --- /dev/null +++ b/mariadb/hooks/mysql_database-relation-joined @@ -0,0 +1,62 @@ +#!/bin/bash + +## When writing relation script, remember: +## - they should be idempotents +## - they can be launched while the dockers is already up +## - they are launched from the host +## - the target of the link is launched first, and get a chance to ``relation-set`` +## - both side of the scripts get to use ``relation-get``. + +DBNAME=$(relation-get dbname 2>/dev/null) || { + DBNAME="$BASE_SERVICE_NAME" + relation-set dbname "$DBNAME" +} + +USER=$(relation-get user 2>/dev/null) || { + USER="$BASE_SERVICE_NAME" + relation-set user "$USER" +} + + +PASSWORD="$(relation-get password 2>/dev/null)" + +. lib/common + +set -e + +## is there a previous password set for user $USER ? + +NO_PREVIOUS_PASS= +PREVIOUS_PASSWORD_PATH="$state_tmpdir/$SERVICE_NAME/pwd/$USER" +PREVIOUS_PASSWORD=$(cat "$PREVIOUS_PASSWORD_PATH" 2>/dev/null) || NO_PREVIOUS_PASS=true + +if PASSWORD="$(relation-get password 2>/dev/null)"; then + if [ -z "$NO_PREVIOUS_PASS" -a "$PREVIOUS_PASSWORD" != "$PASSWORD" ]; then + die "Inconsistent password specification for user '$USER' on ${DARKYELLOW}$TARGET_SERVICE_NAME$NORMAL." + fi +else + if [ "$PREVIOUS_PASSWORD" ]; then + PASSWORD="${PREVIOUS_PASSWORD}" + else + PASSWORD="$(gen_password)" + info "Generated a new password for user '$USER'." + fi +fi + + + +ensure_db_docker_running || exit 1 +if [ "$?" == 0 ] && check_access "$DBNAME" "$USER" "$PASSWORD"; then + info "Access to database '$DBNAME' from user '$USER' verified working." + exit 0 +fi + + +db_create "$DBNAME" + +db_grant_rights "$DBNAME" "$USER" "$PASSWORD" +info "Granted rights on database '$DBNAME' to user '$USER'." + + + +relation-set password "$PASSWORD" diff --git a/mariadb/hooks/schedule_command-relation-joined b/mariadb/hooks/schedule_command-relation-joined new file mode 100755 index 0000000..50e5e5b --- /dev/null +++ b/mariadb/hooks/schedule_command-relation-joined @@ -0,0 +1,44 @@ +#!/bin/bash + +## When writing relation script, remember: +## - they should be idempotents +## - they can be launched while the dockers is already up +## - they are launched from the host +## - the target of the link is launched first, and get a chance to ``relation-set`` +## - both side of the scripts get to use ``relation-get``. + +. lib/common + + +set -e + +## XXXvlab: should use container name here so that it could support +## multiple mysql +label=${SERVICE_NAME}-mysql-backup +DST=$CONFIGSTORE/$TARGET_SERVICE_NAME/etc/cron/$label +schedule=$(relation-get schedule) + +if ! echo "$schedule" | egrep '^\s*(([0-9/,*-]+\s+){4,4}[0-9/,*-]+|@[a-z]+)\s*$' >/dev/null 2>&1; then + err "Unrecognized schedule '$schedule'." + exit 1 +fi + +## Warning: using '\' in heredoc will be removed in the final cron file, which +## is totally wanted: cron does not support multilines. + +## Warning: 'docker -v' will use HOST directory even if launched from +## 'cron' container. +file_put "$DST" <&1 | ts '\%F \%T \%Z' >> /var/log/cron/${label}_script.log +EOF +chmod +x "$DST" diff --git a/mariadb/lib/common b/mariadb/lib/common new file mode 100644 index 0000000..c270c10 --- /dev/null +++ b/mariadb/lib/common @@ -0,0 +1,76 @@ +# -*- mode: shell-script -*- + +include pretty + +export DB_NAME="$SERVICE_NAME" ## general type of database (ie: postgres/mysql...) +export DB_DATADIR=/var/lib/mysql + +export DATA_DIR=$SERVICE_DATASTORE$DB_DATADIR + +export LOCAL_DB_PASSFILE="$DATA_DIR/my.cnf" +export CLIENT_DB_PASSFILE=/root/.my.cnf + + +is_db_locked() { + local host_db_volume + if [ "${HOST_DATASTORE+x}" ]; then + host_db_volume="$HOST_DATASTORE/${SERVICE_NAME}" + else + host_db_volume="$DATASTORE/${SERVICE_NAME}" + fi + ## ``are_files_locked_in_dir`` doesn't work with mysql here as + ## tested + is_volume_used "$host_db_volume" +} + + +_set_db_params() { + local docker_ip="$1" docker_network="$2" + + if [ "${HOST_DATASTORE+x}" ]; then + export HOST_DB_PASSFILE="$HOST_DATASTORE/${SERVICE_NAME}$DB_DATADIR/my.cnf" + else + export HOST_DB_PASSFILE="$CLIENT_DB_PASSFILE" + fi + + [ -f "$CLIENT_DB_PASSFILE" ] || touch "$CLIENT_DB_PASSFILE" + + server_docker_opts+=() + db_docker_opts+=("--network" "$docker_network") + db_cmd_opts+=("-h" "$docker_ip") + check_command="SELECT 1;" +} + +_set_server_db_params() { + server_docker_opts+=() +} + + +ddb () { dcmd mysql "$@"; } + + +## +## Entrypoints +## + + +db_create () { + local dbname="$1" + ## Using this instead of pipes is important so that trap works + debug "Create if not exists '$dbname' database..." + ddb < <(echo "CREATE DATABASE IF NOT EXISTS \`$dbname\`") +} + +db_grant_rights () { + local dbname="$1" user="$2" password="$3" + ## Using this instead of pipes is important so that trap works + debug "Grant all on $dbname to $user" + ddb < <(echo "GRANT ALL ON \`$dbname\`.* TO '$user'@'%' IDENTIFIED BY '$password'") +} + +check_access() { + local dbname="$1" user="$2" password="$3" + ## Using this instead of pipes is important so that trap works + debug "Check if credentials for user '$user' can access database '$dbname'" + ddb --user="$user" --password="$password" "$dbname" < <(echo "SELECT 1") +} diff --git a/mariadb/metadata.yml b/mariadb/metadata.yml new file mode 100644 index 0000000..414a21d --- /dev/null +++ b/mariadb/metadata.yml @@ -0,0 +1,18 @@ +name: MariaDB +## From: mysql Ver 15.1 Distrib 10.0.21-MariaDB +docker-image: docker.0k.io/mariadb:1.0.0 +maintainer: "Valentin Lab " +provides: + mysql-database: +data-resources: + - /var/lib/mysql + - /var/backups/mysql + +uses: + schedule-command: + constraint: optional + auto: pair + solves: + backup: "Automatic regular dumps for backuping purpose" + default-options: + schedule: "31 * * * *" ## schedule backup every hour diff --git a/mariadb/resources/bin/mysql-backup b/mariadb/resources/bin/mysql-backup new file mode 100755 index 0000000..afe5b8f --- /dev/null +++ b/mariadb/resources/bin/mysql-backup @@ -0,0 +1,52 @@ +#!/bin/bash + +m() { + mysql "${mysql_opts[@]}" -Bs "$@" +} + +md() { + mysqldump "${mysql_opts[@]}" "$@" +} + +mysql_databases() { + echo "SHOW DATABASES" | m +} + +mysql_tables() { + local db="$1" + echo "SHOW TABLES" | m "$db" +} + +mysql_opts=() +if [ "$MYSQLHOST" ]; then + mysql_opts+=(-h "$MYSQLHOST") +fi + + +DBS=($(mysql_databases)) || exit 1 + +mkdir -p /var/backups/mysql + +for db in "${DBS[@]}"; do + if [[ "$db" == "information_schema" || "$db" == "performance_schema" || "$db" == "mysql" ]]; then + continue + fi + echo "Dumping database $db..." >&2 + # omitting all the rotation logic + dst=/"var/backups/mysql/$db" + [ -d "$dst.old" ] && rm -rf "$dst.old" + [ -d "$dst" ] && mv "$dst" "$dst.old" + mkdir -p "$dst.inprogress" + (( start = SECONDS )) + md "$db" --routines --no-data --add-drop-database --database "$db" | gzip --rsyncable > "$dst.inprogress/schema.sql.gz" + tables=$(mysql_tables "$db") + for table in $tables; do + backup_file="$dst.inprogress/${table}.sql.gz" + echo " Dumping $table into ${backup_file}" + md "$db" "$table" | gzip --rsyncable > "$backup_file" || break + done + mv "$dst.inprogress" "$dst" + [ -d "$dst.old" ] && rm -rf "$dst.old" + (( elapsed = SECONDS - start )) + echo " ..dumped $db to $dst ($(du -sh "$dst" | cut -f 1) in ${elapsed}s)" >&2 +done