Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pgbackrest-build:
pgbackrest: pgbackrest-build;

postgres-build:

docker build $(ROOTPATH) \
--file $(ROOTPATH)/docker/postgres/Dockerfile \
--tag cybertec-pg-container/postgres:$(IMAGE_TAG) \
Expand All @@ -86,7 +87,8 @@ postgres-build:
--build-arg OLD_PG_VERSIONS="$(OLD_PG_VERSIONS)" \
--build-arg PGVERSION=$(PGVERSION) \
--build-arg ETCD_VERSION=$(ETCD_VERSION) \
--build-arg ARCH=$(ARCH)
--build-arg PGVERSION=$(PGVERSION) \
--build-arg ARCH=$(ARCH)

postgres: postgres-build

Expand Down
63 changes: 63 additions & 0 deletions bootstrap/clone_with_pgbackrest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import argparse
import logging
import os
import subprocess
import sys

from collections import namedtuple
from dateutil.parser import parse

logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)

def read_configuration():
parser = argparse.ArgumentParser(description="Script to clone using pgbackrest. ")
parser.add_argument('--scope', required=True, help='target cluster name')
parser.add_argument('--datadir', required=True, help='target cluster postgres data directory')
parser.add_argument('--recovery-target-time',
help='the timestamp up to which recovery will proceed (including time zone)',
dest='recovery_target_time_string')
parser.add_argument('--dry-run', action='store_true', help='find a matching backup and build the wal-e '
'command to fetch that backup without running it')
args = parser.parse_args()

options = namedtuple('Options', 'name datadir recovery_target_time dry_run')
if args.recovery_target_time_string:
recovery_target_time = parse(args.recovery_target_time_string)
if recovery_target_time.tzinfo is None:
raise Exception("recovery target time must contain a timezone")
else:
recovery_target_time = None

return options(args.scope, args.datadir, recovery_target_time, args.dry_run)

def run_clone_from_pgbackrest(options):
env = os.environ.copy()

pg_path_argument = "--pg1-path={0}".format(options.datadir)

if options.recovery_target_time:
target_time_argument = "--target={0}".format(options.recovery_target_time)
pgbackrest_command = ['/usr/bin/pgbackrest', '--stanza=db', '--type=time', target_time_argument, 'restore', pg_path_argument]
else:
pgbackrest_command = ['/usr/bin/pgbackrest', '--stanza=db', 'restore', pg_path_argument]

logger.info("cloning cluster %s using %s", options.name, ' '.join(pgbackrest_command))

if not options.dry_run:
ret = subprocess.call(pgbackrest_command, env=env)
if ret != 0:
raise Exception("pgbackrest restore exited with exit code {0}".format(ret))

return 0

def main():
options = read_configuration()
try:
run_clone_from_pgbackrest(options)
except Exception:
logger.exception("Clone with pgbackrest failed")
return 1

if __name__ == '__main__':
sys.exit(main())
16 changes: 16 additions & 0 deletions cron_unprivileged.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include <sys/types.h>

int setuid(uid_t euid)
{
return 0;
}

int seteuid(uid_t euid)
{
return 0;
}

int initgroups(const char *user, gid_t group)
{
return 0;
}
3 changes: 3 additions & 0 deletions docker/postgres/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ RUN ${PACKAGER} -y update && ${PACKAGER} -y install --nodocs --noplugins --setop
dumb-init \
libicu \
pgbackrest-${PGBACKREST_VERSION} \
cronie \
&& ${PACKAGER} -y clean all;

# install etcdctl
Expand All @@ -64,6 +65,7 @@ RUN curl -L https://github.com/coreos/etcd/releases/download/v${ETCD_VERSION}/et
ENV PATHBACKUP = $PATH

RUN wget https://smarden.org/runit/runit-2.1.2.tar.gz -P /package/
COPY cron_unprivileged.c /package/

# Install pam_oauth2.so
#RUN #git clone -b $PAM_OAUTH2 --recurse-submodules https://github.com/zalando-pg/pam-oauth2.git \
Expand Down Expand Up @@ -110,6 +112,7 @@ RUN pip3 install 'PyYAML<6.0' setuptools pystache loader kazoo meld3 boto python
done \
&& ${PACKAGER} -y install --nodocs --noplugins --setopt=install_weak_deps=0 glibc-static \
&& ${PACKAGER} -y clean all;
RUN gcc -s -shared -fPIC -o /usr/local/lib/cron_unprivileged.so /package/cron_unprivileged.c

RUN cd /package && tar -xvzf runit-2.1.2.tar.gz && rm runit-2.1.2.tar.gz \
&& cd admin/runit-2.1.2 && package/install \
Expand Down
12 changes: 11 additions & 1 deletion runit/cron/run
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
if [ "$(id -u)" -ne 0 ]; then
LD_PRELOAD=/usr/local/lib/cron_unprivileged.so
fi
CROND_PATH=/usr/sbin/crond

exec 2>&1
exec env -i LD_PRELOAD=$LD_PRELOAD /usr/sbin/cron -f
# Check if the crond binary exists
if [ -f "$CROND_PATH" ]; then
# Execute the command if the file exists
exec env -i LD_PRELOAD=$LD_PRELOAD $CROND_PATH -n
else
# Print a message or handle the case where the file does not exist
echo "Error: $CROND_PATH does not exist (is cron enabled durring build time?). Command not executed."
sv -w 86400 stop /etc/service/cron
exit 1
fi
45 changes: 38 additions & 7 deletions scripts/configure_spilo.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ def deep_update(a, b):
recovery_target_inclusive: false
{{/CLONE_TARGET_INCLUSIVE}}
{{/CLONE_WITH_WALE}}
{{#CLONE_WITH_PGBACKREST}}
method: clone_with_pgbackrest
clone_with_pgbackrest:
command: python3 /scripts/clone_with_pgbackrest.py
--recovery-target-time="{{CLONE_TARGET_TIME}}"
recovery_conf:
restore_command: pgbackrest --stanza=db archive-get %f "%p"
{{/CLONE_WITH_PGBACKREST}}
{{#CLONE_WITH_BASEBACKUP}}
method: clone_with_basebackup
clone_with_basebackup:
Expand Down Expand Up @@ -532,7 +540,8 @@ def get_placeholders(provider):

placeholders.setdefault('PGHOME', os.path.expanduser('~'))
placeholders.setdefault('APIPORT', '8008')
placeholders.setdefault('BACKUP_SCHEDULE', '0 1 * * *')
placeholders.setdefault('BACKUP_SCHEDULE', '0 1 * * SAT')
placeholders.setdefault('BACKUP_SCHEDULE_INCREMENTAL', '0 1 * * *')
placeholders.setdefault('BACKUP_NUM_TO_RETAIN', '5')
placeholders.setdefault('CRONTAB', '[]')
placeholders.setdefault('PGROOT', os.path.join(placeholders['PGHOME'], 'pgroot'))
Expand Down Expand Up @@ -592,6 +601,7 @@ def get_placeholders(provider):
placeholders.setdefault('USE_PAUSE_AT_RECOVERY_TARGET', False)
placeholders.setdefault('CLONE_METHOD', '')
placeholders.setdefault('CLONE_WITH_WALE', '')
placeholders.setdefault('CLONE_WITH_PGBACKREST', '')
placeholders.setdefault('CLONE_WITH_BASEBACKUP', '')
placeholders.setdefault('CLONE_TARGET_TIME', '')
placeholders.setdefault('CLONE_TARGET_INCLUSIVE', True)
Expand Down Expand Up @@ -962,16 +972,28 @@ def write_clone_pgpass(placeholders, overwrite):

def check_crontab(user):
with open(os.devnull, 'w') as devnull:
cron_exit = subprocess.call(['crontab', '-lu', user], stdout=devnull, stderr=devnull)
if cron_exit == 0:
return logging.warning('Cron for %s is already configured. (Use option --force to overwrite cron)', user)
try:
cron_exit = subprocess.call(['crontab', '-lu', user], stdout=devnull, stderr=devnull)
if cron_exit == 0:
return logging.warning('Cron for %s is already configured. (Use option --force to overwrite cron)', user)
except:
logging.error('We were not able to add cron for user %s. Is cron enabled during build?', user)
return True


def setup_crontab(user, lines):
lines += [''] # EOF requires empty line for cron
c = subprocess.Popen(['crontab', '-u', user, '-'], stdin=subprocess.PIPE)
c.communicate(input='\n'.join(lines).encode())
c = subprocess.Popen(['crontab', '-u', user, '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = c.communicate(input='\n'.join(lines).encode())
if stderr:
return logging.error('Error while adding a crontab: %s', stderr)

def setup_crontab_postgres(lines):
lines += [''] # EOF requires empty line for cron
c = subprocess.Popen(['crontab','-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = c.communicate(input='\n'.join(lines).encode())
if stderr:
return logging.error('Error while adding a crontab for user postgres: %s', stderr)


def setup_runit_cron(placeholders):
Expand Down Expand Up @@ -1017,6 +1039,12 @@ def write_crontab(placeholders, overwrite):
hash_dir = os.path.join(placeholders['RW_DIR'], 'tmp')
lines += ['*/5 * * * * {0} /scripts/test_reload_ssl.sh {1}'.format(env, hash_dir)]


if bool(placeholders.get('USE_PGBACKREST')) and not USE_KUBERNETES:
lines += [('{BACKUP_SCHEDULE} /usr/bin/pgbackrest --stanza=db --type=full backup').format(**placeholders)]
lines += [('{BACKUP_SCHEDULE_INCREMENTAL} /usr/bin/pgbackrest --stanza=db --type=incr backup').format(**placeholders)]


if bool(placeholders.get('USE_WALE')):
lines += [('{BACKUP_SCHEDULE} envdir "{WALE_ENV_DIR}" /scripts/postgres_backup.sh' +
' "{PGDATA}"').format(**placeholders)]
Expand All @@ -1031,7 +1059,10 @@ def write_crontab(placeholders, overwrite):
setup_runit_cron(placeholders)

if len(lines) > 1 and (overwrite or check_crontab('postgres')):
setup_crontab('postgres', lines)
try:
setup_crontab_postgres(lines)
except:
logging.error('Unable to add crontab, is cron as service enabled during build? ')

if root_lines and (overwrite or check_crontab('root')):
setup_crontab('root', root_lines)
Expand Down